ExampleComponents/DampenTest.cs

#nullable enable

public sealed class DampenTest : Component
{
	[Property]
	public GameObject? Target { get; set; }

	[Property]
	public TextRenderer? Text { get; set; }

	[Property]
	public float UpdateRate { get; set; } = 60f;

	[Property]
	public float Variance { get; set; }

	private Vector3.SpringDamped _spring;

	private float _time;
	private float _updateRate;

	protected override void OnStart()
	{
		_spring = new( 0f, Vector3.Up * 100f, 2f, 0.125f );
		_updateRate = UpdateRate;
	}

	public void SetPosition( Vector3 position )
	{
		_spring.Current = position;
	}

	public void SetVelocity( Vector3 velocity )
	{
		_spring.Velocity = velocity;
	}

	protected override void OnUpdate()
	{
		_time += Time.Delta;

		var dt = 1f / _updateRate;

		if ( dt <= 0f ) return;

		if ( _time >= dt )
		{
			while ( _time >= dt )
			{
				_time -= dt;
				_spring.Update( dt );
			}

			_updateRate = Math.Max( 1f, UpdateRate + Random.Shared.Float( -Variance, Variance ) );

			if ( Text is { } text )
			{
				text.Text = $"{_updateRate:F1} Hz";
			}
		}

		if ( Target is { } target )
		{
			target.LocalPosition = _spring.Current;
			target.LocalRotation = Rotation.LookAt( target.LocalPosition, Vector3.Forward );

			DebugOverlay.Sphere( new Sphere( target.WorldPosition + _spring.Velocity, 10f ), Color.Red );
			DebugOverlay.Line( target.WorldPosition, target.WorldPosition + _spring.Velocity, Color.Red );
		}
	}
}

public readonly record struct LegacySpringDamperModel( float Frequency, float Damping )
{
	public (float Position, float Velocity) Simulate( float position, float velocity, float deltaTime )
	{
		if ( deltaTime <= 0.0f ) return (position, velocity);

		// Angular frequency (how fast the spring oscillates)
		var omega = Frequency * MathF.PI * 2.0f;

		// Damping factor to control how much oscillation decays over time
		var dampingFactor = Damping * omega;

		// Compute the velocity using spring physics
		var force = omega * omega * -position - 2.0f * dampingFactor * velocity;
		velocity += force * deltaTime;

		// Update position
		return (position + velocity * deltaTime, velocity);
	}
}

public readonly record struct LegacySmoothDamperModel( float SmoothTime )
{
	public (float Position, float Velocity) Simulate( float position, float velocity, float deltaTime )
	{
		// If smoothing time is zero, directly jump to target (independent of timestep)
		if ( SmoothTime <= 0.0f )
		{
			return (0f, velocity);
		}

		// If timestep is zero, stay at current position
		if ( deltaTime <= 0.0f )
		{
			return (position, velocity);
		}

		// Implicit integration of critically damped spring
		var omega = MathF.PI * 2.0f / SmoothTime;
		velocity = (velocity - (omega * omega) * deltaTime * position) / ((1.0f + omega * deltaTime) * (1.0f + omega * deltaTime));

		return (position + velocity * deltaTime, velocity);
	}
}

public sealed class DampenTestManager : Component
{
	[Property, InputAction]
	public string? KickAction { get; set; }

	[Property, Range( -128f, 128f ), Step( 0.25f )]
	public float InitialPosition { get; set; }

	[Property, Range( -1024f, 1024f ), Step(1 )]
	public float InitialVelocity { get; set; }

	[Property, Range( 0.1f, 4f )]
	public float Frequency { get; set; } = 2f;

	[Property, Range( 0f, 1f )]
	public float Damping { get; set; } = 0.5f;

	[Property, Range( 1f, 120f )]
	public float FrameRate { get; set; } = 60f;

	protected override void OnUpdate()
	{
		if ( Input.Pressed( KickAction ) )
		{
			var position = Random.Shared.VectorInSphere( 128f );
			var velocity = Random.Shared.VectorInSphere( 1024f );

			foreach ( var test in Scene.GetAllComponents<DampenTest>() )
			{
				test.SetPosition( position );
				test.SetVelocity( velocity );
			}
		}
	}

	protected override void DrawGizmos()
	{
		var dt = 1f / FrameRate;
		var offset = new Vector3( 512f, 0f, 0f );

		Gizmo.Draw.Color = Color.White;
		Gizmo.Draw.Line( new Vector3( 0f, 0f, 0f ) + offset, new Vector3( 10f * 64f, 0f, 0f ) + offset );

		Draw( (pos, vel, deltaTime) =>
		{
			Vector3 vel3 = vel;

			pos = Vector3.SpringDamp( pos, 0f, ref vel3, deltaTime, Frequency, Damping ).x;

			return (pos, vel3.x);
		}, offset, 10f, dt, Color.Green );
		Draw( new LegacySpringDamperModel( Frequency, Damping ).Simulate, offset, 10f, dt, Color.Red );
	}

	private void Draw( Func<float, float, float, (float, float)> simulate, Vector3 offset, float duration, float dt, Color color )
	{
		float x = InitialPosition, v = InitialVelocity;

		Gizmo.Draw.Color = color;

		for ( var t = 0f; t <= duration; t += dt )
		{
			var prev = x;

			(x, v) = simulate( x, v, dt );

			if ( Math.Abs( x ) > 1024f )
			{
				x = 0f;
				v = 0f;
			}

			Gizmo.Draw.Line( new Vector3( t * 64f, prev, 0f ) + offset, new Vector3( (t + dt) * 64f, x, 0f ) + offset );
		}
	}
}