using System; using System.Threading; using UnityEngine; using UnityEngine.UI; namespace AibisDream.FrameAnimation { [DisallowMultipleComponent] public sealed class FrameClipPlayer : MonoBehaviour { [SerializeField] private FrameClip clip; [SerializeField] private bool playOnEnable; [SerializeField] private float speed = 1f; private static long nextRequestId; private IFrameAnimationTarget target; private FrameAnimationPlaybackSession session; private FrameAnimationPlaybackHandle activeHandle; private FrameAnimationPlaybackState state = FrameAnimationPlaybackState.Stopped; private Action applySpriteCallback; private bool skipInitialPlayOnEnable; private bool timelineControlled; private FrameClip timelineClip; private Sprite timelineBaselineSprite; private bool timelineBaselineEnabled; public FrameClip Clip => clip; public bool PlayOnEnable => playOnEnable; public FrameAnimationPlaybackState State => state; public FrameClip CurrentClip => session?.CurrentClip; public string CurrentClipId => session?.CurrentClipId ?? string.Empty; public int CurrentFrameIndex => session?.CurrentFrameIndex ?? -1; public float Speed => speed; private void Awake() { skipInitialPlayOnEnable = playOnEnable; if (TryCreateTarget(out var resolvedTarget, out _)) { target = resolvedTarget; } } private void OnEnable() { if (!playOnEnable) return; if (skipInitialPlayOnEnable) { skipInitialPlayOnEnable = false; return; } TryAutoPlay(); } private void Start() { if (playOnEnable) TryAutoPlay(); } private void TryAutoPlay() { var handle = Play(); if (handle.IsCompleted && handle.Result.Reason == FrameAnimationCompletionReason.Failed) { Debug.LogError($"FrameClipPlayer 自动播放失败:{handle.Result.Error}", this); } } private void Update() { if (timelineControlled) return; Evaluate(Time.deltaTime); } private void OnDisable() { ReleaseTimelineControl(null); TerminateActive(FrameAnimationCompletionReason.Stopped, FrameAnimationPlaybackError.None); } private void OnDestroy() { ReleaseTimelineControl(null); TerminateActive(FrameAnimationCompletionReason.Stopped, FrameAnimationPlaybackError.None); } public FrameAnimationPlaybackHandle Play() { return PlayInternal(clip, default); } public FrameAnimationPlaybackHandle Play(FrameAnimationPlayOptions options) { return PlayInternal(clip, options); } public FrameAnimationPlaybackHandle Play(FrameClip value) { return PlayInternal(value, default); } public FrameAnimationPlaybackHandle Play(FrameClip value, FrameAnimationPlayOptions options) { return PlayInternal(value, options); } public FrameAnimationPlaybackHandle RestoreTerminalState() { return RestoreTerminalState(clip); } public FrameAnimationPlaybackHandle RestoreTerminalState(FrameClip value) { ReleaseTimelineControl(null); var requestId = Interlocked.Increment(ref nextRequestId); var handle = new FrameAnimationPlaybackHandle(requestId); var playableId = value != null ? value.Id : string.Empty; if (!TryPrepare(value, default, handle, out var plan, out var resolvedTarget)) return handle; if (activeHandle != null && !activeHandle.IsCompleted) { CompleteHandle(activeHandle, CurrentClipId, FrameAnimationCompletionReason.Replaced, FrameAnimationPlaybackError.None); } target = resolvedTarget; target.Enabled = true; var terminalStep = plan.Steps[plan.Steps.Count - 1]; if (terminalStep.TerminalEndBehavior == FrameClipEndBehavior.Loop) { session = new FrameAnimationPlaybackSession(plan); activeHandle = handle; state = FrameAnimationPlaybackState.Playing; session.Start(ApplySpriteCallback); return handle; } ApplySprite(terminalStep.Clip.Frames[terminalStep.Clip.FrameCount - 1]?.Sprite); ApplyEndBehavior(terminalStep.TerminalEndBehavior); session = null; activeHandle = null; state = FrameAnimationPlaybackState.Stopped; CompleteHandle(handle, playableId, FrameAnimationCompletionReason.Completed, FrameAnimationPlaybackError.None); return handle; } public CustomYieldInstruction WaitForFirstPass(FrameAnimationPlaybackHandle playbackHandle) { if (playbackHandle == null) throw new ArgumentNullException(nameof(playbackHandle)); return new FirstPassYieldInstruction(this, playbackHandle); } public void Stop(FrameAnimationStopMode mode = FrameAnimationStopMode.HoldCurrentFrame) { ReleaseTimelineControl(null); TerminateActive(FrameAnimationCompletionReason.Stopped, FrameAnimationPlaybackError.None); if (target == null || !target.IsValid) { if (!TryCreateTarget(out var resolvedTarget, out _)) return; target = resolvedTarget; } ApplyStopMode(mode); } public void Pause() { if (state == FrameAnimationPlaybackState.Playing && session != null) state = FrameAnimationPlaybackState.Paused; } public void Resume() { if (state == FrameAnimationPlaybackState.Paused && session != null) state = FrameAnimationPlaybackState.Playing; } public void SetSpeed(float value) { if (!FrameAnimationValueUtility.IsValidSpeed(value)) { Debug.LogError($"FrameClipPlayer speed 必须是有限且不小于 0 的数值,收到:{value}", this); return; } speed = value; } internal void ConfigureForAuthoring(FrameClip value, bool shouldPlayOnEnable, float playerSpeed) { clip = value; playOnEnable = shouldPlayOnEnable; speed = playerSpeed; } internal void EvaluateForTests(double deltaTimeSeconds) { Evaluate(deltaTimeSeconds); } internal FrameAnimationPlaybackError BeginTimelineControl() { if (timelineControlled) { return FrameAnimationPlaybackError.None; } if (!TryCreateTarget(out var resolvedTarget, out var targetError)) { return targetError; } TerminateActive( FrameAnimationCompletionReason.Replaced, FrameAnimationPlaybackError.None); target = resolvedTarget; timelineBaselineSprite = target.Sprite; timelineBaselineEnabled = target.Enabled; timelineClip = null; timelineControlled = true; state = FrameAnimationPlaybackState.Stopped; return FrameAnimationPlaybackError.None; } internal FrameAnimationPlaybackError SampleTimeline(FrameClip value, double timeSeconds) { var beginError = BeginTimelineControl(); if (beginError.Code != FrameAnimationPlaybackErrorCode.None) { return beginError; } if (value == null) { return new FrameAnimationPlaybackError( FrameAnimationPlaybackErrorCode.PlayableNotFound, "Timeline Clip 没有绑定 FrameClip。"); } if (timelineClip != value || session == null) { if (!FrameAnimationResolver.TryResolveClip( value, default, out var plan, out var resolveError)) { session = null; timelineClip = null; state = FrameAnimationPlaybackState.Stopped; return resolveError; } timelineClip = value; session = new FrameAnimationPlaybackSession(plan); } if (!target.Enabled) { target.Enabled = true; } state = FrameAnimationPlaybackState.Playing; var evaluation = session.Seek(Math.Max(0d, timeSeconds), ApplySpriteCallback); if (evaluation.IsFailed) { session = null; timelineClip = null; state = FrameAnimationPlaybackState.Stopped; return evaluation.Error; } if (evaluation.IsCompleted) { ApplyEndBehavior(evaluation.EndBehavior); } return FrameAnimationPlaybackError.None; } internal FrameAnimationPlaybackError ApplyTimelineEndBehavior(FrameClip value) { var beginError = BeginTimelineControl(); if (beginError.Code != FrameAnimationPlaybackErrorCode.None) { return beginError; } if (value == null) { return new FrameAnimationPlaybackError( FrameAnimationPlaybackErrorCode.PlayableNotFound, "Timeline Clip 没有绑定 FrameClip。"); } if (value.DefaultEndBehavior == FrameClipEndBehavior.Loop) { if (timelineClip == value && session != null) { return FrameAnimationPlaybackError.None; } var duration = GetEffectiveClipDurationSeconds(value); return SampleTimeline(value, Math.Max(0d, duration - 0.000001d)); } if (value.DefaultEndBehavior == FrameClipEndBehavior.Clear || value.DefaultEndBehavior == FrameClipEndBehavior.HideTarget) { ApplyEndBehavior(value.DefaultEndBehavior); return FrameAnimationPlaybackError.None; } var sampleError = SampleTimeline(value, GetEffectiveClipDurationSeconds(value)); if (sampleError.Code == FrameAnimationPlaybackErrorCode.None) { ApplyEndBehavior(value.DefaultEndBehavior); } return sampleError; } internal void RestoreTimelineBaseline() { if (!timelineControlled || target == null || !target.IsValid) return; ApplySprite(timelineBaselineSprite); if (target.Enabled != timelineBaselineEnabled) { target.Enabled = timelineBaselineEnabled; } session = null; timelineClip = null; state = FrameAnimationPlaybackState.Stopped; } internal void ReleaseTimelineControl(FrameClip terminalClip) { if (!timelineControlled) return; if (terminalClip != null) { ApplyTimelineEndBehavior(terminalClip); } else { RestoreTimelineBaseline(); } timelineControlled = false; timelineClip = null; session = null; state = FrameAnimationPlaybackState.Stopped; } private static double GetEffectiveClipDurationSeconds(FrameClip value) { if (value == null || value.TotalDurationMs <= 0 || value.Speed <= 0f) { return 0d; } return value.TotalDurationMs / 1000d / value.Speed; } private void Evaluate(double deltaTimeSeconds) { if (state != FrameAnimationPlaybackState.Playing || session == null) return; if (target == null || !target.IsValid) { FailActive(FrameAnimationPlaybackErrorCode.TargetMissing, "播放期间显示目标失效。"); return; } if (session.CurrentClip == null) { FailActive(FrameAnimationPlaybackErrorCode.InvalidPlayableData, "播放期间 Clip 数据失效。"); return; } if (!FrameAnimationValueUtility.IsValidSpeed(speed)) { FailActive(FrameAnimationPlaybackErrorCode.InvalidSpeed, "播放期间 Player speed 变为无效值。"); return; } var evaluation = session.Evaluate(deltaTimeSeconds, speed, ApplySpriteCallback); if (evaluation.IsFailed) FailActive(evaluation.Error.Code, evaluation.Error.Message); else if (evaluation.IsCompleted) CompleteNaturally(evaluation.EndBehavior); } private FrameAnimationPlaybackHandle PlayInternal(FrameClip value, FrameAnimationPlayOptions options) { ReleaseTimelineControl(null); var handle = new FrameAnimationPlaybackHandle(Interlocked.Increment(ref nextRequestId)); if (!TryPrepare(value, options, handle, out var plan, out var resolvedTarget)) return handle; if (activeHandle != null && !activeHandle.IsCompleted) { CompleteHandle(activeHandle, CurrentClipId, FrameAnimationCompletionReason.Replaced, FrameAnimationPlaybackError.None); } session = new FrameAnimationPlaybackSession(plan); activeHandle = handle; state = FrameAnimationPlaybackState.Playing; target = resolvedTarget; target.Enabled = true; session.Start(ApplySpriteCallback); return handle; } private bool TryPrepare( FrameClip value, FrameAnimationPlayOptions options, FrameAnimationPlaybackHandle handle, out ResolvedPlaybackPlan plan, out IFrameAnimationTarget resolvedTarget) { plan = null; resolvedTarget = null; var playableId = value != null ? value.Id : string.Empty; if (!isActiveAndEnabled) { CompleteFailed(handle, playableId, FrameAnimationPlaybackErrorCode.PlayerNotReady, "FrameClipPlayer 未启用或 GameObject 未激活。"); return false; } if (value == null) { CompleteFailed(handle, playableId, FrameAnimationPlaybackErrorCode.PlayableNotFound, "FrameClipPlayer 没有可播放的 FrameClip。"); return false; } if (!FrameAnimationValueUtility.IsValidSpeed(speed)) { CompleteFailed(handle, playableId, FrameAnimationPlaybackErrorCode.InvalidSpeed, "FrameClipPlayer speed 无效。"); return false; } if (!TryCreateTarget(out resolvedTarget, out var targetError)) { CompleteFailed(handle, playableId, targetError.Code, targetError.Message); return false; } if (!FrameAnimationResolver.TryResolveClip(value, options, out plan, out var resolveError)) { CompleteFailed(handle, playableId, resolveError.Code, resolveError.Message); return false; } return true; } private bool TryCreateTarget(out IFrameAnimationTarget resolvedTarget, out FrameAnimationPlaybackError error) { if (GetComponent() != null) { resolvedTarget = null; error = new FrameAnimationPlaybackError(FrameAnimationPlaybackErrorCode.TargetConflict, "同一 GameObject 不能同时包含 FrameAnimationPlayer 和 FrameClipPlayer。"); return false; } var spriteRenderer = GetComponent(); var image = GetComponent(); if (spriteRenderer != null && image != null) { resolvedTarget = null; error = new FrameAnimationPlaybackError(FrameAnimationPlaybackErrorCode.TargetConflict, "FrameClipPlayer 所在对象不能同时包含 SpriteRenderer 和 Image。"); return false; } if (spriteRenderer == null && image == null) { resolvedTarget = null; error = new FrameAnimationPlaybackError(FrameAnimationPlaybackErrorCode.TargetMissing, "FrameClipPlayer 所在对象缺少 SpriteRenderer 或 Image。"); return false; } resolvedTarget = spriteRenderer != null ? new SpriteRendererFrameAnimationTarget(spriteRenderer) : new ImageFrameAnimationTarget(image); error = FrameAnimationPlaybackError.None; return true; } private static FrameAnimationPlaybackHandle CompleteFailed( FrameAnimationPlaybackHandle handle, string playableId, FrameAnimationPlaybackErrorCode code, string message) { CompleteHandle(handle, playableId, FrameAnimationCompletionReason.Failed, new FrameAnimationPlaybackError(code, message)); return handle; } private void CompleteNaturally(FrameClipEndBehavior endBehavior) { var handle = activeHandle; var playableId = CurrentClipId; ApplyEndBehavior(endBehavior); session = null; activeHandle = null; state = FrameAnimationPlaybackState.Stopped; CompleteHandle(handle, playableId, FrameAnimationCompletionReason.Completed, FrameAnimationPlaybackError.None); } private void FailActive(FrameAnimationPlaybackErrorCode code, string message) { var handle = activeHandle; var playableId = CurrentClipId; session = null; activeHandle = null; state = FrameAnimationPlaybackState.Stopped; CompleteHandle(handle, playableId, FrameAnimationCompletionReason.Failed, new FrameAnimationPlaybackError(code, message)); } private void TerminateActive(FrameAnimationCompletionReason reason, FrameAnimationPlaybackError error) { if (activeHandle == null || activeHandle.IsCompleted) { session = null; activeHandle = null; state = FrameAnimationPlaybackState.Stopped; return; } var handle = activeHandle; var playableId = CurrentClipId; session = null; activeHandle = null; state = FrameAnimationPlaybackState.Stopped; CompleteHandle(handle, playableId, reason, error); } private static void CompleteHandle( FrameAnimationPlaybackHandle handle, string playableId, FrameAnimationCompletionReason reason, FrameAnimationPlaybackError error) { handle?.TryComplete(new FrameAnimationPlaybackResult(handle.RequestId, playableId, reason, error)); } private void ApplySprite(Sprite sprite) { if (target != null && target.IsValid && target.Sprite != sprite) { target.Sprite = sprite; } } private Action ApplySpriteCallback => applySpriteCallback ??= ApplySprite; private bool ShouldWaitForFirstPass(FrameAnimationPlaybackHandle playbackHandle) { return !playbackHandle.IsCompleted && activeHandle == playbackHandle && session != null && !session.HasCompletedFirstTerminalLoopCycle; } private sealed class FirstPassYieldInstruction : CustomYieldInstruction { private readonly FrameClipPlayer player; private readonly FrameAnimationPlaybackHandle playbackHandle; public FirstPassYieldInstruction(FrameClipPlayer player, FrameAnimationPlaybackHandle playbackHandle) { this.player = player; this.playbackHandle = playbackHandle; } public override bool keepWaiting => player != null && player.ShouldWaitForFirstPass(playbackHandle); } private void ApplyEndBehavior(FrameClipEndBehavior endBehavior) { switch (endBehavior) { case FrameClipEndBehavior.Clear: if (target != null && target.IsValid) { if (!target.Enabled) { target.Enabled = true; } ApplySprite(null); } break; case FrameClipEndBehavior.HideTarget: if (target != null && target.IsValid && target.Enabled) { target.Enabled = false; } break; } } private void ApplyStopMode(FrameAnimationStopMode mode) { switch (mode) { case FrameAnimationStopMode.Clear: if (!target.Enabled) { target.Enabled = true; } ApplySprite(null); break; case FrameAnimationStopMode.HideTarget: if (target.Enabled) { target.Enabled = false; } break; } } } }