using System; using System.Collections.Generic; using Cielonos.MainGame.UI; using Sirenix.OdinInspector; using SLSUtilities.WwiseAssistance; using UnityEngine; namespace Cielonos.MainGame { [System.Serializable] public struct SegmentDataMapping { [Tooltip("Wwise 中 Music Segment 的名称 (例如 BGM_Intensity_01)")] public string segmentName; [Tooltip("对应的 Unity MusicBeatData 谱面数据")] public MusicBeatData beatData; } /// /// 局部节拍追踪器接口,用于接收物体上局部播放的音乐片段切换事件 /// public interface ILocalRhythmTracker { void OnSegmentTransitioned(MusicBeatData localBeatData); void ReceiveSyncBeat(float musicPositionSec, float beatDuration); bool IsPlaying { get; } float CurrentSongTime { get; } MusicBeatData CurrentBeatData { get; } } /// /// 音乐节拍战斗系统。激活时覆盖 BackgroundMusicManager 播放对应 BGM, /// 通过 Wwise AK_MusicSyncBeat 回调 + MusicBeatData 谱面进行双轨节拍追踪, /// 对外提供 Judge / IsOnBeat / GetBeatAccuracy 判定 API 和节拍事件 /// public class MusicBeatSystem : CombatSystemBase { #region Constants /// /// Perfect 判定窗口(秒),节拍前后各此值范围内判定为 Perfect /// private const float PERFECT_TOLERANCE = 0.15f; /// /// Good 判定窗口(秒) /// private const float GOOD_TOLERANCE = 0.2f; /// /// Miss 判定窗口(秒),超出此范围不触发判定 /// private const float MISS_TOLERANCE = 0.2f; /// /// Wwise 回调到 Unity 主线程的时间吸附阈值(秒), /// 超过此值的校准可能是异常数据,忽略 /// private const float SYNC_SNAP_THRESHOLD = 0.5f; #endregion #region Public State /// /// 当前加载的谱面数据 /// [ShowInInspector, ReadOnly] public MusicBeatData CurrentBeatData { get; private set; } /// /// 系统是否已激活(谱面已加载、BGM 已请求播放) /// [ShowInInspector, ReadOnly] public bool IsActive { get; private set; } /// /// Wwise 音乐是否已真正开始播放(首次 MusicSync 回调到来后为 true) /// [ShowInInspector, ReadOnly] public bool IsPlaying { get; private set; } /// /// 当前音乐播放时间(秒),由 deltaTime 推进 + Wwise 回调校准 /// [ShowInInspector, ReadOnly] public float CurrentSongTime { get; private set; } /// /// 当前实际 BPM(由 Wwise 回调报告的 beatDuration 反推,若无回调则使用谱面 BPM) /// [ShowInInspector, ReadOnly] public float CurrentBPM { get; private set; } /// /// 当前播放的 Wwise Segment 名称 (例如 Func_00) /// [ShowInInspector, ReadOnly] public string CurrentWwiseSegmentName { get; private set; } /// /// 当前小节内的拍号(1-based)。由 Wwise MusicSyncBeat 回调每拍递增,在 Entry 时归 1。 /// 例如 4/4 拍时值为 1~4 循环。 /// [ShowInInspector, ReadOnly] public int CurrentBarBeat { get; private set; } = 1; #endregion #region Events /// /// 每次到达谱面中的节拍时触发(UI 和敌人 AI 订阅) /// public event Action OnBeat; /// /// 玩家操作被判定后触发 /// public event Action OnPlayerBeatJudged; /// /// 系统激活时触发 /// public event Action OnActivated; /// /// 系统停用时触发 /// public event Action OnDeactivated; /// /// 当收到任何 Wwise User Cue 时触发。 /// 参数:1. playingID, 2. cueName。 /// public event Action OnUserCueReceived; /// /// 当全局主音乐(MusicBeatSystem 自身 GameObject)收到 PrepareNext Cue 时触发。 /// BGM 系统可以监听此事件,在音乐边界精确地起播 Back 音轨。 /// public event Action OnGlobalPrepareNext; #endregion #region Private Fields /// /// 下一个待触发的节拍索引(在 beatMarkers 中的 index) /// private int nextBeatIndex; /// /// 由 MusicSyncBeat 回调累计的原始拍号计数(每个 Entry 时清零),用于推算 CurrentBarBeat /// private int rawBeatCount; /// /// Wwise 播放实例 ID /// private uint wwisePlayingID; /// /// BackgroundMusicManager 引用缓存 /// private BackgroundMusicManager bgmManager; /// /// Wwise 回调产生的待处理校准数据(从回调线程安全传递到主线程) /// [ShowInInspector] private volatile float pendingSyncTime = -1f; [ShowInInspector] private volatile float pendingBeatDuration = -1f; [Header("Dynamic Music Configurations")] [Tooltip("Wwise 中控制音乐片段切换的 Switch Group 名称")] public string musicSegmentSwitchGroup = "Music_Segment"; [Tooltip("Wwise 音乐片段与 Unity 谱面资产的映射表")] public List segmentMappings = new List(); /// /// 当音乐运行到 PrepareNext 标记时触发。 /// 参数:1. 触发该事件的 GameObject,2. 当前播放的片段名称。 /// 返回值:下一个要播放的片段名(Switch 值)。 /// public event Func OnPrepareNextSegment; private struct PendingCallbackData { public enum Type { Entry, Prepare, BeatSync } public Type callbackType; public GameObject targetGO; public string segmentName; public float musicPositionSec; public float beatDuration; public uint playingID; } private readonly List pendingCallbacks = new List(); private readonly object callbackLock = new object(); private static readonly List activeRhythmGameObjects = new List(); #endregion #region Lifecycle private void Awake() { bgmManager = AudioManager.Instance.backgroundMusicManager; RegisterRhythmGameObject(gameObject); } private void Update() { if (!IsActive) return; // 处理 Wwise 回调带来的时间校准 ProcessPendingSync(); // 处理 Entry/Prepare 等回调队列事件 ProcessPendingCallbacks(); if (!IsPlaying) return; // 推进音乐时间 CurrentSongTime += Time.deltaTime; // 检查并触发节拍事件 ProcessBeatEvents(); } private void OnDestroy() { UnregisterRhythmGameObject(gameObject); if (IsActive) { Deactivate(); } } #endregion #region Activate / Deactivate [Button("Play Music")] public void Activate() { var controller = GetComponent(); if (controller != null) { controller.PlayFunctionalMusic(); } else { Debug.LogWarning("[MusicBeatSystem] No CombatMusicController or testData found to play music."); } } /// /// 激活节拍系统:使用 Wwise 事件和初始 Switch 直接启动动态音乐流程 /// public void Activate(AK.Wwise.Event wwiseEvent, string initialSwitch = null) { if (IsActive) { Deactivate(); } CurrentBeatData = null; CurrentBPM = 240f; // 默认临时 BPM,直到 Entry Cue 触发表格映射替换 CurrentSongTime = 0f; nextBeatIndex = 0; IsPlaying = false; IsActive = true; // 覆盖 BackgroundMusicManager if (bgmManager != null) { bgmManager.StopMusic(); bgmManager.SetOverride(true); } if (wwiseEvent != null && wwiseEvent.IsValid()) { uint callbackFlags = (uint)( AkCallbackType.AK_MusicSyncBeat | AkCallbackType.AK_MusicSyncEntry | AkCallbackType.AK_MusicSyncUserCue | AkCallbackType.AK_EndOfEvent ); if (!string.IsNullOrEmpty(initialSwitch)) { AkUnitySoundEngine.SetSwitch(musicSegmentSwitchGroup, initialSwitch, gameObject); } PlayerCanvas.CombatSystemsUIArea.beatTimelineUI.Initialize(this); wwisePlayingID = wwiseEvent.Post( gameObject, callbackFlags, OnWwiseMusicCallback, null ); if (wwisePlayingID == 0) { Debug.LogWarning("[MusicBeatSystem] Wwise Post returned playingID 0 for dynamic music event"); } else { Debug.Log($"[MusicBeatSystem] Activated dynamic music flow with Event: '{wwiseEvent.Name}', playingID={wwisePlayingID}"); } } else { Debug.LogWarning("[MusicBeatSystem] No valid Wwise Event provided for dynamic music activation"); } OnActivated?.Invoke(null); } /// /// 停用节拍系统:停止追踪、恢复 BackgroundMusicManager 控制 /// public void Deactivate() { if (!IsActive) return; IsActive = false; IsPlaying = false; CurrentSongTime = 0f; nextBeatIndex = 0; pendingSyncTime = -1f; pendingBeatDuration = -1f; CurrentWwiseSegmentName = null; // 停止 MusicBeatSystem 自己 Post 的节拍音乐 if (wwisePlayingID != 0) { AkUnitySoundEngine.StopPlayingID(wwisePlayingID); wwisePlayingID = 0; } // 如果 beatData 有独立的 stopMusicEvent,也 Post 一下确保停止 if (CurrentBeatData != null && CurrentBeatData.stopMusicEvent != null && CurrentBeatData.stopMusicEvent.IsValid()) { CurrentBeatData.stopMusicEvent.Post(gameObject); } // 恢复 BackgroundMusicManager if (bgmManager != null) { bgmManager.SetOverride(false); bgmManager.PlayMusic("NormalMusic"); } CurrentBeatData = null; Debug.Log("[MusicBeatSystem] Deactivated"); OnDeactivated?.Invoke(); } #endregion #region Judgement API public BeatJudgement Judge() { return Judge(CurrentSongTime); } /// /// 判定指定时间点的操作是否卡拍 /// /// 操作发生时的 CurrentSongTime(通常传入 CurrentSongTime) /// 判定结果,若超出 Miss 窗口则 accuracy 为 Miss public BeatJudgement Judge(float actionTime) { BeatMarker nearest = CurrentBeatData?.GetNearestBeat(actionTime, MISS_TOLERANCE); if (nearest == null) { return new BeatJudgement(BeatAccuracy.Miss, float.MaxValue, null, 1f); } float timeDiff = actionTime - nearest.time; float absDiff = Mathf.Abs(timeDiff); BeatAccuracy accuracy; float normalized; if (absDiff <= PERFECT_TOLERANCE) { accuracy = BeatAccuracy.Perfect; normalized = absDiff / PERFECT_TOLERANCE; } else if (absDiff <= GOOD_TOLERANCE) { accuracy = BeatAccuracy.Good; normalized = absDiff / GOOD_TOLERANCE; } else { accuracy = BeatAccuracy.Miss; normalized = Mathf.Clamp01(absDiff / MISS_TOLERANCE); } var judgement = new BeatJudgement(accuracy, timeDiff, nearest, normalized); OnPlayerBeatJudged?.Invoke(judgement); return judgement; } /// /// 简化版判定,仅返回精度等级 /// public BeatAccuracy GetBeatAccuracy(float actionTime) { return Judge(actionTime).accuracy; } /// /// 判定指定时间点是否处于节拍窗口内 /// /// 操作时间 /// 自定义容差,-1 表示使用 GOOD_TOLERANCE public bool IsOnBeat(float actionTime, float customTolerance = -1f) { float tolerance = customTolerance > 0f ? customTolerance : GOOD_TOLERANCE; BeatMarker nearest = CurrentBeatData?.GetNearestBeat(actionTime, tolerance); return nearest != null; } #endregion #region Beat Query API public BeatMarker GetNearestBeat(float tolerance) { return CurrentBeatData?.GetNearestBeat(CurrentSongTime, tolerance); } public BeatMarker GetLastBeat() { return CurrentBeatData?.GetLastBeat(CurrentSongTime); } /// /// 获取下一个节拍标记 /// public BeatMarker GetNextBeat() { return CurrentBeatData?.GetNextBeat(CurrentSongTime); } /// /// 获取下一个带有特定 tag 的节拍标记 /// public BeatMarker GetNextBeatWithTag(string tag) { return CurrentBeatData?.GetNextBeatWithTag(CurrentSongTime, tag); } /// /// 到下一拍的剩余时间(秒),若无下一拍返回 -1 /// public float GetTimeUntilNextBeat() { BeatMarker next = GetNextBeat(); return next != null ? next.time - CurrentSongTime : -1f; } /// /// 到指定节拍的剩余时间(秒) /// public float GetTimeUntilBeat(BeatMarker beat) { if (beat == null) return -1f; return beat.time - CurrentSongTime; } /// /// 获取指定时间范围内的所有节拍标记 /// public List GetBeatsInRange(float startTime, float endTime) { return CurrentBeatData?.GetBeatsInRange(startTime, endTime) ?? new List(); } #endregion #region Internal - Wwise Callback /// /// Wwise 音乐回调处理器(可能在非主线程调用) /// internal void OnWwiseMusicCallback(object in_cookie, AkCallbackType in_type, AkCallbackInfo in_info) { GameObject go = GetGameObjectFromWwiseId(in_info.gameObjID); if (go == null) go = gameObject; if (in_type == AkCallbackType.AK_MusicSyncBeat) { if (in_info is AkMusicSyncCallbackInfo syncInfo) { // 从 Wwise 获取精确的音乐播放位置和节拍信息 // segmentInfo_iCurrentPosition 是当前 segment 内的播放位置(毫秒) float musicPositionSec = syncInfo.segmentInfo_iCurrentPosition / 1000f; float beatDuration = syncInfo.segmentInfo_fBeatDuration; if (go == gameObject) { // 将校准数据传递到主线程处理 (防空保护) float offset = CurrentBeatData != null ? CurrentBeatData.audioStartOffset : 0f; pendingSyncTime = musicPositionSec + offset; pendingBeatDuration = beatDuration; } else { lock (callbackLock) { pendingCallbacks.Add(new PendingCallbackData { callbackType = PendingCallbackData.Type.BeatSync, targetGO = go, segmentName = "", musicPositionSec = musicPositionSec, beatDuration = beatDuration }); } } } else { Debug.LogWarning($"[MusicBeatSystem] Received MusicSync callback with unexpected info type: {in_info.GetType().Name}"); } } else if (in_type == AkCallbackType.AK_MusicSyncUserCue && in_info is AkMusicSyncCallbackInfo cueInfo) { string cueName = cueInfo.userCueName; lock (callbackLock) { // 所有的 User Cue 都会触发 OnUserCueReceived pendingCallbacks.Add(new PendingCallbackData { callbackType = PendingCallbackData.Type.Prepare, targetGO = go, segmentName = cueName, playingID = cueInfo.playingID }); // 如果是 Entry_ 开头,则同时触发 Entry 谱面替换逻辑 if (!string.IsNullOrEmpty(cueName) && cueName.StartsWith("Entry_")) { string segmentName = cueName.Substring(6); pendingCallbacks.Add(new PendingCallbackData { callbackType = PendingCallbackData.Type.Entry, targetGO = go, segmentName = segmentName }); } } } else if (in_type == AkCallbackType.AK_EndOfEvent && in_info is AkEventCallbackInfo eventInfo) { lock (callbackLock) { pendingCallbacks.Add(new PendingCallbackData { callbackType = PendingCallbackData.Type.Prepare, targetGO = go, segmentName = "EndOfEvent", playingID = eventInfo.playingID }); } Debug.Log("[MusicBeatSystem] Music playback ended"); } } #endregion #region Internal - Beat Processing /// /// 处理来自 Wwise 回调的时间校准(在主线程 Update 中调用) /// private void ProcessPendingSync() { float syncTime = pendingSyncTime; float beatDur = pendingBeatDuration; if (syncTime < 0f) return; // 消费 pending 数据 pendingSyncTime = -1f; pendingBeatDuration = -1f; // 首次收到回调,标记为正在播放 if (!IsPlaying) { IsPlaying = true; CurrentSongTime = syncTime; Debug.Log($"[MusicBeatSystem] First beat sync received, music is now playing. SyncTime={syncTime:F3}s"); return; } // 时间校准:将 currentSongTime 吸附到 Wwise 报告的位置 float drift = Mathf.Abs(CurrentSongTime - syncTime); if (drift < SYNC_SNAP_THRESHOLD) { CurrentSongTime = syncTime; } else { Debug.LogWarning($"[MusicBeatSystem] Large sync drift detected: {drift:F3}s, ignoring calibration"); } // 更新实际 BPM if (beatDur > 0f) { CurrentBPM = 60f / beatDur; } } private void ProcessPendingCallbacks() { List localCallbacks = null; lock (callbackLock) { if (pendingCallbacks.Count > 0) { localCallbacks = new List(pendingCallbacks); pendingCallbacks.Clear(); } } if (localCallbacks == null) return; foreach (var callback in localCallbacks) { if (callback.callbackType == PendingCallbackData.Type.Prepare) { OnUserCueReceived?.Invoke(callback.playingID, callback.segmentName); if (callback.segmentName == "PrepareNext" && callback.targetGO == gameObject) { string nextSegment = OnPrepareNextSegment?.Invoke(callback.targetGO, CurrentWwiseSegmentName); if (!string.IsNullOrEmpty(nextSegment)) { AkUnitySoundEngine.SetSwitch(musicSegmentSwitchGroup, nextSegment, callback.targetGO); Debug.Log($"[MusicBeatSystem] PrepareNext: transition scheduled on '{callback.targetGO.name}' from '{CurrentWwiseSegmentName}' to '{nextSegment}'"); } // 通知所有订阅者:全局主音乐到达了 PrepareNext 边界 OnGlobalPrepareNext?.Invoke(); } } else if (callback.callbackType == PendingCallbackData.Type.BeatSync) { var localTracker = callback.targetGO.GetComponent(); if (localTracker != null) { localTracker.ReceiveSyncBeat(callback.musicPositionSec, callback.beatDuration); } } else if (callback.callbackType == PendingCallbackData.Type.Entry) { // Find mapped MusicBeatData MusicBeatData mappedData = null; if (segmentMappings != null) { foreach (var mapping in segmentMappings) { if (mapping.segmentName == callback.segmentName) { mappedData = mapping.beatData; break; } } } if (mappedData != null) { if (callback.targetGO == gameObject) { // Global Master BGM Transition CurrentWwiseSegmentName = callback.segmentName; CurrentBeatData = mappedData; CurrentBPM = mappedData.bpm; CurrentSongTime = mappedData.audioStartOffset; nextBeatIndex = 0; rawBeatCount = 0; CurrentBarBeat = 1; IsPlaying = true; // Re-initialize UI PlayerCanvas.CombatSystemsUIArea.beatTimelineUI.Initialize(this); Debug.Log($"[MusicBeatSystem] Global Transitioned to Segment: '{callback.segmentName}', BPM={mappedData.bpm}"); OnActivated?.Invoke(mappedData); } else { // Local GameObject Segment Transition (e.g. Enemy) var localTracker = callback.targetGO.GetComponent(); if (localTracker != null) { localTracker.OnSegmentTransitioned(mappedData); } Debug.Log($"[MusicBeatSystem] Local Segment Entry: '{callback.segmentName}' on GameObject '{callback.targetGO.name}'"); } } else { Debug.LogWarning($"[MusicBeatSystem] Unmapped segment entry callback: '{callback.segmentName}' on '{callback.targetGO.name}'"); } } } } public static void RegisterRhythmGameObject(GameObject go) { if (go == null) return; lock (activeRhythmGameObjects) { if (!activeRhythmGameObjects.Contains(go)) { activeRhythmGameObjects.Add(go); } } } public static void UnregisterRhythmGameObject(GameObject go) { if (go == null) return; lock (activeRhythmGameObjects) { activeRhythmGameObjects.Remove(go); } } private GameObject GetGameObjectFromWwiseId(ulong gameObjID) { if (gameObjID == (ulong)gameObject.GetInstanceID()) return gameObject; lock (activeRhythmGameObjects) { for (int i = activeRhythmGameObjects.Count - 1; i >= 0; i--) { var go = activeRhythmGameObjects[i]; if (go == null) { activeRhythmGameObjects.RemoveAt(i); continue; } if ((ulong)go.GetInstanceID() == gameObjID) { return go; } } } // Fallback lookup #if UNITY_6000_0_OR_NEWER var objs = FindObjectsByType(FindObjectsSortMode.None); #else var objs = FindObjectsOfType(); #endif foreach (var obj in objs) { if (obj != null && (ulong)obj.gameObject.GetInstanceID() == gameObjID) { RegisterRhythmGameObject(obj.gameObject); return obj.gameObject; } } return null; } /// /// 检查是否到达下一个节拍,触发 onBeat 事件 /// private void ProcessBeatEvents() { if (CurrentBeatData == null || CurrentBeatData.beatMarkers == null) return; var markers = CurrentBeatData.beatMarkers; // 触发所有已经过去的未处理节拍 while (nextBeatIndex < markers.Count && CurrentSongTime >= markers[nextBeatIndex].time) { BeatMarker beat = markers[nextBeatIndex]; nextBeatIndex++; // 更新 CurrentBarBeat(根据 BPM 和小节拍数推算) rawBeatCount++; int beatsPerBar = CurrentBeatData.beatsPerBar > 0 ? CurrentBeatData.beatsPerBar : 4; CurrentBarBeat = ((rawBeatCount - 1) % beatsPerBar) + 1; OnBeat?.Invoke(beat); } } #endregion } }