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
}