Respawn

This page is part of the 3D Game Kit example.

When the game starts or after the character dies and is moved back to their last checkpoint, they play a Respawn animation where they stand up from a crouch along with some glowing particle effects:

Mecanim

The Mecanim character has a Respawn state which plays an animation then transitions to Idle when it's done. Seems simple right?

Unfortunately it is not quite that easy. Several other scripts need to know when the player is respawning (for AI behaviour) and invulnerable (you cannot take damage while respawning). So that state actually has a StateMachineBehaviour attached to notify the PlayerController when it is done:

public class EllenSetTargetableSMB : StateMachineBehaviour
{
    // OnStateExit is called when a transition ends and the state machine finishes evaluating this state
    override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        PlayerController controller = animator.GetComponent<PlayerController>();

        if (controller != null)
        {
            controller.RespawnFinished();
        }
    }
}

// And in PlayerController.cs:
public void RespawnFinished()
{
    m_Respawning = false;
    m_Damageable.isInvulnerable = false;
}

This means that the logic for respawning is scattered all over the place:

  • You need to check the transition to determine what causes it.
  • You need to check if the state has any StateMachineBehaviours attached (most other states do not) and read the EllenSetTargetableSMB script to find out what it actually does. Even more so since it has a vaguely misleading name. Something like OnRespawnFinished would have been a much better name.
  • Then you need to check the PlayerController script to find out what the RespawnFinished method actually does.

So in the end this approach uses a new script for the Respawn state, but all it does is call a function somewhere else. It tries to use the Animator Controller as a state machine, but the limitations of that system prevent it from doing everything that is needed so the remaining responsibilities fall back onto scripts anyway, resulting in far more overall complexity than if it was all handled by scripts.

Animancer

The Animancer implementation is much simpler. The Creature has a reference to the Respawn state which it uses as the default when creating the state machine:

[SerializeField] private CreatureState _Respawn;

private void Awake()
{
    StateMachine = new StateMachine<CreatureState>(_Respawn);
}

The actual state implementation is also very straightforward:

In Awake (called once on startup) it sets the animation's End Event to return to the Idle state and stores the starting position:

using System;
using UnityEngine;

public sealed class RespawnState : CreatureState
{
    [SerializeField] private ClipState.Transition _Animation;
    [SerializeField] private UnityEvent _OnEnterState;
    [SerializeField] private UnityEvent _OnExitState;

    private Vector3 _StartingPosition;

    private void Awake()
    {
        _Animation.Events.OnEnd = Creature.ForceEnterIdleState;
        _StartingPosition = transform.position;
    }

Then in OnEnable (called every time this state is entered) it teleports the creature back to the starting position and plays the animation:

    private void OnEnable()
    {
        Creature.Animancer.Play(_Animation);
        Creature.transform.position = _StartingPosition;
Code Inspector

It needs to make the Damageable component recover to full health and activate invulnerability during this state, but due to the Script Referencing issue it needs to use UnityEvents rather than controlling that component directly:

        // In OnEnable.
        _OnEnterState.Invoke();
    }

    private void OnDisable()
    {
        _OnExitState.Invoke();
    }

And it also prevents any other state from interrupting it (the ForceEnterIdleState above will skip this check):

    public override bool CanExitState => false;
}

This gives us a simple component that we can set up in the Inspector:

Particle Effects

We also need to activate the blue particle effect when the animation starts. As with the Damageable component, the Script Referencing issue prevents us from calling the EllenSpawn.StartEffect method directly. But since the Creature enters this state before any other scripts get a chance to initialise (thanks to its [DefaultExecutionOrder] attribute), the EllenSpawn script will not be initialised yet so instead of adding more methods to _OnEnterState we can create an Animancer Event at time 0 which will get called the first time the animation updates:

AI Chasing

Unfortunately, the enemy AI in the 3D Game Kit Lite is too tightly coupled to the PlayerController script we are replacing for us to get it to work with the Animancer character.

If an AI script needs to check whether a creature is currently respawning specifically, it could easily do so with either of the following:

if (creature.StateMachine.CurrentState is RespawnState)
if (creature.StateMachine.CurrentState == creature.Respawn)

That is a bit too specific though. In this case, the enemies were using it to stop chasing the player while they are respawning but it would be better if the AI did not specifically depend on the RespawnState class or even particularly care why they are not allowed to chase the player at that time so it might be better to use Inheritance:

// Set the default in the base CreatureState:
public virtual bool CanBeChased => true;

// Override it in RespawnState:
public override bool CanBeChased => false;

// Now the AI can check that property:
if (creature.StateMachine.CurrentState.CanBeChased)

This would allow the DeathState to also stop enemies from chasing it without any modification to the AI code.

Modularity

Another thing to consider is the effort that would be involved if you wanted to add or remove a particular state when modifying the player or creating a new creature. To make the player just appear immediately without a respawning animation in the Mecanim setup you would need to do the following:

  1. Remove the Respawning state from the Animator Controller.
  2. Set IdleSM as the default state.
  3. Edit the PlayerController script so it does not rely on something else calling RespawnFinished.

But to do the same with Animancer you could basically do the same thing without the last step:

  1. Remove the RespawnState component.
  2. Assign the IdleState component to the Creature._Respawn field in the Inspector (so the same state is assigned to both Respawn and Idle).

We did not intentionally plan the Animancer setup to support creatures without a respawn animation, but we have the ability to do so anyway since we structured the code properly. Being able to try out ideas and adapt to changing requirements like this is extremely valuable in software development.