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
StateMachineBehaviour
s attached (most other states do not) and read theEllenSetTargetableSMB
script to find out what it actually does. Even more so since it has a vaguely misleading name. Something likeOnRespawnFinished
would have been a much better name. - Then you need to check the
PlayerController
script to find out what theRespawnFinished
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
|
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:
- Remove the
Respawning
state from the Animator Controller. - Set
IdleSM
as the default state. - Edit the
PlayerController
script so it doesn't rely on something else callingRespawnFinished
.
But to do the same with Animancer you could basically do the same thing without the last step:
- Remove the
RespawnState
component. - Assign the
IdleState
component to theStateMachine
'sCurrentState
field in the Inspector (so the same state is assigned to bothCurrentState
andDefaultState
).
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.