This page is part of the 3D Game Kit sample.
Mecanim
Rather than having individual states for Jump and Fall animations, the character only has one Airborne Blend Tree which uses the character's AirborneVerticalSpeed
to control its blending. This is a useful setup because it means that you can tweak the physics details like gravity and jump speed and the animation will still be correct for any given point in the jump arc.
Note that it actually has two vertical speed parameters: VerticalSpeed
and AirborneVerticalSpeed
. The latter is used in the Airborne Blend Tree and various other places while the former is not set by any script and seems to be unused, however there is no way to actually make sure without manually going through every transition in the Animator Controller.
Every PlayerController.FixedUpdate
calls CalculateVerticalMovement
which is responsible for enforcing a constant downward speed while grounded to stick to the ground, checking if the PlayerInput
wants to jump, applying gravity while airborne, and applying additional downward acceleration if the player jumps and releases the button before they reach the peak of the jump:
// LocomotionState.cs:
void CalculateVerticalMovement()
{
// If jump is not currently held and Ellen is on the ground then she is ready to jump.
if (!m_Input.JumpInput && m_IsGrounded)
m_ReadyToJump = true;
if (m_IsGrounded)
{
// When grounded we apply a slight negative vertical speed to make Ellen "stick" to the ground.
m_VerticalSpeed = -gravity * k_StickingGravityProportion;
// If jump is held, Ellen is ready to jump and not currently in the middle of a melee combo...
if (m_Input.JumpInput && m_ReadyToJump && !m_InCombo)
{
// ... then override the previously set vertical speed and make sure she cannot jump again.
m_VerticalSpeed = jumpSpeed;
m_IsGrounded = false;
m_ReadyToJump = false;
}
}
else
{
// If Ellen is airborne, the jump button is not held and Ellen is currently moving upwards...
if (!m_Input.JumpInput && m_VerticalSpeed > 0.0f)
{
// ... decrease Ellen's vertical speed.
// This is what causes holding jump to jump higher that tapping jump.
m_VerticalSpeed -= k_JumpAbortSpeed * Time.deltaTime;
}
// If a jump is approximately peaking, make it absolute.
if (Mathf.Approximately(m_VerticalSpeed, 0f))
{
m_VerticalSpeed = 0f;
}
// If Ellen is airborne, apply gravity.
m_VerticalSpeed -= gravity * Time.deltaTime;
}
}
Checking when the vertical speed is approximately 0 to set it to exactly 0 seems totally pointless because the threshold is so small that it will rarely happen and even if it does it will only be for a single frame and would not look visibly different anyway.
After that method, OnAnimatorMove
adds the m_VerticalSpeed
to the movement it executes that frame and also sets it as the AirborneVerticalSpeed
parameter in the Animator Controller to control the Blend Tree.
Animancer
Animancer manages the above logic in the AirborneState
script which uses a Mixer to achieve the same thing as that Blend Tree, allowing it to be set up in the Inspector instead of in an Animator Controller:
using Animancer;
using Animancer.Units;
using UnityEngine;
public class AirborneState : CharacterState
{
[SerializeField] private LinearMixerTransition _Animations;
[SerializeField, MetersPerSecond] private float _JumpSpeed = 10;
[SerializeField, MetersPerSecond] private float _JumpAbortSpeed = 10;
[SerializeField, Multiplier] private float _TurnSpeedProportion = 5.4f;
[SerializeField] private LandingState _LandingState;
[SerializeField] private UnityEvent _PlayAudio;
private bool _IsJumping;
protected virtual void OnEnable()
{
_IsJumping = false;
Character.Animancer.Play(_Animations);
}
public override bool StickToGround => false;
public override Vector3 RootMotion
=> Character.Parameters.MovementDirection * (Character.Parameters.ForwardSpeed * Time.deltaTime);
protected virtual void FixedUpdate()
{
if (_IsJumping)
{
if (Character.Parameters.VerticalSpeed <= 0)
_IsJumping = false;
}
else
{
if (_LandingState != null)
{
if (Character.StateMachine.TrySetState(_LandingState))
return;
}
else
{
if (Character.CheckMotionState())
return;
}
if (Character.Parameters.VerticalSpeed > 0)
Character.Parameters.VerticalSpeed -= _JumpAbortSpeed * Time.deltaTime;
}
_Animations.State.Parameter = Character.Parameters.VerticalSpeed;
Character.Movement.UpdateSpeedControl();
Vector3 input = Character.Parameters.MovementDirection;
float turnSpeed = Vector3.Angle(Character.transform.forward, input) * (1f / 180) *
_TurnSpeedProportion *
Character.Movement.CurrentTurnSpeed;
Character.Movement.TurnTowards(input, turnSpeed);
}
public bool TryJump()
{
if (Character.Movement.IsGrounded &&
Character.StateMachine.TryResetState(this))
{
_IsJumping = true;
Character.Parameters.VerticalSpeed = _JumpSpeed;
_PlayAudio.Invoke();
return true;
}
return false;
}
public void CancelJump() => _IsJumping = false;
}
Updates
There are several aspects to this state every update.
Firstly, we override the StickToGround
property to always be false and the RootMotion
property to go whichever direction the character is trying to move as explained on the Movement page.
public override bool StickToGround => false;
public override Vector3 RootMotion
=> Character.Parameters.MovementDirection * (Character.Parameters.ForwardSpeed * Time.deltaTime);
Then the FixedUpdate
method executes the main logic of this state.
It starts by making sure that when you jump, it does not start checking if you have landed until you stop going up:
protected virtual void FixedUpdate()
{
if (_IsJumping)
{
if (Character.Parameters.VerticalSpeed <= 0)
_IsJumping = false;
}
Otherwise, if there is a Landing state we try to enter it, which that script will not allow until the CharacterController
is grounded. Or if we do not have a specific state for landing, just check the default transitions to Idle or Locomotion.
else
{
if (_LandingState != null)
{
if (Character.StateMachine.TrySetState(_LandingState))
return;
}
else
{
if (Character.CheckMotionState())
return;
}
Note that the LandingState
isn't referenced by any other scripts. It is not a standard state that every Character
needs and nothing else needs to directly know about it.
Here we also apply some additional downward acceleration (on top of the regular gravity applied in CharacterMovement.OnAnimatorMove
) if the player releases the jump button early (this is still inside the block where _IsJumping
is false, meaning the jump was cancelled):
if (Character.Parameters.VerticalSpeed > 0)
Character.Parameters.VerticalSpeed -= _JumpAbortSpeed * Time.deltaTime;
}
Lastly, we send the vertical speed to the mixer parameter and update the Character
's speed and turning. Since we do not have quick turn animations like the LocomotionState
, we just increase the turn speed when the direction we want to go is further away from the direction we are currently facing.
_Animations.State.Parameter = Character.Parameters.VerticalSpeed;
Character.Movement.UpdateSpeedControl();
Vector3 input = Character.Parameters.MovementDirection;
float turnSpeed = Vector3.Angle(Character.transform.forward, input) * (1f / 180) *
_TurnSpeedProportion *
Character.Movement.CurrentTurnSpeed;
Character.TurnTowards(input, turnSpeed);
}
Jumping
The Mecanim character's implementation of jumping was extremely disorganised and hard to follow:
PlayerInput.Update
checks for a button press to set abool
field with a public wrapper property.- Searching for references to that property finds one in
PlayerController.CalculateVerticalMovement
which makes sure the button had been released at some point since the last jump (PlayerInput
should have handled that). - It also checks to make sure the character is not attacking.
- Then it sets the
m_VerticalSpeed = jumpSpeed;
andm_IsGrounded = false;
. - If you search for the actual call to
CalculateVerticalMovement
you'll find it in the middle ofFixedUpdate
. Note that this means it can miss fast keypresses if they happen during oneUpdate
and another one occurs before the nextFixedUpdate
. - That method does not actually tell the Animator Controller anything, but if you search for references to
m_IsGrounded
you can find thatOnAnimatorMove
(which is called separately afterFixedUpdate
is done) passes it on to the Animator Controller. - If you look at the Animator Controller you can find that once the
IsGrounded
parameter is set, the Animator Controller will transition to the Airborne state ... except that sometimes it does not. - At the end of the
FixedUpdate
it callsPlayAudio
which checksif (!m_IsGrounded && m_PreviouslyGrounded && m_VerticalSpeed > 0f)
in order to play the jump sound.
The Animancer implementation is much easier to follow:
GameKitCharacterBrain
has anAirborneState _Jump
field which happens to reference the sameAirborneState
that the character already uses for falling.GameKitCharacterBrain.UpdateActions
checks for a button press to call_Jump.TryJump();
.- That method clearly defines everything involved in jumping:
You can only jump while grounded:
public bool TryJump()
{
if (Character.Movement.IsGrounded &&
Then we try to enter this state. We did not override CanEnterState
to check if the Character
is grounded because this state is also used if you walk off a ledge, so instead we only check that condition here when specifically attempting to jump. That means this state will always allow itself to be entered, so we are only depending on the previous state's CanExitState
to return true. Unlike the Mecanim character, this method does not specifically care if the character is attacking, it simply lets the state machine ask if the previous state will allow the change. If you add some other ability or action that does not want to be interrupted, this method will continue working properly without needing to be modified.
Character.StateMachine.TryResetState(this))
If the state is changed, it will enable this script (because it inherits from StateBehaviour
) which will call its OnEnable
method.
Now that we are in this state, flag it as a jump (so it can be cancelled to apply additional downward acceleration), apply the jump speed, and play a jump sound:
{
_IsJumping = true;
Character.Parameters.VerticalSpeed = _JumpSpeed;
_PlayAudio.Invoke();
And finally, use the return value to indicate whether the jump was successful:
return true;
}
return false;
}
Then if you want to know what happens over time during the jump, you can simply examine the rest of this class, particularly the FixedUpdate
method.