using System.Collections.Generic; using AK.Wwise; using Sirenix.OdinInspector; using UnityEngine; using Event = AK.Wwise.Event; namespace Cielonos.MainGame { /// /// 音乐谱面数据容器,存储 BGM 元信息和手动/自动生成的节拍标记 /// [CreateAssetMenu(fileName = "MusicBeatData", menuName = "Cielonos/CombatSystem/MusicBeat/Data", order = 0)] public class MusicBeatData : SerializedScriptableObject { private const float DEFAULT_BPM = 120f; private const int DEFAULT_BEATS_PER_BAR = 4; [TitleGroup("Wwise Configuration")] [Tooltip("Wwise 音乐 Switch,用于切换 BGM 变体")] public Switch musicSwitch; [TitleGroup("Wwise Configuration")] [Tooltip("播放 BGM 使用的 Wwise Event(需注册 MusicSync 回调)")] public Event musicEvent; [TitleGroup("Wwise Configuration")] [Tooltip("停止 BGM 使用的 Wwise Event")] public Event stopMusicEvent; [TitleGroup("Music Properties")] [Tooltip("基准 BPM")] [MinValue(1f)] public float bpm = DEFAULT_BPM; [TitleGroup("Music Properties")] [Tooltip("每小节拍数")] [MinValue(1)] public int beatsPerBar = DEFAULT_BEATS_PER_BAR; [TitleGroup("Music Properties")] [Tooltip("音频起始偏移量(秒),用于对齐第一拍与音频起始的差异")] public float audioStartOffset; [TitleGroup("Music Properties")] [Tooltip("音乐总时长(秒)")] [MinValue(0f)] public float totalDuration; [TitleGroup("Beat Markers")] [Tooltip("手动标记的节拍列表")] [ListDrawerSettings(ShowFoldout = true)] public List beatMarkers = new List(); /// /// 单拍间隔时间(秒) /// public float BeatInterval => 60f / bpm; /// /// 单小节时长(秒) /// public float BarDuration => BeatInterval * beatsPerBar; /// /// 获取指定时间之前的最后一个节拍标记 /// /// /// public BeatMarker GetLastBeat(float time) { if (beatMarkers == null || beatMarkers.Count == 0) return null; BeatMarker last = null; for (int i = 0; i < beatMarkers.Count; i++) { if (beatMarkers[i].time <= time) { last = beatMarkers[i]; } else { break; } } return last; } /// /// 获取最接近指定时间的节拍标记 /// /// 目标时间(秒) /// 最大容差(秒),超出此范围返回 null /// 最近的节拍标记,若超出容差则返回 null public BeatMarker GetNearestBeat(float time, float tolerance) { if (beatMarkers == null || beatMarkers.Count == 0) return null; BeatMarker nearest = null; float minDiff = float.MaxValue; for (int i = 0; i < beatMarkers.Count; i++) { float diff = Mathf.Abs(beatMarkers[i].time - time); if (diff < minDiff) { minDiff = diff; nearest = beatMarkers[i]; } } return minDiff <= tolerance ? nearest : null; } /// /// 获取指定时间之后最近的节拍标记 /// public BeatMarker GetNextBeat(float currentTime) { if (beatMarkers == null) return null; for (int i = 0; i < beatMarkers.Count; i++) { if (beatMarkers[i].time > currentTime) { return beatMarkers[i]; } } return null; } /// /// 获取指定时间之后最近的、带有特定 tag 的节拍标记 /// public BeatMarker GetNextBeatWithTag(float currentTime, string tag) { if (beatMarkers == null) return null; for (int i = 0; i < beatMarkers.Count; i++) { if (beatMarkers[i].time > currentTime && beatMarkers[i].HasTag(tag)) { return beatMarkers[i]; } } return null; } /// /// 获取时间范围内的所有节拍标记 /// public List GetBeatsInRange(float startTime, float endTime) { var result = new List(); if (beatMarkers == null) return result; for (int i = 0; i < beatMarkers.Count; i++) { if (beatMarkers[i].time >= startTime && beatMarkers[i].time <= endTime) { result.Add(beatMarkers[i]); } } return result; } /// /// 按 BPM 在指定范围内一键生成等距节拍 /// /// 生成起始时间(秒) /// 生成结束时间(秒) /// 每个生成的节拍默认附带的 tags public void GenerateBeatsFromBPM(float startTime, float endTime, List defaultTags = null) { float interval = BeatInterval; if (interval <= 0f) return; // 对齐起始时间到 audioStartOffset float firstBeatTime = audioStartOffset; while (firstBeatTime < startTime) { firstBeatTime += interval; } int beatCounter = Mathf.RoundToInt((firstBeatTime - audioStartOffset) / interval); for (float t = firstBeatTime; t <= endTime; t += interval) { int bar = beatCounter / beatsPerBar; int beatInBar = beatCounter % beatsPerBar; var marker = new BeatMarker( time: t, tags: defaultTags != null ? new List(defaultTags) : new List(), barIndex: bar, beatInBar: beatInBar ); beatMarkers.Add(marker); beatCounter++; } SortBeats(); } /// /// 清除所有节拍标记 /// public void ClearBeats() { beatMarkers.Clear(); } /// /// 按时间排序所有节拍标记 /// public void SortBeats() { beatMarkers.Sort(); } /// /// 根据 BPM 和拍号自动计算小节/拍索引 /// public void RecalculateBarIndices() { float interval = BeatInterval; if (interval <= 0f) return; for (int i = 0; i < beatMarkers.Count; i++) { float adjustedTime = beatMarkers[i].time - audioStartOffset; int totalBeat = Mathf.RoundToInt(adjustedTime / interval); beatMarkers[i].barIndex = totalBeat / beatsPerBar; beatMarkers[i].beatInBar = totalBeat % beatsPerBar; } } } }