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 Character's StateMachine has Respawn assigned as its Default State:

And the Die State also has it assigned as the Respawn State:

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 : CharacterState
{
    [SerializeField] private ClipTransition _Animation;
    [SerializeField] private UnityEvent _OnEnterState;
    [SerializeField] private UnityEvent _OnExitState;

    private Vector3 _StartingPosition;

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

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

    private void OnEnable()
    {
        Character.Animancer.Play(_Animation);
        Character.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:

    private void 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 Character enters this state before any other scripts get a chance to initialize (thanks to its [DefaultExecutionOrder] attribute), the EllenSpawn script will not be initialized 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're replacing for us to get it to work with the Animancer character. So enemies aren't able to follow the Animancer character in the example scene, but this section goes over how it could work anyway.

If an AI script needs to check whether a character is currently respawning specifically, it could easily do so withg:

if (character.StateMachine.CurrentState is RespawnState)

That's 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 didn't specifically depend on the RespawnState class or even particularly care why they aren't allowed to chase the player at that time so it might be better to use Inheritance:

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

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

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

This would allow the DieState 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 character. 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 doesn't 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 StateMachine's CurrentState field in the Inspector (so the same state is assigned to both CurrentState and DefaultState).

We didn't intentionally plan the Animancer setup to support characters without a respawn animation, but well structured code makes it easy to implement unforseen changes like that. Being able to try out ideas and adapt to changing requirements like this is extremely valuable in software development.