做不出来

This commit is contained in:
SoulliesOfficial
2026-06-30 01:48:58 -04:00
parent 9a9e48f8a5
commit ddd387ef35
132 changed files with 8945 additions and 2943 deletions

View File

@@ -7,6 +7,27 @@ 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;
}
/// <summary>
/// 局部节拍追踪器接口,用于接收物体上局部播放的音乐片段切换事件
/// </summary>
public interface ILocalRhythmTracker
{
void OnSegmentTransitioned(MusicBeatData localBeatData);
void ReceiveSyncBeat(float musicPositionSec, float beatDuration);
bool IsPlaying { get; }
float CurrentSongTime { get; }
MusicBeatData CurrentBeatData { get; }
}
/// <summary>
/// 音乐节拍战斗系统。激活时覆盖 BackgroundMusicManager 播放对应 BGM
/// 通过 Wwise AK_MusicSyncBeat 回调 + MusicBeatData 谱面进行双轨节拍追踪,
@@ -71,6 +92,19 @@ namespace Cielonos.MainGame
[ShowInInspector, ReadOnly]
public float CurrentBPM { get; private set; }
/// <summary>
/// 当前播放的 Wwise Segment 名称 (例如 Func_00)
/// </summary>
[ShowInInspector, ReadOnly]
public string CurrentWwiseSegmentName { get; private set; }
/// <summary>
/// 当前小节内的拍号1-based。由 Wwise MusicSyncBeat 回调每拍递增,在 Entry 时归 1。
/// 例如 4/4 拍时值为 1~4 循环。
/// </summary>
[ShowInInspector, ReadOnly]
public int CurrentBarBeat { get; private set; } = 1;
#endregion
#region Events
@@ -95,6 +129,18 @@ namespace Cielonos.MainGame
/// </summary>
public event Action OnDeactivated;
/// <summary>
/// 当收到任何 Wwise User Cue 时触发。
/// 参数1. playingID, 2. cueName。
/// </summary>
public event Action<uint, string> OnUserCueReceived;
/// <summary>
/// 当全局主音乐MusicBeatSystem 自身 GameObject收到 PrepareNext Cue 时触发。
/// BGM 系统可以监听此事件,在音乐边界精确地起播 Back 音轨。
/// </summary>
public event Action OnGlobalPrepareNext;
#endregion
#region Private Fields
@@ -104,6 +150,11 @@ namespace Cielonos.MainGame
/// </summary>
private int nextBeatIndex;
/// <summary>
/// 由 MusicSyncBeat 回调累计的原始拍号计数(每个 Entry 时清零),用于推算 CurrentBarBeat
/// </summary>
private int rawBeatCount;
/// <summary>
/// Wwise 播放实例 ID
/// </summary>
@@ -120,21 +171,47 @@ namespace Cielonos.MainGame
[ShowInInspector]
private volatile float pendingSyncTime = -1f;
/// <summary>
/// Wwise 回调报告的 beatDuration用于反推实际 BPM
/// </summary>
[ShowInInspector]
private volatile float pendingBeatDuration = -1f;
[Header("Dynamic Music Configurations")]
[Tooltip("Wwise 中控制音乐片段切换的 Switch Group 名称")]
public string musicSegmentSwitchGroup = "Music_Segment";
[Tooltip("Wwise 音乐片段与 Unity 谱面资产的映射表")]
public List<SegmentDataMapping> segmentMappings = new List<SegmentDataMapping>();
/// <summary>
/// 当音乐运行到 PrepareNext 标记时触发。
/// 参数1. 触发该事件的 GameObject2. 当前播放的片段名称。
/// 返回值下一个要播放的片段名Switch 值)。
/// </summary>
public event Func<GameObject, string, string> 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<PendingCallbackData> pendingCallbacks = new List<PendingCallbackData>();
private readonly object callbackLock = new object();
private static readonly List<GameObject> activeRhythmGameObjects = new List<GameObject>();
#endregion
#region Lifecycle
public MusicBeatData testData;
private void Awake()
{
bgmManager = AudioManager.Instance.backgroundMusicManager;
RegisterRhythmGameObject(gameObject);
}
private void Update()
@@ -144,6 +221,9 @@ namespace Cielonos.MainGame
// 处理 Wwise 回调带来的时间校准
ProcessPendingSync();
// 处理 Entry/Prepare 等回调队列事件
ProcessPendingCallbacks();
if (!IsPlaying) return;
// 推进音乐时间
@@ -155,6 +235,7 @@ namespace Cielonos.MainGame
private void OnDestroy()
{
UnregisterRhythmGameObject(gameObject);
if (IsActive)
{
Deactivate();
@@ -165,61 +246,60 @@ namespace Cielonos.MainGame
#region Activate / Deactivate
[Button("Activate Test Data")]
[Button("Play Music")]
public void Activate()
{
if (testData != null)
var controller = GetComponent<CombatMusicController>();
if (controller != null)
{
Activate(testData);
controller.PlayFunctionalMusic();
}
else
{
Debug.LogWarning("[MusicBeatSystem] No test data assigned for activation");
Debug.LogWarning("[MusicBeatSystem] No CombatMusicController or testData found to play music.");
}
}
/// <summary>
/// 激活节拍系统:加载谱面、覆盖 BGM、注册 Wwise 回调
/// </summary>
/// <param name="beatData">要加载的谱面数据</param>
public void Activate(MusicBeatData beatData)
{
if (beatData == null)
{
Debug.LogError("[MusicBeatSystem] Activate failed: beatData is null");
return;
}
/// <summary>
/// 激活节拍系统:使用 Wwise 事件和初始 Switch 直接启动动态音乐流程
/// </summary>
public void Activate(AK.Wwise.Event wwiseEvent, string initialSwitch = null)
{
if (IsActive)
{
Deactivate();
}
// Ensure beat markers are sorted before starting tracking
beatData.SortBeats();
CurrentBeatData = beatData;
CurrentBPM = beatData.bpm;
CurrentBeatData = null;
CurrentBPM = 240f; // 默认临时 BPM直到 Entry Cue 触发表格映射替换
CurrentSongTime = 0f;
nextBeatIndex = 0;
IsPlaying = false;
IsActive = true;
// 覆盖 BackgroundMusicManager:先停止当前 BGM再标记覆盖
// 覆盖 BackgroundMusicManager
if (bgmManager != null)
{
bgmManager.StopMusic();
bgmManager.SetOverride(true);
}
// 在 MusicBeatSystem 自身的 gameObject 上播放节拍音乐
// 与 BackgroundMusicManager 的 gameObject 隔离,避免 Stop Event 作用域冲突
if (beatData.musicEvent != null && beatData.musicEvent.IsValid())
if (wwiseEvent != null && wwiseEvent.IsValid())
{
uint callbackFlags = (uint)(AkCallbackType.AK_MusicSyncBeat | AkCallbackType.AK_EndOfEvent);
beatData.musicSwitch.SetValue(gameObject); // 设置 Switch 以选择正确的音乐变体
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 = beatData.musicEvent.Post(
wwisePlayingID = wwiseEvent.Post(
gameObject,
callbackFlags,
OnWwiseMusicCallback,
@@ -228,24 +308,19 @@ namespace Cielonos.MainGame
if (wwisePlayingID == 0)
{
Debug.LogWarning("[MusicBeatSystem] Wwise Post returned playingID 0, music may not play. " +
"Check: 1) musicEvent references a Music type (not Sound SFX) " +
"2) SoundBank is loaded 3) gameObject is active");
Debug.LogWarning("[MusicBeatSystem] Wwise Post returned playingID 0 for dynamic music event");
}
else
{
Debug.Log($"[MusicBeatSystem] Activated with '{beatData.name}', playingID={wwisePlayingID}, " +
$"posting on GameObject '{gameObject.name}'");
Debug.Log($"[MusicBeatSystem] Activated dynamic music flow with Event: '{wwiseEvent.Name}', playingID={wwisePlayingID}");
}
}
else
{
// 无 Wwise Event 时,使用纯谱面模式(仅基于 deltaTime 和谱面数据)
Debug.LogWarning("[MusicBeatSystem] No valid Wwise Event on beatData, running in offline mode");
IsPlaying = true;
Debug.LogWarning("[MusicBeatSystem] No valid Wwise Event provided for dynamic music activation");
}
OnActivated?.Invoke(beatData);
OnActivated?.Invoke(null);
}
/// <summary>
@@ -261,7 +336,8 @@ namespace Cielonos.MainGame
nextBeatIndex = 0;
pendingSyncTime = -1f;
pendingBeatDuration = -1f;
CurrentWwiseSegmentName = null;
// 停止 MusicBeatSystem 自己 Post 的节拍音乐
if (wwisePlayingID != 0)
{
@@ -421,8 +497,11 @@ namespace Cielonos.MainGame
/// <summary>
/// Wwise 音乐回调处理器(可能在非主线程调用)
/// </summary>
private void OnWwiseMusicCallback(object in_cookie, AkCallbackType in_type, AkCallbackInfo in_info)
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)
@@ -431,19 +510,73 @@ namespace Cielonos.MainGame
// segmentInfo_iCurrentPosition 是当前 segment 内的播放位置(毫秒)
float musicPositionSec = syncInfo.segmentInfo_iCurrentPosition / 1000f;
float beatDuration = syncInfo.segmentInfo_fBeatDuration;
// 将校准数据传递到主线程处理
pendingSyncTime = musicPositionSec + CurrentBeatData.audioStartOffset;
pendingBeatDuration = beatDuration;
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_EndOfEvent)
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");
}
}
@@ -493,6 +626,163 @@ namespace Cielonos.MainGame
}
}
private void ProcessPendingCallbacks()
{
List<PendingCallbackData> localCallbacks = null;
lock (callbackLock)
{
if (pendingCallbacks.Count > 0)
{
localCallbacks = new List<PendingCallbackData>(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<ILocalRhythmTracker>();
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<ILocalRhythmTracker>();
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<AkGameObj>(FindObjectsSortMode.None);
#else
var objs = FindObjectsOfType<AkGameObj>();
#endif
foreach (var obj in objs)
{
if (obj != null && (ulong)obj.gameObject.GetInstanceID() == gameObjID)
{
RegisterRhythmGameObject(obj.gameObject);
return obj.gameObject;
}
}
return null;
}
/// <summary>
/// 检查是否到达下一个节拍,触发 onBeat 事件
/// </summary>
@@ -508,6 +798,11 @@ namespace Cielonos.MainGame
BeatMarker beat = markers[nextBeatIndex];
nextBeatIndex++;
// 更新 CurrentBarBeat根据 BPM 和小节拍数推算)
rawBeatCount++;
int beatsPerBar = CurrentBeatData.beatsPerBar > 0 ? CurrentBeatData.beatsPerBar : 4;
CurrentBarBeat = ((rawBeatCount - 1) % beatsPerBar) + 1;
OnBeat?.Invoke(beat);
}
}