using System.Collections.Generic; using UnityEngine; using Sirenix.OdinInspector; namespace Cielonos.MainGame { /// /// 战斗音乐控制器,负责处理功能音乐的无缝随机切换,以及并行的 BGM 音轨按小节边界起播/停止。 /// [AddComponentMenu("Cielonos/Rhythm/CombatMusicController")] public class CombatMusicController : MonoBehaviour { [Header("System References")] [Tooltip("全局节拍战斗系统的引用")] public MusicBeatSystem beatSystem; [Header("BGM Settings")] [Tooltip("播放背景音乐 Switch 容器的 Wwise Event")] public AK.Wwise.Event bgmMusicEvent; [Tooltip("所有可用的背景音乐 Wwise Segment 名称 (对应 Wwise 中的 Switch 值)")] public List bgmSegments = new List { "Back_00", "Back_01", "Back_02" }; [Header("Functional Music Settings")] [Tooltip("播放功能音乐 Switch 容器的 Wwise Event")] public AK.Wwise.Event functionalMusicEvent; [Tooltip("初始播放的音乐片段 Switch 名称")] public string initialSegment = "Func_00"; [Tooltip("所有可用的功能音乐 Wwise Segment 名称 (对应 Wwise 中的 Switch 值)")] public List functionalSegments = new List { "Func_00", "Func_01", "Func_02", "Func_03" }; // 运行时 BGM 状态跟踪字典 private readonly Dictionary bgmTargetStates = new Dictionary(); private readonly Dictionary bgmIsPlaying = new Dictionary(); private readonly Dictionary bgmPlayingIDs = new Dictionary(); private readonly Dictionary bgmGameObjects = new Dictionary(); // 等待下一个全局 PrepareNext 边界才起播的 BGM 片段队列 private readonly HashSet pendingBgmStarts = new HashSet(); private void Awake() { // 初始化每个 BGM 段落的运行状态与子 GameObject foreach (var seg in bgmSegments) { bgmTargetStates[seg] = false; bgmIsPlaying[seg] = false; bgmPlayingIDs[seg] = 0; // 为每个 BGM 片段创建独立的子 GameObject,以便并行播放和接收独立的节拍回调 GameObject go = new GameObject($"BGM_Layer_{seg}"); go.transform.SetParent(transform); bgmGameObjects[seg] = go; // 在节拍注册表中注册此子物体 MusicBeatSystem.RegisterRhythmGameObject(go); } } private void OnEnable() { if (beatSystem == null) { beatSystem = GetComponent(); } if (beatSystem != null) { beatSystem.OnPrepareNextSegment += DecideNextSegment; beatSystem.OnUserCueReceived += HandleTrackUserCue; beatSystem.OnGlobalPrepareNext += OnGlobalPrepareNextFired; } } private void OnDisable() { if (beatSystem != null) { beatSystem.OnPrepareNextSegment -= DecideNextSegment; beatSystem.OnUserCueReceived -= HandleTrackUserCue; beatSystem.OnGlobalPrepareNext -= OnGlobalPrepareNextFired; } } private void OnDestroy() { // 释放所有动态创建的子 GameObject foreach (var pair in bgmGameObjects) { if (pair.Value != null) { MusicBeatSystem.UnregisterRhythmGameObject(pair.Value); Destroy(pair.Value); } } } #region BGM Inspector Controls [Button("Play/Stop Back_00 (8 Bars)")] public void ToggleBack_00() => ToggleBgmTrack("Back_00"); [Button("Play/Stop Back_01 (8 Bars)")] public void ToggleBack_01() => ToggleBgmTrack("Back_01"); [Button("Play/Stop Back_02 (16 Bars)")] public void ToggleBack_02() => ToggleBgmTrack("Back_02"); #endregion #region BGM Implementation private void ToggleBgmTrack(string segmentName) { if (!bgmTargetStates.ContainsKey(segmentName)) return; bgmTargetStates[segmentName] = !bgmTargetStates[segmentName]; Debug.Log($"[CombatMusicController] Toggle BGM Segment '{segmentName}': Target state is now {bgmTargetStates[segmentName]}"); if (bgmTargetStates[segmentName]) { // 确保 beatSystem 已激活(用于接收 PrepareNext 通知) if (beatSystem != null && !beatSystem.IsActive) { beatSystem.Activate(null); } if (!bgmIsPlaying[segmentName] && !pendingBgmStarts.Contains(segmentName)) { // 如果没有任何音乐在播放或排队,立即起播(第一次) // 否则,排入队列等待下一个全局 PrepareNext 边界对齐 if (!AnyBgmPlaying() && !AnyBgmPending()) { Debug.Log($"[CombatMusicController] No music playing, starting BGM '{segmentName}' immediately."); StartBgmImmediately(segmentName); } else { pendingBgmStarts.Add(segmentName); Debug.Log($"[CombatMusicController] BGM '{segmentName}' queued, will start at next PrepareNext boundary."); } } } else { // 取消播放:从待播队列中移除(如果还没起播) pendingBgmStarts.Remove(segmentName); } } private bool AnyBgmPlaying() { foreach (var pair in bgmIsPlaying) { if (pair.Value) return true; } return false; } private bool AnyBgmPending() { return pendingBgmStarts.Count > 0; } /// /// 当全局主音乐(Func 轨)的 PrepareNext 到达时,将所有等待中的 BGM 片段一起起播。 /// Wwise 引擎会将它们精确对齐到 Exit Cue 边界! /// private void OnGlobalPrepareNextFired() { if (pendingBgmStarts.Count == 0) return; var toStart = new List(pendingBgmStarts); pendingBgmStarts.Clear(); uint callbackFlags = (uint)( AkCallbackType.AK_MusicSyncBeat | AkCallbackType.AK_MusicSyncEntry | AkCallbackType.AK_MusicSyncUserCue | AkCallbackType.AK_EndOfEvent ); foreach (var seg in toStart) { if (!bgmTargetStates.ContainsKey(seg) || !bgmTargetStates[seg]) continue; if (bgmIsPlaying[seg]) continue; bgmIsPlaying[seg] = true; AkUnitySoundEngine.SetSwitch(beatSystem.musicSegmentSwitchGroup, seg, bgmGameObjects[seg]); uint newID = bgmMusicEvent.Post( bgmGameObjects[seg], callbackFlags, beatSystem.OnWwiseMusicCallback, null ); if (newID != 0) { bgmPlayingIDs[seg] = newID; Debug.Log($"[CombatMusicController] BGM '{seg}' started at PrepareNext boundary. ID={newID}"); } else { bgmIsPlaying[seg] = false; Debug.LogError($"[CombatMusicController] Failed to post BGM event for '{seg}'"); } } } private void StartBgmImmediately(string segmentName) { if (bgmMusicEvent == null || !bgmMusicEvent.IsValid()) { Debug.LogError($"[CombatMusicController] Cannot play BGM: Event is invalid."); return; } GameObject go = bgmGameObjects[segmentName]; uint callbackFlags = (uint)( AkCallbackType.AK_MusicSyncBeat | AkCallbackType.AK_MusicSyncEntry | AkCallbackType.AK_MusicSyncUserCue | AkCallbackType.AK_EndOfEvent ); // 设置 Wwise Switch 以让此子物体播放当前 segment 音频 AkUnitySoundEngine.SetSwitch(beatSystem.musicSegmentSwitchGroup, segmentName, go); uint playingID = bgmMusicEvent.Post( go, callbackFlags, beatSystem.OnWwiseMusicCallback, null ); if (playingID != 0) { bgmIsPlaying[segmentName] = true; bgmPlayingIDs[segmentName] = playingID; Debug.Log($"[CombatMusicController] Posted BGM Event for '{segmentName}' immediately on '{go.name}', playingID={playingID}"); } } private void HandleTrackUserCue(uint playingID, string cueName) { // 查找是哪个 BGM 片段触发的回调 string triggeringSegment = null; foreach (var pair in bgmPlayingIDs) { if (pair.Value == playingID) { triggeringSegment = pair.Key; break; } } if (triggeringSegment == null) return; if (cueName == "PrepareNext") { uint callbackFlags = (uint)( AkCallbackType.AK_MusicSyncBeat | AkCallbackType.AK_MusicSyncEntry | AkCallbackType.AK_MusicSyncUserCue | AkCallbackType.AK_EndOfEvent ); // 1. 如果该 BGM 被要求继续播放,由于 Wwise 可能没有配置原生循环, // 我们在 PrepareNext 时刻(提前)再次 Post Event,让 Wwise 引擎将其调度到下一个 Exit 点无缝衔接起播! if (bgmTargetStates[triggeringSegment]) { uint newID = bgmMusicEvent.Post( bgmGameObjects[triggeringSegment], callbackFlags, beatSystem.OnWwiseMusicCallback, null ); if (newID != 0) { bgmPlayingIDs[triggeringSegment] = newID; Debug.Log($"[CombatMusicController] Loop transition: Re-posted Event for BGM '{triggeringSegment}'. New ID: {newID}"); } } // 如果被要求停止,我们什么都不做,让当前片段自然播放完毕并触发 EndOfEvent 即可。 // 2. 检查是否有排队等待播放的其他 BGM 片段。 // 同样在此时刻 Post 它们,Wwise 引擎会自动将它们与当前音频的小节边界完美对齐! foreach (var seg in bgmSegments) { if (seg != triggeringSegment && bgmTargetStates[seg] && !bgmIsPlaying[seg]) { bgmIsPlaying[seg] = true; // 确保对应的 GameObject Switch 正确 AkUnitySoundEngine.SetSwitch(beatSystem.musicSegmentSwitchGroup, seg, bgmGameObjects[seg]); uint newSegID = bgmMusicEvent.Post( bgmGameObjects[seg], callbackFlags, beatSystem.OnWwiseMusicCallback, null ); if (newSegID != 0) { bgmPlayingIDs[seg] = newSegID; Debug.Log($"[CombatMusicController] Queued start: Posted Event for BGM '{seg}' during PrepareNext. ID: {newSegID}"); } } } } else if (cueName == "EndOfEvent") { // 注意:由于我们在 PrepareNext 处更新了 bgmPlayingIDs, // 此时旧的 playingID 已经与字典中的不匹配了,所以旧事件的 EndOfEvent 会在上面的循环中返回 null 并忽略! // 只有真正停止播放(未更新 ID)的 EndOfEvent 才会走到这里,实现完美的状态清理! bgmIsPlaying[triggeringSegment] = false; bgmPlayingIDs[triggeringSegment] = 0; Debug.Log($"[CombatMusicController] BGM Segment '{triggeringSegment}' terminated naturally in Wwise (EndOfEvent)."); if (!AnyBgmPlaying()) { bool queuedBgmStarted = false; foreach (var seg in bgmSegments) { if (bgmTargetStates[seg] && !bgmIsPlaying[seg]) { Debug.Log($"[CombatMusicController] Starting queued BGM '{seg}' since all others stopped."); if (beatSystem != null && !beatSystem.IsActive) { beatSystem.Activate(null); } StartBgmImmediately(seg); queuedBgmStarted = true; } } if (!queuedBgmStarted && beatSystem != null && beatSystem.IsActive) { beatSystem.Deactivate(); } } } else if (cueName.StartsWith("Entry_")) { bgmIsPlaying[triggeringSegment] = true; Debug.Log($"[CombatMusicController] BGM Segment '{triggeringSegment}' entered and is now running."); } } #endregion #region Functional Music Implementation [Button("Play Functional Music")] public void PlayFunctionalMusic() { if (beatSystem == null) { beatSystem = GetComponent(); } if (beatSystem != null) { beatSystem.Activate(functionalMusicEvent, initialSegment); } else { Debug.LogError("[CombatMusicController] Cannot play: MusicBeatSystem reference is missing."); } } private string DecideNextSegment(GameObject targetGO, string currentSegmentName) { if (beatSystem == null || targetGO != beatSystem.gameObject) { return ""; } if (functionalSegments == null || functionalSegments.Count == 0) { return ""; } List candidates = new List(functionalSegments); if (!string.IsNullOrEmpty(currentSegmentName)) { candidates.Remove(currentSegmentName); } if (candidates.Count > 0) { int randomIndex = UnityEngine.Random.Range(0, candidates.Count); string selectedSegment = candidates[randomIndex]; Debug.Log($"[CombatMusicController] Transition Decision: GameObject '{targetGO.name}' currently playing '{currentSegmentName}', selected next: '{selectedSegment}'"); return selectedSegment; } return functionalSegments[0]; } #endregion } }