using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Audio;
namespace OCES.Audio
{
///
/// 音效系统
///
public class SfxSystem : MonoBehaviour
{
public UnityEngine.UI.Text audioSystemTextBox;
const int k_maxGlobalConcurrent = 128;
//记录某个 AudioObject 最近一次触发播放的时刻,用于 MinInterval 节流判断,是"上次什么时候播过"。
readonly Dictionary m_lastPlayTime = new();
readonly Dictionary m_clipConcurrentCount = new();
readonly List m_activeSounds = new();
// 复用列表,避免 TryPlay 每帧分配临时 List
readonly List m_tempSameObject = new();
readonly List m_tempSameGroup = new();
readonly List 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 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));
return;
}
ExecutePlay(active);
}
///
/// 连续容器播放协程(Random / Sequence 持续模式)
///
IEnumerator PlayContainerContinuous(AudioSource source, AudioObject audioObject, ActiveSound chainActive, int startIndex, float pitch)
{
bool isRandom = audioObject.ContainerType == ContainerType.Random;
// 初始化 Random 模式的辅助结构
List remainingPool = isRandom ? new List(Enumerable.Range(0, audioObject.Name.Count)) : null;
Queue recentPlayed = isRandom ? new Queue() : 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)
{
Debug.Log($"Delaying for {audioObject.InitialDelay} second(s).");
yield return new WaitForSeconds(audioObject.InitialDelay);
if (!this.m_activeSounds.Contains(active)) yield break;
ExecutePlay(active, isRegistered: true);
}
// ─────────────────────────────────────────────
// 工具方法
// ─────────────────────────────────────────────
///
/// 尝试打断一个候选声音。返回 false 表示应取消当前播放(Newest 模式)。
///
bool TryKill(List 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;
}
///
/// 从候选列表中选出开始时间最早的声音
///
ActiveSound SelectOldest(List candidates)
{
ActiveSound oldest = null;
foreach (ActiveSound s in candidates)
{
if (oldest == null || s.StartTime < oldest.StartTime)
{
oldest = s;
}
}
return oldest;
}
///
/// 根据 MixingType 获取对应的 AudioMixerGroup
///
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($"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);
}
}
///
/// 活跃的声音封装
///
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;
}
}