解决重复切换State的时候会导致重复播放对应Segment的问题。
修复FadeIn读取了FadeOut参数的问题。 增加Initial Delay功能。 重构AudioScheduler.ConfigureSource() -> SetupSource(), RegisterActiveSound(), StartPlayBack()。 移动长音频相关功能至LongAudio文件夹。
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace OCES.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// 环境音通道播放器。
|
||||
/// 与 MusicChannelPlayer 逻辑相同,但使用 AmbienceTransition 表,不涉及节拍对齐。
|
||||
/// </summary>
|
||||
public class AmbienceChannelPlayer
|
||||
{
|
||||
readonly AmbienceTransitionConfig m_transitionConfig;
|
||||
readonly MonoBehaviour m_coroutineHost;
|
||||
readonly ChannelFader m_fader;
|
||||
|
||||
ContainerPlayHandle m_currentHandle;
|
||||
Coroutine m_transitionCoroutine;
|
||||
Coroutine m_currentFadeOutCoroutine;
|
||||
Coroutine m_currentFadeInCoroutine;
|
||||
|
||||
uint m_currentContainerId;
|
||||
|
||||
public AmbienceChannelPlayer(
|
||||
AmbienceTransitionConfig transitionConfig,
|
||||
MusicContainerPlayer player,
|
||||
MonoBehaviour coroutineHost)
|
||||
{
|
||||
this.m_transitionConfig = transitionConfig;
|
||||
this.m_coroutineHost = coroutineHost;
|
||||
this.m_fader = new ChannelFader(player, coroutineHost);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 公开接口
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
public void SwitchTo(uint newContainerId, uint fromPathId, uint toPathId)
|
||||
{
|
||||
if (newContainerId == this.m_currentContainerId && this.m_currentHandle != null)
|
||||
return;
|
||||
|
||||
AmbienceTransition transition = ResolveTransition(fromPathId, toPathId);
|
||||
|
||||
if (this.m_transitionCoroutine != null)
|
||||
this.m_coroutineHost.StopCoroutine(this.m_transitionCoroutine);
|
||||
|
||||
this.m_transitionCoroutine = this.m_coroutineHost.StartCoroutine(
|
||||
DoTransition(newContainerId, transition));
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
if (this.m_transitionCoroutine != null)
|
||||
this.m_coroutineHost.StopCoroutine(this.m_transitionCoroutine);
|
||||
|
||||
this.m_fader.StopCurrent();
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Transition 流程(无节拍对齐)
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
IEnumerator DoTransition(uint newContainerId, AmbienceTransition transition)
|
||||
{
|
||||
if (newContainerId == this.m_fader.CurrentContainerId && this.m_fader.CurrentHandle != null) yield break;
|
||||
// 如果等待期间被切回来了,就不淡变了
|
||||
|
||||
if (this.m_currentFadeInCoroutine != null)
|
||||
{
|
||||
this.m_coroutineHost.StopCoroutine(this.m_currentFadeInCoroutine);
|
||||
this.m_currentFadeInCoroutine = null;
|
||||
}
|
||||
|
||||
if (this.m_currentFadeOutCoroutine != null)
|
||||
{
|
||||
this.m_coroutineHost.StopCoroutine(this.m_currentFadeOutCoroutine);
|
||||
this.m_currentFadeOutCoroutine = null;
|
||||
}
|
||||
|
||||
ContainerPlayHandle outgoing = this.m_fader.CurrentHandle;
|
||||
float outgoingVolume = this.m_fader.CurrentVolume;
|
||||
|
||||
this.m_currentFadeOutCoroutine = this.m_coroutineHost.StartCoroutine(
|
||||
this.m_fader.FadeOutBranch(outgoing, outgoingVolume, transition));
|
||||
|
||||
this.m_currentFadeInCoroutine = this.m_coroutineHost.StartCoroutine(
|
||||
this.m_fader.FadeInBranch(newContainerId, transition));
|
||||
yield return this.m_currentFadeInCoroutine;
|
||||
this.m_currentFadeInCoroutine = null;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 工具
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
AmbienceTransition ResolveTransition(uint fromPathId, uint toPathId)
|
||||
{
|
||||
// 优先精确匹配
|
||||
uint exactId = fromPathId * 1000 + toPathId;
|
||||
AmbienceTransition exact = this.m_transitionConfig.QueryById(exactId);
|
||||
if (exact != null) return exact;
|
||||
|
||||
// From 为任意
|
||||
uint fromWildcard = 999u * 1000 + toPathId;
|
||||
AmbienceTransition fromWild = this.m_transitionConfig.QueryById(fromWildcard);
|
||||
if (fromWild != null) return fromWild;
|
||||
|
||||
// To 为任意
|
||||
uint toWildcard = fromPathId * 1000 + 999u;
|
||||
AmbienceTransition toWild = this.m_transitionConfig.QueryById(toWildcard);
|
||||
if (toWild != null) return toWild;
|
||||
|
||||
// 全通配
|
||||
const uint allWild = 999u * 1000 + 999u;
|
||||
return this.m_transitionConfig.QueryById(allWild);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c4d8f04a88faf499baa9e72098bd1083
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,147 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace OCES.Audio
|
||||
{
|
||||
public class AudioContainerSelector
|
||||
{
|
||||
// TODO: 根据 audioObject.ContainerScope 控制随机历史的共享范围:
|
||||
// - Global(当前实现):同一 AudioObject 的所有并发实例共享同一份 playHistory 和 limitRepetition 队列,
|
||||
// 不放回随机在全局层面生效,并发实例之间互相感知已播放记录。
|
||||
// - PerInstance:每个并发播放实例持有独立的 playHistory 和 limitRepetition 队列,
|
||||
// 不放回随机仅在该实例内生效,实例之间完全隔离。
|
||||
// 实现思路:PerInstance 模式下将 HashSet/Queue 作为局部变量传入协程,不再写入这两个 Dictionary。
|
||||
readonly Dictionary<uint, HashSet<int>> m_randomPlayedHistories = new();
|
||||
readonly Dictionary<uint, Queue<int>> m_randomRecentQueues = new();
|
||||
readonly Dictionary<uint, int> m_sequenceNextIndex = new();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Shuffle 随机模式下选取 index(支持 LimitRepetition 队列)
|
||||
/// </summary>
|
||||
public int PickShuffleIndex(AudioObject audioObject)
|
||||
{
|
||||
int count = audioObject.Name.Count;
|
||||
|
||||
// LimitRepetition 队列
|
||||
int limitRepetition = Mathf.Clamp(audioObject.LimitRepetition, 0, count - 1);
|
||||
if (!this.m_randomRecentQueues.TryGetValue(audioObject.Id, out Queue<int> recent))
|
||||
{
|
||||
recent = new Queue<int>();
|
||||
this.m_randomRecentQueues[audioObject.Id] = recent;
|
||||
}
|
||||
|
||||
if (!audioObject.RandomType)
|
||||
{
|
||||
List<int> candidates = Enumerable.Range(0, count)
|
||||
.Where(i => !recent.Contains(i))
|
||||
.ToList();
|
||||
|
||||
if (candidates.Count == 0)
|
||||
candidates = Enumerable.Range(0, count).ToList();
|
||||
|
||||
int chosen = candidates[Random.Range(0, candidates.Count)];
|
||||
|
||||
if (limitRepetition > 0)
|
||||
{
|
||||
recent.Enqueue(chosen);
|
||||
if (recent.Count > limitRepetition)
|
||||
recent.Dequeue();
|
||||
}
|
||||
|
||||
return chosen;
|
||||
}
|
||||
|
||||
// 不放回随机
|
||||
if (!this.m_randomPlayedHistories.TryGetValue(audioObject.Id, out HashSet<int> history))
|
||||
{
|
||||
history = new HashSet<int>();
|
||||
this.m_randomPlayedHistories[audioObject.Id] = history;
|
||||
}
|
||||
|
||||
List<int> available = Enumerable.Range(0, count)
|
||||
.Where(i => !history.Contains(i) && !recent.Contains(i))
|
||||
.ToList();
|
||||
|
||||
if (available.Count == 0)
|
||||
{
|
||||
history.Clear();
|
||||
available = Enumerable.Range(0, count)
|
||||
.Where(i => !recent.Contains(i))
|
||||
.ToList();
|
||||
|
||||
if (available.Count == 0)
|
||||
available = Enumerable.Range(0, count).ToList();
|
||||
}
|
||||
|
||||
int chosenIndex = available[Random.Range(0, available.Count)];
|
||||
history.Add(chosenIndex);
|
||||
|
||||
if (limitRepetition > 0)
|
||||
{
|
||||
recent.Enqueue(chosenIndex);
|
||||
if (recent.Count > limitRepetition)
|
||||
recent.Dequeue();
|
||||
}
|
||||
|
||||
return chosenIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 连续容器的 Random 模式下,带 LimitRepetition 逻辑地选取 index
|
||||
/// </summary>
|
||||
public int PickNextRandomIndex(AudioObject audioObject, List<int> remainingPool, Queue<int> recentPlayed, int limitRepetition)
|
||||
{
|
||||
if (!this.m_randomPlayedHistories.TryGetValue(audioObject.Id, out HashSet<int> history))
|
||||
{
|
||||
history = new HashSet<int>();
|
||||
this.m_randomPlayedHistories[audioObject.Id] = history;
|
||||
}
|
||||
|
||||
List<int> candidates = remainingPool
|
||||
.Where(i => !recentPlayed.Contains(i))
|
||||
.ToList();
|
||||
|
||||
if (candidates.Count == 0)
|
||||
candidates = new List<int>(remainingPool);
|
||||
|
||||
int index = candidates[Random.Range(0, candidates.Count)];
|
||||
|
||||
if (audioObject.RandomType)
|
||||
remainingPool.Remove(index);
|
||||
|
||||
if (limitRepetition > 0)
|
||||
recentPlayed.Enqueue(index);
|
||||
|
||||
if (recentPlayed.Count > limitRepetition)
|
||||
recentPlayed.Dequeue();
|
||||
|
||||
history.Add(index);
|
||||
return index;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Sequence 模式下的当前 index 并推进游标
|
||||
/// </summary>
|
||||
public int GetNextSequenceIndex(AudioObject audioObject)
|
||||
{
|
||||
int current = this.m_sequenceNextIndex.GetValueOrDefault(audioObject.Id, 0);
|
||||
this.m_sequenceNextIndex[audioObject.Id] = (current + 1) % audioObject.Name.Count;
|
||||
return current;
|
||||
}
|
||||
|
||||
public void ResetHistory(uint id)
|
||||
{
|
||||
if (this.m_randomPlayedHistories.TryGetValue(id, out HashSet<int> history))
|
||||
{
|
||||
history.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public int GetHistoryCount(uint id)
|
||||
{
|
||||
return this.m_randomPlayedHistories.TryGetValue(id, out HashSet<int> history) ? history.Count : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c3d3c29064584bf1a0cf1f023d5e3f61
|
||||
timeCreated: 1773368689
|
||||
@@ -0,0 +1,152 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace OCES.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// 与Transition无关的音量/生命周期管理逻辑
|
||||
/// </summary>
|
||||
public class ChannelFader
|
||||
{
|
||||
readonly MusicContainerPlayer m_player;
|
||||
readonly MonoBehaviour m_coroutineHost;
|
||||
|
||||
public ContainerPlayHandle CurrentHandle { get; private set; }
|
||||
public uint CurrentContainerId { get; private set; }
|
||||
public float CurrentVolume { get; private set; }
|
||||
|
||||
public ChannelFader(MusicContainerPlayer player, MonoBehaviour coroutineHost)
|
||||
{
|
||||
this.m_player = player;
|
||||
this.m_coroutineHost = coroutineHost;
|
||||
}
|
||||
|
||||
void StartNew(uint containerId, float startVolume)
|
||||
{
|
||||
CurrentContainerId = containerId;
|
||||
CurrentVolume = startVolume;
|
||||
CurrentHandle = this.m_player.Play(containerId);
|
||||
}
|
||||
|
||||
public void StopCurrent()
|
||||
{
|
||||
StopHandle(CurrentHandle);
|
||||
CurrentHandle = null;
|
||||
CurrentContainerId = 0;
|
||||
}
|
||||
|
||||
void StopHandle(ContainerPlayHandle handle)
|
||||
{
|
||||
if (handle == null) return;
|
||||
this.m_player.Stop(handle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 淡出分支:fire-and-forget,由调用方 StartCoroutine
|
||||
/// </summary>
|
||||
internal IEnumerator FadeOutBranch(ContainerPlayHandle outgoingHandle, float outgoingVolume, ITransitionConfig transition)
|
||||
{
|
||||
if (outgoingHandle == null) yield break;
|
||||
|
||||
if (transition?.FadeOutOffset > 0f)
|
||||
{
|
||||
//Debug.Log($"Waiting for {transition.FadeOutOffset} to fade out.");
|
||||
yield return new WaitForSeconds(transition.FadeOutOffset);
|
||||
}
|
||||
|
||||
if (transition?.FadeOutTime > 0f )
|
||||
yield return this.m_coroutineHost.StartCoroutine(
|
||||
FadeOut(outgoingHandle, outgoingVolume, transition.FadeOutTime));
|
||||
else
|
||||
StopHandle(outgoingHandle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 淡入分支:等待 FadeInOffset 后启动新音乐并淡入。
|
||||
/// 主协程 yield return 此分支,以便 DoTransition 在新音乐就绪后才结束。
|
||||
/// </summary>
|
||||
internal IEnumerator FadeInBranch(uint newContainerId, ITransitionConfig transition, Action onContainerStarted = null)
|
||||
{
|
||||
if (transition?.FadeInOffset > 0f)
|
||||
{
|
||||
//Debug.Log($"Waiting {transition.FadeInOffset} to fade in.");
|
||||
yield return new WaitForSeconds(transition.FadeInOffset);
|
||||
}
|
||||
|
||||
if (newContainerId == 0)
|
||||
{
|
||||
CurrentHandle = null;
|
||||
CurrentContainerId = 0;
|
||||
yield break;
|
||||
}
|
||||
|
||||
float startVolume = transition?.FadeInTime > 0f ? 0f : 1f;
|
||||
StartNew(newContainerId, startVolume);
|
||||
onContainerStarted?.Invoke();
|
||||
|
||||
if (transition?.FadeInTime > 0f)
|
||||
{
|
||||
yield return this.m_coroutineHost.StartCoroutine(
|
||||
FadeIn(CurrentHandle, transition.FadeInTime));
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator FadeOut(ContainerPlayHandle handle, float fromVolume, float duration)
|
||||
{
|
||||
//Debug.Log($"Fading out in {duration} seconds.");
|
||||
if (handle == null || handle.Cancelled) yield break;
|
||||
|
||||
float elapsed = 0f;
|
||||
List<AudioSource> sources = new();
|
||||
handle.CollectActiveSources(sources);
|
||||
|
||||
while (elapsed < duration)
|
||||
{
|
||||
if (handle.Cancelled) break;
|
||||
elapsed += Time.deltaTime;
|
||||
float t = Mathf.Clamp01(elapsed / duration);
|
||||
float vol = Mathf.Lerp(fromVolume, 0f, t);
|
||||
foreach (AudioSource src in sources)
|
||||
if (src) src.volume = vol;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
StopHandle(handle);
|
||||
}
|
||||
|
||||
IEnumerator FadeIn(ContainerPlayHandle handle, float duration)
|
||||
{
|
||||
//Debug.Log($"Fading In {duration} seconds.");
|
||||
if (handle == null || handle.Cancelled) yield break;
|
||||
|
||||
float elapsed = 0f;
|
||||
List<AudioSource> sources = new();
|
||||
|
||||
while (elapsed < duration)
|
||||
{
|
||||
if (handle.Cancelled) yield break;
|
||||
elapsed += Time.deltaTime;
|
||||
float t = Mathf.Clamp01(elapsed / duration);
|
||||
|
||||
// 每帧重新收集(Blend 模式下新 source 可能中途加入)
|
||||
sources.Clear();
|
||||
handle.CollectActiveSources(sources);
|
||||
foreach (AudioSource src in sources)
|
||||
if (src) src.volume = Mathf.Lerp(0f, 1f, t);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// 确保最终音量准确
|
||||
sources.Clear();
|
||||
handle.CollectActiveSources(sources);
|
||||
foreach (AudioSource src in sources)
|
||||
if (src) src.volume = 1f;
|
||||
|
||||
CurrentVolume = 1f;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2794446aae0840ec83fa7c5510ee55bf
|
||||
timeCreated: 1773992080
|
||||
@@ -0,0 +1,201 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace OCES.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// 音乐通道播放器。
|
||||
/// 负责:切换目标 Container、等待节拍对齐、执行淡入淡出 Transition。
|
||||
/// </summary>
|
||||
public class MusicChannelPlayer
|
||||
{
|
||||
readonly MusicContainerConfig m_containerConfig;
|
||||
readonly MusicTransitionConfig m_transitionConfig;
|
||||
readonly MonoBehaviour m_coroutineHost;
|
||||
readonly ChannelFader m_fader;
|
||||
|
||||
Coroutine m_currentFadeInCoroutine;
|
||||
Coroutine m_currentFadeOutCoroutine;
|
||||
|
||||
// 当前正在播放的句柄
|
||||
ContainerPlayHandle m_currentHandle;
|
||||
uint m_currentContainerId;
|
||||
float m_currentVolume = 1f;
|
||||
|
||||
// 当前播放的 Container(用于读取 bpm/timeSig 做节拍对齐)
|
||||
MusicContainer m_currentContainer;
|
||||
|
||||
// 当前播放开始的时间(用于计算当前播到哪个拍子)
|
||||
double m_playStartTime;
|
||||
|
||||
// 正在进行的 transition 协程(防止重叠)
|
||||
Coroutine m_transitionCoroutine;
|
||||
|
||||
public MusicChannelPlayer(
|
||||
MusicContainerConfig containerConfig,
|
||||
MusicTransitionConfig transitionConfig,
|
||||
MusicContainerPlayer player,
|
||||
MonoBehaviour coroutineHost)
|
||||
{
|
||||
this.m_containerConfig = containerConfig;
|
||||
this.m_transitionConfig = transitionConfig;
|
||||
this.m_coroutineHost = coroutineHost;
|
||||
this.m_fader = new ChannelFader(player, coroutineHost);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 公开接口
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 切换到新的 Container。
|
||||
/// fromPathId / toPathId 用于查询 Transition 配置。
|
||||
/// </summary>
|
||||
internal void SwitchTo(uint newContainerId, uint fromPathId, uint toPathId)
|
||||
{
|
||||
if (newContainerId == this.m_currentContainerId && this.m_currentHandle != null)
|
||||
return; // 已经在播目标,无需切换
|
||||
|
||||
MusicTransition transition = ResolveTransition(fromPathId, toPathId);
|
||||
|
||||
if (this.m_transitionCoroutine != null)
|
||||
this.m_coroutineHost.StopCoroutine(this.m_transitionCoroutine);
|
||||
|
||||
this.m_transitionCoroutine = this.m_coroutineHost.StartCoroutine(
|
||||
DoTransition(newContainerId, transition));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 立即停止当前播放(无淡出)
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
if (this.m_transitionCoroutine != null)
|
||||
this.m_coroutineHost.StopCoroutine(this.m_transitionCoroutine);
|
||||
|
||||
this.m_fader.StopCurrent();
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Transition 流程
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
IEnumerator DoTransition(uint newContainerId, MusicTransition transition)
|
||||
{
|
||||
if (this.m_currentFadeInCoroutine != null)
|
||||
{
|
||||
this.m_coroutineHost.StopCoroutine(this.m_currentFadeInCoroutine);
|
||||
this.m_currentFadeInCoroutine = null;
|
||||
}
|
||||
|
||||
if (this.m_currentFadeOutCoroutine != null)
|
||||
{
|
||||
this.m_coroutineHost.StopCoroutine(this.m_currentFadeOutCoroutine);
|
||||
this.m_currentFadeOutCoroutine = null;
|
||||
}
|
||||
|
||||
// ── 1. 等待节拍对齐(由 Transition 的 AlignMode 决定)──
|
||||
if (transition != null && this.m_currentContainer != null)
|
||||
{
|
||||
yield return this.m_coroutineHost.StartCoroutine(
|
||||
WaitForAlignment(transition.AlignMode, this.m_currentContainer));
|
||||
}
|
||||
|
||||
// ── 2 & 3. 淡出与淡入并行:两条分支从同一时刻起算各自的 Offset,互不等待 ──
|
||||
if (newContainerId == this.m_fader.CurrentContainerId && this.m_fader.CurrentHandle != null) yield break;
|
||||
// 如果等待期间被切回来了,就不淡变了
|
||||
|
||||
ContainerPlayHandle outgoing = this.m_fader.CurrentHandle;
|
||||
float outVol = this.m_fader.CurrentVolume;
|
||||
|
||||
this.m_currentFadeOutCoroutine = this.m_coroutineHost.StartCoroutine(
|
||||
this.m_fader.FadeOutBranch(outgoing, outVol, transition));
|
||||
|
||||
this.m_currentFadeInCoroutine = this.m_coroutineHost.StartCoroutine(
|
||||
this.m_fader.FadeInBranch(newContainerId, transition,
|
||||
onContainerStarted: () =>
|
||||
{
|
||||
// Music 独有:记录新 container 和播放起始时间
|
||||
this.m_currentContainer = this.m_containerConfig.QueryById(newContainerId);
|
||||
this.m_playStartTime = AudioSettings.dspTime;
|
||||
}));
|
||||
yield return this.m_currentFadeInCoroutine;
|
||||
this.m_currentFadeInCoroutine = null;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 节拍对齐等待
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
IEnumerator WaitForAlignment(AlignMode mode, MusicContainer container)
|
||||
{
|
||||
if (mode == AlignMode.Immediate || container.Bpm <= 0f)
|
||||
yield break;
|
||||
|
||||
double now = AudioSettings.dspTime;
|
||||
double elapsed = now - this.m_playStartTime;
|
||||
|
||||
double secondsPerBeat = 60.0 / container.Bpm;
|
||||
|
||||
if (mode == AlignMode.Beat)
|
||||
{
|
||||
double beatsElapsed = elapsed / secondsPerBeat;
|
||||
double nextBeat = System.Math.Ceiling(beatsElapsed);
|
||||
double waitSeconds = (nextBeat - beatsElapsed) * secondsPerBeat;
|
||||
if (waitSeconds > 0.001)
|
||||
yield return new WaitForSeconds((float)waitSeconds);
|
||||
}
|
||||
else if (mode == AlignMode.Bar)
|
||||
{
|
||||
int beatsPerBar = ParseBeatsPerBar(container.TimeSig);
|
||||
double secondsPerBar = secondsPerBeat * beatsPerBar;
|
||||
double barsElapsed = elapsed / secondsPerBar;
|
||||
double nextBar = System.Math.Ceiling(barsElapsed);
|
||||
double waitSeconds = (nextBar - barsElapsed) * secondsPerBar;
|
||||
if (waitSeconds > 0.001)
|
||||
yield return new WaitForSeconds((float)waitSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析拍号字符串(如 "4/4", "3/4"),返回每小节拍数。
|
||||
/// </summary>
|
||||
static int ParseBeatsPerBar(string timeSig)
|
||||
{
|
||||
if (string.IsNullOrEmpty(timeSig)) return 4;
|
||||
string[] parts = timeSig.Split('/');
|
||||
if (parts.Length >= 1 && int.TryParse(parts[0], out int beats))
|
||||
return beats;
|
||||
return 4;
|
||||
}
|
||||
// ─────────────────────────────────────────────
|
||||
// 工具
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 查询 Transition 配置,支持精确匹配和 999 通配符。
|
||||
/// ID 规则:FromPathId × 1000 + ToPathId,999 表示任意。
|
||||
/// </summary>
|
||||
MusicTransition ResolveTransition(uint fromPathId, uint toPathId)
|
||||
{
|
||||
// 优先精确匹配
|
||||
uint exactId = fromPathId * 1000 + toPathId;
|
||||
MusicTransition exact = this.m_transitionConfig.QueryById(exactId);
|
||||
if (exact != null) return exact;
|
||||
|
||||
// From 为任意
|
||||
uint fromWildcard = 999u * 1000 + toPathId;
|
||||
MusicTransition fromWild = this.m_transitionConfig.QueryById(fromWildcard);
|
||||
if (fromWild != null) return fromWild;
|
||||
|
||||
// To 为任意
|
||||
uint toWildcard = fromPathId * 1000 + 999u;
|
||||
MusicTransition toWild = this.m_transitionConfig.QueryById(toWildcard);
|
||||
if (toWild != null) return toWild;
|
||||
|
||||
// 全通配
|
||||
const uint allWild = 999u * 1000 + 999u;
|
||||
return this.m_transitionConfig.QueryById(allWild);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f1f58aa9d667d4a379642a33e5fb8d76
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,391 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace OCES.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// 负责递归播放一个 MusicContainer 树。
|
||||
/// 被 MusicChannelPlayer 和 AmbienceChannelPlayer 共同使用。
|
||||
/// 不持有状态机逻辑,只负责"把这个 Container 播完"。
|
||||
/// </summary>
|
||||
public class MusicContainerPlayer
|
||||
{
|
||||
readonly MusicContainerConfig m_containerConfig;
|
||||
readonly MusicSegmentConfig m_segmentConfig;
|
||||
readonly AudioSourcePool m_pool;
|
||||
readonly MonoBehaviour m_coroutineHost;
|
||||
UnityEngine.Audio.AudioMixerGroup m_mixerGroup;
|
||||
|
||||
|
||||
// Sequence Step 模式的全局游标,key = containerId
|
||||
readonly Dictionary<uint, int> m_sequenceStepIndex = new();
|
||||
|
||||
// Random 模式的不放回历史,key = containerId
|
||||
readonly Dictionary<uint, HashSet<uint>> m_randomHistory = new();
|
||||
|
||||
public MusicContainerPlayer(
|
||||
MusicContainerConfig containerConfig,
|
||||
MusicSegmentConfig segmentConfig,
|
||||
AudioSourcePool pool,
|
||||
MonoBehaviour coroutineHost)
|
||||
{
|
||||
this.m_containerConfig = containerConfig;
|
||||
this.m_segmentConfig = segmentConfig;
|
||||
this.m_pool = pool;
|
||||
this.m_coroutineHost = coroutineHost;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 公开入口
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 开始播放指定 Container,播放完毕后调用 onFinished。
|
||||
/// 返回可用于外部停止的句柄。
|
||||
/// </summary>
|
||||
public ContainerPlayHandle Play(uint containerId, System.Action onFinished = null)
|
||||
{
|
||||
MusicContainer container = this.m_containerConfig.QueryById(containerId);
|
||||
if (container == null)
|
||||
{
|
||||
Debug.LogError($"[MusicContainerPlayer] 找不到 ContainerId: {containerId}");
|
||||
onFinished?.Invoke();
|
||||
return null;
|
||||
}
|
||||
|
||||
ContainerPlayHandle handle = new();
|
||||
handle.TargetVolume = 1f;
|
||||
handle.Coroutine = this.m_coroutineHost.StartCoroutine(
|
||||
PlayContainerCoroutine(container, handle, onFinished));
|
||||
return handle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止一个播放句柄(立即停止,不淡出)
|
||||
/// </summary>
|
||||
public void Stop(ContainerPlayHandle handle)
|
||||
{
|
||||
if (handle == null) return;
|
||||
handle.Cancelled = true;
|
||||
|
||||
foreach (ContainerPlayHandle child in handle.ChildHandles)
|
||||
{
|
||||
Stop(child);
|
||||
}
|
||||
handle.ChildHandles.Clear();
|
||||
|
||||
|
||||
if (handle.Coroutine != null)
|
||||
this.m_coroutineHost.StopCoroutine(handle.Coroutine);
|
||||
foreach (AudioSource src in handle.ActiveSources)
|
||||
{
|
||||
ReturnSource(src);
|
||||
}
|
||||
handle.ActiveSources.Clear();
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 核心协程
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
IEnumerator PlayContainerCoroutine(
|
||||
MusicContainer container,
|
||||
ContainerPlayHandle handle,
|
||||
System.Action onFinished)
|
||||
{
|
||||
int loopsCompleted = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
yield return this.m_coroutineHost.StartCoroutine(
|
||||
PlayContainerOnce(container, handle.TargetVolume, handle));
|
||||
|
||||
if (handle.Cancelled) yield break;
|
||||
|
||||
loopsCompleted++;
|
||||
|
||||
// -1 = 无限循环,一直重复
|
||||
if (container.LoopCount == -1)
|
||||
continue;
|
||||
|
||||
// 0 = 不循环,播一次就结束
|
||||
if (container.LoopCount == 0)
|
||||
break;
|
||||
|
||||
// >= 1,播满次数后结束
|
||||
if (loopsCompleted >= container.LoopCount)
|
||||
break;
|
||||
}
|
||||
|
||||
if (!handle.Cancelled)
|
||||
onFinished?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 播放一个 Container 一轮(不含循环逻辑)
|
||||
/// </summary>
|
||||
IEnumerator PlayContainerOnce(MusicContainer container, float volumeScale, ContainerPlayHandle handle)
|
||||
{
|
||||
if (container.Segments == null || container.Segments.Count == 0)
|
||||
yield break;
|
||||
|
||||
switch (container.ContainerType)
|
||||
{
|
||||
case ContainerType.Blend:
|
||||
yield return PlayBlend(container, volumeScale, handle);
|
||||
break;
|
||||
|
||||
case ContainerType.Sequence:
|
||||
yield return PlaySequence(container, volumeScale, handle);
|
||||
break;
|
||||
|
||||
case ContainerType.Random:
|
||||
yield return PlayRandom(container, volumeScale, handle);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Blend(同时播放)
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
IEnumerator PlayBlend(MusicContainer container, float volumeScale, ContainerPlayHandle handle)
|
||||
{
|
||||
// 同时启动所有子元素,等待全部结束
|
||||
var childHandles = new List<ContainerPlayHandle>();
|
||||
bool allDone = false;
|
||||
int remaining = container.Segments.Count;
|
||||
|
||||
foreach (uint segId in container.Segments)
|
||||
{
|
||||
if (handle.Cancelled) yield break;
|
||||
|
||||
ContainerPlayHandle childHandle = PlayChild(segId, volumeScale, () =>
|
||||
{
|
||||
remaining--;
|
||||
if (remaining <= 0) allDone = true;
|
||||
});
|
||||
if (childHandle != null)
|
||||
{
|
||||
childHandles.Add(childHandle);
|
||||
handle.ChildHandles.AddRange(childHandles);
|
||||
}
|
||||
}
|
||||
|
||||
yield return new WaitUntil(() => allDone || handle.Cancelled);
|
||||
|
||||
if (handle.Cancelled)
|
||||
{
|
||||
foreach (var ch in childHandles)
|
||||
Stop(ch);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Sequence
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
IEnumerator PlaySequence(MusicContainer container, float volumeScale, ContainerPlayHandle handle)
|
||||
{
|
||||
bool isStep = container.ContainerPlayMode;
|
||||
|
||||
if (isStep)
|
||||
{
|
||||
// Step: 每次只播一个,游标全局推进
|
||||
int index = GetNextSequenceIndex(container);
|
||||
yield return PlayChildAndWait(container.Segments[index], volumeScale, handle);
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
// Continuous: 顺序播完所有子元素,算一轮
|
||||
for (int i = 0; i < container.Segments.Count; i++)
|
||||
{
|
||||
if (handle.Cancelled) yield break;
|
||||
|
||||
// StrategyParam 作为两段之间的间隔时间(秒)
|
||||
if (i > 0 && container.StrategyParam > 0)
|
||||
yield return new WaitForSeconds(container.StrategyParam);
|
||||
|
||||
yield return PlayChildAndWait(container.Segments[i], volumeScale, handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Random
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
IEnumerator PlayRandom(MusicContainer container, float volumeScale, ContainerPlayHandle handle)
|
||||
{
|
||||
bool isStep = container.ContainerPlayMode; // 同上,音乐系统默认 Continuous
|
||||
|
||||
if (isStep)
|
||||
{
|
||||
// Step Random: 随机选一个播放,算一次 loopCount
|
||||
uint chosen = PickRandomChild(container);
|
||||
yield return PlayChildAndWait(chosen, volumeScale, handle);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Continuous Random: 随机不放回地播完所有子元素,算一轮
|
||||
var remaining = new List<uint>(container.Segments);
|
||||
|
||||
while (remaining.Count > 0)
|
||||
{
|
||||
if (handle.Cancelled) yield break;
|
||||
|
||||
int idx = Random.Range(0, remaining.Count);
|
||||
uint chosen = remaining[idx];
|
||||
remaining.RemoveAt(idx);
|
||||
|
||||
if (container.StrategyParam > 0 && remaining.Count < container.Segments.Count - 1)
|
||||
yield return new WaitForSeconds(container.StrategyParam);
|
||||
|
||||
yield return PlayChildAndWait(chosen, volumeScale, handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 子元素分发(Segment 或嵌套 Container)
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 播放一个子元素(segment 或 container),等待其完成后返回。
|
||||
/// </summary>
|
||||
IEnumerator PlayChildAndWait(uint id, float volumeScale, ContainerPlayHandle parentHandle)
|
||||
{
|
||||
bool done = false;
|
||||
ContainerPlayHandle child = PlayChild(id, volumeScale, () => done = true);
|
||||
if (child != null)
|
||||
parentHandle.ChildHandles.Add(child);
|
||||
|
||||
yield return new WaitUntil(() => done || parentHandle.Cancelled);
|
||||
|
||||
if (parentHandle.Cancelled && child != null)
|
||||
Stop(child);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动一个子元素的播放,不等待,返回句柄。
|
||||
/// </summary>
|
||||
ContainerPlayHandle PlayChild(uint id, float volumeScale, System.Action onDone)
|
||||
{
|
||||
// ID < 1000000 是 MusicSegment,否则是嵌套 Container
|
||||
return id < 1000000u ? PlaySegment(id, volumeScale, onDone) : Play(id, onDone);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Segment 播放
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
ContainerPlayHandle PlaySegment(uint segmentId, float volumeScale, System.Action onFinished)
|
||||
{
|
||||
MusicSegment segment = this.m_segmentConfig.QueryById(segmentId);
|
||||
if (segment == null)
|
||||
{
|
||||
Debug.LogError($"[MusicContainerPlayer] 找不到 SegmentId: {segmentId}");
|
||||
onFinished?.Invoke();
|
||||
return null;
|
||||
}
|
||||
|
||||
AudioClip clip = Resources.Load<AudioClip>($"Audios/{segment.Name}");
|
||||
if (!clip)
|
||||
{
|
||||
Debug.LogError($"[MusicContainerPlayer] 音频文件未找到: {segment.Name}");
|
||||
onFinished?.Invoke();
|
||||
return null;
|
||||
}
|
||||
|
||||
AudioSource source = this.m_pool.AcquireAudioSource();
|
||||
source.clip = clip;
|
||||
source.loop = false;
|
||||
source.volume = volumeScale;
|
||||
source.Play();
|
||||
|
||||
var handle = new ContainerPlayHandle();
|
||||
handle.ActiveSources.Add(source);
|
||||
handle.Coroutine = this.m_coroutineHost.StartCoroutine(
|
||||
WaitSegmentFinish(source, handle, onFinished));
|
||||
return handle;
|
||||
}
|
||||
|
||||
IEnumerator WaitSegmentFinish(AudioSource source, ContainerPlayHandle handle, System.Action onFinished)
|
||||
{
|
||||
yield return new WaitWhile(() => source.isPlaying && !handle.Cancelled);
|
||||
|
||||
|
||||
source.Stop();
|
||||
ReturnSource(source);
|
||||
handle.ActiveSources.Remove(source);
|
||||
if (!handle.Cancelled)
|
||||
onFinished?.Invoke();
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 工具
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
void ReturnSource(AudioSource source)
|
||||
{
|
||||
source.Stop();
|
||||
this.m_pool.ReturnToPool(source.gameObject);
|
||||
}
|
||||
|
||||
int GetNextSequenceIndex(MusicContainer container)
|
||||
{
|
||||
int current = this.m_sequenceStepIndex.GetValueOrDefault(container.Id, 0);
|
||||
this.m_sequenceStepIndex[container.Id] = (current + 1) % container.Segments.Count;
|
||||
return current;
|
||||
}
|
||||
|
||||
uint PickRandomChild(MusicContainer container)
|
||||
{
|
||||
if (!this.m_randomHistory.TryGetValue(container.Id, out HashSet<uint> history))
|
||||
{
|
||||
history = new HashSet<uint>();
|
||||
this.m_randomHistory[container.Id] = history;
|
||||
}
|
||||
|
||||
List<uint> available = container.Segments.Where(id => !history.Contains(id)).ToList();
|
||||
if (available.Count == 0)
|
||||
{
|
||||
history.Clear();
|
||||
available = new List<uint>(container.Segments);
|
||||
}
|
||||
|
||||
uint chosen = available[Random.Range(0, available.Count)];
|
||||
history.Add(chosen);
|
||||
return chosen;
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 播放句柄
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 一次 Container 播放的句柄,用于外部停止或淡出时访问正在播放的 AudioSource。
|
||||
/// </summary>
|
||||
public class ContainerPlayHandle
|
||||
{
|
||||
public Coroutine Coroutine;
|
||||
public bool Cancelled;
|
||||
public float TargetVolume = 1f;
|
||||
public List<AudioSource> ActiveSources = new();
|
||||
public List<ContainerPlayHandle> ChildHandles = new();
|
||||
|
||||
/// <summary>
|
||||
/// 递归收集所有正在发声的 AudioSource(用于淡出)
|
||||
/// </summary>
|
||||
public void CollectActiveSources(List<AudioSource> result)
|
||||
{
|
||||
result.AddRange(this.ActiveSources);
|
||||
foreach (ContainerPlayHandle child in this.ChildHandles)
|
||||
child.CollectActiveSources(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 860a68dfcf02146c2b41351d6a887df9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,143 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace OCES.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// 维护当前激活的游戏状态,在状态变化时全量匹配 Path 表,返回应当播放的 ContainerId。
|
||||
/// </summary>
|
||||
public class MusicStateRouter
|
||||
{
|
||||
// key: StateGroup enum Type,value: 当前激活的 enum 整数值
|
||||
readonly Dictionary<Type, int> m_activeStates = new();
|
||||
|
||||
readonly MusicPathConfig m_musicPaths;
|
||||
readonly AmbiencePathConfig m_ambiencePaths;
|
||||
|
||||
// 上一次匹配到的 PathId,用于 Transition 表的 FromPathId 查询
|
||||
public uint LastMusicPathId { get; private set; }
|
||||
public uint LastAmbiencePathId { get; private set; }
|
||||
|
||||
public MusicStateRouter(MusicPathConfig musicPaths, AmbiencePathConfig ambiencePaths)
|
||||
{
|
||||
this.m_musicPaths = musicPaths;
|
||||
this.m_ambiencePaths = ambiencePaths;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新某个 StateGroup 的当前值,并重新全量匹配两张 Path 表。
|
||||
/// </summary>
|
||||
public void SetState<TEnum>(TEnum state, out uint musicContainerId, out uint ambienceContainerId)
|
||||
where TEnum : Enum
|
||||
{
|
||||
// Dictionary<Type, int> 天然保证同一 StateGroup 只保留最新值,直接覆盖即可
|
||||
this.m_activeStates[typeof(TEnum)] = Convert.ToInt32(state);
|
||||
|
||||
musicContainerId = MatchBestPath(this.m_musicPaths.MusicPathList(), out uint musicPathId);
|
||||
ambienceContainerId = MatchBestPath(this.m_ambiencePaths.AmbiencePathList(), out uint ambiencePathId);
|
||||
|
||||
LastMusicPathId = musicPathId;
|
||||
LastAmbiencePathId = ambiencePathId;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
Editor.DebugInfoCollector.Instance.ActiveStates = this.m_activeStates;
|
||||
#endif
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 内部匹配逻辑
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 遍历路径列表,找到所有满足当前状态的规则,返回 priority 最小(最高优先级)的那条的 ContainerId。
|
||||
/// </summary>
|
||||
uint MatchBestPath<T>(List<T> paths, out uint matchedPathId) where T : IPathEntry
|
||||
{
|
||||
IPathEntry best = null;
|
||||
foreach (T path in paths)
|
||||
{
|
||||
if (PathMatches(path.Path) && (best == null || path.Priority < best.Priority))
|
||||
best = path;
|
||||
}
|
||||
|
||||
if (best != null)
|
||||
{
|
||||
matchedPathId = best.Id;
|
||||
return best.ContainerId;
|
||||
}
|
||||
|
||||
matchedPathId = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析 path 字符串,判断当前激活状态是否满足该条规则。
|
||||
/// 格式:TypeID1,子状态|TypeID2,子状态……
|
||||
/// 规则中的每一个条件都必须满足(AND 关系)。
|
||||
/// </summary>
|
||||
bool PathMatches(string pathStr)
|
||||
{
|
||||
if (string.IsNullOrEmpty(pathStr))
|
||||
return true; // 空 path 视为无条件匹配(默认兜底规则)
|
||||
|
||||
string[] conditions = pathStr.Split('|');
|
||||
foreach (string condition in conditions)
|
||||
{
|
||||
string[] parts = condition.Split(',');
|
||||
if (parts.Length != 2)
|
||||
{
|
||||
Debug.LogWarning($"[MusicStateRouter] Path格式错误: '{condition}'");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!int.TryParse(parts[0].Trim(), out int typeId) ||
|
||||
!int.TryParse(parts[1].Trim(), out int stateValue))
|
||||
{
|
||||
Debug.LogWarning($"[MusicStateRouter] Path解析失败: '{condition}'");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool conditionMet = false;
|
||||
foreach (KeyValuePair<Type, int> kv in this.m_activeStates)
|
||||
{
|
||||
if (StateGroupRegistry.GetTypeId(kv.Key) != typeId || kv.Value != stateValue)
|
||||
continue;
|
||||
conditionMet = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!conditionMet)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// StateGroup 注册表:将 enum Type 映射为策划表中填写的 TypeId 整数
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 程序员在此注册所有 StateGroup enum 与其对应 TypeId 的映射关系。
|
||||
/// 策划在 MusicPath.path 字段中填写的 TypeId 数字必须与此处一致。
|
||||
/// </summary>
|
||||
public static class StateGroupRegistry
|
||||
{
|
||||
static readonly Dictionary<Type, int> s_typeIdMap = new();
|
||||
|
||||
public static void Register<TEnum>(int typeId) where TEnum : Enum
|
||||
{
|
||||
s_typeIdMap[typeof(TEnum)] = typeId;
|
||||
}
|
||||
|
||||
public static int GetTypeId(Type enumType)
|
||||
{
|
||||
if (s_typeIdMap.TryGetValue(enumType, out int id))
|
||||
return id;
|
||||
Debug.LogWarning($"[StateGroupRegistry] 未注册的StateGroup类型: {enumType.Name},请调用StateGroupRegistry.Register<T>()");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c82693d4d73e64daf9bbd6854a818dc8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace OCES.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// 音乐与环境音系统。由 AudioSystem 持有并初始化。
|
||||
/// 对外只暴露 OnStateChanged,由 AudioSystem.SetState 转发调用。
|
||||
/// </summary>
|
||||
public class MusicSystem : MonoBehaviour
|
||||
{
|
||||
MusicStateRouter m_stateRouter;
|
||||
MusicChannelPlayer m_musicChannel;
|
||||
AmbienceChannelPlayer m_ambienceChannel;
|
||||
|
||||
// 记录上一次两个通道各自匹配到的 PathId,用于查 Transition 表
|
||||
uint m_lastMusicPathId;
|
||||
uint m_lastAmbiencePathId;
|
||||
|
||||
public void Initialize(
|
||||
MusicSegmentConfig segments,
|
||||
MusicContainerConfig containers,
|
||||
MusicPathConfig musicPaths,
|
||||
AmbiencePathConfig ambiencePaths,
|
||||
MusicTransitionConfig musicTransitions,
|
||||
AmbienceTransitionConfig ambienceTransitions,
|
||||
AudioSourcePool musicPool,
|
||||
AudioSourcePool ambiencePool)
|
||||
{
|
||||
MusicContainerPlayer musicContainerPlayer = new(containers, segments, musicPool, this);
|
||||
MusicContainerPlayer ambientContainerPlayer = new(containers, segments, ambiencePool, this);
|
||||
|
||||
this.m_stateRouter = new MusicStateRouter(musicPaths, ambiencePaths);
|
||||
this.m_musicChannel = new MusicChannelPlayer(containers, musicTransitions, musicContainerPlayer, this);
|
||||
this.m_ambienceChannel = new AmbienceChannelPlayer(ambienceTransitions, ambientContainerPlayer, this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 由 AudioSystem.SetState 调用,更新状态并驱动两个通道切换。
|
||||
/// </summary>
|
||||
public void OnStateChanged<TEnum>(TEnum state) where TEnum : Enum
|
||||
{
|
||||
this.m_stateRouter.SetState(
|
||||
state,
|
||||
out uint musicContainerId,
|
||||
out uint ambienceContainerId);
|
||||
|
||||
uint newMusicPathId = this.m_stateRouter.LastMusicPathId;
|
||||
uint newAmbiencePathId = this.m_stateRouter.LastAmbiencePathId;
|
||||
|
||||
this.m_musicChannel.SwitchTo(musicContainerId, this.m_lastMusicPathId, newMusicPathId);
|
||||
this.m_ambienceChannel.SwitchTo(ambienceContainerId, this.m_lastAmbiencePathId, newAmbiencePathId);
|
||||
|
||||
this.m_lastMusicPathId = newMusicPathId;
|
||||
this.m_lastAmbiencePathId = newAmbiencePathId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 936ca15cb5c944ca99e98fece679861e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user