Pawn/SkateAnimator.cs

Pawn animator for the skateboard player. Smooths turn input, sets animation booleans from the controller and pawn state, and drives a procedural board trick system that spins/raises the board GameObject for flips and grabs.

Native Interop
namespace Skateboard;

public sealed class SkateAnimator : PawnAnimator
{
	[Property] public Skateboard.Player.SkatePawn Pawn { get; set; }
	[Property] public SkinnedModelRenderer BoardRenderer { get; set; }

	[Property] public float LeanSmoothing { get; set; } = 9f;

	[Property] public float FlipTrickDuration { get; set; } = 0.36f;
	[Property] public float GrabTrickDuration { get; set; } = 0.5f;

	private float _smoothLeft;

	// Procedural trick state - fakes flips/grabs by spinning the board GameObject,
	// since the anim graph has no authored flip/grab clips.
	private Rotation _boardRestRotation;
	private Vector3 _boardRestPosition;
	private bool _boardRestCaptured;

	private bool _trickActive;
	private bool _trickIsGrab;
	private float _trickTime;
	private float _trickDuration;
	private float _trickPitch, _trickYaw, _trickRoll;
	private float _grabRaise;

	protected override void OnUpdate()
	{
		var pawn = Pawn ?? Components.Get<Skateboard.Player.SkatePawn>();
		if ( pawn is null )
			return;

		var controller = pawn.Controller;
		SetAnimParameter( "b_grind", controller?.OnGrind ?? false );
		SetAnimParameter( "b_onground", controller?.IsGrounded ?? false );
		SetAnimParameter( "b_crouch", pawn.Crouch );

		// TurnRight is a digital -1/0/1 value, so feeding it straight into the
		// anim graph makes the lean snap. Ease it instead for a smoother turn.
		_smoothLeft = _smoothLeft.LerpTo( -pawn.TurnRight, Time.Delta * LeanSmoothing );
		SetAnimParameter( "f_left", _smoothLeft );

		if ( BoardRenderer.IsValid() )
			BoardRenderer.Set( "f_left", _smoothLeft );

		UpdateProceduralTrick();
	}

	/// <summary>
	/// Kick off a fake, code-driven trick animation on the board. Flips spin the board
	/// through a full rotation; grabs tuck it up against the feet and back.
	/// </summary>
	public void PlayTrick( string trickName, bool isGrab )
	{
		CaptureBoardRest();

		_trickActive = true;
		_trickIsGrab = isGrab;
		_trickTime = 0f;
		_trickPitch = _trickYaw = _trickRoll = 0f;
		_grabRaise = 0f;

		if ( isGrab )
		{
			_trickDuration = GrabTrickDuration;
			_grabRaise = 12f;

			switch ( trickName )
			{
				case "Nosegrab": _trickPitch = -18f; break;
				case "Tailgrab": _trickPitch = 18f; break;
				case "Melon": _trickRoll = 22f; break;
				default: _trickRoll = -22f; break; // Indy
			}
		}
		else
		{
			_trickDuration = FlipTrickDuration;

			switch ( trickName )
			{
				case "Heelflip": _trickRoll = -360f; break;
				case "Pop Shove-it": _trickYaw = 360f; break;
				case "360 Flip": _trickYaw = 360f; _trickRoll = 360f; break;
				default: _trickRoll = 360f; break; // Kickflip
			}
		}
	}

	private void CaptureBoardRest()
	{
		if ( _boardRestCaptured || !BoardRenderer.IsValid() )
			return;

		_boardRestRotation = BoardRenderer.GameObject.LocalRotation;
		_boardRestPosition = BoardRenderer.GameObject.LocalPosition;
		_boardRestCaptured = true;
	}

	private void UpdateProceduralTrick()
	{
		if ( !BoardRenderer.IsValid() )
			return;

		CaptureBoardRest();
		var board = BoardRenderer.GameObject;

		if ( _trickActive )
		{
			_trickTime += Time.Delta;
			float t = _trickDuration > 0f ? (_trickTime / _trickDuration) : 1f;
			if ( t >= 1f )
			{
				t = 1f;
				_trickActive = false;
			}

			// Flips run a full spin (ends back at rest); grabs bump up and back down.
			float amount = _trickIsGrab ? (4f * t * (1f - t)) : t;

			board.LocalRotation = _boardRestRotation * Rotation.From( _trickPitch * amount, _trickYaw * amount, _trickRoll * amount );
			board.LocalPosition = _boardRestPosition + Vector3.Up * (_grabRaise * (_trickIsGrab ? amount : 0f));
		}
		else
		{
			// Settle back to rest in case anything is left over.
			board.LocalRotation = Rotation.Lerp( board.LocalRotation, _boardRestRotation, Time.Delta * 12f );
			board.LocalPosition = Vector3.Lerp( board.LocalPosition, _boardRestPosition, Time.Delta * 12f );
		}
	}

	public void HandleEvent( string name )
	{
		switch ( name )
		{
			case "jump":
				Trigger( "b_ollie" );
				BoardRenderer?.Set( "b_ollie", true );
				break;
			case "land":
				Trigger( "b_land" );
				break;
			case "front":
				Trigger( "b_hit_front" );
				break;
			case "back":
				Trigger( "b_hit_back" );
				break;
			case "angry":
				Trigger( "b_angry" );
				break;
		}
	}
}