using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; using Unheimlich.Features.Player; namespace Unheimlich.World.HouseOfLeaves { /// /// Orchestrates the House of Leaves "endless corridor" illusion: /// - The exit is slowly pushed away as the player walks, so the corridor /// never seems to actually run out. /// - A darkness/vignette effect kicks in if the player turns back. /// - Exposes a normalized Progression value [0,1] for visual effects /// such as sand, walls, shaders, etc. /// /// The corridor itself remains infinite. Progression only represents /// visual advancement over a configurable distance. /// public class HouseOfLeavesController : MonoBehaviour { // ───────────────────────────────────────────────────────────────────────── // Serialized Fields // ───────────────────────────────────────────────────────────────────────── [Header("Endless Corridor (Receding Exit)")] [Tooltip( "The transform representing the visual end of the corridor " + "(exit door, light at the end, etc).")] [SerializeField] private Transform _exit; [Tooltip( "Fraction of each meter walked that pushes the exit further away. " + "0 = exit never recedes. Close to 1 = exit recedes almost as fast " + "as the player walks.")] [SerializeField, Range(0f, 0.95f)] private float _recedeFactor = 0.4f; [Tooltip( "How slowly the exit catches up to its receded target, in seconds.")] [SerializeField, Min(0.01f)] private float _recedeSmoothTime = 2f; [Header("Visual Progression")] [Tooltip( "Distance in meters required for visual effects to reach " + "Progression = 1. The corridor itself remains infinite.")] [SerializeField, Min(0.01f)] private float _progressionDistance = 50f; [Header("Darkness (Turning Back)")] [SerializeField] private Volume _postProcessVolume; [SerializeField] private float _turningBackAngle = 100f; [SerializeField, Range(0f, 1f)] private float _maxVignetteIntensity = 0.6f; [SerializeField] private float _maxExposureReduction = -3f; [SerializeField] private float _darknessLerpSpeed = 2f; // ───────────────────────────────────────────────────────────────────────── // Private Fields // ───────────────────────────────────────────────────────────────────────── private Vignette _vignette; private ColorAdjustments _colorAdjustments; private Vector3 _lastPlayerPosition; // Unbounded distance travelled through the corridor. private float _distanceTravelled; private Vector3 _lightForwardDirection; private Vector3 _exitRestPosition; private bool _isInitialized; // ───────────────────────────────────────────────────────────────────────── // Unity Messages // ───────────────────────────────────────────────────────────────────────── private void Start() { if (!TryResolvePlayerReferences()) return; InitializeLightDirection(); InitializePostProcess(); InitializeExit(); _lastPlayerPosition = PlayerController.Instance.transform.position; _isInitialized = true; } private void Update() { if (!_isInitialized) return; TrackDistance(); TickExitRecede(); TickDarkness(); } // ───────────────────────────────────────────────────────────────────────── // Initialization // ───────────────────────────────────────────────────────────────────────── private bool TryResolvePlayerReferences() { if (PlayerController.Instance == null) { Debug.LogError( "[HouseOfLeavesController] PlayerController.Instance not found." ); return false; } return true; } /// /// Uses the player's initial facing direction as the corridor's forward axis. /// private void InitializeLightDirection() { _lightForwardDirection = PlayerController.Instance.transform.forward; _lightForwardDirection.y = 0f; if (_lightForwardDirection.sqrMagnitude > 0f) _lightForwardDirection.Normalize(); } private void InitializePostProcess() { if (_postProcessVolume == null) { Debug.LogWarning( "[HouseOfLeavesController] No URP Volume assigned — " + "darkness effect disabled." ); return; } _postProcessVolume.profile.TryGet(out _vignette); _postProcessVolume.profile.TryGet(out _colorAdjustments); } private void InitializeExit() { if (_exit == null) { Debug.LogWarning( "[HouseOfLeavesController] No Exit transform assigned — " + "endless-corridor recede disabled." ); return; } _exitRestPosition = _exit.position; } // ───────────────────────────────────────────────────────────────────────── // Distance Tracking // ───────────────────────────────────────────────────────────────────────── private void TrackDistance() { Vector3 currentPosition = PlayerController.Instance.transform.position; Vector3 delta = currentPosition - _lastPlayerPosition; delta.y = 0f; // Signed distance along the corridor direction. _distanceTravelled += Vector3.Dot(delta, _lightForwardDirection); // Prevent the visual progression from becoming negative if // the player walks backwards past the starting point. _distanceTravelled = Mathf.Max(0f, _distanceTravelled); _lastPlayerPosition = currentPosition; } // ───────────────────────────────────────────────────────────────────────── // Tick Methods // ───────────────────────────────────────────────────────────────────────── private void TickExitRecede() { if (_exit == null) return; float recedeDistance = _distanceTravelled * _recedeFactor; Vector3 targetPosition = _exitRestPosition + _lightForwardDirection * recedeDistance; float t = Time.deltaTime / Mathf.Max(_recedeSmoothTime, 0.01f); _exit.position = Vector3.Lerp( _exit.position, targetPosition, t ); } private void TickDarkness() { if (_vignette == null && _colorAdjustments == null) { return; } Vector3 playerForward = PlayerController.Instance.transform.forward; playerForward.y = 0f; if (playerForward.sqrMagnitude > 0f) playerForward.Normalize(); float dot = Vector3.Dot( playerForward, _lightForwardDirection ); float angleDegrees = Mathf.Acos( Mathf.Clamp(dot, -1f, 1f) ) * Mathf.Rad2Deg; float darknessT = Mathf.Clamp01( (angleDegrees - _turningBackAngle) / Mathf.Max( 180f - _turningBackAngle, 0.01f ) ); float lerpDelta = Time.deltaTime * _darknessLerpSpeed; if (_vignette != null) { _vignette.intensity.value = Mathf.Lerp( _vignette.intensity.value, _maxVignetteIntensity * darknessT, lerpDelta ); } if (_colorAdjustments != null) { _colorAdjustments.postExposure.value = Mathf.Lerp( _colorAdjustments.postExposure.value, _maxExposureReduction * darknessT, lerpDelta ); } } // ───────────────────────────────────────────────────────────────────────── // Public API // ───────────────────────────────────────────────────────────────────────── /// /// Total distance travelled along the corridor direction. /// public float DistanceTravelled => _distanceTravelled; /// /// Normalized visual progression. /// /// 0 = start of the corridor. /// 1 = progression distance has been reached. /// /// This value is clamped to [0,1] even though DistanceTravelled /// can continue increasing indefinitely. /// public float Progression => Mathf.Clamp01( _distanceTravelled / Mathf.Max(_progressionDistance, 0.01f) ); /// /// Resets corridor progression and the receding exit. /// public void ResetProgression() { _distanceTravelled = 0f; if (PlayerController.Instance != null) { _lastPlayerPosition = PlayerController.Instance.transform.position; } if (_exit != null) { _exit.position = _exitRestPosition; } } /// /// Overrides the corridor forward direction. /// public void SetLightDirection(Vector3 worldDirection) { worldDirection.y = 0f; if (worldDirection.sqrMagnitude <= 0f) return; _lightForwardDirection = worldDirection.normalized; } // ───────────────────────────────────────────────────────────────────────── // Editor // ───────────────────────────────────────────────────────────────────────── #if UNITY_EDITOR private void OnDrawGizmosSelected() { Gizmos.color = Color.yellow; Gizmos.DrawRay( transform.position + Vector3.up, _lightForwardDirection * 4f ); if (_exit != null) { Gizmos.color = Color.cyan; Gizmos.DrawWireSphere( _exit.position, 0.5f ); } } #endif } }