389 lines
14 KiB
C#
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
|
|
}
|
|
}
|