Files
AppleHillsProduction/Assets/Scripts/Common/Camera/CameraStateManager.cs
tschesky c27f22ef0a MVP of the plane throwing game (#77)
Co-authored-by: Michal Pikulski <michal@foolhardyhorizons.com>
Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com>
Reviewed-on: #77
2025-12-07 19:36:57 +00:00

300 lines
10 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using Core;
using Core.Lifecycle;
using Unity.Cinemachine;
using UnityEngine;
namespace Common.Camera
{
/// <summary>
/// Serializable mapping between a camera state and its Cinemachine camera.
/// Used to assign cameras in the Inspector for each enum state.
/// </summary>
[Serializable]
public class CameraStateMapping<TState> where TState : Enum
{
[Tooltip("The state this camera represents")]
public TState state;
[Tooltip("The Cinemachine camera for this state")]
public CinemachineCamera camera;
public CameraStateMapping(TState state)
{
this.state = state;
this.camera = null;
}
}
/// <summary>
/// Generic state-based camera controller using Cinemachine.
/// Manages camera transitions by setting priorities on virtual cameras.
/// Type parameter TState must be an enum representing camera states.
/// </summary>
///
public abstract class CameraStateManager<TState> : ManagedBehaviour where TState : Enum
{
#region Configuration
[Header("Camera Mappings")]
[Tooltip("Assign cameras for each state - list auto-populates from enum")]
[SerializeField] protected List<CameraStateMapping<TState>> cameraMappings = new List<CameraStateMapping<TState>>();
[Header("Camera Priority Settings")]
[Tooltip("Priority for inactive cameras")]
[SerializeField] protected int inactivePriority = 10;
[Tooltip("Priority for the active camera")]
[SerializeField] protected int activePriority = 20;
[Header("Debug")]
[SerializeField] protected bool showDebugLogs = false;
#endregion
#region State
private Dictionary<TState, CinemachineCamera> _cameraMap = new Dictionary<TState, CinemachineCamera>();
private TState _currentState;
private bool _isInitialized = false;
public TState CurrentState => _currentState;
#endregion
#region Events
/// <summary>
/// Fired when camera state changes. Parameters: (TState oldState, TState newState)
/// </summary>
public event Action<TState, TState> OnStateChanged;
#endregion
#region Lifecycle
/// <summary>
/// Initialize camera mappings and validate them.
/// Subclasses should call base.OnManagedAwake() to get automatic initialization.
/// If custom initialization is needed, override without calling base.
/// </summary>
internal override void OnManagedAwake()
{
base.OnManagedAwake();
// Initialize cameras from Inspector mappings
InitializeCameraMap();
// Validate all cameras are assigned
ValidateCameras();
}
#endregion
#region Initialization
/// <summary>
/// Initialize camera mappings from Inspector-assigned list.
/// Call this in OnManagedAwake - no need to manually register cameras!
/// This is the preferred method for new implementations.
/// </summary>
protected void InitializeCameraMap()
{
_cameraMap.Clear();
// Build dictionary from serialized mappings
foreach (var mapping in cameraMappings)
{
if (mapping.camera == null)
{
Logging.Warning($"[{GetType().Name}] No camera assigned for state {mapping.state}");
continue;
}
_cameraMap[mapping.state] = mapping.camera;
mapping.camera.Priority.Value = inactivePriority;
if (showDebugLogs)
Logging.Debug($"[{GetType().Name}] Registered camera '{mapping.camera.gameObject.name}' for state {mapping.state}");
}
_isInitialized = true;
if (_cameraMap.Count == 0)
{
Logging.Warning($"[{GetType().Name}] No cameras registered!");
}
if (showDebugLogs) Logging.Debug($"[{GetType().Name}] Initialized with {_cameraMap.Count} cameras");
}
/// <summary>
/// DEPRECATED: Use InitializeCameraMap() instead for cleaner code.
/// Kept for backward compatibility with existing implementations.
/// </summary>
protected void RegisterCamera(TState state, CinemachineCamera pCamera)
{
if (pCamera == null)
{
Logging.Warning($"[{GetType().Name}] Attempted to register null camera for state {state}");
return;
}
if (_cameraMap.ContainsKey(state))
{
Logging.Warning($"[{GetType().Name}] Camera for state {state} already registered, overwriting");
}
_cameraMap[state] = pCamera;
pCamera.Priority.Value = inactivePriority;
if (showDebugLogs) Logging.Debug($"[{GetType().Name}] Registered camera '{pCamera.gameObject.name}' for state {state}");
}
/// <summary>
/// DEPRECATED: Use InitializeCameraMap() instead.
/// Kept for backward compatibility with existing implementations.
/// </summary>
protected void FinalizeInitialization()
{
_isInitialized = true;
if (_cameraMap.Count == 0)
{
Logging.Warning($"[{GetType().Name}] No cameras registered!");
}
if (showDebugLogs) Logging.Debug($"[{GetType().Name}] Initialized with {_cameraMap.Count} cameras");
}
#endregion
#region State Management
/// <summary>
/// Switch to a specific camera state
/// </summary>
public virtual void SwitchToState(TState newState)
{
if (!_isInitialized)
{
Logging.Error($"[{GetType().Name}] Cannot switch state - not initialized!");
return;
}
if (!_cameraMap.ContainsKey(newState))
{
Logging.Error($"[{GetType().Name}] No camera registered for state {newState}!");
return;
}
TState oldState = _currentState;
_currentState = newState;
// Set all cameras to inactive priority
foreach (var kvp in _cameraMap)
{
kvp.Value.Priority.Value = inactivePriority;
}
// Set target camera to active priority
_cameraMap[newState].Priority.Value = activePriority;
if (showDebugLogs) Logging.Debug($"[{GetType().Name}] Switched from {oldState} to {newState} (camera: {_cameraMap[newState].gameObject.name})");
OnStateChanged?.Invoke(oldState, newState);
}
/// <summary>
/// Get the camera for a specific state
/// </summary>
public CinemachineCamera GetCamera(TState state)
{
if (_cameraMap.TryGetValue(state, out CinemachineCamera pCamera))
{
return pCamera;
}
Logging.Warning($"[{GetType().Name}] No camera found for state {state}");
return null;
}
/// <summary>
/// Check if a camera is registered for a state
/// </summary>
public bool HasCamera(TState state)
{
return _cameraMap.ContainsKey(state);
}
#endregion
#region Validation
/// <summary>
/// Validate that all enum states have cameras registered.
/// Override to add custom validation (e.g., check for specific components).
/// </summary>
protected virtual void ValidateCameras()
{
// Check that all enum values have cameras assigned
foreach (TState state in Enum.GetValues(typeof(TState)))
{
if (!_cameraMap.ContainsKey(state))
{
Logging.Warning($"[{GetType().Name}] No camera assigned for state {state}");
}
else if (_cameraMap[state] == null)
{
Logging.Error($"[{GetType().Name}] Camera for state {state} is null!");
}
}
}
#endregion
#region Editor Support
#if UNITY_EDITOR
/// <summary>
/// Auto-populate camera mappings list with all enum values.
/// Called automatically in the Editor when the component is added or values change.
/// </summary>
protected virtual void OnValidate()
{
// Get all enum values
TState[] allStates = (TState[])Enum.GetValues(typeof(TState));
// Add missing states to the list
foreach (TState state in allStates)
{
bool exists = cameraMappings.Any(m => EqualityComparer<TState>.Default.Equals(m.state, state));
if (!exists)
{
cameraMappings.Add(new CameraStateMapping<TState>(state));
}
}
// Remove mappings for states that no longer exist in the enum
cameraMappings.RemoveAll(m => !System.Array.Exists(allStates, s => EqualityComparer<TState>.Default.Equals(s, m.state)));
// Sort by enum order for cleaner Inspector display
cameraMappings = cameraMappings.OrderBy(m => (int)(object)m.state).ToList();
}
/// <summary>
/// Initialize list when component is first added
/// </summary>
protected virtual void Reset()
{
OnValidate();
}
#endif
#endregion
}
}