Files
Cielonos/Assets/Scripts/SLSUtilities/Feedback/Base/FeedbackData.cs

256 lines
8.7 KiB
C#
Raw Normal View History

2026-04-18 13:57:19 -04:00
using System;
2026-04-12 02:11:15 -04:00
using System.Collections.Generic;
using System.Linq;
using Sirenix.OdinInspector;
using UnityEngine;
namespace SLSUtilities.Feedback
{
2026-07-18 03:16:20 -04:00
public enum FeedbackPlaybackStrategy
{
OwnerBound = 0,
Detached = 1
}
2026-04-12 02:11:15 -04:00
/// <summary>
2026-07-18 03:16:20 -04:00
/// 单个反馈序列的模板资产。运行时播放前会复制为 RuntimeFeedback避免动态参数污染模板。
2026-04-12 02:11:15 -04:00
/// </summary>
2026-06-05 04:21:00 -04:00
[CreateAssetMenu(fileName = "NewFeedbackData", menuName = "SLSUtilities/Feedback/FeedbackData")]
2026-04-18 13:57:19 -04:00
public partial class FeedbackData : SerializedScriptableObject
2026-04-12 02:11:15 -04:00
{
[ReadOnly, ShowInInspector]
public FeedbackDataCollection parentCollection;
[Title("Editor Settings")]
public string feedbackName;
2026-07-18 03:16:20 -04:00
[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;
2026-06-13 18:43:40 -04:00
[Title("Play Limits")]
[LabelText("限制播放频次")]
[Tooltip("如果勾选,将限制此 Feedback 的播放频次。默认每帧最多播放1次")]
public bool limitPlay = true;
[ShowIf("limitPlay")]
[LabelText("最低播放间隔 (秒)")]
2026-07-18 03:16:20 -04:00
[Tooltip("两次播放之间的最低时间间隔,以 unscaledTime 计算。为 0 代表每帧最多播放 1 次。")]
2026-06-13 18:43:40 -04:00
public float minPlayInterval = 0f;
2026-04-12 02:11:15 -04:00
public FeedbackTimeSettings defaultTimeSettings = new FeedbackTimeSettings();
[Title("Feedback Tracks")]
[ListDrawerSettings(ShowFoldout = true, ListElementLabelName = "trackName")]
public List<FeedbackTrack> tracks = new List<FeedbackTrack>();
public float TotalDuration => tracks.Count > 0 ? tracks.Max(t => t.TotalDuration) : 0f;
2026-07-18 03:16:20 -04:00
public RuntimeFeedback CreateRuntimeFeedback()
{
return RuntimeFeedback.FromTemplate(this);
}
2026-04-12 02:11:15 -04:00
[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)
{
2026-07-18 03:16:20 -04:00
Debug.LogWarning("[FeedbackData] Preview failed: FeedbackManager not found in scene. Add a GameObject with FeedbackManager component.");
2026-04-12 02:11:15 -04:00
return;
}
2026-07-18 03:16:20 -04:00
FeedbackManager.Instance.Play(this);
2026-04-12 02:11:15 -04:00
Debug.Log($"[FeedbackData] Previewing '{feedbackName}' (Duration: {TotalDuration:F2}s)");
}
}
2026-04-18 13:57:19 -04:00
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}'.");
}
2026-07-18 03:16:20 -04:00
2026-04-18 13:57:19 -04:00
return track;
}
2026-07-18 03:16:20 -04:00
2026-04-18 13:57:19 -04:00
public FeedbackClip Clip(string trackName, Func<FeedbackClip, bool> 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}'.");
}
2026-07-18 03:16:20 -04:00
2026-04-18 13:57:19 -04:00
return clip;
}
2026-07-18 03:16:20 -04:00
2026-04-18 13:57:19 -04:00
public FeedbackClip Clip<T>(string trackName) where T : FeedbackActionBase
{
2026-07-18 03:16:20 -04:00
return Clip(trackName, c => c.action != null && c.action.GetType() == typeof(T));
2026-04-18 13:57:19 -04:00
}
2026-07-18 03:16:20 -04:00
2026-04-18 13:57:19 -04:00
public FeedbackClip Clip(string trackName, string clipName)
{
return Clip(trackName, c => c.clipName == clipName);
}
2026-07-18 03:16:20 -04:00
2026-04-18 13:57:19 -04:00
public T Action<T>(string trackName) where T : FeedbackActionBase
{
FeedbackTrack track = Track(trackName);
2026-07-18 03:16:20 -04:00
FeedbackClip clip = track?.clips.FirstOrDefault(c => c.action != null && c.action.GetType() == typeof(T));
2026-04-18 13:57:19 -04:00
return clip?.action as T;
}
2026-06-13 18:43:40 -04:00
public bool CanPlay()
{
2026-07-18 03:16:20 -04:00
return FeedbackPlaybackGate.CanPlay(this);
}
public void RecordPlay()
{
FeedbackPlaybackGate.RecordPlay(this);
}
}
/// <summary>
/// 单次播放使用的可变运行时副本。它保留来源模板用于限流、Stop 匹配和调试。
/// </summary>
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<FeedbackTrack> tracks = new List<FeedbackTrack>();
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<FeedbackTrack>()
};
}
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<FeedbackClip, bool> predicate)
{
FeedbackTrack track = Track(trackName);
if (track == null) return null;
2026-06-13 18:43:40 -04:00
2026-07-18 03:16:20 -04:00
FeedbackClip clip = track.clips.FirstOrDefault(predicate);
if (clip == null)
2026-06-13 18:43:40 -04:00
{
2026-07-18 03:16:20 -04:00
Debug.LogWarning($"[RuntimeFeedback] Clip matching predicate not found in Track '{trackName}' of RuntimeFeedback '{feedbackName}'.");
2026-06-13 18:43:40 -04:00
}
2026-07-18 03:16:20 -04:00
return clip;
}
public FeedbackClip Clip<T>(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<T>(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;
}
}
/// <summary>
/// 运行时播放限流服务。限流语义属于模板来源,但状态不能保存在 FeedbackData 资产实例中。
/// </summary>
public static class FeedbackPlaybackGate
{
private struct PlayRecord
{
public float lastPlayTime;
public int lastPlayFrame;
}
private static readonly Dictionary<FeedbackData, PlayRecord> PlayRecords = new Dictionary<FeedbackData, PlayRecord>();
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)
2026-06-13 18:43:40 -04:00
{
2026-07-18 03:16:20 -04:00
record.lastPlayTime = -9999f;
record.lastPlayFrame = -1;
PlayRecords[data] = record;
2026-06-13 18:43:40 -04:00
}
2026-07-18 03:16:20 -04:00
if (data.minPlayInterval <= 0f)
2026-06-13 18:43:40 -04:00
{
2026-07-18 03:16:20 -04:00
return Time.frameCount != record.lastPlayFrame;
2026-06-13 18:43:40 -04:00
}
2026-07-18 03:16:20 -04:00
return Time.unscaledTime - record.lastPlayTime >= data.minPlayInterval;
2026-06-13 18:43:40 -04:00
}
2026-07-18 03:16:20 -04:00
public static void RecordPlay(FeedbackData data)
2026-06-13 18:43:40 -04:00
{
2026-07-18 03:16:20 -04:00
if (data == null || !data.limitPlay) return;
2026-06-13 18:43:40 -04:00
2026-07-18 03:16:20 -04:00
PlayRecords[data] = new PlayRecord
{
lastPlayFrame = Time.frameCount,
lastPlayTime = Time.unscaledTime
};
2026-06-13 18:43:40 -04:00
}
2026-04-18 13:57:19 -04:00
}
2026-04-12 02:11:15 -04:00
}