Files
Arena/Assets/_Content/Scripts/Characters/CharacterAnimator.cs
2026-11-04 15:51:04 +01:00

206 lines
6.6 KiB
C#

using UnityEngine;
namespace Arena.Characters
{
/// <summary>
/// Animates a character from the movement of its transform and from the calls of its owner (Player).
/// Placed on the character root. Only sets parameters: the states, transitions and their durations
/// are set in the Animator Controller.
/// Floats Forward and Right: speed in the character space, divided by the walk speed (locomotion blend tree).
/// Bools IsGrounded, IsRunning, IsStunned. Triggers Jump, Dash, Die, Win, Lose.
/// </summary>
public class CharacterAnimator : MonoBehaviour
{
[System.Serializable]
private class Settings
{
[Tooltip("Speed that plays the walk animations at full blend, in km/h.")]
[Min(0.1f)] public float WalkSpeed = 10f;
[Tooltip("Smoothing of the Forward and Right parameters, in seconds.")]
[Min(0f)] public float Damping = 0.1f;
[Tooltip("Speed below which a running character is not shown running, in km/h.")]
[Min(0f)] public float RunMinSpeed = 2f;
}
[System.Serializable]
private struct References
{
public Animator Animator;
}
[System.Serializable]
public class StateContainer
{
public bool IsGrounded = true;
public bool IsRunning;
public bool IsStunned;
public bool IsFinal;
[Tooltip("Speed in the character space, divided by the walk speed. x: right, y: forward.")]
public Vector2 Locomotion;
}
#region FIELDS
const float KMH_TO_MS = 1 / 3.6f;
// Parameters, named as in the Animator Controller
private static readonly int ForwardParameter = Animator.StringToHash("Forward");
private static readonly int RightParameter = Animator.StringToHash("Right");
private static readonly int IsGroundedParameter = Animator.StringToHash("IsGrounded");
private static readonly int IsRunningParameter = Animator.StringToHash("IsRunning");
private static readonly int IsStunnedParameter = Animator.StringToHash("IsStunned");
private static readonly int JumpTrigger = Animator.StringToHash("Jump");
private static readonly int DashTrigger = Animator.StringToHash("Dash");
private static readonly int DieTrigger = Animator.StringToHash("Die");
private static readonly int WinTrigger = Animator.StringToHash("Win");
private static readonly int LoseTrigger = Animator.StringToHash("Lose");
[SerializeField] private Settings _settings;
[SerializeField] private References _references;
[SerializeField] private StateContainer _state;
public StateContainer State => _state;
private Vector3 _lastPosition;
private float _speed;
private float _stunEndTime;
#endregion
#region UNITY LIFECYCLES
void Awake()
{
_lastPosition = transform.position;
_references.Animator.SetBool(IsGroundedParameter, _state.IsGrounded);
}
void Update()
{
float t = Time.deltaTime;
// A paused game (time scale at 0) gives no speed to measure
if (t > 0f)
UpdateLocomotion(t);
if (_state.IsStunned && Time.time >= _stunEndTime)
SetStunned(false);
_references.Animator.SetBool(IsRunningParameter, _state.IsRunning && _speed >= _settings.RunMinSpeed * KMH_TO_MS);
}
#endregion
#region PUBLIC METHODS
/// <summary>
/// Tells whether the character stands on the ground. Called by the owner when it changes.
/// </summary>
public void SetGrounded(bool isGrounded)
{
_state.IsGrounded = isGrounded;
_references.Animator.SetBool(IsGroundedParameter, isGrounded);
}
/// <summary>
/// Tells whether the character is running. Called by the owner when it changes.
/// IsRunning is true in the Animator only while the character also moves.
/// </summary>
public void SetRunning(bool isRunning)
{
_state.IsRunning = isRunning;
}
public void Jump()
{
SetTrigger(JumpTrigger);
}
public void Dash()
{
SetTrigger(DashTrigger);
}
/// <summary>
/// Sets IsStunned for a duration, in seconds.
/// </summary>
public void Stun(float duration)
{
if (_state.IsFinal)
return;
_stunEndTime = Time.time + duration;
SetStunned(true);
}
public void Die()
{
SetFinalTrigger(DieTrigger);
}
public void Win()
{
SetFinalTrigger(WinTrigger);
}
public void Lose()
{
SetFinalTrigger(LoseTrigger);
}
#endregion
#region PRIVATE METHODS
private void UpdateLocomotion(float t)
{
Vector3 position = transform.position;
Vector3 velocity = (position - _lastPosition) / t;
_lastPosition = position;
velocity.y = 0f;
_speed = velocity.magnitude;
// Speed in the character space, so that a sideways move plays walkRight
Vector3 localVelocity = transform.InverseTransformDirection(velocity);
Vector2 locomotion = new Vector2(localVelocity.x, localVelocity.z) / (_settings.WalkSpeed * KMH_TO_MS);
_state.Locomotion = Vector2.ClampMagnitude(locomotion, 1f);
_references.Animator.SetFloat(ForwardParameter, _state.Locomotion.y, _settings.Damping, t);
_references.Animator.SetFloat(RightParameter, _state.Locomotion.x, _settings.Damping, t);
}
private void SetStunned(bool isStunned)
{
_state.IsStunned = isStunned;
_references.Animator.SetBool(IsStunnedParameter, isStunned);
}
private void SetTrigger(int trigger)
{
if (_state.IsFinal || _state.IsStunned)
return;
_references.Animator.SetTrigger(trigger);
}
private void SetFinalTrigger(int trigger)
{
if (_state.IsFinal)
return;
_state.IsFinal = true;
SetStunned(false);
// Win and lose are played while the game is paused (time scale at 0)
_references.Animator.updateMode = AnimatorUpdateMode.UnscaledTime;
_references.Animator.SetTrigger(trigger);
}
#endregion
}
}