A visual-only component for a ball model. It orients and spins the mesh to match movement direction, applies squash-and-stretch on sudden direction changes, and does not affect physics or gameplay.
using Sandbox;
using System;
namespace Breakout;
/// <summary>
/// Purely visual. Spins the ball model so it looks like it's rolling in its travel direction, and
/// adds a quick squash-and-stretch when the ball suddenly changes direction (a bounce). None of
/// this touches gameplay; the Ball component does the actual movement and physics.
/// </summary>
public sealed class BallAnimator : Component
{
[Property, RequireComponent] Ball Ball { get; set; }
[Property, Range( 0f, 3f )] public float RollSpeedScale { get; set; } = 1f;
[Property, Range( 1f, 30f )] public float Smoothing { get; set; } = 12f;
[Property, Range( 0f, 0.6f )] public float SquashAmount { get; set; } = 0.28f;
[Property, Range( 0.05f, 0.6f )] public float SquashRecover { get; set; } = 0.16f;
const float HitThreshold = 0.12f;
Vector3 moveDir = Vector3.Up;
float moveSpeed = 0f;
float rollAngle = 0f;
Vector3 prevRaw = Vector3.Zero;
Vector3 squashDir = Vector3.Up;
float squashT = 0f;
float squashStrength = 0f;
protected override void OnEnabled()
{
rollAngle = 0f;
}
protected override void OnUpdate()
{
if ( !Ball.IsValid() )
return;
var targetDir = Ball.Direction.WithY( 0f );
if ( targetDir.Length > 0.001f )
moveDir = Vector3.Lerp( moveDir, targetDir.Normal, Time.Delta * Smoothing ).Normal;
float targetSpeed = Ball.Stuck ? 0f : Ball.Speed;
moveSpeed += (targetSpeed - moveSpeed) * MathF.Min( Time.Delta * Smoothing, 1f );
DetectHit();
squashT = MathF.Max( 0f, squashT - Time.Delta / MathF.Max( SquashRecover, 0.01f ) );
ApplyTransform();
}
// Spot a sudden change in travel direction (a bounce) and kick off a squash toward it.
void DetectHit()
{
var raw = Ball.Direction.WithY( 0f );
if ( raw.Length < 0.1f || Ball.Stuck )
return;
raw = raw.Normal;
if ( prevRaw.Length > 0.1f )
{
float change = (raw - prevRaw).Length;
if ( change > HitThreshold )
{
squashStrength = MathF.Min( 1f, change / 1.4f );
squashT = 1f;
squashDir = raw;
}
}
prevRaw = raw;
}
// Spin the model so it looks like it's rolling the way it moves, and squash it: stretch it
// along the direction it's travelling while squeezing it in the other direction.
void ApplyTransform()
{
var frameDir = squashT > 0.001f ? squashDir : moveDir;
var axis = new Vector3( -frameDir.z, 0f, frameDir.x );
axis = axis.Length < 0.001f ? Vector3.Forward : axis.Normal;
if ( moveSpeed > 1f )
{
float radius = MathF.Max( Ball.Radius, 1f );
rollAngle += (moveSpeed / radius) * RollSpeedScale * (180f / MathF.PI) * Time.Delta;
}
var up = MathF.Abs( axis.z ) > 0.99f ? Vector3.Forward : Vector3.Up;
LocalRotation = Rotation.LookAt( axis, up ) * Rotation.FromAxis( Vector3.Forward, rollAngle );
float s = SquashAmount * squashStrength * squashT;
LocalScale = new Vector3( 1f + s * 0.5f, 1f - s, 1f - s );
}
}