Created
April 15, 2026 04:03
-
-
Save phosphoer/baca59776095f6be653cef7d00829921 to your computer and use it in GitHub Desktop.
Camera Controller Stack
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using UnityEngine; | |
| public abstract class CameraControllerBase : MonoBehaviour | |
| { | |
| public float AxisX { get; set; } | |
| public float AxisY { get; set; } | |
| public float AxisZ { get; set; } | |
| public Transform MountPoint => _mountPoint != null ? _mountPoint : transform; | |
| public float FieldOfView = 65.0f; | |
| public float OrthoSize = 6; | |
| public float CameraShakeScalar = 1f; | |
| [SerializeField] private Transform _mountPoint = null; | |
| public void MatchCameraTransform() | |
| { | |
| MountPoint.SetPositionAndRotation(MainCamera.Instance.transform.position, MainCamera.Instance.transform.rotation); | |
| } | |
| public abstract void CameraStart(); | |
| public abstract void CameraUpdate(); | |
| public abstract void CameraStop(); | |
| private void OnDrawGizmos() | |
| { | |
| Gizmos.color = Color.white; | |
| Gizmos.matrix = MountPoint.localToWorldMatrix; | |
| Gizmos.DrawFrustum(Vector3.zero, FieldOfView, 1, 0.1f, Screen.width / (float)Screen.height); | |
| Gizmos.matrix = Matrix4x4.identity; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using UnityEngine; | |
| using System.Collections.Generic; | |
| [System.Serializable] | |
| public struct CameraShakeParams | |
| { | |
| public float Duration; | |
| public float Magnitude; | |
| public float MinRadius; | |
| [UnityEngine.Serialization.FormerlySerializedAs("Radius")] | |
| public float MaxRadius; | |
| } | |
| public class CameraControllerStack : MonoBehaviour | |
| { | |
| public Camera Camera | |
| { | |
| get { return _camera; } | |
| set { _camera = value; } | |
| } | |
| public Camera UICamera | |
| { | |
| get { return _uiCamera; } | |
| set { _uiCamera = value; } | |
| } | |
| public IReadOnlyList<CameraControllerBase> Stack => _cameraControllers; | |
| public CameraControllerBase CurrentCameraController | |
| { | |
| get { return _cameraControllers.Count > 0 ? _cameraControllers[_cameraControllers.Count - 1] : null; } | |
| } | |
| public float CurrentFovOverride | |
| { | |
| get { return _fovStack.Count > 0 ? _fovStack[_fovStack.Count - 1].Value : -1; } | |
| } | |
| [SerializeField] private Camera _camera = null; | |
| [SerializeField] private Camera _uiCamera = null; | |
| [SerializeField] private CameraControllerBase _defaultCameraController = null; | |
| private List<CameraControllerBase> _cameraControllers = new List<CameraControllerBase>(); | |
| private List<KeyValuePair<string, float>> _fovStack = new List<KeyValuePair<string, float>>(); | |
| private Transform _proxyTransform = null; | |
| private bool _isShuttingDown; | |
| private List<CameraShakeInfo> _activeShakes = new List<CameraShakeInfo>(); | |
| private struct CameraShakeInfo | |
| { | |
| public float Duration; | |
| public float Timer; | |
| public float Magnitude; | |
| public Vector3 ShakeDir; | |
| } | |
| public void CameraShakeFromPosition(Vector3 fromPos, CameraShakeParams shakeParams) | |
| { | |
| CameraShakeFromPosition(fromPos, shakeParams.MinRadius, shakeParams.MaxRadius, shakeParams.Magnitude, shakeParams.Duration); | |
| } | |
| public void CameraShakeFromPosition(Vector3 fromPos, float minRadius, float maxRadius, float magnitude, float duration) | |
| { | |
| if (maxRadius > 0 && magnitude > 0 && duration > 0) | |
| { | |
| float dist = Vector3.Distance(_camera.transform.position, fromPos); | |
| float shakeScale = 1.0f - Mathf.InverseLerp(minRadius, maxRadius, dist); | |
| if (shakeScale > 0) | |
| { | |
| CameraShake(shakeScale * magnitude, duration); | |
| } | |
| } | |
| } | |
| public void CameraShake(float magnitude, float duration) | |
| { | |
| _activeShakes.Add(new CameraShakeInfo() | |
| { | |
| Duration = duration, | |
| Timer = 0, | |
| Magnitude = magnitude, | |
| ShakeDir = Random.insideUnitSphere, | |
| }); | |
| } | |
| public void PushController(CameraControllerBase cameraController) | |
| { | |
| if (_cameraControllers.Contains(cameraController)) | |
| { | |
| return; | |
| } | |
| if (CurrentCameraController != null) | |
| CurrentCameraController.CameraStop(); | |
| _cameraControllers.Add(cameraController); | |
| cameraController.CameraStart(); | |
| EnsureCameraStack(); | |
| } | |
| public void PopController(CameraControllerBase cameraController) | |
| { | |
| if (_cameraControllers.Remove(cameraController)) | |
| { | |
| if (cameraController != null) | |
| cameraController.CameraStop(); | |
| if (CurrentCameraController != null) | |
| CurrentCameraController.CameraStart(); | |
| EnsureCameraStack(); | |
| } | |
| } | |
| public void SwitchController(CameraControllerBase cameraController) | |
| { | |
| PopCurrentController(); | |
| PushController(cameraController); | |
| } | |
| public void PopCurrentController() | |
| { | |
| PopController(CurrentCameraController); | |
| } | |
| public void PushFovOverride(string key, float fov) | |
| { | |
| _fovStack.Add(new KeyValuePair<string, float>(key, fov)); | |
| } | |
| public void PopFovOverride(string key) | |
| { | |
| for (int i = 0; i < _fovStack.Count; ++i) | |
| { | |
| if (_fovStack[i].Key == key) | |
| { | |
| _fovStack.RemoveAt(i); | |
| return; | |
| } | |
| } | |
| } | |
| public void ClearStack() | |
| { | |
| _cameraControllers.Clear(); | |
| EnsureCameraStack(); | |
| } | |
| public void SnapTransformToTarget(float maxDist = 0) | |
| { | |
| if (_proxyTransform.localPosition.magnitude >= maxDist) | |
| { | |
| _proxyTransform.localPosition = Vector3.zero; | |
| _proxyTransform.localRotation = Quaternion.identity; | |
| } | |
| } | |
| private void Start() | |
| { | |
| if (_defaultCameraController != null) | |
| PushController(_defaultCameraController); | |
| } | |
| private void LateUpdate() | |
| { | |
| if (_cameraControllers.Count > 0) | |
| { | |
| if (CurrentCameraController == null) | |
| { | |
| PopCurrentController(); | |
| return; | |
| } | |
| // Update the current controller | |
| CurrentCameraController.CameraUpdate(); | |
| // Align camera with current mount point | |
| _proxyTransform.localPosition = Mathfx.Damp(_proxyTransform.localPosition, Vector3.zero, 0.5f, Time.unscaledDeltaTime * 3.0f); | |
| _proxyTransform.localRotation = Mathfx.Damp(_proxyTransform.localRotation, Quaternion.identity, 0.5f, Time.unscaledDeltaTime * 3.0f); | |
| // Handle FOV | |
| float desiredFov = CurrentCameraController.FieldOfView; | |
| if (_fovStack.Count > 0) | |
| desiredFov = _fovStack[_fovStack.Count - 1].Value; | |
| _camera.fieldOfView = Mathfx.Damp(_camera.fieldOfView, desiredFov, 0.25f, Time.unscaledDeltaTime * 5.0f); | |
| // Handle ortho scale | |
| float desiredOrthoSize = CurrentCameraController.OrthoSize; | |
| _camera.orthographicSize = Mathfx.Damp(_camera.orthographicSize, desiredOrthoSize, 0.25f, Time.unscaledDeltaTime * 5.0f); | |
| // Camera shake | |
| for (int i = 0; i < _activeShakes.Count; ++i) | |
| { | |
| CameraShakeInfo shakeInfo = _activeShakes[i]; | |
| shakeInfo.Timer += Time.unscaledDeltaTime; | |
| shakeInfo.ShakeDir = Vector3.ClampMagnitude(shakeInfo.ShakeDir + Random.insideUnitSphere * 10 * Time.unscaledDeltaTime, 1); | |
| _activeShakes[i] = shakeInfo; | |
| if (shakeInfo.Timer < shakeInfo.Duration) | |
| { | |
| float shakeT = 1.0f - Mathf.Clamp01(shakeInfo.Timer / shakeInfo.Duration); | |
| Vector3 shakePos = Random.insideUnitSphere * shakeInfo.Magnitude * shakeT * Time.unscaledDeltaTime * 30; | |
| Vector3 shakeRot = shakeInfo.ShakeDir * shakeInfo.Magnitude * shakeT * Time.unscaledDeltaTime * 60; | |
| _proxyTransform.position += shakePos * CurrentCameraController.CameraShakeScalar; | |
| _proxyTransform.localEulerAngles += shakeRot * CurrentCameraController.CameraShakeScalar; | |
| } | |
| else | |
| { | |
| _activeShakes.RemoveAt(i); | |
| i -= 1; | |
| } | |
| } | |
| // Match actual camera transform to the target | |
| Vector3 prevPos = _camera.transform.position; | |
| _camera.transform.SetPositionAndRotation(_proxyTransform.position, _proxyTransform.rotation); | |
| } | |
| } | |
| private void OnApplicationQuit() | |
| { | |
| _isShuttingDown = true; | |
| } | |
| private void OnProxyDestroyed(GameObject proxyObject) | |
| { | |
| if (!_isShuttingDown && gameObject) | |
| CreateProxyTransform(); | |
| } | |
| private void CreateProxyTransform() | |
| { | |
| _proxyTransform = new GameObject($"{name}-proxy").transform; | |
| _proxyTransform.SetPositionAndRotation(_camera.transform.position, _camera.transform.rotation); | |
| var objEvents = _proxyTransform.gameObject.AddComponent<MonoBehaviourEvents>(); | |
| objEvents.EventOnDestroy += OnProxyDestroyed; | |
| } | |
| private bool IsTransformFucked(Transform t) | |
| { | |
| Vector3 pos = t.position; | |
| return float.IsNaN(pos.x) | |
| || float.IsNaN(pos.y) | |
| || float.IsNaN(pos.z) | |
| || Mathf.Abs(pos.x) > 10000 | |
| || Mathf.Abs(pos.y) > 500 | |
| || Mathf.Abs(pos.z) > 10000; | |
| } | |
| private bool IsCameraFucked() | |
| { | |
| return IsTransformFucked(_proxyTransform) || IsTransformFucked(_camera.transform); | |
| } | |
| private void RecoverFuckedCamera() | |
| { | |
| _proxyTransform.position = Vector3.zero; | |
| _camera.transform.position = _proxyTransform.position; | |
| } | |
| private void EnsureCameraStack() | |
| { | |
| if (_proxyTransform == null) | |
| { | |
| CreateProxyTransform(); | |
| } | |
| if (_cameraControllers.Count > 0) | |
| { | |
| CameraControllerBase activeController = _cameraControllers[_cameraControllers.Count - 1]; | |
| if (activeController != null) | |
| { | |
| _proxyTransform.SetParent(activeController.MountPoint, true); | |
| } | |
| } | |
| else | |
| { | |
| _proxyTransform.SetParent(null, true); | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment