Hallucination system setup & implementation with the infinite staircase + Fake volumetric lights test

This commit is contained in:
Ferlat Thibaud
2026-03-27 20:47:11 +01:00
parent 34d8ffd5b0
commit 395f6266d6
119 changed files with 44996 additions and 816 deletions
@@ -1,54 +1,118 @@
using System.Collections.Generic;
using UnityEngine;
using Unheimlich.Features.Player;
using Unheimlich.Features.Hallucination;
namespace Unheimlich.World.Environment
{
// ─── Data ─────────────────────────────────────────────────────────────────────
/// <summary>
/// Configuration for a single stair slot.
/// Slot indices are relative: 0 = ground level, positive = up, negative = down.
/// </summary>
[System.Serializable]
public class StaircaseSlotData
{
[Tooltip("Slot index this data applies to. 0 = ground, positive = up, negative = down.")]
public int SlotIndex;
[Tooltip("Word displayed on the FloatingLabel of this segment.")]
public string Word;
[Tooltip("Optional hallucination event fired when the player enters this segment's zone.")]
public HallucinationEvent HallucinationEvent;
}
/// <summary>
/// ScriptableObject holding the full word/event sequence for one staircase.
/// Create via Assets > Create > Unheimlich > Hallucination > Staircase Sequence Data.
/// </summary>
[CreateAssetMenu(
fileName = "StaircaseSequenceData",
menuName = "Unheimlich/Hallucination/Staircase Sequence Data")]
public class StaircaseSequenceData : ScriptableObject
{
[Tooltip("Per-slot configuration. Slots not listed here get no word and no event.")]
public List<StaircaseSlotData> Slots = new();
}
// ─── Controller ───────────────────────────────────────────────────────────────
public class InfiniteStaircaseController : MonoBehaviour
{
// ─── Prefabs ─────────────────────────────────────────────────────────
[Header("Stair Prefabs")]
[Tooltip("Segment standard utilisé pour remplir la tour.")]
[Tooltip("Standard segment used to fill the tower.")]
[SerializeField] private GameObject _stairPrefab;
[Tooltip("Segment toujours placé tout en HAUT de la tour (slot +maxIterationsUp).")]
[Tooltip("Segment always placed at the very TOP of the tower (slot +maxIterationsUp).")]
[SerializeField] private GameObject _stairTopPrefab;
[Tooltip("Segment toujours placé tout en BAS de la tour (slot -maxIterationsDown).")]
[Tooltip("Segment always placed at the very BOTTOM of the tower (slot -maxIterationsDown).")]
[SerializeField] private GameObject _stairBottomPrefab;
[Tooltip("Si true, le premier enfant avec renderers est réutilisé comme slot 0.")]
[Tooltip("If true, the first child with renderers is reused as slot 0 instead of being instantiated from the prefab.")]
[SerializeField] private bool _useExistingChildAsBase = true;
// ─── Iterations ──────────────────────────────────────────────────────
[Header("Iterations")]
[Tooltip("Nombre de segments au-dessus du sol (montée).")]
[Tooltip("Number of segments above ground level (upward traversal).")]
[SerializeField] private int _maxIterationsUp = 4;
[Tooltip("Nombre de segments en-dessous du sol (descente).")]
[Tooltip("Number of segments below ground level (downward traversal).")]
[SerializeField] private int _maxIterationsDown = 4;
// ─── Segment Bounds ──────────────────────────────────────────────────
[Header("Segment Bounds — Auto-Detection")]
[Tooltip("Renderer of the central column mesh. When assigned, segment height is read from its world bounds.")]
[SerializeField] private Renderer _centralColumnRenderer;
[Tooltip("Fallback segment height (m) used when no central column renderer is assigned.")]
[SerializeField] private float _segmentHeight = 4.82f;
[Tooltip("Y offset added to the segment height for fine-tuning snapping.")]
[SerializeField] private float _segmentHeightOffset = 0f;
// ─── Scroll Control ───────────────────────────────────────────────────
[Header("Scroll Speed Control")]
[Tooltip("Proportional gain: scroll speed (m/s) per metre of player deviation from the trigger center.")]
[SerializeField] private float _correctionGain = 4f;
[Tooltip("Maximum tower scroll speed in m/s.")]
[SerializeField] private float _maxScrollSpeed = 8f;
[Tooltip("SmoothDamp time for scroll speed transitions — prevents sudden jolts.")]
[SerializeField] private float _scrollSmoothTime = 0.1f;
// ─── Rotation Offset ─────────────────────────────────────────────────
[Header("Rotation Offset")]
[Tooltip("Y rotation applied to every spawned segment around its geometric bounds center.")]
[SerializeField] private float _segmentRotationOffsetY = 0f;
// ─── Hallucination / Word Data ────────────────────────────────────────
[Header("Hallucination — Word & Event Data")]
[Tooltip("ScriptableObject holding word + event data per slot. " +
"Recommended: create a StaircaseSequenceData asset and assign it here.")]
[SerializeField] private StaircaseSequenceData _sequenceData;
[Space]
[Tooltip("When true, the word for each slot is picked randomly from _randomWordPool instead of from _sequenceData.")]
[SerializeField] private bool _useRandomWords = false;
[Tooltip("Pool of words to pick from when _useRandomWords is enabled.")]
[SerializeField] private List<string> _randomWordPool = new();
[Tooltip("HallucinationEvent applied to all slots when _useRandomWords is enabled " +
"(slot-specific events from _sequenceData are ignored in random mode).")]
[SerializeField] private HallucinationEvent _randomModeEvent;
// ─── Debug ───────────────────────────────────────────────────────────
[Header("Debug")]
@@ -60,10 +124,23 @@ namespace Unheimlich.World.Environment
private StairState _currentState = StairState.Idle;
private Transform _playerTransform;
/// <summary>Single parent moved to scroll the entire tower at once.</summary>
private Transform _towerRoot;
/// <summary>World Y of the trigger collider center, cached on Awake.</summary>
private float _triggerCenterWorldY;
/// <summary>True when the player entered the trigger from below (travelling upward).</summary>
private bool _enteredFromBelow;
/// <summary>
/// towerRoot.localPosition.y threshold signalling stock exhaustion.
/// Negative when going up, positive when going down.
/// </summary>
private float _limitLocalY;
/// <summary>Current scroll speed (signed). Positive means the tower moves downward.</summary>
private float _scrollSpeed;
private float _scrollSpeedVelocity;
@@ -71,9 +148,7 @@ namespace Unheimlich.World.Environment
// ─── Properties ──────────────────────────────────────────────────────
private float EffectiveSegmentHeight => _segmentHeight + _segmentHeightOffset;
/// <summary>Déplacement max dépend de la direction.</summary>
private float EffectiveSegmentHeight => _segmentHeight + _segmentHeightOffset;
private float MaxTowerDisplacementUp => _maxIterationsUp * EffectiveSegmentHeight;
private float MaxTowerDisplacementDown => _maxIterationsDown * EffectiveSegmentHeight;
@@ -105,6 +180,9 @@ namespace Unheimlich.World.Environment
// ─── Trigger Events ───────────────────────────────────────────────────
/// <summary>
/// Called by StaircaseTriggerZone when the player enters the collider.
/// </summary>
public void OnPlayerEnteredTrigger()
{
if (_currentState != StairState.Idle && _currentState != StairState.Done) return;
@@ -120,7 +198,41 @@ namespace Unheimlich.World.Environment
_currentState = StairState.Active;
}
public void OnPlayerExitedTrigger() { }
/// <summary>
/// Called by StaircaseTriggerZone when the player exits the collider.
/// </summary>
public void OnPlayerExitedTrigger()
{
if (_currentState != StairState.Active && _currentState != StairState.Stopping) return;
bool towerHardlyMoved = _towerRoot != null &&
Mathf.Abs(_towerRoot.localPosition.y) < EffectiveSegmentHeight * 0.5f;
if (towerHardlyMoved)
{
_segmentsBySlot.TryGetValue(0, out GameObject baseSegment);
if (baseSegment != null)
{
baseSegment.transform.SetParent(transform, true);
baseSegment.transform.position = transform.position;
}
DestroyTower();
if (baseSegment != null)
_segmentsBySlot[0] = baseSegment;
_scrollSpeed = 0f;
_scrollSpeedVelocity = 0f;
_playerTransform = null;
_currentState = StairState.Idle;
}
else
{
_currentState = StairState.Stopping;
}
}
// ─── Tower Construction ───────────────────────────────────────────────
@@ -136,7 +248,6 @@ namespace Unheimlich.World.Environment
if (_enteredFromBelow)
{
// Slot 0 → +maxIterationsUp
for (int i = 0; i <= _maxIterationsUp; i++)
SpawnSegmentAtSlot(i);
@@ -144,7 +255,6 @@ namespace Unheimlich.World.Environment
}
else
{
// Slot 0 → -maxIterationsDown
for (int i = 0; i >= -_maxIterationsDown; i--)
SpawnSegmentAtSlot(i);
@@ -162,21 +272,14 @@ namespace Unheimlich.World.Environment
_segmentsBySlot.Clear();
}
/// <summary>
/// Choisit le bon prefab selon le slot :
/// - slot == +maxIterationsUp → _stairTopPrefab
/// - slot == -maxIterationsDown → _stairBottomPrefab
/// - tout le reste → _stairPrefab
/// Le slot 0 peut aussi réutiliser l'enfant existant si l'option est activée.
/// </summary>
private void SpawnSegmentAtSlot(int slotIndex)
{
if (_segmentsBySlot.ContainsKey(slotIndex)) return;
float localY = slotIndex * EffectiveSegmentHeight;
float localY = slotIndex * EffectiveSegmentHeight;
GameObject segment;
// ── Slot 0 : réutilisation de l'enfant existant ──────────────────
// Slot 0 — attempt to reuse the existing scene child.
if (slotIndex == 0 && _useExistingChildAsBase)
{
Transform existing = FindExistingStairChild();
@@ -187,12 +290,12 @@ namespace Unheimlich.World.Environment
segment.transform.localPosition = new Vector3(0f, localY, 0f);
segment.transform.localRotation = Quaternion.identity;
ApplyRotationOffsetFromCenter(segment);
ConfigureSegment(segment, slotIndex);
_segmentsBySlot[slotIndex] = segment;
return;
}
}
// ── Choix du prefab ───────────────────────────────────────────────
GameObject prefabToUse = PickPrefabForSlot(slotIndex);
if (prefabToUse == null) return;
@@ -200,26 +303,66 @@ namespace Unheimlich.World.Environment
segment.transform.localPosition = new Vector3(0f, localY, 0f);
segment.transform.localRotation = Quaternion.identity;
ApplyRotationOffsetFromCenter(segment);
ConfigureSegment(segment, slotIndex);
_segmentsBySlot[slotIndex] = segment;
}
/// <summary>
/// Renvoie le prefab approprié pour un slot donné.
/// Top = slot +maxIterationsUp (sommet absolu de la tour montante)
/// Bottom = slot -maxIterationsDown (fond absolu de la tour descendante)
/// Sinon = prefab standard.
/// </summary>
private GameObject PickPrefabForSlot(int slotIndex)
{
if (slotIndex == _maxIterationsUp && _stairTopPrefab != null)
return _stairTopPrefab;
if (slotIndex == -_maxIterationsDown && _stairBottomPrefab != null)
return _stairBottomPrefab;
if (slotIndex == _maxIterationsUp && _stairTopPrefab != null) return _stairTopPrefab;
if (slotIndex == -_maxIterationsDown && _stairBottomPrefab != null) return _stairBottomPrefab;
return _stairPrefab;
}
// ─── Segment Configuration ────────────────────────────────────────────
/// <summary>
/// Resolves the word and event for this slot then calls StaircaseSegment.Configure().
/// If the segment has no StaircaseSegment component, this is a silent no-op.
/// </summary>
private void ConfigureSegment(GameObject segmentGO, int slotIndex)
{
StaircaseSegment segment = segmentGO.GetComponent<StaircaseSegment>();
if (segment == null) return;
string word = ResolveWord(slotIndex);
HallucinationEvent evt = ResolveEvent(slotIndex);
segment.Configure(word, evt);
}
private string ResolveWord(int slotIndex)
{
if (_useRandomWords)
{
if (_randomWordPool == null || _randomWordPool.Count == 0)
return null;
return _randomWordPool[Random.Range(0, _randomWordPool.Count)];
}
return FindSlotData(slotIndex)?.Word;
}
private HallucinationEvent ResolveEvent(int slotIndex)
{
if (_useRandomWords)
return _randomModeEvent;
return FindSlotData(slotIndex)?.HallucinationEvent;
}
/// <summary>Returns the StaircaseSlotData matching slotIndex, or null if not found.</summary>
private StaircaseSlotData FindSlotData(int slotIndex)
{
if (_sequenceData == null) return null;
foreach (StaircaseSlotData data in _sequenceData.Slots)
{
if (data.SlotIndex == slotIndex)
return data;
}
return null;
}
// ─── Scroll ───────────────────────────────────────────────────────────
private void UpdateScrollSpeed()
@@ -282,7 +425,6 @@ namespace Unheimlich.World.Environment
_currentState = StairState.Done;
if (_towerRoot == null) return;
// Le segment qui doit rester visible après la fin = l'extrémité atteinte
int keepSlot = _enteredFromBelow ? _maxIterationsUp : -_maxIterationsDown;
_segmentsBySlot.TryGetValue(keepSlot, out GameObject keepSegment);
@@ -367,27 +509,23 @@ namespace Unheimlich.World.Environment
{
if (!_drawGizmos) return;
float h = EffectiveSegmentHeight;
float h = EffectiveSegmentHeight;
Vector3 o = transform.position;
// Tour montante — vert
Gizmos.color = new Color(0f, 1f, 0.5f, 0.35f);
for (int i = 1; i <= _maxIterationsUp; i++)
Gizmos.DrawWireCube(new Vector3(o.x, o.y + i * h + h * 0.5f, o.z), new Vector3(1.8f, h * 0.95f, 1.8f));
// Tour descendante — orange
Gizmos.color = new Color(1f, 0.6f, 0f, 0.25f);
for (int i = 1; i <= _maxIterationsDown; i++)
Gizmos.DrawWireCube(new Vector3(o.x, o.y - i * h + h * 0.5f, o.z), new Vector3(1.8f, h * 0.95f, 1.8f));
// Ligne du centre du trigger — cyan
Gizmos.color = Color.cyan;
Gizmos.DrawLine(
new Vector3(o.x - 2f, _triggerCenterWorldY, o.z),
new Vector3(o.x + 2f, _triggerCenterWorldY, o.z)
);
// Slot top — magenta
if (_stairTopPrefab != null)
{
Gizmos.color = Color.magenta;
@@ -397,7 +535,6 @@ namespace Unheimlich.World.Environment
);
}
// Slot bottom — rouge
if (_stairBottomPrefab != null)
{
Gizmos.color = Color.red;