CameraManager component that selects and drives the scene camera. It collects CameraBehaviour instances from the GameObject and its children, picks the highest-priority behaviour that wants control, handles activation and cross-fading between behaviours, applies shake from CameraNoiseSystem, and writes position, rotation and FOV to Scene.Camera.
using Machines.Systems;
namespace Machines.Components;
/// <summary>
/// Sole writer of <see cref="Scene.Camera"/>; picks the active behaviour, cross-fades, and applies shake.
/// </summary>
public sealed class CameraManager : Component
{
private readonly List<CameraBehaviour> _behaviours = new();
private CameraBehaviour _active;
private CameraPose _pose;
private bool _hasPose;
// Cross-fade state
private bool _blending;
private float _blendElapsed;
private float _blendDuration;
private CameraPose _blendFrom;
/// <summary>
/// The behaviour currently driving the camera, or null before the first update.
/// </summary>
public CameraBehaviour ActiveBehaviour => _active;
protected override void OnStart()
{
// Gather behaviours from this GameObject and children once.
_behaviours.Clear();
foreach ( var behaviour in Components.GetAll<CameraBehaviour>( FindMode.EverythingInSelfAndChildren ) )
_behaviours.Add( behaviour );
}
protected override void OnUpdate()
{
var next = SelectBehaviour();
if ( next is null )
return;
// Seed from the live camera the first time so behaviours fall back here, not the origin.
if ( !_hasPose )
{
var cam = Scene.Camera;
_pose = new CameraPose( cam.WorldPosition, cam.WorldRotation, cam.FieldOfView );
_hasPose = true;
}
if ( next != _active )
{
var isFirst = _active is null;
_active?.SetActive( false );
next.SetActive( true );
_active = next;
next.OnActivated( _pose );
// Cross-fade from the held pose; first behaviour snaps (nothing to blend from).
_blendFrom = _pose;
_blendElapsed = 0f;
_blendDuration = isFirst ? 0f : next.BlendInTime;
_blending = _blendDuration > 0f;
}
var target = next.Evaluate( _pose );
CameraPose pose;
if ( _blending )
{
_blendElapsed += Time.Delta;
var progress = _blendDuration > 0f ? MathX.Clamp( _blendElapsed / _blendDuration, 0f, 1f ) : 1f;
// Ease-out.
var t = 1f - (1f - progress) * (1f - progress);
pose = CameraPose.Lerp( _blendFrom, target, t );
if ( progress >= 1f )
_blending = false;
}
else
{
pose = target;
}
// Store noise-free pose so shake doesn't feed into the next blend.
_pose = pose;
Apply( pose );
}
private void Apply( CameraPose pose )
{
var cam = Scene.Camera;
cam.WorldPosition = pose.Position;
// Shake layered on top of the blended rotation.
CameraNoiseSystem.Update();
cam.WorldRotation = CameraNoiseSystem.Apply( pose.Rotation );
cam.FieldOfView = pose.FieldOfView;
}
/// <summary>
/// Highest-priority enabled behaviour that wants control.
/// </summary>
private CameraBehaviour SelectBehaviour()
{
CameraBehaviour best = null;
foreach ( var behaviour in _behaviours )
{
if ( !behaviour.Enabled || !behaviour.WantsControl )
continue;
if ( best is null || behaviour.Priority > best.Priority )
best = behaviour;
}
return best;
}
}