Component that animates car wheel visuals. It finds wheel bone objects from the Car resource, marks them procedural, and updates steering and wheel spin each frame based on CarMovement inputs and speed.
using Machines.Events;
namespace Machines.Player;
/// <summary>
/// Animates wheel steering and spin; finds bones by name from CarResource
/// </summary>
public sealed class CarWheelVisuals : Component, ICarChangeListener
{
[RequireComponent]
public Car Car { get; private set; }
/// <summary>
/// Maximum visual steer angle for the front wheels (degrees).
/// </summary>
[Property, Group( "Steering" )]
public float MaxSteerAngle { get; set; } = 35f;
/// <summary>
/// How quickly the front wheels turn to the target steer angle.
/// </summary>
[Property, Group( "Steering" )]
public float SteerSpeed { get; set; } = 15f;
/// <summary>
/// Wheel radius in units, used to compute spin rate from speed
/// </summary>
[Property, Group( "Spin" )]
public float WheelRadius { get; set; } = 10f;
private CarMovement _movement;
private GameObject _wheelFL;
private GameObject _wheelFR;
private GameObject _wheelBL;
private GameObject _wheelBR;
private float _currentSteerAngle;
private float _spinAngle;
protected override void OnStart()
{
_movement = GetComponent<CarMovement>();
ResolveWheels();
}
void ICarChangeListener.OnCarChanged( Car car )
{
ResolveWheels();
}
private void ResolveWheels()
{
if ( !Car.IsValid() )
return;
var resource = Car.Resource;
if ( !resource.IsValid() || !Car.Renderer.IsValid() )
{
return;
}
_wheelFL = ResolveBone( resource.WheelFLBone );
_wheelFR = ResolveBone( resource.WheelFRBone );
_wheelBL = ResolveBone( resource.WheelBLBone );
_wheelBR = ResolveBone( resource.WheelBRBone );
}
private GameObject ResolveBone( string name )
{
var boneObject = Car.Renderer.GetBoneObject( name );
boneObject?.Flags |= GameObjectFlags.ProceduralBone;
return boneObject;
}
protected override void OnUpdate()
{
if ( !_movement.IsValid() )
return;
var dt = Time.Delta;
if ( dt <= 0f )
return;
UpdateSteering( dt );
UpdateSpin( dt );
}
private void UpdateSteering( float dt )
{
var targetAngle = _movement.TurnInput * MaxSteerAngle;
_currentSteerAngle = MoveToward( _currentSteerAngle, targetAngle, SteerSpeed * MaxSteerAngle * dt );
}
private void UpdateSpin( float dt )
{
if ( WheelRadius <= 0f )
return;
var angularSpeed = _movement.CurrentSpeed / WheelRadius * (180f / MathF.PI);
_spinAngle += angularSpeed * dt;
_spinAngle %= 360f;
// Apply combined rotation to all wheels
ApplyWheelRotation( _wheelFL, _currentSteerAngle );
ApplyWheelRotation( _wheelFR, _currentSteerAngle );
ApplyWheelRotation( _wheelBL, 0f );
ApplyWheelRotation( _wheelBR, 0f );
}
private void ApplyWheelRotation( GameObject wheel, float steerYaw )
{
if ( !wheel.IsValid() )
return;
wheel.LocalRotation = Rotation.From( _spinAngle, steerYaw, 0f );
}
private static float MoveToward( float current, float target, float maxDelta )
{
if ( MathF.Abs( target - current ) <= maxDelta )
return target;
return current + MathF.Sign( target - current ) * maxDelta;
}
}