Files
AudioSystem/Assets/Scripts/OCES/Audio/HandWritten/SfxSystem.cs
T

604 lines
23 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using OCES.Haptic;
using UnityEngine;
using UnityEngine.Audio;
namespace OCES.Audio
{
/// <summary>
/// 音效系统
/// </summary>
public class SfxSystem : MonoBehaviour
{
public UnityEngine.UI.Text audioSystemTextBox;
//记录某个 AudioObject 最近一次触发播放的时刻,用于 MinInterval 节流判断,是"上次什么时候播过"。
readonly Dictionary<uint, double> m_lastPlayTime = new();
readonly Dictionary<uint, int> m_clipConcurrentCount = new();
readonly List<ActiveSound> m_activeSounds = new();
// 复用列表,避免 TryPlay 每帧分配临时 List
readonly List<ActiveSound> m_tempSameObject = new();
readonly List<ActiveSound> m_tempSameGroup = new();
readonly List<ActiveSound> m_tempLowerPriority = new();
AudioGroupConfig m_groupConfig;
AudioMixerGroup m_sfxGroup;
AudioMixerGroup m_voiceGroup;
AudioMixerGroup m_accentSfxGroup;
AudioSourcePool m_pool;
AudioContainerSelector m_containerSelector;
PitchStepResolver m_pitchStepResolver;
VolumeStepResolver m_volumeStepResolver;
#if UNITY_EDITOR || DEVELOPMENT_BUILD
void Update()
{
Editor.DebugInfoCollector.Instance.ActiveSounds = this.m_activeSounds;
Editor.DebugInfoCollector.Instance.ClipConcurrentCount = this.m_clipConcurrentCount;
}
#endif
int GetClipCount(uint id)
{
return this.m_clipConcurrentCount.GetValueOrDefault(id, 0);
}
void IncrementClipCount(uint id)
{
this.m_clipConcurrentCount[id] = GetClipCount(id) + 1;
#if UNITY_EDITOR
//Debug.Log($"{id} count added to {GetClipCount(id)}");
#endif
}
void DecrementClipCount(uint id)
{
int cur = GetClipCount(id);
if (cur > 0) this.m_clipConcurrentCount[id] = cur - 1;
//Debug.Log($"{id} clip count minus to {GetClipCount(id)}");
}
// ─────────────────────────────────────────────
// 初始化
// ─────────────────────────────────────────────
public void Initialize(AudioGroupConfig groups, AudioMixer mixer, AudioSourcePool pool)
{
this.m_groupConfig = groups;
this.m_pool = pool;
this.m_containerSelector = new AudioContainerSelector();
this.m_pitchStepResolver = new PitchStepResolver();
this.m_volumeStepResolver = new VolumeStepResolver();
AudioMixerGroup[] sfx = Find(AudioExtendSettings.Instance.sfxGroupPath);
if (sfx.Length > 0) this.m_sfxGroup = sfx[0];
AudioMixerGroup[] voice = Find(AudioExtendSettings.Instance.voiceGroupPath);
if (voice.Length > 0) this.m_voiceGroup = voice[0];
AudioMixerGroup[] accentSfx = Find(AudioExtendSettings.Instance.accentSfxGroupPath);
if (accentSfx.Length > 0) this.m_accentSfxGroup = accentSfx[0];
return;
AudioMixerGroup[] Find(string path) => mixer.FindMatchingGroups(path);
}
internal void Stop(uint audioId)
{
List<ActiveSound> stopTargets = this.m_activeSounds.FindAll(activeSound => activeSound.AudioObject.Id == audioId);
foreach (ActiveSound stopTarget in stopTargets)
{
StopSound(stopTarget);
}
}
internal void SetVolume(uint audioId, float targetVolume)
{
List<ActiveSound> targets = this.m_activeSounds.FindAll(activeSound => activeSound.AudioObject.Id == audioId);
foreach (ActiveSound target in targets)
{
target.Volume = targetVolume;
if (target.Source)
{
target.Source.volume = targetVolume;
}
}
}
internal void SetPitch(uint audioId, float targetPitch)
{
List<ActiveSound> targets = this.m_activeSounds.FindAll(activeSound => activeSound.AudioObject.Id == audioId);
foreach (ActiveSound target in targets)
{
target.Pitch = targetPitch;
if (target.Source)
{
target.Source.pitch = targetPitch;
}
}
}
internal void ResetVolume(uint audioId)
{
double now = Time.realtimeSinceStartupAsDouble * 1000.0;
List<ActiveSound> targets = this.m_activeSounds.FindAll(activeSound => activeSound.AudioObject.Id == audioId);
foreach (ActiveSound target in targets)
{
float defaultVolume = this.m_volumeStepResolver.ResolveVolume(target.AudioObject, now);
target.Volume = defaultVolume;
if (target.Source)
{
target.Source.volume = defaultVolume;
}
}
}
internal void ResetPitch(uint audioId)
{
double now = Time.realtimeSinceStartupAsDouble * 1000.0;
List<ActiveSound> targets = this.m_activeSounds.FindAll(activeSound => activeSound.AudioObject.Id == audioId);
foreach (ActiveSound target in targets)
{
float defaultPitch = this.m_pitchStepResolver.ResolvePitch(target.AudioObject, now);
target.Pitch = defaultPitch;
if (target.Source != null)
{
target.Source.pitch = defaultPitch;
}
}
}
// ─────────────────────────────────────────────
// 节流 & 调度入口
// ─────────────────────────────────────────────
internal void TryPlay(AudioObject audioObject, Action onPlay = null)
{
double now = Time.realtimeSinceStartupAsDouble * 1000.0;
// 第一层:时间节流 单位毫秒
if (this.m_lastPlayTime.TryGetValue(audioObject.Id, out double lastTime) &&
now - lastTime < audioObject.MinInterval)
{
Debug.LogWarning($"[Throttle] {audioObject.Name[0]} 未达到最小间隔,取消播放。");
return;
}
// 第二层:单 AudioObject/Clip 并发控制
if (audioObject.ThrottleCount != 0)
{
this.m_tempSameObject.Clear();
foreach (ActiveSound activeSound in this.m_activeSounds)
{
if (activeSound.AudioObject.Id == audioObject.Id)
{
this.m_tempSameObject.Add(activeSound);
}
}
if (this.m_tempSameObject.Count >= audioObject.ThrottleCount &&
!TryKill(this.m_tempSameObject, audioObject.KillMode, $"[Object] {audioObject.Name[0]}"))
return;
}
// 第三层:Group 并发控制
AudioGroup groupConfig = this.m_groupConfig.QueryById(audioObject.Group);
if (groupConfig == null)
{
groupConfig = this.m_groupConfig.QueryById(1);
Debug.Log($"未找到{audioObject.Id}对应的组文件,已使用默认配置组1。");
}
this.m_tempSameGroup.Clear();
foreach (ActiveSound activeSound in this.m_activeSounds)
{
if (activeSound.AudioObject.Group == audioObject.Group)
{
this.m_tempSameGroup.Add(activeSound);
}
}
if (this.m_tempSameGroup.Count >= groupConfig.GroupThrottleCount)
{
this.m_tempLowerPriority.Clear();
foreach (ActiveSound activeSound in this.m_tempSameGroup)
{
if (activeSound.AudioObject.Priority > audioObject.Priority)
{
this.m_tempLowerPriority.Add(activeSound);
}
}
List<ActiveSound> killCandidates = this.m_tempLowerPriority.Count > 0 ? this.m_tempLowerPriority : this.m_tempSameGroup;
KillMode killMode = this.m_tempLowerPriority.Count > 0 ? groupConfig.KillMode : audioObject.KillMode;
if (!TryKill(killCandidates, killMode, $"[Group] {audioObject.Name[0]}"))
return;
}
// 第四层:全局并发控制
//从Unity原生Audio Settings中读取最大发音数限制
if (this.m_activeSounds.Count >= AudioSettings.GetConfiguration().numVirtualVoices)
{
this.m_tempLowerPriority.Clear();
foreach (ActiveSound activeSound in this.m_activeSounds)
{
if (activeSound.AudioObject.Priority > audioObject.Priority)
{
this.m_tempLowerPriority.Add(activeSound);
}
}
if (this.m_tempLowerPriority.Count == 0 || !TryKill(this.m_tempLowerPriority, KillMode.Oldest, "[Global]"))
return;
}
// 执行播放
float pitch = this.m_pitchStepResolver.ResolvePitch(audioObject, now); //算一下用不用变调
float volume = this.m_volumeStepResolver.ResolveVolume(audioObject, now);
PlayNewSound(audioObject, pitch, volume);
this.m_lastPlayTime[audioObject.Id] = now;
onPlay?.Invoke();
}
// ─────────────────────────────────────────────
// 播放逻辑
// ─────────────────────────────────────────────
void PlayNewSound(AudioObject audioObject, float pitch, float volume)
{
ActiveSound active = new()
{
AudioObject = audioObject,
Pitch = pitch,
Volume = volume,
State = ActiveSoundState.Pending,
StartTime = Time.realtimeSinceStartupAsDouble,
};
ExecutePlay(active);
}
/// <summary>
/// 连续容器播放协程(Random / Sequence 持续模式)
/// </summary>
IEnumerator PlayContainerContinuous(AudioSource source, AudioObject audioObject, ActiveSound chainActive, int startIndex)
{
bool isRandom = audioObject.ContainerType == ContainerType.Random;
if (chainActive.AudioObject.InitialDelay > 0)
{
chainActive.State = ActiveSoundState.Pending;
}
// 初始化 Random 模式的辅助结构
List<int> remainingPool = isRandom ? new List<int>(Enumerable.Range(0, audioObject.Name.Count)) : null;
Queue<int> recentPlayed = isRandom ? new Queue<int>() : null;
int limitRepetition = isRandom ? Mathf.Clamp(audioObject.LimitRepetition, 0, audioObject.Name.Count - 1) : 0;
int index = startIndex;
while (true)
{
// 选择本轮播放的 index
if (isRandom)
index = this.m_containerSelector.PickNextRandomIndex(
audioObject,
remainingPool,
recentPlayed,
limitRepetition);
// 配置并播放
if (!SetupSource(source, chainActive, index))
{
Debug.LogError($"音频文件未找到:{audioObject.Name[index]}");
yield break;
}
if (chainActive.State == ActiveSoundState.Pending)
{
chainActive.State = ActiveSoundState.Playing;
StartPlayback(source, audioObject.InitialDelay);
}
else
{
StartPlayback(source);
}
yield return new WaitWhile(() => source.isPlaying);
// 判断本轮是否播完
bool roundFinished = isRandom
? remainingPool.Count == 0 || this.m_containerSelector.GetHistoryCount(audioObject.Id) == audioObject.Name.Count
: ++index >= audioObject.Name.Count;
if (roundFinished)
{
DecrementClipCount(audioObject.Id);
this.m_activeSounds.Remove(chainActive);
if (isRandom)
this.m_containerSelector.ResetHistory(audioObject.Id);
this.m_pool.ReturnToPool(source.gameObject);
//Debug.Log($"[Container - Continuous] 协程正常结束: {audioObject.Name[0]}");
// TryStopHaptic(audioObject.Haptic);
yield break;
}
}
}
static void StartPlayback(AudioSource source, float delay = 0f)
{
if (delay > 0f)
source.PlayDelayed(delay);
else
source.Play();
}
// ─────────────────────────────────────────────
// 工具方法
// ─────────────────────────────────────────────
/// <summary>
/// 尝试打断一个候选声音。返回 false 表示应取消当前播放(Newest 模式)。
/// </summary>
bool TryKill(List<ActiveSound> candidates, KillMode killMode, string logPrefix)
{
if (killMode == KillMode.Newest)
{
Debug.LogWarning($"{logPrefix} 被 KillNewest 取消播放。");
return false;
}
ActiveSound toKill = SelectOldest(candidates);
if (toKill != null)
{
Debug.LogWarning($"{logPrefix} 打断了 {toKill.AudioObject.Name[0]}。");
StopSound(toKill);
}
return true;
}
/// <summary>
/// 从候选列表中选出开始时间最早的声音
/// </summary>
ActiveSound SelectOldest(List<ActiveSound> candidates)
{
ActiveSound oldest = null;
foreach (ActiveSound s in candidates)
{
if (oldest == null || s.StartTime < oldest.StartTime)
{
oldest = s;
}
}
return oldest;
}
/// <summary>
/// 根据 MixingType 获取对应的 AudioMixerGroup
/// </summary>
AudioMixerGroup GetMixerGroup(MixingType type) => type switch
{
MixingType.Voice => this.m_voiceGroup,
MixingType.Accented => this.m_accentSfxGroup,
_ => GetDefaultGroup(type),
};
AudioMixerGroup GetDefaultGroup(MixingType type)
{
if (type != MixingType.Sfx)
{
Debug.LogWarning($"[SfxSystem] Unrecognized MixingType: {type}, defaulting to SFX.");
}
return this.m_sfxGroup;
}
bool SetupSource(AudioSource source, ActiveSound activeSound, int clipIndex = 0)
{
AudioObject audioObject = activeSound.AudioObject;
AudioClip clip =
AudioSystem.Instance.ResourceLoader.LoadSync<AudioClip>(
$"{AudioExtendSettings.Instance.audioResourcePath}/{audioObject.Name[clipIndex]}");
if (!clip)
{
Debug.LogError($"音频文件未找到:{audioObject.Name[clipIndex]}");
return false;
}
source.clip = clip;
source.loop = audioObject.LoopCount < 0;
source.priority = audioObject.Priority;
source.outputAudioMixerGroup = GetMixerGroup(audioObject.MixingType);
source.pitch = activeSound.Pitch;
source.volume = activeSound.Volume;
return true;
}
void RegisterActiveSound(ActiveSound activeSound, AudioSource audioSource, bool isRegistered = false)
{
activeSound.Source = audioSource;
activeSound.State = ActiveSoundState.Playing;
activeSound.StartTime = Time.realtimeSinceStartupAsDouble;
if (!isRegistered)
{
this.m_activeSounds.Add(activeSound);
IncrementClipCount(activeSound.AudioObject.Id);
}
if (activeSound.AudioObject.ContainerType == ContainerType.Blend && activeSound.AudioObject.Haptic > 0)
{
Debug.LogWarning($"[Haptic System] Blend container {activeSound.AudioObject.Id} should not have haptic feedback!");
}
else
{
TryStartHaptic(activeSound);
}
activeSound.Coroutine = StartCoroutine(RemoveWhenFinished(activeSound));
}
void ExecutePlay(ActiveSound active, bool isRegistered = false)
{
AudioObject audioObject = active.AudioObject;
float pitch = active.Pitch;
float volume = active.Volume;
// =======================
// Blend(每个clip一个ActiveSound
// =======================
if (audioObject.ContainerType == ContainerType.Blend)
{
for (int i = 0; i < audioObject.Name.Count; i++)
{
AudioSource source = this.m_pool.AcquireAudioSource();
ActiveSound child = new()
{
AudioObject = audioObject,
Pitch = pitch,
Volume = volume,
State = ActiveSoundState.Playing,
};
if (!SetupSource(source, child, i))
{
this.m_pool.ReturnToPool(source.gameObject);
continue;
}
StartPlayback(source, audioObject.InitialDelay);
RegisterActiveSound(child, source);
}
// 删除 pending 占位
if (this.m_activeSounds.Contains(active))
{
DecrementClipCount(audioObject.Id);
this.m_activeSounds.Remove(active);
}
return;
}
AudioSource sourceSingle = this.m_pool.AcquireAudioSource();
// =======================
// Continuous
// =======================
if (audioObject.Name.Count > 1 && audioObject.ContainerPlayMode)
{
active.Source = sourceSingle;
active.State = ActiveSoundState.Playing;
this.m_activeSounds.Add(active);
IncrementClipCount(audioObject.Id);
int start = audioObject.ContainerType == ContainerType.Random ? -1 : 0;
active.Coroutine = StartCoroutine(
PlayContainerContinuous(sourceSingle, audioObject, active, start)
);
return;
}
// =======================
// 单次播放
// =======================
int index = audioObject.ContainerType switch
{
ContainerType.Random => m_containerSelector.PickShuffleIndex(audioObject),
ContainerType.Sequence => m_containerSelector.GetNextSequenceIndex(audioObject),
_ => 0,
};
if (!SetupSource(sourceSingle, active, index))
{
this.m_pool.ReturnToPool(sourceSingle.gameObject);
return;
}
StartPlayback(sourceSingle, active.AudioObject.InitialDelay);
RegisterActiveSound(active, sourceSingle, isRegistered);
}
IEnumerator RemoveWhenFinished(ActiveSound active)
{
if (active.AudioObject.LoopCount < 0)
{
//Debug.Log($"RemoveWhenFinished协程已结束,因为播放的是无限循环音效{active.Source.clip.name}");
yield break;
}
do
{
yield return new WaitWhile(() => active.Source.isPlaying);
active.CurrentLoopCount++;
}
while (active.AudioObject.LoopCount > 0
&& active.CurrentLoopCount < active.AudioObject.LoopCount
&& PlayAgain(active));
DecrementClipCount(active.AudioObject.Id);
this.m_activeSounds.Remove(active);
this.m_pool.ReturnToPool(active.Source.gameObject);
}
bool PlayAgain(ActiveSound active)
{
StartPlayback(active.Source);
return true;
}
void StopSound(ActiveSound active)
{
if (active.Coroutine != null)
{
StopCoroutine(active.Coroutine);
//Debug.Log($"[StopSound] 协程已终止: {active.AudioObject.Name[0]}");
}
if(active.Source) active.Source.Stop();
DecrementClipCount(active.AudioObject.Id);
this.m_activeSounds.Remove(active);
this.m_pool.ReturnToPool(active.Source.gameObject);
}
static void TryStartHaptic(ActiveSound active)
{
uint hapticId = active.AudioObject.Haptic;
if (hapticId == 0) return;
HapticSystem.Instance.Play(hapticId, isDirectCall: false);
}
static void TryStopHaptic(uint hapticId)
{
HapticSystem.Instance.Stop(hapticId);
}
}
/// <summary>
/// 活跃的声音封装
/// </summary>
public class ActiveSound
{
public AudioSource Source;
public AudioObject AudioObject;
// 记录某个具体播放实例的开始时间,用于 SelectOldest 在多个并发实例中挑出最老的那个来打断,是"这个实例是什么时候开始的"。
public double StartTime;
public int CurrentLoopCount;
public Coroutine Coroutine;
public float Pitch;
public float Volume;
public ActiveSoundState State;
}
}