• Unity
  • Replay system - Replaying animation manually

  • 編集済み
Related Discussions
...

Hi!

I would like to implement a replay system in my game and was wondering how should I approach this problem. I'm using the skeleton animation. Basically, I need to be able to save the state of the animation in order to replay it in my replay system. It looks like I would need to save a LOT of stuff. I think I would need to serialize everything in the animation state + serialize track entries.

Anybody had to do anything close to this ?

There are multiple possible approaches, depending on what requirements you have. Shall the replay system allow for arbitrary back and forth scrubbing, or only forward playback?

If you only need forward playback (which I doubt), you could get away with only recording calls that have an effect on AnimationState, such as AddAnimation() or modifying TrackEntry properties.

If you also need arbitrary positioning within the timeline, and forward as well as backward playback, recording track entries of AnimationState would be a good approach. It would also be possible to not save the AnimationState but to instead save all bone rotations and locations, but this will most likely require more storage space. If storage space is not a problem, recording all bone's locations could be easier implementation-wise. The lines below describe recording AnimationState and not how to record individual bone state.

1) What to record/serialize:
You would for each track record the list of currently enqueued TrackEntry objects (at least the first two track entries to cover transitions), with all relevant settings that deviate from the default. You need to at least record the Animation of the TrackEntry as well as the TrackTime. The perfect approach would be to basically serialize all attributes of TrackEntry but to remove unnecessary and redundant data, as follows below.

2) How to store the data:
If you need to record long playback periods and need to save space, it would be advisable to only save what actually changed instead of the full redundant data every time. So you could save only the delta to:
1) a default TrackEntry, e.g. you don't need to save the default loop=0 all the time ("content-wise delta compression")
2) a delta to the last recording timepoint when no new TrackEntry was enqueued or removed ("time-wise delta compression"), e.g. when only TrackTime changes, don't record the non-default but unchanged loop=1 every time.

3) When to serialize data:
Depending on your use case you might either want to serialize the state every 0.1 seconds, or every frame. When storage size does not matter and you can save every frame and don't need slow-motion playback, you could theoretically get away without interpolation between recorded timepoints. The perfect solution that support playback with playback-timepoints different from recording-timepoints would require to use an interpolated TrackEntry.TrackTime of two recording timepoints.

4) How to apply serialized data:

Obviously, you would then need to unpack any delta-compressed saved TrackEntry states to fully filled-out TrackEntry objects (in the perfect solution also interpolating TrackEntry.TrackTime when time lies between two entries), assign them to their respective AnimationState.tracks and then call AnimationState.Apply() to apply the animation state to the skeleton.

Note that when changing attachments or skins via code, these changes should also be recorded.

Please let us know if this makes sense to you or if anything is unclear.
Of course, any additional input by other users on the forum would be highly welcome, we would love to hear your ideas and solutions as well! Perhaps Timeline functionality could also be utilized successfully for such use cases.

I can't tell you how much I appreciate a response that quick and detailed. As a matter of fact, I think I could get away without the need to use backward playback, but I do need to be able to start from an arbitrary location which is why I can't really use an event driven approach and save add animation calls.

The bones rotation and location is pretty interesting too because I dont think the storage will be that much of an issue. I tried doing it and I was not able to apply it during playback. I will try and save Track Entries and the timescale of animation state. I will report back when I have it working and show you the end result and possibly helping others that would like to accomplish this sort of things as well.

Thanks very much for your kind words, very glad it helped!

jnic wrote

I tried doing it and I was not able to apply it during playback.

Just listing this for the sake of completion, in case anyone would like to go down this road:
When applying local or world bone location values for playback (we would suggest saving and applying world values), some required calls need to be issued afterwards to ensure that the skeleton mesh is constructed based on the updated bone locations.

The general update order is as follows (SkeletonAnimation.cs):
1) state.Apply(skeleton); Here AnimationState is applied to the skeleton, setting local bone rotation values and constraint mix values.
2) skeleton.UpdateWorldTransform(); Here world transform values are updated, based on local values and constraint mix values.
3) base.LateUpdate(); This calls SkeletonRenderer.LateUpdate, which updates the skeleton mesh based on bone and attachment state.

I will try and save Track Entries and the timescale of animation state. I will report back when I have it working and show you the end result and possibly helping others that would like to accomplish this sort of things as well.

This would be very much appreciated! 🙂 If you have any problems or questions along the way, feel free to ask.

It's almost working flawlessly.

Here is a recording I made : https://drive.google.com/file/d/1sHGXfqKlkMgvFYvrgsZWfDppzvlcjTzD/view?usp=sharing

The way the player skates is pretty much perfect. The goaltender when facing south has a real intense glitchy animation. Southwest and Southeast seems ok for some reason. Same for the skater when he celebrates at the end of the goal, all the bones are all over the place and I am not sure why. I'm thinking its probably related to some sort of attribute that I am not saving. The puck and the shadow is weird but it's normal, I have not gotten yet to saving the fake shadow position/rotation.

Here is what and how I serialize :


public class RecordableSkeletonAnimation : ReplayRecordableBehaviour
{

private SkeletonAnimation _skeletonAnimation;
private AnimationState animState = null;

public override void Awake()
{
    SetAnimState();
}

void SetAnimState()
{
    _skeletonAnimation = GetComponent<SkeletonAnimation>();
    if (_skeletonAnimation != null)
    {
        animState = _skeletonAnimation.AnimationState;
    }
}

public override void OnReplaySerialize(ReplayState state)
{
    if (animState == null)
    {
        SetAnimState();
    }

    state.Write(animState.TimeScale);
    state.Write(animState.Tracks.Count);

    animState.Tracks.ForEach(t =>
    {
        state.Write(t.Animation.Name);
        state.Write(t.Alpha);
        state.Write(t.Loop);
        state.Write(t.AnimationStart);
        state.Write(t.AnimationEnd);
        state.Write(t.AnimationLast);
        state.Write(t.AttachmentThreshold);
        state.Write(t.Delay);
        state.Write(t.DrawOrderThreshold);
        state.Write(t.EventThreshold);
        state.Write(t.HoldPrevious);
        state.Write(t.MixDuration);
        state.Write(t.MixTime);
        state.Write(t.TrackEnd);
        state.Write(t.TrackTime);
        state.Write(t.TimeScale);
    });
}

public override void OnReplayUpdate(ReplayTime replayTime)
{
    animState.Apply(_skeletonAnimation.Skeleton);
}

public override void OnReplayPlayPause(bool paused)
{
    animState.TimeScale = 0f;
    animState.Apply(_skeletonAnimation.Skeleton);
}

public override void OnReplayStart()
{
    _skeletonAnimation.enabled = true;
    _skeletonAnimation.UpdateMode = UpdateMode.FullUpdate;
}

public override void OnReplayDeserialize(ReplayState state)
{
    animState.TimeScale = state.ReadFloat();
    animState.Tracks.Clear();
    int numTracks = state.ReadInt32();

    for (var i = 0; i < numTracks; i++)
    {
        TrackEntry track = new TrackEntry();
        track.Animation = animState.Data.SkeletonData.FindAnimation(state.ReadString());
        track.Alpha = state.ReadFloat();
        track.Loop = state.ReadBool();
        track.AnimationStart = state.ReadFloat();
        track.AnimationEnd = state.ReadFloat();
        track.AnimationLast = state.ReadFloat();
        track.AttachmentThreshold = state.ReadFloat();
        track.Delay = state.ReadFloat();
        track.DrawOrderThreshold = state.ReadFloat();
        track.EventThreshold = state.ReadFloat();
        track.HoldPrevious = state.ReadBool();
        track.MixDuration = state.ReadFloat();
        track.MixTime = state.ReadFloat();
        track.TrackEnd = state.ReadFloat();
        track.TrackTime = state.ReadFloat();
        track.TimeScale = state.ReadFloat();
        
        animState.Tracks.Add(track);
    }
}
}

Any idea why and how I could fix the small glitches ?

Glad to hear you've gotten so far already! BTW, your game looks great! 8)

One thing I noticed missing is that in OnReplayDeserialize you added a single TrackEntry per track index via animState.Tracks.Add(track);. What is different from normal playback is that a TrackEntry normally has it's preceding entry assigned at TrackEntry.mixingFrom during a transition.

You could simply ignore transitions to make things easier, but then you should not assign the real values at mixTime and mixDuration, because this will lead to mix = to.mixTime / to.mixDuration;. So during a recorded transition, you would get an animation mix-alpha value of the to animation of 0.0-1.0 but have no preceding from animation that this should be combined with. To ignore transitions you could assign the same value of e.g. 0.1 to both mixTime and mixDuration to receive a mix alpha value of 1.0.

I hit a wall :sucks:

I don't understand how I can get the regular skater to animate almost seamlessly, while the goaltender have its mesh all deformed. Basically we have animations facing south, south-east and south-west. When I transition from SE to SW it's working, but whenever I add animation to the south the mesh gets completely deformed. Also, I tried adding mixing by saving MixingFrom and MixingTo but it still is not mixing during playback. I really don't get how I can recreate the state for playing it back correctly.

Here is the goaltender deforming : https://drive.google.com/file/d/1l43t_UqpPRTtavIhZbvpGHFMI7gvxwwL/view?usp=sharing

Sorry to hear that you're experiencing problems. Could you please post some code that shows how you are saving and applying mixingFrom and mixingTo?

Unfortunately after having watched the linked video a few times, I fail to see too much of a difference between the initially recorded animation part and the playback version. What I noticed is that the shoulders seem to not line up as well in playback, with the arm being higher than the shoulder. Or is this could be due to the changed perspective in the playback part of the video?

Please note that we don't know your game as well as you do and have a much harder time discovering things that don't look like they should. Anyway, a video without code does not help very much in this regard, so if you could post an updated version of your code that would help a lot.

Ok my bad, I thought it was obvious by looking at it. Here is an example where if the player is in the same "direction", its working perfectly. The player skates in south west then transitions to the celebration that is south west too. Works perfectly :

https://drive.google.com/file/d/1cF5nXMsaH1hFJPrYCt7V4J0lgkwvwHN9/view?usp=sharing

The next sequence is broken and the skater is going in north-east but the anim celebration transition to south west. In game it looks weird but we will eventually make a transition anim. In replay, it looks like the mesh is not updating at all and are still stuck with the anim in north east:

https://drive.google.com/file/d/1pgyvOoKSbzTL4DJdYy7Gdr5Ujab9D9ft/view?usp=sharing

And here is my code, I tried adding the mixing, it was not changing anything so I commented out.


using Spine;
using Spine.Unity;
using UltimateReplay;
using AnimationState = Spine.AnimationState;


public class RecordableSkeletonAnimation : ReplayRecordableBehaviour
{

private SkeletonAnimation _skeletonAnimation;
private AnimationState animState;

private TrackEntry currentTrack;
private TrackEntry mixingFrom;
private TrackEntry mixingTo;
private TrackEntry mixingNextFrom;
private TrackEntry mixingNextTo;
private TrackEntry next;

private float timescale;

public override void Awake()
{
    SetAnimState();
}

void SetAnimState()
{
    _skeletonAnimation = GetComponent<SkeletonAnimation>();
    if (_skeletonAnimation != null)
    {
        animState = _skeletonAnimation.AnimationState;
    }
}

private TrackEntry GenerateTrack(ReplayState state)
{
    TrackEntry track = new TrackEntry();
    track.Animation = animState.Data.SkeletonData.FindAnimation(state.ReadString());
    track.Alpha = state.ReadFloat();
    track.Loop = state.ReadBool();
    track.AnimationStart = state.ReadFloat();
    track.AnimationEnd = state.ReadFloat();
    track.AnimationLast = state.ReadFloat();
    track.AttachmentThreshold = state.ReadFloat();
    track.Delay = state.ReadFloat();
    track.DrawOrderThreshold = state.ReadFloat();
    track.EventThreshold = state.ReadFloat();
    track.HoldPrevious = state.ReadBool();
    track.MixDuration = 0.2f;//state.ReadFloat();
    track.MixTime = 0.2f;//state.ReadFloat();
    track.TrackEnd = state.ReadFloat();
    track.TrackTime = state.ReadFloat();
    track.TimeScale = state.ReadFloat();

    return track;
}

private void FillTrack(TrackEntry t, ReplayState state)
{
    state.Write(t.Animation.Name);
    state.Write(t.Alpha);
    state.Write(t.Loop);
    state.Write(t.AnimationStart);
    state.Write(t.AnimationEnd);
    state.Write(t.AnimationLast);
    state.Write(t.AttachmentThreshold);
    state.Write(t.Delay);
    state.Write(t.DrawOrderThreshold);
    state.Write(t.EventThreshold);
    state.Write(t.HoldPrevious);
    //state.Write(t.MixDuration);
    //state.Write(t.MixTime);
    state.Write(t.TrackEnd);
    state.Write(t.TrackTime);
    state.Write(t.TimeScale);
}

public override void OnReplaySerialize(ReplayState state)
{
    if (animState == null)
    {
        SetAnimState();
    }

    state.Write(animState.TimeScale);
    state.Write(animState.Tracks.Count > 0);

    if (animState.Tracks.Count <= 0)
    {
        return;
    }
    var currTrack = animState.Tracks.Items[0];

    FillTrack(currTrack, state);
    bool hasMixingFrom = currTrack.MixingFrom != null;
    bool hasMixingTo = currTrack.MixingTo != null;
    bool hasNext = currTrack.Next != null;
    bool hasNextMixingFrom = currTrack.Next != null && currTrack.Next.MixingFrom != null;
    bool hasNextMixingTo = currTrack.Next != null && currTrack.Next.MixingTo != null;;
    
    state.Write(hasMixingFrom);
    state.Write(hasMixingTo);
    state.Write(hasNext);
    state.Write(hasNextMixingFrom);
    state.Write(hasNextMixingTo);
    
    if (hasMixingFrom)
    {
        //FillTrack(currTrack.MixingFrom, state);
    }
    if (hasMixingTo)
    {
        //FillTrack(currTrack.MixingTo, state);
    }
    if (hasNext)
    {
        FillTrack(currTrack.Next, state);
    }
    if (hasNextMixingFrom)
    {
        //FillTrack(currTrack.Next.MixingFrom, state);
    }
    if (hasNextMixingTo)
    {
        //FillTrack(currTrack.Next.MixingTo, state);
    }
    
}

public override void OnReplayUpdate(ReplayTime replayTime)
{
    animState.Tracks.Clear();
    animState.TimeScale = timescale;
    
    animState.Tracks.Add(currentTrack);
    animState.Apply(_skeletonAnimation.Skeleton);
}



public override void OnReplayPlayPause(bool paused)
{
    if (paused)
    {
        animState.TimeScale = 0f;
        return;
    }
   
    animState.TimeScale = 1f;
}

public override void OnReplayStart()
{        
    _skeletonAnimation.UpdateMode = UpdateMode.FullUpdate;
    _skeletonAnimation.enabled = true;

}

/*private void LateUpdate()
{
    if (IsReplaying)
    {
        _skeletonAnimation.LateUpdate();
    }
}*/

public override void OnReplayDeserialize(ReplayState state)
{

    currentTrack = null;
    next = null;
    mixingFrom = null;
    mixingNextFrom = null;
    mixingNextTo = null;
    
    timescale = state.ReadFloat();
    if (!state.ReadBool())
    {
        return;
    }

    currentTrack = GenerateTrack(state);
    
    bool hasMixingFrom = state.ReadBool();
    bool hasMixingTo = state.ReadBool();
    bool hasNext = state.ReadBool();
    bool hasNextMixingFrom = state.ReadBool();
    bool hasNextMixingTo = state.ReadBool();


    if (hasMixingFrom)
    {
        //mixingFrom = GenerateTrack(state);
        //currentTrack.MixingFrom = mixingFrom;
    }
    
    if (hasMixingTo)
    {
        //mixingTo = GenerateTrack(state);
        //currentTrack.MixingTo = mixingTo;
    }
    
    if (hasNext)
    {
        next = GenerateTrack(state);
    }
    
    if (hasNextMixingFrom)
    {
        //mixingNextFrom = GenerateTrack(state);
        //next.MixingFrom = mixingNextFrom;
    }
    
    if (hasNextMixingTo)
    {
        //mixingNextTo = GenerateTrack(state);
        //next.MixingTo = mixingNextTo;
    }

    currentTrack.Next = next;
    if (hasNext)
    {
        currentTrack.Next.Previous = currentTrack;
    }
}
}


Do you think simply adding mixing would make it work ? I feel like there is some kind of call that is missing so that I can update the skeleton while this transition is happening.

Thanks for the followup, in this video the problem is now indeed very apparent. I think I misunderstood the video in your previous post after "Here is the goaltender deforming:" as "here you can see it incorrectly deforming" which was intended as "here it is properly deforming". Anyway, thanks for the additional video.

I think what's missing in the code is a call to skeleton.SetToSetupPost() before applying any animation.

Long version:
During "normal" animation, mix-out (e.g. reset of attachments that the animation changed) will happen at the end of each animation when one track ends and is transitioned to the next (or to the empty animation). This will be missing when manually clearing tracks (even when using AnimationState.ClearTracks()), leaving the skeleton in the current state. This is problematic when playback timing is not the same as recording timing, sometimes playback time then reaches over the track end time and mixes out properly, sometimes it will end slightly before, but in the next playback frame the track is cleared and thus not mixed out.

Regarding playback vs. recording timing: you might also want to consider not letting skeletonAnimation.Update() be called normally with random playback-framerate based Time.delta, but instead disable the SkeletonAnimation component and manually call skeletonAnimation.Update(recordedDeltaTime) to be sure. Then you also need to call skeletonAnimation.LateUpdate() when the component is disabled.

How do you handle these direction-changes? Are you swapping out attachments via animations when changing direction, or are you using the same attachment and scaling or deforming them differently to match direction? You could have a check with the Skeleton Debug window that is available via the SkeletonAnimation Inspector, in the Advanced foldout, the Debug button on the right side. Here it can be shown which attachments are active at the (paused) time, and also which scale individual bones currently have.

Oh wow, thank you so much! I just added a call to SetToSetupPost() if the animation is different and it's working!!! There is still a couple of small glitches here and there, but you clearly explained why and I'll be able to change my code to a more delta-ing approach which will probably fix everything. There is no interpolation but so far it's pretty pretty close to the real thing and its good enough for our state of development. Thank you so much.

What it looks like now : https://drive.google.com/file/d/1u_-61iYWLbEzA9hC9D_4_pz8Z6DXTjWS/view

Really glad you could fix the issue, it's looking awesome already!! Great work! 8)

1ヶ月 後

Hello again!

I finally tried to do the bone method. Once again, it's almoooost perfect. There is a little glitch here and there, sometimes major sometimes minor. Basically I am saving attachment changed and Getting/Setting them. I do the same with bones data. Finally, I do SkeletonAnimation.Update(), AnimationState.Apply(), Skeleton.UpdateWorldTransform() and then a LateUpdate() as mentionned in an earlier post.

What is happening is that for some reason during playback its like the bones are kind of stuck in the SetupPose. Here is a look in game play :

Here it is during playback:

which is how it looks when I click in the debug menu to SetToSetupPose().

Here is my code :

using System.Collections.Generic;
using Sirenix.Utilities;
using Spine;
using Spine.Unity;
using UltimateReplay;
using UnityEngine;
using AnimationState = Spine.AnimationState;


public class RecordableSkeletonAnimation : ReplayRecordableBehaviour
{

private SkeletonAnimation _skeletonAnimation;
private SkeletonUtility _skeletonUtility;
private AnimationState animState;

private TrackEntry currentTrack;
private TrackEntry next;

private float timescale;

private string previousAnimName;

private List<string> slotName = new List<string>();
private List<string> attachmentName = new List<string>();
private List<int> sortOrder = new List<int>();
private List<float> boneData = new List<float>();


public override void Awake()
{
    SetAnimState();
}

void SetAnimState()
{
    _skeletonAnimation = GetComponent<SkeletonAnimation>();
    if (_skeletonAnimation != null)
    {
        animState = _skeletonAnimation.AnimationState;
    }
}
public override void OnReplaySerialize(ReplayState state)
{
    if (animState == null)
    {
        SetAnimState();
    }
    
    state.Write(_skeletonAnimation.skeleton.Slots.Items.Length);
    _skeletonAnimation.skeleton.Slots.Items.ForEach(slot =>
    {
        state.Write(slot.Data.Name);
        if (slot.Attachment == null)
        {
            state.Write("null");
        }
        else
        {
            state.Write(slot.Attachment.Name);
        }
    });
    
    _skeletonAnimation.skeleton.DrawOrder.Items.ForEach(slotOrder =>
    {
        state.Write(slotOrder.Data.Index);
    });
    _skeletonAnimation.skeleton.Bones.ForEach(bone =>
    {
         state.Write(bone.WorldX);
         state.Write(bone.X);
         state.Write(bone.WorldY);
         state.Write(bone.Y);
         state.Write(bone.Rotation);
         state.Write(bone.ScaleX);
         state.Write(bone.ScaleY);
         state.Write(bone.ShearX);
         state.Write(bone.ShearY);
    });
    
}

public override void OnReplayPlayPause(bool paused)
{
    if (paused)
    {
        animState.TimeScale = 0f;
        return;
    }
    animState.TimeScale = 1f;
}

public override void OnReplayEnd()
{
    _skeletonAnimation.enabled = true;
}

public override void OnReplayStart()
{
    _skeletonAnimation.enabled = false;
    animState.ClearTracks();
}

private void LateUpdate()
{
    if (IsReplaying)
    {
        _skeletonAnimation.LateUpdate();
    }
}

private void Update()
{
    if (IsReplaying)
    {
        int idxBone = 0;
        _skeletonAnimation.skeleton.Bones.ForEach(bone =>
        {
            bone.WorldX = boneData[idxBone++];
            bone.X = boneData[idxBone++];
            bone.WorldY = boneData[idxBone++];
            bone.Y = boneData[idxBone++];
            bone.Rotation = boneData[idxBone++];
            bone.ScaleX = boneData[idxBone++];
            bone.ScaleY = boneData[idxBone++];
            bone.ShearX = boneData[idxBone++];
            bone.ShearY = boneData[idxBone++];
        });
        

        _skeletonAnimation.skeleton.DrawOrder.Sort((slot1, slot2) =>
        {
            var idxSlot1 = sortOrder.FindIndex(slotIdx => slot1.Data.Index == slotIdx);
            var idxSlot2 = sortOrder.FindIndex(slotIdx => slot2.Data.Index == slotIdx);
            return idxSlot1 - idxSlot2;
        });
        for (var i = 0; i < slotName.Count; i++)
        {
            if (attachmentName[i] == "null")
            {
                _skeletonAnimation.skeleton.SetAttachment(slotName[i], null);
                continue;
            }
            _skeletonAnimation.skeleton.SetAttachment(slotName[i], attachmentName[i]);
        }
        _skeletonAnimation.Update(Time.deltaTime);
        _skeletonAnimation.state.Apply(_skeletonAnimation.skeleton);
        _skeletonAnimation.skeleton.UpdateWorldTransform();
    }
}

public override void OnReplayDeserialize(ReplayState state)
{
    
    slotName.Clear();
    attachmentName.Clear();
    sortOrder.Clear();
    boneData.Clear();
    var numSlots = state.ReadInt32();
    for (var i = 0; i < numSlots; i++)
    {
        slotName.Add(state.ReadString());
        attachmentName.Add(state.ReadString());
    }
    for (var i = 0; i < numSlots; i++)
    {
        sortOrder.Add(state.ReadInt32());
    }
    _skeletonAnimation.skeleton.Bones.ForEach(bone =>
    {
        boneData.Add(state.ReadFloat());
        boneData.Add(state.ReadFloat());
        boneData.Add(state.ReadFloat());
        boneData.Add(state.ReadFloat());
        boneData.Add(state.ReadFloat());
        boneData.Add(state.ReadFloat());
        boneData.Add(state.ReadFloat());
        boneData.Add(state.ReadFloat());
        boneData.Add(state.ReadFloat());
    });
}
}

What should I do to make it render the correct bones positions ? I tried saving differents things but always got the same result.

Hello,

I just wanted to give you a heads-up that Harald, our resident Unity expert, is on vacation. I apologize for the inconvenience.

Ok I think I figured it out, I need to save the deform by slots as well! It's starting to be a whole lot of data to save by frame hehe.

5日 後

FWIW, you might be better off saving the state of the animation. The animation already holds how the bones, mesh deforms, and everything else should look for a given time, so if you store which animation and what time, you can get your skeleton back to that state.

AnimationState stores quite a bit of things, the TrackEntry objects and all their settings. We have a task to make it easier to serialize, but haven't been able to get to it yet.

1ヶ月 後

That's what I tried first but I had a bit of difficulty serializing everything and there was always a couple of glitches so I changed to the bone method. Let me know if you ever do something about the serialization of the animation state, ill be more than interested.


Any update on the serialization task ? I'm implementing a multiplayer mode, so I tried to switch to mecanim in order to use out of the box network animator, but our setup is a bit too complex and I could not get the mixing to be as smooth than with SkeletonAnimation. So now I'm trying to send as little as possible data on the network to sync all animations, hence why the serialization of animation state would help me a bunch.

Here is the replay in action on the last goal in the video : https://twitter.com/Tape2Tape_Game/status/1486070308494876679

We haven't implemented AnimationState serialization yet, sorry.

FWIW, I would not expect AnimationState to be synchronized over the network. I wouldn't want to couple my game state with the animations, and definitely not the exact frame and mixing/etc of animations. If you are using animations for game logic, I'd suggest not doing that. Instead I would send the game state and let the animation that should be played be derived from that.

For example, if a player has a sword and isn't moving, that fact would be in the game state. A client would use the game state to determine it should draw the player equipped with a sword and to play the idle animation since it isn't moving. If the player starts moving, the game state would store that, plus maybe the speed and direction. The client would see the player is moving and know it needs to play a walk animation. Since the client was playing idle, the walk animation could mix in over time.

That mix is (likely) not important to the game state and doesn't need to be serialized and stored there. The mix from idle to walking is just cosmetic (for most games). If you were to store the game state during the mix, close the app, open the app again, and restore the game state, then the client would know from the game state that the player is walking and it would play the walking animation. This is probably fine most of the time.

Anyway, if you want to serialize AnimationState, you'll need to write the data for each of its fields, including the TrackEntrys and those fields.

Yes you are completely right. I had the replay mindset which in this case I need the anim to be pretty much identical as what happened during the play. But for online I must indeed only pass the gamestate and reproduce the animations accordingly.

Thank you!