And this is the method to spawn and place separate body parts so they will exactly match sprite sheet animation. In this case we can't access skeleton to get position and rotation of the bones and attachments. But we can serialize this data beforehand and load it into RAM at runtime. For each sprite sheet animation frame we must serialize frame name and position, rotation and draw order of each attachment in the skeleton. Then we can access this data by the frame name.
This serialization method executes in Update. It must have reference to the skeleton animation object from the scene hierarchy. It writes the data from the skeleton frame by frame.
void WriteAnimationData()
{
//currentFrame and totalFrames are fields.
//Current frame = 0. Total frames = amount of sprite sheet animation frames.
if (currentFrame <= totalFrames)
{
//In my case the sprite sheet animation has 30 frames.
//So, every Update we must move the animation to the next frame before we capture the data.
//We can't set the animation to specific frame directly. We can only set the time of animation.
//The time for each frame can be calculated and set like that:
var stepTime = 1f / 30f;
var currentTime = stepTime * currentFrame;
var state = _skeletonAnimation.state;
var currentTrack = state.GetCurrent(0);
currentTrack.AnimationStart = currentTime;
//For the test purposes I used simple serialization into a text file.
//In actual project it's better to use serializers like Protobuf or others.
//Each frame data is written as one text line "Frame Index|AttachmentName|X-Position|Y-Position|Rotation..."
string frameInfo = $"frame|{currentFrame}";
foreach (var slot in _skeletonAnimation.skeleton.Slots)
{
var bone = slot.Bone;
var regionAttachment = slot.Attachment as RegionAttachment;
bone.LocalToWorld(regionAttachment.X, regionAttachment.Y, out float x, out float y);
frameInfo = String.Concat(frameInfo, $" {slot.Attachment.Name}|{x}|{y}|{bone.WorldRotationX}");
}
using (TextWriter writer = new StreamWriter(path, true))
{
writer.WriteLine(frameInfo);
writer.Close();
}
currentFrame++;
}
}
Next we have to deserialize that text file in application:
void ReadAnimationData()
{
int count = 0;
foreach (var line in File.ReadLines(path))
{
//BodyPartSpawnData is a simple struct with attachment name, position and rotation fields.
var list = new List<BodyPartSpawnData>();
var split = line.Split(" ");
for (int i = 0; i < split.Length; i++)
{
if (i == 0)
continue;
var attachmentInfoSplit = split[i].Split("|");
string attName = attachmentInfoSplit[0];
Vector2 pos = new Vector2( float.Parse(attachmentInfoSplit[1]),float.Parse(attachmentInfoSplit[2]));
float rot = float.Parse(attachmentInfoSplit[3]);
BodyPartSpawnData spawnData = new BodyPartSpawnData(attName,pos,rot);
list.Add(spawnData);
}
//spawnData is a dictionary with frame index as a key and BodyPartSpawnData as a value.
spawnData.Add(count, list);
count++;
}
}
And when we need to spawn body parts we need to execute this code:
//_sequenceRenderer is a reference to the SpriteRenderer of the object with sprite sheet animation.
var spriteName = _sequenceRenderer.sprite.name;
//In my case the last two characters of the frame name contain its index.
//So I can parse them to int and get current frame index of sprite sheet animation.
int sequenceFrame = int.Parse(spriteName.Substring(spriteName.Length - 2));
//Get array of deserialized frame data by the frame index and iterate it.
foreach (var frameData in spawnData[sequenceFrame])
{
//In this test project I didn't serialize drawOrder, because the skeleton was in the scene. So I could calculate drawOrder from it.
//But it must be serialized in real project, if there is no reference to the skeleton.
int drawOrder =
_skeletonAnimation.Skeleton.DrawOrder.FindIndex(
(slot => slot.Attachment.Name == frameData.bodyPartName));
var prefab = Resources.Load(frameData.bodyPartName);
//Vector3.zero is the position of the skeleton when the data from it was captured and serialized.
Vector3 offset = _sequenceRenderer.transform.position - Vector3.zero;
Vector3 position = new Vector3(frameData.position.x, frameData.position.y, drawOrder * -0.1f) + offset;
var bodyPart = Instantiate(prefab, position,Quaternion.Euler(0,0,frameData.rotation)) as GameObject;
}