Hallucination system setup & implementation with the infinite staircase + Fake volumetric lights test
This commit is contained in:
+117
-111
@@ -3,41 +3,27 @@ using UnityEngine.Rendering;
|
||||
using UnityEngine.Rendering.Universal;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Unheimlich.Core;
|
||||
using Unheimlich.Features.Dialogue.UI;
|
||||
|
||||
namespace Unheimlich.Features.Hallucination
|
||||
{
|
||||
/// <summary>
|
||||
/// Central manager for the hallucination system.
|
||||
/// Tracks the player's sanity level (0 = sane, 1 = fully insane) and drives all
|
||||
/// hallucination effects: individual URP post-process overrides (with lerp), word shake
|
||||
/// intensity, and ghost-word visibility via the HallucinationWordSpawner.
|
||||
///
|
||||
/// Effects are applied directly on VolumeProfile overrides rather than on volume.weight,
|
||||
/// so multiple volumes coexist without parameter conflicts.
|
||||
/// </summary>
|
||||
public class HallucinationManager : Singleton<HallucinationManager>
|
||||
{
|
||||
// ─── Constants ─────────────────────────────────────────────────────────────
|
||||
|
||||
private const float MinSanity = 0f;
|
||||
private const float MaxSanity = 1f;
|
||||
|
||||
// ─── Inspector ─────────────────────────────────────────────────────────────
|
||||
|
||||
[Header("Sanity")]
|
||||
[Tooltip("Starting sanity level (0 = sane, 1 = fully insane)")]
|
||||
[SerializeField, Range(0f, 1f)] private float _initialSanity = 0f;
|
||||
|
||||
[Tooltip("Sanity naturally recovers over time at this rate per second (0 = no recovery)")]
|
||||
[SerializeField, Range(0f, 0.1f)] private float _sanityRecoveryRate = 0f;
|
||||
|
||||
[Header("Effect Lerp")]
|
||||
[Tooltip("Speed at which visual effects lerp toward the target sanity value")]
|
||||
[SerializeField] private float _effectLerpSpeed = 2f;
|
||||
|
||||
[Header("URP Post-Process Volume")]
|
||||
[Tooltip("Volume containing the hallucination overrides (Chromatic Aberration, Lens Distortion, etc.)")]
|
||||
[SerializeField] private Volume _hallucinationVolume;
|
||||
|
||||
[Header("Chromatic Aberration")]
|
||||
@@ -62,151 +48,175 @@ namespace Unheimlich.Features.Hallucination
|
||||
|
||||
[Header("Bloom")]
|
||||
[SerializeField] private bool _driveBloom = true;
|
||||
[Tooltip("Bloom intensity at sanity = 0")]
|
||||
[SerializeField, Min(0f)] private float _bloomIntensityMin = 0f;
|
||||
[Tooltip("Bloom intensity at sanity = 1")]
|
||||
[SerializeField, Min(0f)] private float _bloomIntensityMax = 1f;
|
||||
[Tooltip("Bloom threshold at sanity = 0 — raise this to suppress any bleed at zero sanity")]
|
||||
[SerializeField, Min(0f)] private float _bloomThresholdMin = 1f;
|
||||
[Tooltip("Bloom threshold at sanity = 1")]
|
||||
[SerializeField, Min(0f)] private float _bloomThresholdMax = 0.5f;
|
||||
[Tooltip("Bloom scatter (spread) at sanity = 0")]
|
||||
[SerializeField, Min(0f)] private float _bloomIntensityMin = 0f;
|
||||
[SerializeField, Min(0f)] private float _bloomIntensityMax = 1f;
|
||||
[SerializeField, Min(0f)] private float _bloomThresholdMin = 1f;
|
||||
[SerializeField, Min(0f)] private float _bloomThresholdMax = 0.5f;
|
||||
[SerializeField, Range(0f, 1f)] private float _bloomScatterMin = 0f;
|
||||
[Tooltip("Bloom scatter (spread) at sanity = 1")]
|
||||
[SerializeField, Range(0f, 1f)] private float _bloomScatterMax = 0.7f;
|
||||
[Tooltip("Bloom dirt texture intensity at sanity = 0")]
|
||||
[SerializeField, Min(0f)] private float _bloomDirtIntensityMin = 0f;
|
||||
[Tooltip("Bloom dirt texture intensity at sanity = 1")]
|
||||
[SerializeField, Min(0f)] private float _bloomDirtIntensityMax = 0f;
|
||||
|
||||
[Header("Word Shake")]
|
||||
[Tooltip("Baseline shake multiplier at sanity = 0")]
|
||||
[SerializeField] private float _shakeMultiplierAtZero = 1f;
|
||||
[Header("Floating Label — Baseline Influence")]
|
||||
[Tooltip("Labels within this radius of the player receive the sanity-driven baseline.")]
|
||||
[SerializeField, Min(0f)] private float _labelInfluenceRadius = 12f;
|
||||
|
||||
[Tooltip("Max shake multiplier at sanity = 1")]
|
||||
[SerializeField] private float _shakeMultiplierAtOne = 3f;
|
||||
[Header("Floating Label — Shake")]
|
||||
[Tooltip("Shake radius applied to labels at sanity = 0.")]
|
||||
[SerializeField, Min(0f)] private float _labelShakeRadiusAtZero = 0f;
|
||||
[SerializeField, Min(0f)] private float _labelShakeSpeedAtZero = 1.5f;
|
||||
[SerializeField, Min(0f)] private float _labelShakeSmoothnessAtZero = 2f;
|
||||
|
||||
[Tooltip("Shake radius applied to labels at sanity = 1.")]
|
||||
[SerializeField, Min(0f)] private float _labelShakeRadiusAtOne = 0.2f;
|
||||
[SerializeField, Min(0f)] private float _labelShakeSpeedAtOne = 4f;
|
||||
[SerializeField, Min(0f)] private float _labelShakeSmoothnessAtOne = 5f;
|
||||
|
||||
[Header("Floating Label — Ghost Trail")]
|
||||
[SerializeField] private bool _ghostEnabledAtZero = false;
|
||||
[SerializeField] private bool _ghostEnabledAtOne = true;
|
||||
[SerializeField, Range(0f, 1f)] private float _ghostAlphaMin = 0.1f;
|
||||
[SerializeField, Range(0f, 1f)] private float _ghostAlphaMax = 0.5f;
|
||||
|
||||
[Header("Word Shake (legacy — for other systems)")]
|
||||
[SerializeField] private float _shakeMultiplierLegacyAtZero = 1f;
|
||||
[SerializeField] private float _shakeMultiplierLegacyAtOne = 3f;
|
||||
|
||||
[Header("Debug")]
|
||||
[SerializeField] private bool _debugMode;
|
||||
|
||||
// ─── State ─────────────────────────────────────────────────────────────────
|
||||
// ─── State ─────────────────────────────────────────────────────────────
|
||||
|
||||
private float _currentSanity;
|
||||
private float _displayedSanity; // lerped value used for visual effects
|
||||
private float _currentSanity;
|
||||
private float _displayedSanity;
|
||||
private Coroutine _lerpCoroutine;
|
||||
private bool _vignetteExternallyOverridden;
|
||||
|
||||
// External systems can suppress the hallucination vignette (e.g. diapason listening mode).
|
||||
private bool _vignetteExternallyOverridden;
|
||||
|
||||
// Cached volume overrides
|
||||
private ChromaticAberration _chromaticAberration;
|
||||
private LensDistortion _lensDistortion;
|
||||
private Vignette _vignette;
|
||||
private FilmGrain _filmGrain;
|
||||
private Bloom _bloom;
|
||||
private LensDistortion _lensDistortion;
|
||||
private Vignette _vignette;
|
||||
private FilmGrain _filmGrain;
|
||||
private Bloom _bloom;
|
||||
|
||||
// ─── Public API ────────────────────────────────────────────────────────────
|
||||
private Transform _playerTransform;
|
||||
private readonly List<FloatingLabel> _labelsInRange = new();
|
||||
|
||||
// ─── Public API ────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Current logical sanity level in [0, 1]. 0 = sane, 1 = fully insane.</summary>
|
||||
public float SanityLevel => _currentSanity;
|
||||
|
||||
/// <summary>
|
||||
/// Word shake multiplier computed from the lerped display sanity (smooth).
|
||||
/// </summary>
|
||||
public float WordShakeMultiplier =>
|
||||
Mathf.Lerp(_shakeMultiplierAtZero, _shakeMultiplierAtOne, _displayedSanity);
|
||||
Mathf.Lerp(_shakeMultiplierLegacyAtZero, _shakeMultiplierLegacyAtOne, _displayedSanity);
|
||||
|
||||
/// <summary>Raised whenever the logical sanity level changes. Payload is the new value in [0, 1].</summary>
|
||||
public event Action<float> OnSanityChanged;
|
||||
|
||||
/// <summary>Raised when a discrete hallucination event is triggered (e.g. ghost word appears).</summary>
|
||||
public event Action<float> OnSanityChanged;
|
||||
public event Action<HallucinationEvent> OnHallucinationTriggered;
|
||||
|
||||
// ─── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
// ─── Lifecycle ─────────────────────────────────────────────────────────
|
||||
|
||||
protected override void OnAwake()
|
||||
{
|
||||
_currentSanity = Mathf.Clamp(_initialSanity, MinSanity, MaxSanity);
|
||||
_currentSanity = Mathf.Clamp(_initialSanity, MinSanity, MaxSanity);
|
||||
_displayedSanity = _currentSanity;
|
||||
CacheVolumeOverrides();
|
||||
ApplyEffectsImmediate(_currentSanity);
|
||||
ApplyPostProcessImmediate(_currentSanity);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_sanityRecoveryRate > 0f && _currentSanity > MinSanity)
|
||||
SetSanity(_currentSanity - _sanityRecoveryRate * Time.deltaTime);
|
||||
|
||||
DriveFloatingLabelBaseline();
|
||||
}
|
||||
|
||||
// ─── Public Methods ────────────────────────────────────────────────────────
|
||||
// ─── Public Methods ────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Increases the sanity level by <paramref name="amount"/> (clamped to [0, 1]).
|
||||
/// Effects lerp smoothly to the new value.
|
||||
/// </summary>
|
||||
public void AddInsanity(float amount)
|
||||
{
|
||||
SetSanity(_currentSanity + amount);
|
||||
}
|
||||
public void AddInsanity(float amount) => SetSanity(_currentSanity + amount);
|
||||
|
||||
/// <summary>
|
||||
/// Directly sets the logical sanity level. Effects lerp smoothly to the new value.
|
||||
/// </summary>
|
||||
public void SetSanity(float value)
|
||||
{
|
||||
float clamped = Mathf.Clamp(value, MinSanity, MaxSanity);
|
||||
|
||||
if (Mathf.Approximately(clamped, _currentSanity))
|
||||
return;
|
||||
if (Mathf.Approximately(clamped, _currentSanity)) return;
|
||||
|
||||
_currentSanity = clamped;
|
||||
OnSanityChanged?.Invoke(_currentSanity);
|
||||
|
||||
if (_debugMode)
|
||||
Debug.Log($"[HallucinationManager] Sanity → {_currentSanity:F2}", this);
|
||||
|
||||
if (_debugMode) Debug.Log($"[HallucinationManager] Sanity → {_currentSanity:F2}", this);
|
||||
RestartLerp();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Suppresses the hallucination vignette so an external system (e.g. diapason) can
|
||||
/// take full control of vignette without additive blending artefacts.
|
||||
/// Call with <c>false</c> when the external system is done.
|
||||
/// </summary>
|
||||
public void SetVignetteOverriddenExternally(bool overridden)
|
||||
{
|
||||
_vignetteExternallyOverridden = overridden;
|
||||
|
||||
// Apply immediately so the vignette drops to zero without waiting for the lerp coroutine
|
||||
if (_vignette != null)
|
||||
_vignette.intensity.value = overridden ? 0f : Mathf.Lerp(_vignetteMin, _vignetteMax, _displayedSanity);
|
||||
_vignette.intensity.value = overridden
|
||||
? 0f
|
||||
: Mathf.Lerp(_vignetteMin, _vignetteMax, _displayedSanity);
|
||||
}
|
||||
|
||||
/// <summary>Fires a discrete hallucination event (ghost word, vision flash, etc.).</summary>
|
||||
/// <summary>
|
||||
/// Fires a discrete hallucination event:
|
||||
/// 1. Applies SanityCost.
|
||||
/// 2. Dispatches per-label overrides defined in the event asset.
|
||||
/// 3. Raises OnHallucinationTriggered.
|
||||
/// </summary>
|
||||
public void TriggerHallucinationEvent(HallucinationEvent hallucinationEvent)
|
||||
{
|
||||
if (hallucinationEvent == null)
|
||||
{
|
||||
Debug.LogError("[HallucinationManager] Attempted to trigger a null HallucinationEvent!", this);
|
||||
Debug.LogError("[HallucinationManager] Null HallucinationEvent!", this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (hallucinationEvent.SanityCost > 0f)
|
||||
AddInsanity(hallucinationEvent.SanityCost);
|
||||
|
||||
hallucinationEvent.ApplyLabelOverrides();
|
||||
|
||||
if (_debugMode)
|
||||
Debug.Log($"[HallucinationManager] Event: {hallucinationEvent.name}", this);
|
||||
Debug.Log($"[HallucinationManager] Event: {hallucinationEvent.Label} " +
|
||||
$"({hallucinationEvent.LabelOverrides.Count} label overrides)", this);
|
||||
|
||||
OnHallucinationTriggered?.Invoke(hallucinationEvent);
|
||||
}
|
||||
|
||||
// ─── Lerp ──────────────────────────────────────────────────────────────────
|
||||
// ─── Floating Label Baseline ───────────────────────────────────────────
|
||||
|
||||
private void DriveFloatingLabelBaseline()
|
||||
{
|
||||
EnsurePlayerTransform();
|
||||
if (_playerTransform == null) return;
|
||||
|
||||
HallucinationLabelRegistry.GetLabelsInRadius(
|
||||
_playerTransform.position, _labelInfluenceRadius, _labelsInRange);
|
||||
|
||||
float t = _displayedSanity;
|
||||
|
||||
float shakeRadius = Mathf.Lerp(_labelShakeRadiusAtZero, _labelShakeRadiusAtOne, t);
|
||||
float shakeSpeed = Mathf.Lerp(_labelShakeSpeedAtZero, _labelShakeSpeedAtOne, t);
|
||||
float shakeSmoothness = Mathf.Lerp(_labelShakeSmoothnessAtZero, _labelShakeSmoothnessAtOne, t);
|
||||
bool ghostEnabled = t >= 0.5f ? _ghostEnabledAtOne : _ghostEnabledAtZero;
|
||||
float ghostAlpha = Mathf.Lerp(_ghostAlphaMin, _ghostAlphaMax, t);
|
||||
|
||||
foreach (FloatingLabel label in _labelsInRange)
|
||||
{
|
||||
label.ApplyBaseline(shakeRadius, shakeSpeed, shakeSmoothness, ghostEnabled, ghostAlpha);
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsurePlayerTransform()
|
||||
{
|
||||
if (_playerTransform != null) return;
|
||||
if (Unheimlich.Features.Player.PlayerController.Instance != null)
|
||||
_playerTransform = Unheimlich.Features.Player.PlayerController.Instance.transform;
|
||||
else if (Camera.main != null)
|
||||
_playerTransform = Camera.main.transform;
|
||||
}
|
||||
|
||||
// ─── Lerp ──────────────────────────────────────────────────────────────
|
||||
|
||||
private void RestartLerp()
|
||||
{
|
||||
if (_lerpCoroutine != null)
|
||||
StopCoroutine(_lerpCoroutine);
|
||||
|
||||
if (_lerpCoroutine != null) StopCoroutine(_lerpCoroutine);
|
||||
_lerpCoroutine = StartCoroutine(LerpEffects());
|
||||
}
|
||||
|
||||
@@ -214,22 +224,21 @@ namespace Unheimlich.Features.Hallucination
|
||||
{
|
||||
while (!Mathf.Approximately(_displayedSanity, _currentSanity))
|
||||
{
|
||||
_displayedSanity = Mathf.MoveTowards(_displayedSanity, _currentSanity, _effectLerpSpeed * Time.deltaTime);
|
||||
ApplyEffectsImmediate(_displayedSanity);
|
||||
_displayedSanity = Mathf.MoveTowards(
|
||||
_displayedSanity, _currentSanity, _effectLerpSpeed * Time.deltaTime);
|
||||
ApplyPostProcessImmediate(_displayedSanity);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
_displayedSanity = _currentSanity;
|
||||
ApplyEffectsImmediate(_displayedSanity);
|
||||
ApplyPostProcessImmediate(_displayedSanity);
|
||||
_lerpCoroutine = null;
|
||||
}
|
||||
|
||||
// ─── Effects ───────────────────────────────────────────────────────────────
|
||||
// ─── Post-Process Effects ──────────────────────────────────────────────
|
||||
|
||||
private void CacheVolumeOverrides()
|
||||
{
|
||||
if (_hallucinationVolume == null || _hallucinationVolume.profile == null)
|
||||
return;
|
||||
if (_hallucinationVolume == null || _hallucinationVolume.profile == null) return;
|
||||
|
||||
_hallucinationVolume.profile.TryGet(out _chromaticAberration);
|
||||
_hallucinationVolume.profile.TryGet(out _lensDistortion);
|
||||
@@ -237,20 +246,19 @@ namespace Unheimlich.Features.Hallucination
|
||||
_hallucinationVolume.profile.TryGet(out _filmGrain);
|
||||
_hallucinationVolume.profile.TryGet(out _bloom);
|
||||
|
||||
// Keep volume weight at 1 permanently — we write individual override values directly.
|
||||
// The hallucination volume must be on its OWN dedicated VolumeProfile, separate from
|
||||
// the dialogue blur volume, to avoid weight conflicts between the two systems.
|
||||
_hallucinationVolume.weight = 1f;
|
||||
_hallucinationVolume.weight = 1f;
|
||||
_hallucinationVolume.enabled = true;
|
||||
}
|
||||
|
||||
private void ApplyEffectsImmediate(float t)
|
||||
private void ApplyPostProcessImmediate(float t)
|
||||
{
|
||||
if (_driveChromaticAberration && _chromaticAberration != null)
|
||||
_chromaticAberration.intensity.value = Mathf.Lerp(_chromaticAberrationMin, _chromaticAberrationMax, t);
|
||||
_chromaticAberration.intensity.value =
|
||||
Mathf.Lerp(_chromaticAberrationMin, _chromaticAberrationMax, t);
|
||||
|
||||
if (_driveLensDistortion && _lensDistortion != null)
|
||||
_lensDistortion.intensity.value = Mathf.Lerp(_lensDistortionMin, _lensDistortionMax, t);
|
||||
_lensDistortion.intensity.value =
|
||||
Mathf.Lerp(_lensDistortionMin, _lensDistortionMax, t);
|
||||
|
||||
if (_driveVignette && _vignette != null && !_vignetteExternallyOverridden)
|
||||
_vignette.intensity.value = Mathf.Lerp(_vignetteMin, _vignetteMax, t);
|
||||
@@ -260,13 +268,11 @@ namespace Unheimlich.Features.Hallucination
|
||||
|
||||
if (_driveBloom && _bloom != null)
|
||||
{
|
||||
// Drive all bloom parameters that can independently cause it to appear.
|
||||
// intensity alone is not enough — threshold=0 or scatter>0 can bleed through.
|
||||
_bloom.intensity.value = Mathf.Lerp(_bloomIntensityMin, _bloomIntensityMax, t);
|
||||
_bloom.threshold.value = Mathf.Lerp(_bloomThresholdMin, _bloomThresholdMax, t);
|
||||
_bloom.scatter.value = Mathf.Lerp(_bloomScatterMin, _bloomScatterMax, t);
|
||||
_bloom.intensity.value = Mathf.Lerp(_bloomIntensityMin, _bloomIntensityMax, t);
|
||||
_bloom.threshold.value = Mathf.Lerp(_bloomThresholdMin, _bloomThresholdMax, t);
|
||||
_bloom.scatter.value = Mathf.Lerp(_bloomScatterMin, _bloomScatterMax, t);
|
||||
_bloom.dirtIntensity.value = Mathf.Lerp(_bloomDirtIntensityMin, _bloomDirtIntensityMax, t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user