KybernetikGames / animancer

Documentation for the Animancer Unity Plugin.
63 stars 8 forks source link

FSM Play Animation on Exit #336

Closed Ghostdreamer11 closed 4 months ago

Ghostdreamer11 commented 4 months ago

Hello! My team has been playing around with Animancer for a bit, and the time has come that we are moving our entire animation system over to Animancer. There have been some growing pains, but we are getting there! We are currently stuck on sequenced animations - Something with a Start, Loop, End. We are layering other animations over the Loop and it is going well so far! We are stuck on the "End" part.

We have a (currently) very basic FSM, and a state that represents the Sequenced Animation. We enter and transition into loop just fine. The trouble comes with exiting....

Our state machine is still very primitive:

public class HumanoidAnimationController : SerializedMonoBehaviour, IAnimationController
{

    [OdinSerialize, ReadOnly]
    public StateMachine<AnimationStates, GameAnimationState>.WithDefault StateMachine { get; private set; } = new();

    [field: SerializeField]
    public AnimancerComponent Animancer { get; private set; }

    [SerializeField]
    private AnimationStates defaultState;

    [SerializeField]
    private List<GameAnimationState> states;

    void Awake()
    {
        MovementController = movementController.Value;

        // Loop through the list of states and create instances
        foreach (var state in states)
        {
            GameAnimationState newState = Instantiate(state);
            newState.Initialize(this);
            StateMachine.Add(newState.StateType, newState);
        }

        StateMachine.DefaultKey = defaultState;

    }

    void Update()
    {
        StateMachine.CurrentState?.Update();
    }

    [Button("Change State")]
    void ChangeState(AnimationStates newState)
    {
        StateMachine.ForceSetState(newState);
    }
}

And the state, which is also still really primitive:

    [CreateAssetMenu(fileName = "_New Pose State", menuName = "Animations/Pose")]
    public class PoseAnimationState : GameAnimationState
    {
        [field: SerializeField, Space(10), Header("Setup")]
        public override AnimationStates StateType { get; protected set; }

        public ClipTransition StartPose;
        public ClipTransition LoopPose;
        public ClipTransition EndPose;

        public override void OnEnterState()
        {
            var state = AnimationController.Animancer.Play(StartPose);
            state.Events.OnEnd = MoveToLoop;
        }

        private void MoveToLoop()
        {
            AnimationController.Animancer.Play(LoopPose);
        }

        public override void OnExitState()
        {
            AnimationController.Animancer.Play(EndPose);
        }
    }

And it "works" , in the sense of the entry loop. However when we exit it just transitions to whatever clip is played OnEnterState of the new state instead of playing the EndPost, then transitioning. We know, logically, this is because we are telling it to (.Play into a .Play). Realistically, we aren't sure what to do with it. We don't really want to do it a "wait until current clip is playing and then play the next one" in the OnEnterState for every state (this will cause issues with more complex states).

Any help/guidance would be apprecated.

KybernetikGames commented 4 months ago

Start and Loop could be combined into a ClipTransitionSequence so you don't need to hook up the End Event yourself.

For End, maybe try something like this:

private AnimationStates _NextState;

private void Awake()
{
    // Initialize transition events once on startup instead of every time you play them.
    EndPose.Events.OnEnd = ForceExitState;
}

public override Bool CanExitState
{
    get
    {
        // Store the target state, play the animation, and then block the state change.
        _NextState = KeyChange<AnimationStates>.NextKey;
        AnimationController.Animancer.Play(EndPose);
        return false;
    }
}

private void ForceExitState()
{
    // You need to get the StateMachine here somehow. You could make all of your states Owned States:
    // https://kybernetik.com.au/animancer/docs/manual/fsm/utilities/owned-states
    // Or you could grab it from StateChange<GameAnimationState>.StateMachine during CanExitState.

    // ForceSetState doesn't check this.CanExitState or _NextState.CanEnterState so we won't block the change again.
    StateMachine.ForceSetState(_NextState);
}
Ghostdreamer11 commented 4 months ago

Awesome, that was a great idea :) We will probably have to tweak it a bit as we go but it's working beautifully (we don't have to talk about the foot slide... really!) Luckily, we already have a ref to the state machine .... we did originally have all of our states owned states, but... we um... we took it back for... technical reasons :p But when we go back to it it'll be much nicer. Also want to try the sharing, so we don't have to hold all the animations in mem... but baby steps!!

Final result:

[CreateAssetMenu(fileName = "_New Pose State", menuName = "Animations/Pose")]
    public class PoseAnimationState : GameAnimationState
    {
        [field: SerializeField, Space(10), Header("Setup")]
        public override CharacterAnimationStates StateType { get; protected set; }

        [field: SerializeField]
        public override AnimationStateInterruptionOrder InterruptionOrder { get; protected set; } =
            AnimationStateInterruptionOrder.AlwaysInterruptible;

        private CharacterAnimationStates _nextState;

        [Header("Animations")]
        public ClipTransitionSequence PoseSequence;
        public ClipTransition ExitPose;

        private void Awake()
        {
            ExitPose.Events.OnEnd = ForceExitState;
        }

        public override void OnEnterState()
        {
            AnimationController.Animancer.Play(PoseSequence);
        }

        public override bool CanExitState
        {
            get
            {
                // Store the target state, play the animation, and then block the state change.
                _nextState = KeyChange<CharacterAnimationStates>.NextKey;
                AnimationController.Animancer.Play(ExitPose);
                return false;
            }
        }

        private void ForceExitState()
        {
            AnimationController.StateMachine.ForceSetState(_nextState);
        }
    }

Truly appreciate your time and effort put into this. Even with our primitive setup we have something thats 1000x easier for our designers to go in a fuss with while keeping our data-oriented approach. It took them less time to figure it out than it took us!!