Files
Ipseity-Demo/Assets/_Content/Scripts/Features/LiminalSpaces/HouseOfLeaves/Runtime/HouseOfLeavesController.cs
T

389 lines
14 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.
/// - 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.
/// </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).")]
[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;
}
/// <summary>
/// Uses the player's initial facing direction as the corridor's forward axis.
/// </summary>
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
// ─────────────────────────────────────────────────────────────────────────
/// <summary>
/// Total distance travelled along the corridor direction.
/// </summary>
public float DistanceTravelled =>
_distanceTravelled;
/// <summary>
/// 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.
/// </summary>
public float Progression =>
Mathf.Clamp01(
_distanceTravelled /
Mathf.Max(_progressionDistance, 0.01f)
);
/// <summary>
/// Resets corridor progression and the receding exit.
/// </summary>
public void ResetProgression()
{
_distanceTravelled = 0f;
if (PlayerController.Instance != null)
{
_lastPlayerPosition =
PlayerController.Instance.transform.position;
}
if (_exit != null)
{
_exit.position =
_exitRestPosition;
}
}
/// <summary>
/// Overrides the corridor forward direction.
/// </summary>
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
}
}