238 lines
12 KiB
C#
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
|
|
}
|
|
} |