422 lines
16 KiB
C#
422 lines
16 KiB
C#
|
|
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
|
|||
|
|
}
|
|||
|
|
}
|