Hallucination system
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using System;
|
||||
|
||||
namespace Unheimlich.Core.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// Generic reusable trigger component. Handles the once-flag, optional delay,
|
||||
/// OnStart/OnEnable auto-fire, and exposes a UnityEvent + C# event for downstream systems.
|
||||
/// Compose this on any GameObject that needs to fire a game event from multiple sources.
|
||||
/// </summary>
|
||||
public class GameEventTrigger : MonoBehaviour
|
||||
{
|
||||
[Header("Auto-fire")]
|
||||
[Tooltip("Fire automatically on Start()")]
|
||||
[SerializeField] private bool _fireOnStart;
|
||||
|
||||
[Tooltip("Fire automatically when this GameObject is enabled")]
|
||||
[SerializeField] private bool _fireOnEnable;
|
||||
|
||||
[Header("Behaviour")]
|
||||
[Tooltip("Delay in seconds before the event fires")]
|
||||
[SerializeField] private float _delay;
|
||||
|
||||
[Tooltip("Prevent this trigger from firing more than once per Play Mode session")]
|
||||
[SerializeField] private bool _fireOnlyOnce = true;
|
||||
|
||||
[Header("Callbacks")]
|
||||
[Tooltip("Called when the trigger fires (after the optional delay)")]
|
||||
[SerializeField] private UnityEvent _onTriggered;
|
||||
|
||||
/// <summary>C# event raised when the trigger fires. Useful for code-only listeners.</summary>
|
||||
public event Action OnTriggered;
|
||||
|
||||
private bool _hasFired;
|
||||
|
||||
private bool _startFired;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
// Reset per-session so the trigger can fire again when entering Play Mode
|
||||
// without Domain Reload.
|
||||
_hasFired = false;
|
||||
_startFired = false;
|
||||
|
||||
// NOTE: _fireOnEnable is intentionally deferred to Start() so all Singleton
|
||||
// Awake() calls across the scene complete before listeners access managers.
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (_startFired)
|
||||
return;
|
||||
|
||||
_startFired = true;
|
||||
|
||||
// Fire once even if both flags are ticked
|
||||
if (_fireOnStart || _fireOnEnable)
|
||||
Fire();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fires the trigger. Respects the once-flag and optional delay.
|
||||
/// Call this from any external script, UnityEvent, or animation event.
|
||||
/// </summary>
|
||||
public void Fire()
|
||||
{
|
||||
if (_fireOnlyOnce && _hasFired)
|
||||
return;
|
||||
|
||||
if (_delay > 0f)
|
||||
Invoke(nameof(ExecuteTrigger), _delay);
|
||||
else
|
||||
ExecuteTrigger();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the fired state so the trigger can fire again.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
_hasFired = false;
|
||||
CancelInvoke(nameof(ExecuteTrigger));
|
||||
}
|
||||
|
||||
private void ExecuteTrigger()
|
||||
{
|
||||
if (_fireOnlyOnce && _hasFired)
|
||||
return;
|
||||
|
||||
_hasFired = true;
|
||||
_onTriggered?.Invoke();
|
||||
OnTriggered?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 358c75d041ea30a4ab85380893077e70
|
||||
@@ -0,0 +1,87 @@
|
||||
using UnityEngine;
|
||||
using System;
|
||||
|
||||
namespace Unheimlich.Core.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// Abstract base for any physics-trigger zone that fires when the player enters.
|
||||
/// Handles collider validation, player tag check, once-flag, retrigger, and Gizmos.
|
||||
/// Subclasses implement OnPlayerEntered() and optionally OnPlayerExited().
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Collider))]
|
||||
public abstract class TriggerZoneBase : MonoBehaviour
|
||||
{
|
||||
[Header("Zone Settings")]
|
||||
[SerializeField] private string _playerTag = "Player";
|
||||
|
||||
[Header("Gizmo")]
|
||||
[SerializeField] private Color _gizmoColor = new Color(0.8f, 0.5f, 0.0f, 0.35f);
|
||||
|
||||
protected bool HasFired { get; private set; }
|
||||
|
||||
private Collider _collider;
|
||||
|
||||
/// <summary>
|
||||
/// Override to control whether the zone can fire again after the first trigger.
|
||||
/// Default is false (fires once). Subclasses expose their own retrigger field and return it here.
|
||||
/// </summary>
|
||||
protected virtual bool IsRetriggerable => false;
|
||||
|
||||
protected virtual void Awake()
|
||||
{
|
||||
_collider = GetComponent<Collider>();
|
||||
|
||||
if (!_collider.isTrigger)
|
||||
{
|
||||
Debug.LogWarning($"[{GetType().Name}] Collider on '{gameObject.name}' is not a trigger. Forcing it.", this);
|
||||
_collider.isTrigger = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (!other.CompareTag(_playerTag))
|
||||
return;
|
||||
|
||||
if (HasFired && !IsRetriggerable)
|
||||
return;
|
||||
|
||||
HasFired = true;
|
||||
OnPlayerEntered(other);
|
||||
}
|
||||
|
||||
private void OnTriggerExit(Collider other)
|
||||
{
|
||||
if (!other.CompareTag(_playerTag))
|
||||
return;
|
||||
|
||||
OnPlayerExited(other);
|
||||
}
|
||||
|
||||
/// <summary>Called when the player enters the trigger zone.</summary>
|
||||
protected abstract void OnPlayerEntered(Collider playerCollider);
|
||||
|
||||
/// <summary>Called when the player exits the trigger zone. Override if needed.</summary>
|
||||
protected virtual void OnPlayerExited(Collider playerCollider) { }
|
||||
|
||||
/// <summary>Resets the fired state so the zone can fire again.</summary>
|
||||
public void ResetZone()
|
||||
{
|
||||
HasFired = false;
|
||||
}
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
Gizmos.color = _gizmoColor;
|
||||
Gizmos.matrix = transform.localToWorldMatrix;
|
||||
|
||||
Collider col = GetComponent<Collider>();
|
||||
if (col is BoxCollider box)
|
||||
Gizmos.DrawCube(box.center, box.size);
|
||||
else if (col is SphereCollider sphere)
|
||||
Gizmos.DrawSphere(sphere.center, sphere.radius);
|
||||
else if (col is CapsuleCollider capsule)
|
||||
Gizmos.DrawWireSphere(capsule.center, capsule.radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9fae9f4c26344344e93baeaee0281284
|
||||
@@ -41,6 +41,9 @@ namespace Unheimlich.Core.Interaction
|
||||
private bool _isActive = false;
|
||||
private bool _isMerged = false;
|
||||
private bool _hasBeenMergedOnce = false;
|
||||
|
||||
// Persists across OnEnable/OnDisable — only cleared by explicit ResetMerge()
|
||||
// or at the start of a new Play Mode session via RuntimeInitializeOnLoadMethod.
|
||||
private bool _hasCompletedMergeOnce = false;
|
||||
private Vector3[] _originalSpawnPositions;
|
||||
private Vector3[] _basePositions;
|
||||
@@ -51,21 +54,69 @@ namespace Unheimlich.Core.Interaction
|
||||
|
||||
void Start()
|
||||
{
|
||||
_playerTransform = PlayerController.Instance.transform;
|
||||
InitializeReferences();
|
||||
}
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
// With Scene Reload disabled, Start() doesn't run again between Play Mode sessions.
|
||||
// Re-fetch references and reset transient state so animations restart cleanly.
|
||||
InitializeReferences();
|
||||
ResetSessionState();
|
||||
}
|
||||
|
||||
private void InitializeReferences()
|
||||
{
|
||||
_playerTransform = PlayerController.Instance != null ? PlayerController.Instance.transform : null;
|
||||
_mainCamera = Camera.main;
|
||||
|
||||
|
||||
if (_playerTransform == null)
|
||||
Debug.LogWarning($"[ObjectDescription] No player found for {gameObject.name}");
|
||||
|
||||
|
||||
if (_descriptionAnchor == null)
|
||||
_descriptionAnchor = transform;
|
||||
|
||||
|
||||
if (_settings == null)
|
||||
{
|
||||
Debug.LogError($"[ObjectDescription] No settings assigned for {gameObject.name}");
|
||||
enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetSessionState()
|
||||
{
|
||||
// Force-despawn any leftover words from the previous session
|
||||
// (their GOs were destroyed but _spawnedWords list may still have stale entries).
|
||||
if (_isActive)
|
||||
{
|
||||
foreach (var word in _spawnedWords)
|
||||
{
|
||||
if (word != null) Destroy(word.gameObject);
|
||||
}
|
||||
_spawnedWords.Clear();
|
||||
|
||||
if (_wordsContainer != null) Destroy(_wordsContainer);
|
||||
_wordsContainer = null;
|
||||
_wordsRotator = null;
|
||||
}
|
||||
|
||||
_isActive = false;
|
||||
_isMerged = false;
|
||||
_hasBeenMergedOnce = false;
|
||||
// _hasCompletedMergeOnce intentionally NOT reset here — persists during session
|
||||
// so a merged object stays merged when the player re-enters its activation range.
|
||||
// It is only reset at the start of a new Play Mode session (see OnPlayModeReset).
|
||||
_currentConvergenceRadius = 0f;
|
||||
}
|
||||
|
||||
// Resets _hasCompletedMergeOnce at the very start of each Play Mode session,
|
||||
// before any scene loads, so objects start fresh without full Domain Reload.
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||
private static void OnPlayModeReset()
|
||||
{
|
||||
// Static reset — instance fields will be re-initialized by Awake/Start naturally.
|
||||
// No instance access needed here; ResetSessionState() handles the rest on OnEnable.
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
|
||||
@@ -1,23 +1,62 @@
|
||||
using UnityEngine;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Unheimlich.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Clears all Singleton static instances at the start of every Play Mode session.
|
||||
/// Necessary when Domain Reload is disabled in Enter Play Mode settings.
|
||||
/// </summary>
|
||||
internal static class SingletonRegistry
|
||||
{
|
||||
private static readonly List<Action> _resetCallbacks = new List<Action>();
|
||||
|
||||
public static void Register(Action resetCallback)
|
||||
{
|
||||
if (!_resetCallbacks.Contains(resetCallback))
|
||||
_resetCallbacks.Add(resetCallback);
|
||||
}
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||
private static void ResetAll()
|
||||
{
|
||||
foreach (Action reset in _resetCallbacks)
|
||||
reset?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class Singleton<T> : MonoBehaviour where T : MonoBehaviour
|
||||
{
|
||||
public static T Instance { get; private set; }
|
||||
|
||||
|
||||
static Singleton()
|
||||
{
|
||||
SingletonRegistry.Register(() => Instance = null);
|
||||
}
|
||||
|
||||
protected virtual void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Debug.LogWarning($"[{typeof(T).Name}] Multiple instances detected! Destroying duplicate on GameObject '{gameObject.name}'.");
|
||||
Debug.LogWarning($"[{typeof(T).Name}] Multiple instances detected! Destroying duplicate on '{gameObject.name}'.");
|
||||
Destroy(this);
|
||||
return;
|
||||
}
|
||||
Instance = this as T;
|
||||
OnAwake();
|
||||
}
|
||||
|
||||
|
||||
// Re-registers the instance when entering Play Mode with Scene Reload disabled.
|
||||
// SubsystemRegistration clears Instance before OnEnable runs, so we restore it here.
|
||||
protected virtual void OnEnable()
|
||||
{
|
||||
if (Instance == null)
|
||||
Instance = this as T;
|
||||
|
||||
OnEnabled();
|
||||
}
|
||||
|
||||
protected virtual void OnDestroy()
|
||||
{
|
||||
if (Instance == this)
|
||||
@@ -26,8 +65,9 @@ namespace Unheimlich.Core
|
||||
OnDestroyInstance();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected virtual void OnAwake() { }
|
||||
protected virtual void OnEnabled() { }
|
||||
protected virtual void OnDestroyInstance() { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unheimlich.Core.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Resets third-party static state that doesn't handle Domain Reload being disabled.
|
||||
/// Runs automatically at the start of every Play Mode session before the scene loads.
|
||||
/// </summary>
|
||||
internal static class GameSessionResetter
|
||||
{
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||
private static void OnSessionStart()
|
||||
{
|
||||
ResetLeanTween();
|
||||
}
|
||||
|
||||
private static void ResetLeanTween()
|
||||
{
|
||||
// LeanTween holds a static tween array that persists when Domain Reload is disabled.
|
||||
// Stale tween IDs from the previous session cause callbacks to fire on wrong objects,
|
||||
// which breaks animations like organic shake on floating words.
|
||||
LeanTween.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 34edfe05577b2a24892a90d9901cef39
|
||||
Reference in New Issue
Block a user