Files
Ipseity-Demo/Assets/_Content/Scripts/Features/LiminalSpaces/HouseOfLeaves/Runtime/HouseOfLeavesController.cs
T
2026-07-22 20:57:34 +02:00

238 lines
12 KiB
C#

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using Unheimlich.Features.Player;
namespace Unheimlich.World.HouseOfLeaves
{
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
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
// ─────────────────────────────────────────────────────────────────────────
/// <summary>
/// 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.
/// </summary>
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
// ─────────────────────────────────────────────────────────────────────────
/// <summary>Total distance the player has walked along the corridor direction (unbounded).</summary>
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
}
}