Smooth 3C & simplify hallucination trigger

This commit is contained in:
Ferlat Thibaud
2026-03-22 18:03:21 +01:00
parent 0d968f75ab
commit 1589a7a543
24 changed files with 1318 additions and 621 deletions
@@ -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