Files
Cielonos/Assets/Scripts/MainGame/Managers/CombatManager/CombatSystems/MusicBeatSystem/MusicBeatSystem.cs
SoulliesOfficial b5cb6152ff MusicBeat
2026-05-26 00:21:27 -04:00

515 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;
using System.Collections.Generic;
using Cielonos.MainGame.UI;
using Sirenix.OdinInspector;
using SLSUtilities.WwiseAssistance;
using UnityEngine;
namespace Cielonos.MainGame
{
/// <summary>
/// 音乐节拍战斗系统。激活时覆盖 BackgroundMusicManager 播放对应 BGM
/// 通过 Wwise AK_MusicSyncBeat 回调 + MusicBeatData 谱面进行双轨节拍追踪,
/// 对外提供 Judge / IsOnBeat / GetBeatAccuracy 判定 API 和节拍事件
/// </summary>
public class MusicBeatSystem : CombatSystemBase
{
#region Constants
/// <summary>
/// Perfect 判定窗口(秒),节拍前后各此值范围内判定为 Perfect
/// </summary>
private const float PERFECT_TOLERANCE = 0.15f;
/// <summary>
/// Good 判定窗口(秒)
/// </summary>
private const float GOOD_TOLERANCE = 0.2f;
/// <summary>
/// Miss 判定窗口(秒),超出此范围不触发判定
/// </summary>
private const float MISS_TOLERANCE = 0.2f;
/// <summary>
/// Wwise 回调到 Unity 主线程的时间吸附阈值(秒),
/// 超过此值的校准可能是异常数据,忽略
/// </summary>
private const float SYNC_SNAP_THRESHOLD = 0.5f;
#endregion
#region Public State
/// <summary>
/// 当前加载的谱面数据
/// </summary>
[ShowInInspector, ReadOnly]
public MusicBeatData CurrentBeatData { get; private set; }
/// <summary>
/// 系统是否已激活谱面已加载、BGM 已请求播放)
/// </summary>
[ShowInInspector, ReadOnly]
public bool IsActive { get; private set; }
/// <summary>
/// Wwise 音乐是否已真正开始播放(首次 MusicSync 回调到来后为 true
/// </summary>
[ShowInInspector, ReadOnly]
public bool IsPlaying { get; private set; }
/// <summary>
/// 当前音乐播放时间(秒),由 deltaTime 推进 + Wwise 回调校准
/// </summary>
[ShowInInspector, ReadOnly]
public float CurrentSongTime { get; private set; }
/// <summary>
/// 当前实际 BPM由 Wwise 回调报告的 beatDuration 反推,若无回调则使用谱面 BPM
/// </summary>
[ShowInInspector, ReadOnly]
public float CurrentBPM { get; private set; }
#endregion
#region Events
/// <summary>
/// 每次到达谱面中的节拍时触发UI 和敌人 AI 订阅)
/// </summary>
public event Action<BeatMarker> OnBeat;
/// <summary>
/// 玩家操作被判定后触发
/// </summary>
public event Action<BeatJudgement> OnPlayerBeatJudged;
/// <summary>
/// 系统激活时触发
/// </summary>
public event Action<MusicBeatData> OnActivated;
/// <summary>
/// 系统停用时触发
/// </summary>
public event Action OnDeactivated;
#endregion
#region Private Fields
/// <summary>
/// 下一个待触发的节拍索引(在 beatMarkers 中的 index
/// </summary>
private int nextBeatIndex;
/// <summary>
/// Wwise 播放实例 ID
/// </summary>
private uint wwisePlayingID;
/// <summary>
/// BackgroundMusicManager 引用缓存
/// </summary>
private BackgroundMusicManager bgmManager;
/// <summary>
/// Wwise 回调产生的待处理校准数据(从回调线程安全传递到主线程)
/// </summary>
[ShowInInspector]
private volatile float pendingSyncTime = -1f;
/// <summary>
/// Wwise 回调报告的 beatDuration用于反推实际 BPM
/// </summary>
[ShowInInspector]
private volatile float pendingBeatDuration = -1f;
#endregion
#region Lifecycle
public MusicBeatData testData;
private void Awake()
{
bgmManager = AudioManager.Instance.backgroundMusicManager;
}
private void Update()
{
if (!IsActive) return;
// 处理 Wwise 回调带来的时间校准
ProcessPendingSync();
if (!IsPlaying) return;
// 推进音乐时间
CurrentSongTime += Time.deltaTime;
// 检查并触发节拍事件
ProcessBeatEvents();
}
private void OnDestroy()
{
if (IsActive)
{
Deactivate();
}
}
#endregion
#region Activate / Deactivate
[Button("Activate Test Data")]
public void Activate()
{
if (testData != null)
{
Activate(testData);
}
else
{
Debug.LogWarning("[MusicBeatSystem] No test data assigned for activation");
}
}
/// <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;
}
if (IsActive)
{
Deactivate();
}
CurrentBeatData = beatData;
CurrentBPM = beatData.bpm;
CurrentSongTime = 0f;
nextBeatIndex = 0;
IsPlaying = false;
IsActive = true;
// 覆盖 BackgroundMusicManager先停止当前 BGM再标记覆盖
if (bgmManager != null)
{
bgmManager.StopMusic();
bgmManager.SetOverride(true);
}
// 在 MusicBeatSystem 自身的 gameObject 上播放节拍音乐
// 与 BackgroundMusicManager 的 gameObject 隔离,避免 Stop Event 作用域冲突
if (beatData.musicEvent != null && beatData.musicEvent.IsValid())
{
uint callbackFlags = (uint)(AkCallbackType.AK_MusicSyncBeat | AkCallbackType.AK_EndOfEvent);
beatData.musicSwitch.SetValue(gameObject); // 设置 Switch 以选择正确的音乐变体
PlayerCanvas.CombatSystemsUIArea.beatTimelineUI.Initialize(this);
wwisePlayingID = beatData.musicEvent.Post(
gameObject,
callbackFlags,
OnWwiseMusicCallback,
null
);
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");
}
else
{
Debug.Log($"[MusicBeatSystem] Activated with '{beatData.name}', playingID={wwisePlayingID}, " +
$"posting on GameObject '{gameObject.name}'");
}
}
else
{
// 无 Wwise Event 时,使用纯谱面模式(仅基于 deltaTime 和谱面数据)
Debug.LogWarning("[MusicBeatSystem] No valid Wwise Event on beatData, running in offline mode");
IsPlaying = true;
}
OnActivated?.Invoke(beatData);
}
/// <summary>
/// 停用节拍系统:停止追踪、恢复 BackgroundMusicManager 控制
/// </summary>
public void Deactivate()
{
if (!IsActive) return;
IsActive = false;
IsPlaying = false;
CurrentSongTime = 0f;
nextBeatIndex = 0;
pendingSyncTime = -1f;
pendingBeatDuration = -1f;
// 停止 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);
}
/// <summary>
/// 判定指定时间点的操作是否卡拍
/// </summary>
/// <param name="actionTime">操作发生时的 CurrentSongTime通常传入 CurrentSongTime</param>
/// <returns>判定结果,若超出 Miss 窗口则 accuracy 为 Miss</returns>
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;
}
/// <summary>
/// 简化版判定,仅返回精度等级
/// </summary>
public BeatAccuracy GetBeatAccuracy(float actionTime)
{
return Judge(actionTime).accuracy;
}
/// <summary>
/// 判定指定时间点是否处于节拍窗口内
/// </summary>
/// <param name="actionTime">操作时间</param>
/// <param name="customTolerance">自定义容差,-1 表示使用 GOOD_TOLERANCE</param>
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);
}
/// <summary>
/// 获取下一个节拍标记
/// </summary>
public BeatMarker GetNextBeat()
{
return CurrentBeatData?.GetNextBeat(CurrentSongTime);
}
/// <summary>
/// 获取下一个带有特定 tag 的节拍标记
/// </summary>
public BeatMarker GetNextBeatWithTag(string tag)
{
return CurrentBeatData?.GetNextBeatWithTag(CurrentSongTime, tag);
}
/// <summary>
/// 到下一拍的剩余时间(秒),若无下一拍返回 -1
/// </summary>
public float GetTimeUntilNextBeat()
{
BeatMarker next = GetNextBeat();
return next != null ? next.time - CurrentSongTime : -1f;
}
/// <summary>
/// 到指定节拍的剩余时间(秒)
/// </summary>
public float GetTimeUntilBeat(BeatMarker beat)
{
if (beat == null) return -1f;
return beat.time - CurrentSongTime;
}
/// <summary>
/// 获取指定时间范围内的所有节拍标记
/// </summary>
public List<BeatMarker> GetBeatsInRange(float startTime, float endTime)
{
return CurrentBeatData?.GetBeatsInRange(startTime, endTime) ?? new List<BeatMarker>();
}
#endregion
#region Internal - Wwise Callback
/// <summary>
/// Wwise 音乐回调处理器(可能在非主线程调用)
/// </summary>
private void OnWwiseMusicCallback(object in_cookie, AkCallbackType in_type, AkCallbackInfo in_info)
{
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;
// 将校准数据传递到主线程处理
pendingSyncTime = musicPositionSec + CurrentBeatData.audioStartOffset;
pendingBeatDuration = beatDuration;
}
else
{
Debug.LogWarning($"[MusicBeatSystem] Received MusicSync callback with unexpected info type: {in_info.GetType().Name}");
}
}
else if (in_type == AkCallbackType.AK_EndOfEvent)
{
// 音乐播放结束
Debug.Log("[MusicBeatSystem] Music playback ended");
}
}
#endregion
#region Internal - Beat Processing
/// <summary>
/// 处理来自 Wwise 回调的时间校准(在主线程 Update 中调用)
/// </summary>
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;
}
}
/// <summary>
/// 检查是否到达下一个节拍,触发 onBeat 事件
/// </summary>
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++;
OnBeat?.Invoke(beat);
}
}
#endregion
}
}