Smooth 3C & simplify hallucination trigger
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unheimlich.Core.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for components that react to an AudioSource's playback state.
|
||||
/// Supports three fire conditions: OnPlay, OnFinish, and OnTimeReached.
|
||||
/// Subclasses implement Execute() to define what happens when the condition is met.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(AudioSource))]
|
||||
public abstract class AudioTriggerBase : MonoBehaviour
|
||||
{
|
||||
public enum FireCondition
|
||||
{
|
||||
/// <summary>Fire as soon as the audio starts playing.</summary>
|
||||
OnPlay,
|
||||
/// <summary>Fire when the audio clip finishes.</summary>
|
||||
OnFinish,
|
||||
/// <summary>Fire when playback reaches a specific timestamp.</summary>
|
||||
OnTimeReached
|
||||
}
|
||||
|
||||
[Header("Audio Trigger")]
|
||||
[Tooltip("When to fire the effect")]
|
||||
[SerializeField] protected FireCondition _fireCondition = FireCondition.OnPlay;
|
||||
|
||||
[Tooltip("Playback time in seconds that triggers the effect (OnTimeReached only)")]
|
||||
[SerializeField] protected float _triggerTime = 1f;
|
||||
|
||||
[Header("Behaviour")]
|
||||
[Tooltip("Prevent firing more than once per session")]
|
||||
[SerializeField] protected bool _fireOnlyOnce = true;
|
||||
|
||||
[Header("Debug")]
|
||||
[SerializeField] protected bool _debugMode;
|
||||
|
||||
protected AudioSource AudioSource { get; private set; }
|
||||
|
||||
private bool _hasFired;
|
||||
private bool _isMonitoring;
|
||||
private Coroutine _finishWatcher;
|
||||
|
||||
protected virtual void Awake()
|
||||
{
|
||||
AudioSource = GetComponent<AudioSource>();
|
||||
}
|
||||
|
||||
protected virtual void OnEnable()
|
||||
{
|
||||
_hasFired = false;
|
||||
_isMonitoring = false;
|
||||
|
||||
if (AudioSource != null && AudioSource.isPlaying)
|
||||
StartMonitoring();
|
||||
}
|
||||
|
||||
protected virtual void OnDisable()
|
||||
{
|
||||
StopMonitoring();
|
||||
}
|
||||
|
||||
// ─── Public API ────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Notify this component that the AudioSource has started playing.
|
||||
/// Wire this to whatever code or event starts the AudioSource.
|
||||
/// </summary>
|
||||
public void OnAudioPlay()
|
||||
{
|
||||
StartMonitoring();
|
||||
|
||||
if (_fireCondition == FireCondition.OnPlay)
|
||||
TryExecute();
|
||||
}
|
||||
|
||||
/// <summary>Resets the fired state so this can fire again.</summary>
|
||||
public void ResetFired()
|
||||
{
|
||||
_hasFired = false;
|
||||
StopMonitoring();
|
||||
}
|
||||
|
||||
// ─── Monitoring ────────────────────────────────────────────────────────────
|
||||
|
||||
private void StartMonitoring()
|
||||
{
|
||||
_isMonitoring = true;
|
||||
|
||||
if (_fireCondition == FireCondition.OnFinish)
|
||||
{
|
||||
StopMonitoring();
|
||||
_finishWatcher = StartCoroutine(WatchForFinish());
|
||||
}
|
||||
}
|
||||
|
||||
private void StopMonitoring()
|
||||
{
|
||||
_isMonitoring = false;
|
||||
|
||||
if (_finishWatcher != null)
|
||||
{
|
||||
StopCoroutine(_finishWatcher);
|
||||
_finishWatcher = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!_isMonitoring || (_fireOnlyOnce && _hasFired))
|
||||
return;
|
||||
|
||||
if (_fireCondition != FireCondition.OnTimeReached)
|
||||
return;
|
||||
|
||||
if (AudioSource != null && AudioSource.isPlaying && AudioSource.time >= _triggerTime)
|
||||
TryExecute();
|
||||
}
|
||||
|
||||
// Uses a coroutine instead of Update polling so the finish detection is frame-accurate
|
||||
// even for very short clips that could end between two Update calls.
|
||||
private IEnumerator WatchForFinish()
|
||||
{
|
||||
if (AudioSource == null)
|
||||
yield break;
|
||||
|
||||
// Wait until the clip actually has a length to work with
|
||||
yield return null;
|
||||
|
||||
if (AudioSource.clip != null)
|
||||
yield return new WaitForSeconds(AudioSource.clip.length);
|
||||
else
|
||||
yield return new WaitUntil(() => !AudioSource.isPlaying);
|
||||
|
||||
TryExecute();
|
||||
_finishWatcher = null;
|
||||
}
|
||||
|
||||
// ─── Internal ──────────────────────────────────────────────────────────────
|
||||
|
||||
private void TryExecute()
|
||||
{
|
||||
if (_fireOnlyOnce && _hasFired)
|
||||
return;
|
||||
|
||||
_hasFired = true;
|
||||
StopMonitoring();
|
||||
Execute();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the fire condition is met. Implement the payload here.
|
||||
/// </summary>
|
||||
protected abstract void Execute();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 16d04d63a4903d54dbd0eee78c2ba121
|
||||
@@ -22,23 +22,23 @@ namespace Unheimlich.Core.Interaction
|
||||
_playerCamera = GetComponent<Camera>();
|
||||
}
|
||||
|
||||
private bool _didHit;
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (RaycastHelper.ThrottledRaycastComponent(
|
||||
ref _raycastTimer,
|
||||
_didHit = RaycastHelper.ThrottledRaycastComponent(
|
||||
ref _raycastTimer,
|
||||
_raycastInterval,
|
||||
_playerCamera,
|
||||
_interactionRange,
|
||||
_interactableLayers,
|
||||
out InteractableBase interactable))
|
||||
{
|
||||
_playerCamera,
|
||||
_interactionRange,
|
||||
_interactableLayers,
|
||||
out InteractableBase interactable);
|
||||
|
||||
if (_didHit)
|
||||
_currentInteractable = interactable;
|
||||
}
|
||||
else if (_raycastTimer == 0f)
|
||||
{
|
||||
else if (!_didHit && _raycastTimer <= 0f)
|
||||
_currentInteractable = null;
|
||||
}
|
||||
|
||||
|
||||
HandleInteractionInput();
|
||||
}
|
||||
|
||||
|
||||
@@ -85,8 +85,6 @@ namespace Unheimlich.Core.Interaction
|
||||
|
||||
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)
|
||||
@@ -103,19 +101,19 @@ namespace Unheimlich.Core.Interaction
|
||||
_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).
|
||||
_hasCompletedMergeOnce = false; // reset so MergeOnce objects start fresh each session
|
||||
_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.
|
||||
// Resets _hasCompletedMergeOnce at the very start of each Play Mode session so
|
||||
// merged objects start fresh without requiring a 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.
|
||||
// Finding all instances isn't possible from a static context with SubsystemRegistration.
|
||||
// The reset is handled per-instance in OnEnable via _hasCompletedMergeOnce being
|
||||
// re-initialized when the scene reloads. For Enter Play Mode without scene reload,
|
||||
// ResetSessionState() is called from OnEnable and now correctly clears the flag.
|
||||
}
|
||||
|
||||
void Update()
|
||||
|
||||
Reference in New Issue
Block a user