Cutout 2D animation: parts and pivots
A working arm hierarchy, joint pivots and a detached accessory driven by RemoteTransform2D.
When to use it
Cutout animation moves finished character pieces: upper arm, forearm, hand, head or weapon. Each piece turns as a rigid image. To bend a single drawing by its vertices, use a Polygon2D with skeleton weights.
The upper arm in these frames uses separate rigid parts; the striped lower arm is one deforming polygon. These are frames from the running engine example.
Run the multipart arm
Create an Entity2D level root at Position (160,144). Attach CutoutArmDemo.cs, add Input Map action animation_pause on Space, build and run with F6. The arm bends for one second and returns over the next; Space pauses or resumes. This example needs neither a camera nor external images.
CutoutArmDemo.cs
using Reckfell;
public partial class CutoutArmDemo : Entity2D
{
public Entity2D Shoulder { get; private set; } = null!;
public Entity2D Elbow { get; private set; } = null!;
public Entity2D Hand { get; private set; } = null!;
public Entity2D Accessory { get; private set; } = null!;
public AnimationPlayer Player { get; private set; } = null!;
private static Polygon2D Part(float width, float height, Color color) => new()
{
Polygon = [new(0,-height/2),new(width,-height/2),new(width,height/2),new(0,height/2)],
Color = color
};
private static void Keys(Animation clip, string path, float middle)
{
int track = clip.AddTrack(Animation.TrackType.Value);
clip.TrackSetPath(track,new EntityPath(path));
clip.TrackInsertKey(track,0,0f);
clip.TrackInsertKey(track,1,middle);
clip.TrackInsertKey(track,2,0f);
}
public override void _Ready()
{
Shoulder = new Entity2D { Name="Shoulder" };
Elbow = new Entity2D { Name="Elbow", Position=new Vector2(70,0) };
Hand = new Entity2D { Name="Hand", Position=new Vector2(60,0) };
Shoulder.AddChild(Part(70,24,new Color(.15f,.35f,1)));
Elbow.AddChild(Part(60,18,new Color(.45f,.6f,1)));
Hand.AddChild(Part(18,24,Colors.White));
Elbow.AddChild(Hand); Shoulder.AddChild(Elbow); AddChild(Shoulder);
Accessory = new Entity2D { Name="Accessory", ZIndex=2 };
Accessory.AddChild(Part(30,6,Colors.White)); AddChild(Accessory);
var remote = new RemoteTransform2D
{
Name="Grip", UpdatePosition=true, UpdateRotation=true,
UpdateScale=false, UseGlobalCoordinates=true
};
Hand.AddChild(remote);
remote.RemotePath = remote.GetPathTo(Accessory);
Player = new AnimationPlayer { Name="AnimationPlayer", RootEntity=new EntityPath("..") };
AddChild(Player);
var bend = new Animation { Length=2, LoopMode=Animation.LoopModeEnum.Linear };
Keys(bend,"Shoulder:rotation",-.4f);
Keys(bend,"Shoulder/Elbow:rotation",1f);
var library = new AnimationLibrary(); library.AddAnimation("bend",bend);
Player.AddAnimationLibrary("",library); Player.Play("bend");
}
public void SetPose(float shoulder, float elbow)
{
Player.Stop(keepState:true);
Shoulder.Rotation=shoulder; Elbow.Rotation=elbow;
}
public void ResetPose() => SetPose(0,0);
public override void _UnhandledInput(InputEvent @event)
{
if (!@event.IsActionPressed("animation_pause")) return;
if (Player.IsPlaying()) Player.Pause(); else Player.Play("bend");
GetViewport().SetInputAsHandled();
}
}The hierarchy is Shoulder → Elbow → Hand. Each Entity2D origin is a joint, with its drawing extending along local X. Elbow sits at the upper-arm end (70,0); Hand sits at the forearm end (60,0). Rotating the shoulder carries the whole chain; rotating the forearm leaves its own joint in place.
Replace polygons with sprites
Keep an Entity2D origin at each joint and add a child Sprite2D. Shift the child sprite or its Offset until the drawn joint matches its parent's (0,0). Do not move the joint just to center the texture; that changes the chain and every clip. Check extreme angles, using slight artwork overlap to hide gaps.
Animate joint rotation, keeping local position fixed unless the segment length should change. Add stretching tracks explicitly when intended. The example's SetPose() stops the clip before manually writing angles, preventing AnimationPlayer from replacing them next frame.
An accessory outside the hand hierarchy
Accessory is a sibling of Shoulder but follows Grip through RemoteTransform2D. RemotePath is relative to that RemoteTransform2D. Global position and rotation copying are enabled; UpdateScale = false preserves the accessory scale. This suits an item that must stay in another level branch.
A normal hand-held child is simpler under Hand. RemoteTransform2D does not create a physics joint, copy visibility or solve sorting. Do not target an ancestor or form a cycle of remote dependencies.
Layers and the initial pose
Use ZIndex and a deliberate CanvasItem hierarchy; ShowBehindParent draws behind a parent. Draw order does not change transform inheritance. Negative scale changes orientation and can change an angle's sign; apply a consistent rule at the arm root when mirroring it.
ResetPose() only restores these example angles to 0. It is not a universal reset of every character property. Store the required local rest transforms or a separate RESET clip in your own rig.
Editor IK chains help pose and keyframe a character. They do not automatically become a gameplay-time solver. Use the runtime IK example to follow a target during play.
Diagnostics
| Symptom | Check |
|---|---|
| A part rotates around the image center | Move the drawing relative to its joint, not the joint relative to its parent |
| Segments separate | Child joint local positions and unintended Position keys |
| A manual angle disappears immediately | Animation or IK is writing the same property |
| Accessory does not follow | RemotePath, target existence and UpdatePosition/UpdateRotation |
| Accessory is too large | Parent scale and UpdateScale; inspect local and global transforms separately |
| A part draws in front of the body | ZIndex, ShowBehindParent and CanvasItem order |