feat: 重构音频系统 - 重命名AudioScheduler为SfxSystem并添加Accented音效类型
- 将AudioScheduler重命名为SfxSystem,统一命名规范 - 新增MixingType.Accented音效类型,支持强调音效独立混音 - 重构音频混合器层级: Master下新增Regular中间层 - SfxSystem使用外部AudioSourcePool,与MusicSystem隔离 - 修复ambiencePoolRoot父节点错误(原错误挂载到musicPoolRoot) - Play方法支持onPlay回调 - 同步更新AudioObject.bytes二进制配置数据
This commit is contained in:
@@ -0,0 +1,512 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Audio;
|
||||
|
||||
namespace OCES.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// 音效系统
|
||||
/// </summary>
|
||||
public class SfxSystem : MonoBehaviour
|
||||
{
|
||||
public UnityEngine.UI.Text audioSystemTextBox;
|
||||
|
||||
const int k_maxGlobalConcurrent = 128;
|
||||
|
||||
//记录某个 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;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
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();
|
||||
|
||||
AudioMixerGroup[] sfx = Find("Master/Regular/SFX");
|
||||
if (sfx.Length > 0) this.m_sfxGroup = sfx[0];
|
||||
AudioMixerGroup[] voice = Find("Master/Regular/Voice");
|
||||
if (voice.Length > 0) this.m_voiceGroup = voice[0];
|
||||
AudioMixerGroup[] accentSfx = Find("Master/SFX_Accent");
|
||||
if (accentSfx.Length > 0) this.m_accentSfxGroup = accentSfx[0];
|
||||
|
||||
return;
|
||||
|
||||
AudioMixerGroup[] Find(string path) => mixer.FindMatchingGroups(path);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 节流 & 调度入口
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 第四层:全局并发控制
|
||||
if (this.m_activeSounds.Count >= k_maxGlobalConcurrent)
|
||||
{
|
||||
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); //算一下用不用变调
|
||||
PlayNewSound(audioObject, pitch);
|
||||
this.m_lastPlayTime[audioObject.Id] = now;
|
||||
onPlay?.Invoke();
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 播放逻辑
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
void PlayNewSound(AudioObject audioObject, float pitch)
|
||||
{
|
||||
ActiveSound active = new()
|
||||
{
|
||||
AudioObject = audioObject,
|
||||
Pitch = pitch,
|
||||
State = ActiveSoundState.Pending,
|
||||
StartTime = Time.realtimeSinceStartupAsDouble,
|
||||
};
|
||||
|
||||
if (audioObject.InitialDelay > 0)
|
||||
{
|
||||
this.m_activeSounds.Add(active);
|
||||
IncrementClipCount(audioObject.Id);
|
||||
|
||||
active.Coroutine = StartCoroutine(PlayAfterDelay(active, audioObject, pitch));
|
||||
return;
|
||||
}
|
||||
|
||||
ExecutePlay(active);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 连续容器播放协程(Random / Sequence 持续模式)
|
||||
/// </summary>
|
||||
IEnumerator PlayContainerContinuous(AudioSource source, AudioObject audioObject, ActiveSound chainActive, int startIndex, float pitch)
|
||||
{
|
||||
bool isRandom = audioObject.ContainerType == ContainerType.Random;
|
||||
|
||||
// 初始化 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, audioObject, pitch, index))
|
||||
{
|
||||
Debug.LogError($"音频文件未找到:{audioObject.Name[index]}");
|
||||
yield break;
|
||||
}
|
||||
|
||||
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]}");
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
}
|
||||
static void StartPlayback(AudioSource source)
|
||||
{
|
||||
source.Play();
|
||||
}
|
||||
|
||||
IEnumerator PlayAfterDelay(ActiveSound active, AudioObject audioObject, float pitch)
|
||||
{
|
||||
Debug.Log($"Delaying for {audioObject.InitialDelay} second(s).");
|
||||
yield return new WaitForSeconds(audioObject.InitialDelay);
|
||||
|
||||
if (!this.m_activeSounds.Contains(active)) yield break;
|
||||
active.Pitch = pitch;
|
||||
|
||||
ExecutePlay(active, isRegistered: true);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 工具方法
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
/// <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, AudioObject audioObject, float pitch, int clipIndex = 0)
|
||||
{
|
||||
AudioClip clip = Resources.Load<AudioClip>($"Audios/{audioObject.Name[clipIndex]}"); // TODO 抽象同一资源加载接口
|
||||
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 = pitch;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
activeSound.Coroutine = StartCoroutine(RemoveWhenFinished(activeSound));
|
||||
}
|
||||
|
||||
void ExecutePlay(ActiveSound active, bool isRegistered = false)
|
||||
{
|
||||
AudioObject audioObject = active.AudioObject;
|
||||
float pitch = active.Pitch;
|
||||
|
||||
// =======================
|
||||
// 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,
|
||||
State = ActiveSoundState.Playing
|
||||
};
|
||||
|
||||
if (!SetupSource(source, audioObject, pitch, i))
|
||||
{
|
||||
this.m_pool.ReturnToPool(source.gameObject);
|
||||
continue;
|
||||
}
|
||||
|
||||
StartPlayback(source);
|
||||
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, pitch)
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// =======================
|
||||
// 单次播放
|
||||
// =======================
|
||||
int index = audioObject.ContainerType switch
|
||||
{
|
||||
ContainerType.Random => m_containerSelector.PickShuffleIndex(audioObject),
|
||||
ContainerType.Sequence => m_containerSelector.GetNextSequenceIndex(audioObject),
|
||||
_ => 0
|
||||
};
|
||||
|
||||
if (!SetupSource(sourceSingle, audioObject, pitch, index))
|
||||
{
|
||||
m_pool.ReturnToPool(sourceSingle.gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
StartPlayback(sourceSingle);
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// <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 ActiveSoundState State;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user