chore: Project Restructuring & Theatre
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unheimlich.Core.Audio
|
||||
{
|
||||
[CreateAssetMenu(fileName = "AudioEmitterData", menuName = "Unheimlich/Audio/Audio Emitter Data")]
|
||||
public class AudioEmitterData : ScriptableObject
|
||||
{
|
||||
[Header("Emitter Info")]
|
||||
public AudioEmitterType EmitterType = AudioEmitterType.Generic;
|
||||
public string DisplayName = "Generic";
|
||||
|
||||
[Header("Procedural Audio Settings")]
|
||||
public float BaseFrequency = 440f;
|
||||
public float FrequencyVariation = 50f;
|
||||
public float VolumeMin = 0.3f;
|
||||
public float VolumeMax = 0.7f;
|
||||
public float DurationMin = 0.5f;
|
||||
public float DurationMax = 2f;
|
||||
|
||||
[Header("Harmonics (for complex sounds)")]
|
||||
public float[] HarmonicMultipliers = { 1f };
|
||||
public float[] HarmonicAmplitudes = { 1f };
|
||||
|
||||
[Header("Noise Settings")]
|
||||
[Range(0f, 1f)]
|
||||
public float NoiseAmount = 0f;
|
||||
|
||||
[Header("Vibration Settings")]
|
||||
public bool EnableVibration = true;
|
||||
public float VibrationIntensityMin = 0.1f;
|
||||
public float VibrationIntensityMax = 0.5f;
|
||||
public float VibrationFrequency = 10f;
|
||||
|
||||
[Header("Distance Settings")]
|
||||
public float MaxDistance = 10f;
|
||||
public AnimationCurve DistanceAttenuation = AnimationCurve.EaseInOut(0f, 1f, 1f, 0f);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c20752cb7065e844aaaedf606c4df2db
|
||||
@@ -0,0 +1,434 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using Unheimlich.Core.Utils;
|
||||
using Unheimlich.World.CameraSystem;
|
||||
|
||||
namespace Unheimlich.Core.Audio
|
||||
{
|
||||
public enum AudioEmitterType
|
||||
{
|
||||
Generic,
|
||||
Wall,
|
||||
Wood,
|
||||
Metal,
|
||||
Glass,
|
||||
Water,
|
||||
Stone,
|
||||
Mechanical,
|
||||
Electrical,
|
||||
Organic
|
||||
}
|
||||
|
||||
public class AudioListenable : MonoBehaviour
|
||||
{
|
||||
// String constants
|
||||
private const string CONTINUOUS_AUDIO_SOURCE_NAME = "Continuous Audio Source";
|
||||
|
||||
// Volume multipliers for concentration system
|
||||
private const float BASELINE_VOLUME_MULTIPLIER = 0.1f;
|
||||
private const float FULL_VOLUME_MULTIPLIER = 1.5f;
|
||||
private const float CONCENTRATION_FADE_DURATION = 1f;
|
||||
private const float RELEASE_FADE_DURATION = 0.5f;
|
||||
|
||||
[Header("Emitter Settings")]
|
||||
[SerializeField] private AudioEmitterType _emitterType = AudioEmitterType.Generic;
|
||||
[SerializeField] private bool _useProceduralAudio = true;
|
||||
|
||||
[Header("Continuous Emission")]
|
||||
[SerializeField] private bool _emitContinuously = true;
|
||||
[SerializeField] private float _continuousBaseVolume = 0.1f;
|
||||
[SerializeField] private float _hearingRangeMin = 4f;
|
||||
[SerializeField] private float _hearingRangeMax = 8f;
|
||||
|
||||
[Header("Listening Enhancement")]
|
||||
[SerializeField] private float _reverbWetMix = 0.5f;
|
||||
|
||||
[Header("Static Audio")]
|
||||
[SerializeField] private AudioClip _staticAudioClip;
|
||||
[SerializeField] private bool _loopStaticAudio = true;
|
||||
|
||||
[Header("Procedural Settings")]
|
||||
[SerializeField] private float _baseFrequency = 440f;
|
||||
[SerializeField] private float _frequencyVariation = 50f;
|
||||
[SerializeField] private float _volumeMin = 0.3f;
|
||||
[SerializeField] private float _volumeMax = 0.7f;
|
||||
[SerializeField] private float _durationMin = 0.5f;
|
||||
[SerializeField] private float _durationMax = 2f;
|
||||
|
||||
[Header("Distance Attenuation")]
|
||||
[SerializeField] private float _maxDistance = 10f;
|
||||
[SerializeField] private AnimationCurve _distanceAttenuation = AnimationCurve.EaseInOut(0f, 1f, 1f, 0f);
|
||||
|
||||
[Header("Visual Feedback")]
|
||||
[SerializeField] private ParticleSystem _emitterParticles;
|
||||
|
||||
[Header("Camera Settings")]
|
||||
[SerializeField] private CameraPanSettings _cameraPanSettings;
|
||||
|
||||
private AudioSource _audioSource;
|
||||
private AudioSource _continuousAudioSource;
|
||||
private AudioReverbFilter _reverbFilter;
|
||||
private bool _isCurrentlyListening;
|
||||
private float _listeningTimer;
|
||||
private float _nextProceduralEventTime;
|
||||
private Vector3 _listenerPosition;
|
||||
private bool _hasBeenTapped;
|
||||
private AudioClip _continuousClip;
|
||||
private Coroutine _volumeFadeCoroutine;
|
||||
private Coroutine _startListeningCoroutine;
|
||||
private int _concentrationFadeTweenId = -1;
|
||||
private int _crossfadeTweenId = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this emitter has been tapped during the current listening session.
|
||||
/// Used to prevent multiple taps on the same emitter.
|
||||
/// </summary>
|
||||
public bool HasBeenTapped => _hasBeenTapped;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the camera pan settings for this emitter, or null if not set.
|
||||
/// </summary>
|
||||
public CameraPanSettings CameraPanSettings => _cameraPanSettings;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_audioSource = GetComponent<AudioSource>();
|
||||
if (_audioSource == null)
|
||||
_audioSource = gameObject.AddComponent<AudioSource>();
|
||||
|
||||
_audioSource.spatialBlend = 1f;
|
||||
_audioSource.maxDistance = _maxDistance;
|
||||
_audioSource.rolloffMode = AudioRolloffMode.Custom;
|
||||
_audioSource.playOnAwake = false;
|
||||
|
||||
_reverbFilter = gameObject.AddComponent<AudioReverbFilter>();
|
||||
_reverbFilter.reverbPreset = AudioReverbPreset.Off;
|
||||
_reverbFilter.dryLevel = 0f;
|
||||
_reverbFilter.room = -10000;
|
||||
|
||||
GameObject continuousSourceObject = new GameObject(CONTINUOUS_AUDIO_SOURCE_NAME);
|
||||
continuousSourceObject.transform.SetParent(transform);
|
||||
continuousSourceObject.transform.localPosition = Vector3.zero;
|
||||
_continuousAudioSource = continuousSourceObject.AddComponent<AudioSource>();
|
||||
_continuousAudioSource.spatialBlend = 1f;
|
||||
_continuousAudioSource.maxDistance = _hearingRangeMax;
|
||||
_continuousAudioSource.minDistance = _hearingRangeMin;
|
||||
_continuousAudioSource.rolloffMode = AudioRolloffMode.Linear;
|
||||
_continuousAudioSource.playOnAwake = false;
|
||||
_continuousAudioSource.loop = true;
|
||||
|
||||
if (_emitterParticles != null)
|
||||
{
|
||||
_emitterParticles.Stop();
|
||||
var emission = _emitterParticles.emission;
|
||||
emission.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (_emitContinuously && _useProceduralAudio)
|
||||
{
|
||||
StartContinuousEmission();
|
||||
}
|
||||
}
|
||||
|
||||
private void StartContinuousEmission()
|
||||
{
|
||||
float frequency = _baseFrequency + Random.Range(-_frequencyVariation * 0.5f, _frequencyVariation * 0.5f);
|
||||
_continuousClip = GenerateProceduralAudioClip(frequency, 10f);
|
||||
|
||||
_continuousAudioSource.clip = _continuousClip;
|
||||
_continuousAudioSource.volume = _continuousBaseVolume * BASELINE_VOLUME_MULTIPLIER;
|
||||
_continuousAudioSource.loop = true;
|
||||
_continuousAudioSource.Play();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the concentration state and fades the emitter volume accordingly.
|
||||
/// When concentrating: fades from 30% to 100% over 1 second.
|
||||
/// When releasing: fades from 100% to 30% over 0.5 seconds.
|
||||
/// </summary>
|
||||
public void SetConcentrating(bool concentrating)
|
||||
{
|
||||
TweenHelper.CancelTween(ref _concentrationFadeTweenId);
|
||||
|
||||
if (_continuousAudioSource == null)
|
||||
return;
|
||||
|
||||
float baseVolume = _continuousBaseVolume;
|
||||
float fromMultiplier = concentrating ? BASELINE_VOLUME_MULTIPLIER : FULL_VOLUME_MULTIPLIER;
|
||||
float toMultiplier = concentrating ? FULL_VOLUME_MULTIPLIER : BASELINE_VOLUME_MULTIPLIER;
|
||||
float duration = concentrating ? CONCENTRATION_FADE_DURATION : RELEASE_FADE_DURATION;
|
||||
|
||||
float fromVolume = baseVolume * fromMultiplier;
|
||||
float toVolume = baseVolume * toMultiplier;
|
||||
|
||||
_concentrationFadeTweenId = TweenHelper.FadeVolume(
|
||||
_continuousAudioSource,
|
||||
toVolume,
|
||||
duration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the listening sequence for this emitter.
|
||||
/// Blocked if the emitter has already been tapped during this session.
|
||||
/// </summary>
|
||||
public void StartListening(Vector3 listenerPosition)
|
||||
{
|
||||
// Prevent multiple taps on the same emitter
|
||||
if (_hasBeenTapped)
|
||||
return;
|
||||
|
||||
if (_startListeningCoroutine != null)
|
||||
StopCoroutine(_startListeningCoroutine);
|
||||
|
||||
_startListeningCoroutine = StartCoroutine(StartListeningSequence(listenerPosition));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Coroutine that handles the full listening sequence:
|
||||
/// - Crossfades from continuous to procedural audio
|
||||
/// - Activates particles
|
||||
/// - Marks emitter as tapped (prevents re-triggering)
|
||||
/// - Starts vibration
|
||||
/// </summary>
|
||||
private IEnumerator StartListeningSequence(Vector3 listenerPosition)
|
||||
{
|
||||
_isCurrentlyListening = true;
|
||||
_listenerPosition = listenerPosition;
|
||||
_listeningTimer = 0f;
|
||||
_nextProceduralEventTime = 0f;
|
||||
|
||||
// Mark as tapped to prevent re-triggering
|
||||
_hasBeenTapped = true;
|
||||
|
||||
if (_volumeFadeCoroutine != null)
|
||||
StopCoroutine(_volumeFadeCoroutine);
|
||||
|
||||
TweenHelper.CancelTween(ref _crossfadeTweenId);
|
||||
|
||||
float crossfadeDuration = 1f;
|
||||
|
||||
if (_continuousAudioSource != null)
|
||||
{
|
||||
_crossfadeTweenId = TweenHelper.FadeVolume(
|
||||
_continuousAudioSource,
|
||||
0f,
|
||||
crossfadeDuration,
|
||||
() =>
|
||||
{
|
||||
if (_continuousAudioSource != null)
|
||||
_continuousAudioSource.Stop();
|
||||
});
|
||||
}
|
||||
|
||||
if (_emitterParticles != null)
|
||||
{
|
||||
var emission = _emitterParticles.emission;
|
||||
emission.enabled = true;
|
||||
_emitterParticles.Play();
|
||||
}
|
||||
|
||||
EnableReverb(true);
|
||||
|
||||
yield return new WaitForSeconds(crossfadeDuration);
|
||||
|
||||
if (_useProceduralAudio)
|
||||
{
|
||||
PlayProceduralSound();
|
||||
}
|
||||
else if (_staticAudioClip != null)
|
||||
{
|
||||
_audioSource.clip = _staticAudioClip;
|
||||
_audioSource.loop = _loopStaticAudio;
|
||||
_audioSource.Play();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the listening mode and resets the emitter state.
|
||||
/// This is the only place where _hasBeenTapped is reset to false,
|
||||
/// allowing the emitter to be used again in a future listening session.
|
||||
/// </summary>
|
||||
public void StopListening(bool cancelConcentrationFade = true)
|
||||
{
|
||||
bool wasBeenTapped = _hasBeenTapped;
|
||||
|
||||
_isCurrentlyListening = false;
|
||||
_hasBeenTapped = false;
|
||||
|
||||
if (_volumeFadeCoroutine != null)
|
||||
StopCoroutine(_volumeFadeCoroutine);
|
||||
|
||||
if (_startListeningCoroutine != null)
|
||||
StopCoroutine(_startListeningCoroutine);
|
||||
|
||||
if (cancelConcentrationFade)
|
||||
TweenHelper.CancelTween(ref _concentrationFadeTweenId);
|
||||
|
||||
if (_audioSource != null && _audioSource.isPlaying)
|
||||
_audioSource.Stop();
|
||||
|
||||
if (_emitterParticles != null && wasBeenTapped)
|
||||
{
|
||||
_emitterParticles.Stop();
|
||||
var emission = _emitterParticles.emission;
|
||||
emission.enabled = false;
|
||||
}
|
||||
|
||||
if (_continuousAudioSource != null && !_continuousAudioSource.isPlaying && gameObject.activeInHierarchy)
|
||||
{
|
||||
StartContinuousEmission();
|
||||
}
|
||||
|
||||
EnableReverb(false);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
|
||||
if (!_isCurrentlyListening)
|
||||
return;
|
||||
|
||||
_listeningTimer += Time.deltaTime;
|
||||
|
||||
if (_useProceduralAudio && _listeningTimer >= _nextProceduralEventTime)
|
||||
{
|
||||
PlayProceduralSound();
|
||||
_nextProceduralEventTime = _listeningTimer + Random.Range(_durationMin, _durationMax);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void EnableReverb(bool enable)
|
||||
{
|
||||
if (_reverbFilter == null)
|
||||
return;
|
||||
|
||||
if (enable)
|
||||
{
|
||||
_reverbFilter.reverbPreset = AudioReverbPreset.Room;
|
||||
_reverbFilter.dryLevel = Mathf.Lerp(0f, -1000f, 1f - _reverbWetMix);
|
||||
_reverbFilter.room = Mathf.Lerp(-1000f, -100f, _reverbWetMix);
|
||||
}
|
||||
else
|
||||
{
|
||||
_reverbFilter.reverbPreset = AudioReverbPreset.Off;
|
||||
_reverbFilter.dryLevel = 0f;
|
||||
_reverbFilter.room = -10000;
|
||||
}
|
||||
}
|
||||
|
||||
private void PlayProceduralSound()
|
||||
{
|
||||
float distance = Vector3.Distance(_listenerPosition, transform.position);
|
||||
float distanceFactor = Mathf.Clamp01(1f - (distance / _maxDistance));
|
||||
float attenuation = _distanceAttenuation.Evaluate(distanceFactor);
|
||||
|
||||
float frequency = _baseFrequency + Random.Range(-_frequencyVariation, _frequencyVariation);
|
||||
float volume = Mathf.Lerp(_volumeMin, _volumeMax, Random.value) * attenuation;
|
||||
|
||||
AudioClip proceduralClip = GenerateProceduralAudioClip(frequency, Random.Range(_durationMin, _durationMax));
|
||||
|
||||
_audioSource.clip = proceduralClip;
|
||||
_audioSource.volume = volume;
|
||||
_audioSource.loop = false;
|
||||
_audioSource.Play();
|
||||
}
|
||||
|
||||
private AudioClip GenerateProceduralAudioClip(float frequency, float duration)
|
||||
{
|
||||
int sampleRate = 44100;
|
||||
int sampleCount = Mathf.RoundToInt(sampleRate * duration);
|
||||
|
||||
AudioClip clip = AudioClip.Create("ProceduralSound", sampleCount, 1, sampleRate, false);
|
||||
float[] samples = new float[sampleCount];
|
||||
|
||||
for (int i = 0; i < sampleCount; i++)
|
||||
{
|
||||
float t = (float)i / sampleRate;
|
||||
float envelope = Mathf.Sin(Mathf.PI * t / duration);
|
||||
|
||||
samples[i] = GenerateSampleForType(frequency, t, _emitterType) * envelope;
|
||||
}
|
||||
|
||||
clip.SetData(samples, 0);
|
||||
return clip;
|
||||
}
|
||||
|
||||
private float GenerateSampleForType(float frequency, float time, AudioEmitterType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case AudioEmitterType.Metal:
|
||||
return Mathf.Sin(2f * Mathf.PI * frequency * time) * 0.5f +
|
||||
Mathf.Sin(2f * Mathf.PI * frequency * 2.1f * time) * 0.3f +
|
||||
Mathf.Sin(2f * Mathf.PI * frequency * 3.8f * time) * 0.2f;
|
||||
|
||||
case AudioEmitterType.Wood:
|
||||
return Mathf.Sin(2f * Mathf.PI * frequency * time) * 0.6f +
|
||||
(Random.value - 0.5f) * 0.1f;
|
||||
|
||||
case AudioEmitterType.Glass:
|
||||
return Mathf.Sin(2f * Mathf.PI * frequency * 1.5f * time) * 0.4f +
|
||||
Mathf.Sin(2f * Mathf.PI * frequency * 2.7f * time) * 0.3f +
|
||||
Mathf.Sin(2f * Mathf.PI * frequency * 4.2f * time) * 0.3f;
|
||||
|
||||
case AudioEmitterType.Stone:
|
||||
return Mathf.Sin(2f * Mathf.PI * frequency * 0.8f * time) * 0.5f +
|
||||
(Random.value - 0.5f) * 0.3f;
|
||||
|
||||
case AudioEmitterType.Water:
|
||||
return (Random.value - 0.5f) * 0.5f +
|
||||
Mathf.Sin(2f * Mathf.PI * frequency * 0.5f * time) * 0.3f;
|
||||
|
||||
case AudioEmitterType.Mechanical:
|
||||
return Mathf.Sin(2f * Mathf.PI * frequency * time) * 0.4f +
|
||||
Mathf.Sign(Mathf.Sin(2f * Mathf.PI * frequency * 0.3f * time)) * 0.3f;
|
||||
|
||||
case AudioEmitterType.Electrical:
|
||||
return (Random.value - 0.5f) * 0.6f +
|
||||
Mathf.Sin(2f * Mathf.PI * frequency * 50f * time) * 0.2f;
|
||||
|
||||
case AudioEmitterType.Organic:
|
||||
return Mathf.Sin(2f * Mathf.PI * frequency * 0.7f * time) * 0.5f +
|
||||
Mathf.Sin(2f * Mathf.PI * frequency * 1.3f * time) * 0.3f +
|
||||
(Random.value - 0.5f) * 0.2f;
|
||||
|
||||
case AudioEmitterType.Wall:
|
||||
return Mathf.Sin(2f * Mathf.PI * frequency * 0.5f * time) * 0.3f +
|
||||
(Random.value - 0.5f) * 0.1f;
|
||||
|
||||
default:
|
||||
return Mathf.Sin(2f * Mathf.PI * frequency * time);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
StopListening();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_continuousAudioSource != null && !_continuousAudioSource.isPlaying && _emitContinuously)
|
||||
{
|
||||
StartContinuousEmission();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
Gizmos.color = Color.cyan;
|
||||
Gizmos.DrawWireSphere(transform.position, _maxDistance);
|
||||
|
||||
if (_isCurrentlyListening)
|
||||
{
|
||||
Gizmos.color = Color.green;
|
||||
Gizmos.DrawLine(transform.position, _listenerPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e15490f7adacb594cb114f535e71d6b6
|
||||
@@ -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
|
||||
@@ -0,0 +1,96 @@
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
|
||||
public enum WwiseMusicState { Menu, Sane, Staircase, Mad, None};
|
||||
public class Audio_AudioManager : MonoBehaviour
|
||||
{
|
||||
public static Audio_AudioManager Instance;
|
||||
|
||||
private bool isInitialized = false;
|
||||
|
||||
|
||||
|
||||
[Header("Music State variables")]
|
||||
[SerializeField] private AK.Wwise.State MusicGlobal_Menu;
|
||||
[SerializeField] private AK.Wwise.State MusicGlobal_Sane;
|
||||
[SerializeField] private AK.Wwise.State MusicGlobal_Staircase;
|
||||
[SerializeField] private AK.Wwise.State MusicGlobal_Mad;
|
||||
[SerializeField] private AK.Wwise.State MusicGlobal_None;
|
||||
|
||||
private WwiseMusicState currentMusicState;
|
||||
|
||||
[Header("Wwise Music Events")]
|
||||
public AK.Wwise.Event PlayMusic;
|
||||
public AK.Wwise.Event StopMusic;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
Initialize();
|
||||
}
|
||||
|
||||
// Start is called once before the first execution of Update after the MonoBehaviour is created
|
||||
void Start()
|
||||
{
|
||||
SetWwiseMusicState(WwiseMusicState.Sane); //Need to switch if we have a menu music.
|
||||
|
||||
PlayMusic.Post(gameObject);
|
||||
}
|
||||
|
||||
|
||||
|
||||
void Initialize()
|
||||
{
|
||||
//Singleton Logic
|
||||
if (Instance == null)
|
||||
{
|
||||
Instance = this;
|
||||
DontDestroyOnLoad(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("AudioManager already exist! Destroying new ibnstance.");
|
||||
Destroy(this);
|
||||
}
|
||||
|
||||
if (isInitialized)
|
||||
{
|
||||
isInitialized = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
public void SetWwiseMusicState(WwiseMusicState MusicState)
|
||||
{
|
||||
if (MusicState == currentMusicState)
|
||||
{
|
||||
Debug.Log("MusicState is already" + MusicState + ".");
|
||||
return;
|
||||
}
|
||||
switch (MusicState)
|
||||
{
|
||||
default:
|
||||
case (WwiseMusicState.Menu):
|
||||
MusicGlobal_Menu.SetValue();
|
||||
break;
|
||||
case (WwiseMusicState.Sane):
|
||||
MusicGlobal_Sane.SetValue();
|
||||
break;
|
||||
case (WwiseMusicState.Staircase):
|
||||
MusicGlobal_Staircase.SetValue();
|
||||
break;
|
||||
case (WwiseMusicState.Mad):
|
||||
MusicGlobal_Mad.SetValue();
|
||||
break;
|
||||
case (WwiseMusicState.None):
|
||||
MusicGlobal_None.SetValue();
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
Debug.Log("New Wwise MusicState" + MusicState + ".");
|
||||
|
||||
currentMusicState = MusicState;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 53583b7aad5dc7044bbd6c7946fe6999
|
||||
@@ -0,0 +1,75 @@
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
using AK.Wwise;
|
||||
|
||||
[RequireComponent(typeof(CharacterController))]
|
||||
public class Audio_FootstepsController : MonoBehaviour
|
||||
{
|
||||
[System.Serializable]
|
||||
public struct SurfaceMapping
|
||||
{
|
||||
public string tag;
|
||||
public AK.Wwise.Switch wwiseSwitch;
|
||||
}
|
||||
|
||||
[Header("Wwise Assets")]
|
||||
public AK.Wwise.Event footstepEvent;
|
||||
|
||||
[Header("Surface Configuration")]
|
||||
|
||||
public AK.Wwise.Switch defaultSwitch;
|
||||
|
||||
[Header("Movement Settings")]
|
||||
[SerializeField] private float _stepInterval = 0.5f;
|
||||
[SerializeField] private float _minVelocity = 0.3f;
|
||||
[SerializeField] private float _rayDistance = 0.2f;
|
||||
[SerializeField] private LayerMask _groundLayer;
|
||||
|
||||
private CharacterController _controller;
|
||||
private float _timer;
|
||||
|
||||
|
||||
void Awake()
|
||||
{
|
||||
_controller = GetComponent<CharacterController>();
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (_controller.isGrounded && _controller.velocity.magnitude > _minVelocity)
|
||||
{
|
||||
_timer += Time.deltaTime;
|
||||
if (_timer >= _stepInterval)
|
||||
{
|
||||
PlayFootstep();
|
||||
_timer = 0f;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_timer = _stepInterval;
|
||||
}
|
||||
}
|
||||
|
||||
private void PlayFootstep()
|
||||
{
|
||||
RaycastHit hit;
|
||||
if (Physics.Raycast(transform.position + Vector3.up * 0.1f, Vector3.down, out hit, _rayDistance, _groundLayer))
|
||||
{
|
||||
|
||||
if (hit.collider.TryGetComponent<Audio_SoundSurface>(out var surface))
|
||||
{
|
||||
surface.wwiseSwitch.SetValue(gameObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
defaultSwitch.SetValue(gameObject);
|
||||
Debug.LogWarning($"[Footsteps] No GroundSurface component on '{hit.collider.name}'. Utilisation du switch par défaut.");
|
||||
}
|
||||
|
||||
|
||||
|
||||
footstepEvent.Post(gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5a11b07babb12d446a356745e7a37303
|
||||
@@ -0,0 +1,48 @@
|
||||
using UnityEngine;
|
||||
|
||||
[RequireComponent(typeof(Collider))]
|
||||
public class Audio_MusicStateTrigger : MonoBehaviour
|
||||
{
|
||||
[Header("Music State Settings")]
|
||||
[SerializeField] private WwiseMusicState targetMusicState = WwiseMusicState.Sane;
|
||||
|
||||
[Header("Trigger Settings")]
|
||||
[SerializeField] private bool triggerOnEnter = true;
|
||||
[SerializeField] private bool triggerOnExit = false;
|
||||
[SerializeField] private WwiseMusicState exitMusicState = WwiseMusicState.None;
|
||||
[SerializeField] private string playerTag = "Player";
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// Ensure the collider is set to trigger
|
||||
GetComponent<Collider>().isTrigger = true;
|
||||
}
|
||||
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (!triggerOnEnter) return;
|
||||
if (!other.CompareTag(playerTag)) return;
|
||||
|
||||
Audio_AudioManager.Instance?.SetWwiseMusicState(targetMusicState);
|
||||
}
|
||||
|
||||
private void OnTriggerExit(Collider other)
|
||||
{
|
||||
if (!triggerOnExit) return;
|
||||
if (!other.CompareTag(playerTag)) return;
|
||||
|
||||
Audio_AudioManager.Instance?.SetWwiseMusicState(exitMusicState);
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void OnDrawGizmos()
|
||||
{
|
||||
BoxCollider box = GetComponent<BoxCollider>();
|
||||
Gizmos.matrix = transform.localToWorldMatrix;
|
||||
Gizmos.color = new Color(0f, 1f, 0.5f, 0.25f);
|
||||
Gizmos.DrawCube(box.center, box.size);
|
||||
Gizmos.color = new Color(0f, 1f, 0.5f, 0.8f);
|
||||
Gizmos.DrawWireCube(box.center, box.size);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b91e11b451fb1494ea8c2c2af03081c7
|
||||
@@ -0,0 +1,34 @@
|
||||
using UnityEngine;
|
||||
|
||||
[RequireComponent(typeof(BoxCollider))]
|
||||
public class Audio_RTPCZone : MonoBehaviour
|
||||
{
|
||||
[HideInInspector] public Audio_RTPCZoneManager manager;
|
||||
|
||||
private bool _triggered = false;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
GetComponent<BoxCollider>().isTrigger = true;
|
||||
}
|
||||
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (!other.CompareTag("Player")) return;
|
||||
if (manager.oneShot && _triggered) return;
|
||||
|
||||
_triggered = true;
|
||||
manager.OnZoneEntered();
|
||||
}
|
||||
#if UNITY_EDITOR
|
||||
private void OnDrawGizmos()
|
||||
{
|
||||
BoxCollider box = GetComponent<BoxCollider>();
|
||||
Gizmos.matrix = transform.localToWorldMatrix;
|
||||
Gizmos.color = new Color(0f, 1f, 0.5f, 0.25f);
|
||||
Gizmos.DrawCube(box.center, box.size);
|
||||
Gizmos.color = new Color(0f, 1f, 0.5f, 0.8f);
|
||||
Gizmos.DrawWireCube(box.center, box.size);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 66b5d13a3d510664faf00b6012abbe32
|
||||
@@ -0,0 +1,33 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class Audio_RTPCZoneManager : MonoBehaviour
|
||||
{
|
||||
[Header("Rtpc")]
|
||||
public AK.Wwise.RTPC rtpc;
|
||||
|
||||
[Tooltip("Value added to the RTPC each time a zone is entered")]
|
||||
public float valueIncrement = 1f;
|
||||
|
||||
[Tooltip("If true, each zone only triggers once per session")]
|
||||
public bool oneShot = true;
|
||||
|
||||
[Header("Clamp")]
|
||||
public float minValue = 0f;
|
||||
public float maxValue = 10f;
|
||||
|
||||
private float _currentValue = 0f;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
foreach (var zone in GetComponentsInChildren<Audio_RTPCZone>())
|
||||
{
|
||||
zone.manager = this;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnZoneEntered()
|
||||
{
|
||||
_currentValue = Mathf.Clamp(_currentValue + valueIncrement, minValue, maxValue);
|
||||
rtpc.SetGlobalValue(_currentValue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 908b8bddba6d5d940b57c57ba1d2ec33
|
||||
@@ -0,0 +1,7 @@
|
||||
using UnityEngine;
|
||||
using AK.Wwise;
|
||||
|
||||
public class Audio_SoundSurface : MonoBehaviour
|
||||
{
|
||||
public AK.Wwise.Switch wwiseSwitch;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3ec78f9fa08450a4fb1c6ef69da4e22c
|
||||
Reference in New Issue
Block a user