Component that listens for car impacts and applies camera shake and controller haptics. It also produces continuous shake and haptic pulses while the local player's car is drifting or boosting, with configurable per-kind amounts, durations and haptic intensities.
using Machines.Components;
using Machines.Events;
using Machines.Player;
namespace Machines.Systems;
/// <summary>
/// Screen shake and haptics on impact, drift, or boost. Intensity scales with impact speed.
/// </summary>
public sealed class CameraShakeOnImpact : Component, ICarImpactListener
{
/// <summary>
/// Shake amount (degrees) per unit of impact speed for wall hits.
/// </summary>
[Property, Group( "Wall" )]
public float WallShakePerSpeed { get; set; } = 0.015f;
/// <summary>
/// Maximum shake amount (degrees) for wall hits.
/// </summary>
[Property, Group( "Wall" )]
public float WallShakeMax { get; set; } = 3f;
/// <summary>
/// Duration (seconds) of wall hit shake.
/// </summary>
[Property, Group( "Wall" )]
public float WallShakeDuration { get; set; } = 0.3f;
/// <summary>
/// Shake amount (degrees) per unit of impact speed for car-to-car hits.
/// </summary>
[Property, Group( "Car" )]
public float CarShakePerSpeed { get; set; } = 0.02f;
/// <summary>
/// Maximum shake amount (degrees) for car-to-car hits.
/// </summary>
[Property, Group( "Car" )]
public float CarShakeMax { get; set; } = 4f;
/// <summary>
/// Duration (seconds) of car-to-car hit shake.
/// </summary>
[Property, Group( "Car" )]
public float CarShakeDuration { get; set; } = 0.4f;
/// <summary>
/// Shake amount (degrees) per unit of impact speed for prop hits.
/// </summary>
[Property, Group( "Prop" )]
public float PropShakePerSpeed { get; set; } = 0.008f;
/// <summary>
/// Maximum shake amount (degrees) for prop hits.
/// </summary>
[Property, Group( "Prop" )]
public float PropShakeMax { get; set; } = 1.2f;
/// <summary>
/// Duration (seconds) of prop hit shake.
/// </summary>
[Property, Group( "Prop" )]
public float PropShakeDuration { get; set; } = 0.2f;
/// <summary>
/// Shake intensity (degrees) while drifting.
/// </summary>
[Property, Group( "Drift" )]
public float DriftShakeAmount { get; set; } = 0.1f;
/// <summary>
/// Controller rumble intensity (0–1) while drifting.
/// </summary>
[Property, Group( "Drift" )]
public float DriftHapticIntensity { get; set; } = 0.15f;
/// <summary>
/// Shake intensity (degrees) while boosting.
/// </summary>
[Property, Group( "Boost" )]
public float BoostShakeAmount { get; set; } = 0.4f;
/// <summary>
/// Controller rumble intensity (0–1) while boosting.
/// </summary>
[Property, Group( "Boost" )]
public float BoostHapticIntensity { get; set; } = 0.4f;
// Timers for continuous drift/boost shake and haptic pulses.
private float _continuousShakeTimer;
private const float ContinuousShakeInterval = 0.05f;
private float _hapticTimer;
private const float HapticInterval = 0.1f;
protected override void OnUpdate()
{
bool anyDrifting = false;
bool anyBoosting = false;
foreach ( var car in Scene.GetAllComponents<Car>() )
{
if ( !car.IsValid() || !car.IsLocalPlayer )
continue;
var drifting = car.Drift.IsValid() && car.Drift.IsDrifting;
var boosting = car.Boost.IsValid() && car.Boost.IsDashing;
if ( drifting ) anyDrifting = true;
if ( boosting ) anyBoosting = true;
if ( (drifting || boosting) && Input.UsingController )
{
_hapticTimer -= Time.Delta;
if ( _hapticTimer <= 0f )
{
var intensity = boosting ? BoostHapticIntensity : DriftHapticIntensity;
Input.TriggerHaptics( intensity, intensity, 0f, 0f, (int)(HapticInterval * 1000f + 50f) );
_hapticTimer = HapticInterval;
}
}
}
if ( !anyDrifting && !anyBoosting )
return;
_continuousShakeTimer -= Time.Delta;
if ( _continuousShakeTimer <= 0f )
{
var amount = anyBoosting ? BoostShakeAmount : DriftShakeAmount;
_ = new Shake( amount, 0.08f );
_continuousShakeTimer = ContinuousShakeInterval;
}
}
public void OnCarImpact( CarImpact impact )
{
if ( !impact.Car.IsValid() || !impact.Car.IsLocalPlayer ) // local only
return;
float amount;
float duration;
float hapticLeft;
float hapticRight;
if ( impact.Kind == CarImpactKind.Wall )
{
amount = MathF.Min( impact.Speed * WallShakePerSpeed, WallShakeMax );
duration = WallShakeDuration;
var hapticScale = (amount / WallShakeMax).Clamp( 0f, 1f );
hapticLeft = hapticScale * 0.6f;
hapticRight = hapticScale * 0.8f;
}
else if ( impact.Kind == CarImpactKind.Prop )
{
amount = MathF.Min( impact.Speed * PropShakePerSpeed, PropShakeMax );
duration = PropShakeDuration;
var hapticScale = (amount / PropShakeMax).Clamp( 0f, 1f );
hapticLeft = hapticScale * 0.4f;
hapticRight = hapticScale * 0.5f;
}
else
{
amount = MathF.Min( impact.Speed * CarShakePerSpeed, CarShakeMax );
duration = CarShakeDuration;
var hapticScale = (amount / CarShakeMax).Clamp( 0f, 1f );
hapticLeft = hapticScale * 0.8f;
hapticRight = hapticScale;
}
if ( amount > 0.01f )
{
_ = new Shake( amount, duration );
if ( Input.UsingController )
{
Input.TriggerHaptics( hapticLeft, hapticRight, 0f, 0f, (int)(duration * 1000f) );
}
}
}
}