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.
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;
}
}
}