using System; using System.Collections.Generic; using System.Linq; using Sirenix.OdinInspector; using UnityEngine; namespace SLSUtilities.Feedback { public enum FeedbackPlaybackStrategy { OwnerBound = 0, Detached = 1 } /// /// 单个反馈序列的模板资产。运行时播放前会复制为 RuntimeFeedback,避免动态参数污染模板。 /// [CreateAssetMenu(fileName = "NewFeedbackData", menuName = "SLSUtilities/Feedback/FeedbackData")] public partial class FeedbackData : SerializedScriptableObject { [ReadOnly, ShowInInspector] public FeedbackDataCollection parentCollection; [Title("Editor Settings")] public string feedbackName; [Title("Playback")] [LabelText("Playback Strategy")] [Tooltip("OwnerBound: driven by the source subcontroller and stopped when the source dies/disables. Detached: driven globally without owner/time-provider binding.")] public FeedbackPlaybackStrategy playbackStrategy = FeedbackPlaybackStrategy.OwnerBound; [Title("Play Limits")] [LabelText("限制播放频次")] [Tooltip("如果勾选,将限制此 Feedback 的播放频次。默认每帧最多播放1次")] public bool limitPlay = true; [ShowIf("limitPlay")] [LabelText("最低播放间隔 (秒)")] [Tooltip("两次播放之间的最低时间间隔,以 unscaledTime 计算。为 0 代表每帧最多播放 1 次。")] public float minPlayInterval = 0f; public FeedbackTimeSettings defaultTimeSettings = new FeedbackTimeSettings(); [Title("Feedback Tracks")] [ListDrawerSettings(ShowFoldout = true, ListElementLabelName = "trackName")] public List tracks = new List(); public float TotalDuration => tracks.Count > 0 ? tracks.Max(t => t.TotalDuration) : 0f; public RuntimeFeedback CreateRuntimeFeedback() { return RuntimeFeedback.FromTemplate(this); } [Button("Preview", ButtonSizes.Medium)] [EnableIf("@UnityEngine.Application.isPlaying")] public void Preview() { if (!Application.isPlaying) { Debug.LogWarning("[FeedbackData] Preview is only available in Play mode."); return; } if (FeedbackManager.Instance == null) { Debug.LogWarning("[FeedbackData] Preview failed: FeedbackManager not found in scene. Add a GameObject with FeedbackManager component."); return; } FeedbackManager.Instance.Play(this); Debug.Log($"[FeedbackData] Previewing '{feedbackName}' (Duration: {TotalDuration:F2}s)"); } } public partial class FeedbackData { public FeedbackTrack Track(string name) { FeedbackTrack track = tracks.FirstOrDefault(t => t.trackName == name); if (track == null) { Debug.LogWarning($"[FeedbackData] Track '{name}' not found in FeedbackData '{feedbackName}'."); } return track; } public FeedbackClip Clip(string trackName, Func predicate) { FeedbackTrack track = Track(trackName); if (track == null) return null; FeedbackClip clip = track.clips.FirstOrDefault(predicate); if (clip == null) { Debug.LogWarning($"[FeedbackData] Clip matching predicate not found in Track '{trackName}' of FeedbackData '{feedbackName}'."); } return clip; } public FeedbackClip Clip(string trackName) where T : FeedbackActionBase { return Clip(trackName, c => c.action != null && c.action.GetType() == typeof(T)); } public FeedbackClip Clip(string trackName, string clipName) { return Clip(trackName, c => c.clipName == clipName); } public T Action(string trackName) where T : FeedbackActionBase { FeedbackTrack track = Track(trackName); FeedbackClip clip = track?.clips.FirstOrDefault(c => c.action != null && c.action.GetType() == typeof(T)); return clip?.action as T; } public bool CanPlay() { return FeedbackPlaybackGate.CanPlay(this); } public void RecordPlay() { FeedbackPlaybackGate.RecordPlay(this); } } /// /// 单次播放使用的可变运行时副本。它保留来源模板,用于限流、Stop 匹配和调试。 /// public sealed class RuntimeFeedback { private static int _nextRuntimeId; public readonly int runtimeId; public FeedbackData templateData; public string feedbackName; public FeedbackPlaybackStrategy playbackStrategy; public FeedbackTimeSettings defaultTimeSettings; public List tracks = new List(); public float TotalDuration => tracks.Count > 0 ? tracks.Max(t => t.TotalDuration) : 0f; private RuntimeFeedback() { runtimeId = ++_nextRuntimeId; } public static RuntimeFeedback FromTemplate(FeedbackData template) { if (template == null) return null; return new RuntimeFeedback { templateData = template, feedbackName = template.feedbackName, playbackStrategy = template.playbackStrategy, defaultTimeSettings = FeedbackRuntimeCloneUtility.CloneSerialized(template.defaultTimeSettings), tracks = template.tracks?.Select(track => track?.CreateRuntimeCopy()).ToList() ?? new List() }; } public FeedbackTrack Track(string name) { FeedbackTrack track = tracks.FirstOrDefault(t => t.trackName == name); if (track == null) { Debug.LogWarning($"[RuntimeFeedback] Track '{name}' not found in RuntimeFeedback '{feedbackName}'."); } return track; } public FeedbackClip Clip(string trackName, Func predicate) { FeedbackTrack track = Track(trackName); if (track == null) return null; FeedbackClip clip = track.clips.FirstOrDefault(predicate); if (clip == null) { Debug.LogWarning($"[RuntimeFeedback] Clip matching predicate not found in Track '{trackName}' of RuntimeFeedback '{feedbackName}'."); } return clip; } public FeedbackClip Clip(string trackName) where T : FeedbackActionBase { return Clip(trackName, c => c.action != null && c.action.GetType() == typeof(T)); } public FeedbackClip Clip(string trackName, string clipName) { return Clip(trackName, c => c.clipName == clipName); } public T Action(string trackName) where T : FeedbackActionBase { FeedbackTrack track = Track(trackName); FeedbackClip clip = track?.clips.FirstOrDefault(c => c.action != null && c.action.GetType() == typeof(T)); return clip?.action as T; } } /// /// 运行时播放限流服务。限流语义属于模板来源,但状态不能保存在 FeedbackData 资产实例中。 /// public static class FeedbackPlaybackGate { private struct PlayRecord { public float lastPlayTime; public int lastPlayFrame; } private static readonly Dictionary PlayRecords = new Dictionary(); public static bool CanPlay(FeedbackData data) { if (data == null) return false; if (!data.limitPlay) return true; PlayRecords.TryGetValue(data, out PlayRecord record); if (Time.unscaledTime < record.lastPlayTime) { record.lastPlayTime = -9999f; record.lastPlayFrame = -1; PlayRecords[data] = record; } if (data.minPlayInterval <= 0f) { return Time.frameCount != record.lastPlayFrame; } return Time.unscaledTime - record.lastPlayTime >= data.minPlayInterval; } public static void RecordPlay(FeedbackData data) { if (data == null || !data.limitPlay) return; PlayRecords[data] = new PlayRecord { lastPlayFrame = Time.frameCount, lastPlayTime = Time.unscaledTime }; } } }