Files
Cielonos/Assets/Scripts/MainGame/Managers/CombatManager/CombatSystems/MusicBeatSystem/CombatMusicController.cs
SoulliesOfficial ddd387ef35 做不出来
2026-06-30 01:48:58 -04:00

422 lines
16 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.Collections.Generic;
using UnityEngine;
using Sirenix.OdinInspector;
namespace Cielonos.MainGame
{
/// <summary>
/// 战斗音乐控制器,负责处理功能音乐的无缝随机切换,以及并行的 BGM 音轨按小节边界起播/停止。
/// </summary>
[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<string> bgmSegments = new List<string>
{
"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<string> functionalSegments = new List<string>
{
"Func_00",
"Func_01",
"Func_02",
"Func_03"
};
// 运行时 BGM 状态跟踪字典
private readonly Dictionary<string, bool> bgmTargetStates = new Dictionary<string, bool>();
private readonly Dictionary<string, bool> bgmIsPlaying = new Dictionary<string, bool>();
private readonly Dictionary<string, uint> bgmPlayingIDs = new Dictionary<string, uint>();
private readonly Dictionary<string, GameObject> bgmGameObjects = new Dictionary<string, GameObject>();
// 等待下一个全局 PrepareNext 边界才起播的 BGM 片段队列
private readonly HashSet<string> pendingBgmStarts = new HashSet<string>();
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<MusicBeatSystem>();
}
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;
}
/// <summary>
/// 当全局主音乐Func 轨)的 PrepareNext 到达时,将所有等待中的 BGM 片段一起起播。
/// Wwise 引擎会将它们精确对齐到 Exit Cue 边界!
/// </summary>
private void OnGlobalPrepareNextFired()
{
if (pendingBgmStarts.Count == 0) return;
var toStart = new List<string>(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<MusicBeatSystem>();
}
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<string> candidates = new List<string>(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
}
}