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 (progress feels real, but is deliberately /// much slower than the player's own walking speed - no speed/FOV trickery involved). /// - A darkness/vignette effect kicks in if the player turns back towards /// where they came from, discouraging backtracking. /// No fixed corridor length is required - this works indefinitely. /// 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). " + "It will be slowly pushed further away as the player walks, to create the illusion of an endless corridor.")] [SerializeField] private Transform _exit; [Tooltip("Fraction of each meter walked that pushes the exit further away. " + "0 = exit never recedes (normal corridor). Close to 1 = exit recedes almost as fast as the player walks " + "(barely any felt progress). Keep this comfortably below 1 so the player still senses real progress, just a lot slower than expected.")] [SerializeField, Range(0f, 0.95f)] private float _recedeFactor = 0.4f; [Tooltip("How slowly the exit's position catches up to its receded target, in seconds. " + "Higher values make the shift smoother and harder to consciously notice.")] [SerializeField] private float _recedeSmoothTime = 2f; [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; private float _distanceTravelled; // Unbounded - there is no fixed corridor length anymore. private Vector3 _lightForwardDirection; private Vector3 _exitRestPosition; // The exit's original position, used as the recede baseline. 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; } /// /// The corridor's reference direction is simply wherever the player is facing when this /// controller starts - no manual light/reference transform needed. This becomes both the /// "forward" axis used to detect turning back, and the axis the exit recedes along. /// private void InitializeLightDirection() { _lightForwardDirection = PlayerController.Instance.transform.forward; _lightForwardDirection.y = 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; // No clamp here on purpose: the corridor has no defined length, so this can grow forever. _distanceTravelled += Vector3.Dot(delta, _lightForwardDirection); _lastPlayerPosition = currentPosition; } // ───────────────────────────────────────────────────────────────────────── // Tick Methods // ───────────────────────────────────────────────────────────────────────── /// /// Pushes the exit further away along the corridor direction, proportionally to how far /// the player has walked. Because _recedeFactor is less than 1, the exit still slowly /// gets closer overall - just much slower than the player's actual walking speed - which /// is what sells the "this corridor never seems to end" feeling without ever touching /// the player's movement speed or FOV. /// private void TickExitRecede() { if (_exit == null) return; float recedeDistance = _distanceTravelled * _recedeFactor; Vector3 targetPosition = _exitRestPosition + _lightForwardDirection * recedeDistance; // Smoothly drift towards the target rather than snapping, so the shift is never perceptible // as a sudden jump - it should read as "the corridor is just long", not as a visible trick. 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; 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) / (180f - _turningBackAngle) ); 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 the player has walked along the corridor direction (unbounded). public float DistanceTravelled => _distanceTravelled; public void ResetProgression() { _distanceTravelled = 0f; _lastPlayerPosition = PlayerController.Instance.transform.position; if (_exit != null) _exit.position = _exitRestPosition; } public void SetLightDirection(Vector3 worldDirection) { _lightForwardDirection = worldDirection; _lightForwardDirection.y = 0f; _lightForwardDirection.Normalize(); } // ───────────────────────────────────────────────────────────────────────── // 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 } }