Components/Camera/CameraBehaviour.cs

Abstract camera behavior component used by CameraManager. It defines whether the behavior wants control, priority, blend time, active state, activation hook, evaluation for each frame, and helpers for a top-down pose and getting a racing-line tangent.

Native Interop
using Machines.Race;

namespace Machines.Components;

/// <summary>
/// A single camera mode; <see cref="CameraManager"/> picks the highest-priority active one and blends.
/// </summary>
public abstract class CameraBehaviour : Component
{
	/// <summary>
	/// Whether this behaviour is applicable right now.
	/// </summary>
	public abstract bool WantsControl { get; }

	/// <summary>
	/// Tie-breaker when several behaviours want control. Higher wins.
	/// </summary>
	public virtual int Priority => 0;

	/// <summary>
	/// Seconds the director eases into this behaviour. 0 = instant.
	/// </summary>
	[Property, Group( "Blend" )]
	public float BlendInTime { get; set; } = 0.5f;

	/// <summary>
	/// True while this behaviour is the one driving the camera.
	/// </summary>
	public bool IsActive { get; private set; }

	internal void SetActive( bool active ) => IsActive = active;

	/// <summary>
	/// Called when this becomes active; seed smoothing from <paramref name="from"/> to avoid popping.
	/// </summary>
	public virtual void OnActivated( CameraPose from ) { }

	/// <summary>
	/// Compute this frame's desired pose; <paramref name="current"/> is the live applied pose.
	/// </summary>
	public abstract CameraPose Evaluate( CameraPose current );

	/// <summary>
	/// Angled top-down pose behind <paramref name="groundCenter"/> along <paramref name="heading"/>, pitched down to look at it.
	/// </summary>
	protected static CameraPose TopDown( Vector3 groundCenter, Vector3 heading, float pitch, float height, float fieldOfView )
	{
		var pitchRad = MathX.DegreeToRadian( pitch );
		var horizontalOffset = height / MathF.Tan( pitchRad );

		var position = groundCenter - heading * horizontalOffset + Vector3.Up * height;
		var rotation = Rotation.LookAt( (groundCenter - position).Normal, Vector3.Up );

		return new CameraPose( position, rotation, fieldOfView );
	}

	/// <summary>
	/// Flat tangent of the racing line nearest <paramref name="point"/>, or <paramref name="fallback"/> if none.
	/// </summary>
	protected static Vector3 RacingLineTangent( Vector3 point, Vector3 fallback )
	{
		var line = RacingPath.Current?.Optimal;
		if ( line is null || !line.IsValid )
			return fallback;

		var dist = line.GetDistanceAtPosition( point );
		var tangent = line.GetTangentAtDistance( dist ).WithZ( 0f );
		return tangent.LengthSquared > 0.001f ? tangent.Normal : fallback;
	}
}