Files
Cielonos/Assets/Scripts/SLSUtilities/Feedback/Base/FeedbackData.cs
SoulliesOfficial 39b43680a9 爆更
2026-07-18 03:16:20 -04:00

256 lines
8.7 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 System.Linq;
using Sirenix.OdinInspector;
using UnityEngine;
namespace SLSUtilities.Feedback
{
public enum FeedbackPlaybackStrategy
{
OwnerBound = 0,
Detached = 1
}
/// <summary>
/// 单个反馈序列的模板资产。运行时播放前会复制为 RuntimeFeedback避免动态参数污染模板。
/// </summary>
[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<FeedbackTrack> tracks = new List<FeedbackTrack>();
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<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}'.");
}
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;
}
public bool CanPlay()
{
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;
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<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)
{
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
};
}
}
}