Составная 2D-анимация: части и шарниры
Рабочая иерархия руки, поворот вокруг сустава и вынесенный аксессуар через RemoteTransform2D.
Когда использовать
Составная анимация двигает готовые части персонажа: плечо, предплечье, кисть, голову или оружие. Часть поворачивается как жёсткое изображение. Чтобы изгибать один рисунок по вершинам, нужен Polygon2D со скелетом и весами.
На кадрах ниже сверху показаны жёсткие части, снизу — один деформируемый полигон. Это кадры работающего примера движка.
Запустить руку из отдельных частей
Создайте уровень с корнем Entity2D, задайте корню Position (160,144). Прикрепите CutoutArmDemo.cs, добавьте Input Map действие animation_pause на Space, соберите C#-проект и запустите F6. Рука сгибается за секунду и возвращается ещё за секунду; Space приостанавливает или продолжает клип. Камера и внешние изображения этому примеру не нужны.
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();
}
}Иерархия — Shoulder → Elbow → Hand. Начало координат каждой Entity2D находится в шарнире, а рисунок вытянут от него вдоль локальной X. Elbow расположен на конце плеча (70,0), Hand — на конце предплечья (60,0). Поэтому поворот плеча переносит всю цепочку, а поворот предплечья не смещает его собственный шарнир.
Заменить полигоны спрайтами
Для каждой части оставьте Entity2D с началом в суставе, а под неё добавьте Sprite2D. Сместите дочерний спрайт или его Offset, чтобы нарисованный сустав совпал с (0,0) родителя. Не двигайте шарнир ради центрирования текстуры: это изменит цепочку и все клипы. Проверяйте стыки при крайних углах; небольшое перекрытие рисунков прячет разрыв.
Анимируйте rotation шарниров, оставляя их локальные position постоянными, если длина сегмента не должна меняться. Для осмысленного растяжения добавляйте отдельную дорожку явно. SetPose() в примере останавливает клип перед ручной записью углов; иначе AnimationPlayer перезапишет их следующим кадром.
Аксессуар вне иерархии кисти
Accessory находится рядом с Shoulder, но следует за Grip через RemoteTransform2D. RemotePath задаётся относительно самого RemoteTransform2D. Включены глобальные позиция и вращение; UpdateScale = false сохраняет масштаб аксессуара. Такой вариант полезен, когда предмет должен оставаться в другой ветви уровня.
У обычного дочернего предмета проще и дешевле оставить родителем Hand. RemoteTransform2D не создаёт физическое соединение, не копирует видимость и не решает сортировку. Не направляйте его на предка или по циклу взаимных зависимостей.
Слои и исходная поза
Для частей используйте ZIndex и осознанную иерархию CanvasItem; для размещения за родителем есть ShowBehindParent. Порядок рисования не меняет направление наследования трансформации. Отрицательный масштаб меняет ориентацию и может менять знак угла; для зеркалирования всей руки применяйте единое правило на её корне.
ResetPose() возвращает только заданные углы в 0. Это исходная поза данного примера, не универсальное восстановление всех свойств персонажа. В собственном риге запишите и сохраняйте нужные локальные исходные трансформации или отдельный RESET-клип.
Редакторские цепочки IK помогают выставлять позы и записывать ключи. Они не являются автоматически работающим во время игры решателем. Для слежения за целью в игре используйте runtime-пример IK.
Диагностика
| Симптом | Проверка |
|---|---|
| Часть вращается вокруг центра изображения | Перенесите геометрию относительно шарнира, а не шарнир относительно родителя |
| Сегменты расходятся | Локальные позиции дочерних суставов и случайные ключи Position |
| Ручной угол сразу пропадает | Активная анимация или IK пишет то же свойство |
| Аксессуар не следует за рукой | RemotePath, существование цели и флаги UpdatePosition/UpdateRotation |
| Аксессуар слишком большой | Масштаб родителя и UpdateScale; проверьте локальную и глобальную трансформации отдельно |
| Часть оказывается перед телом | ZIndex, ShowBehindParent и порядок CanvasItem |