using System; using UnityEngine; namespace AibisDream.FrameAnimation { internal readonly struct FrameAnimationPlaybackSnapshot { public bool HasStarted { get; } public bool IsCompleted { get; } public int StepIndex { get; } public int FrameIndex { get; } public double FrameElapsedSeconds { get; } public FrameClip Clip { get; } public string NodeId { get; } public Sprite Sprite { get; } public float StepSpeed { get; } public FrameClipEndBehavior EndBehavior { get; } internal FrameAnimationPlaybackSnapshot( bool hasStarted, bool isCompleted, int stepIndex, int frameIndex, double frameElapsedSeconds, ResolvedPlaybackStep step) { HasStarted = hasStarted; IsCompleted = isCompleted; StepIndex = stepIndex; FrameIndex = frameIndex; FrameElapsedSeconds = frameElapsedSeconds; Clip = step?.Clip; NodeId = step?.NodeId ?? string.Empty; Sprite = step?.Clip != null && frameIndex >= 0 && frameIndex < step.Clip.FrameCount ? step.Clip.Frames[frameIndex]?.Sprite : null; StepSpeed = step?.PlaybackSpeed ?? 0f; EndBehavior = step?.TerminalEndBehavior ?? FrameClipEndBehavior.HoldLastFrame; } } internal readonly struct FrameAnimationSessionEvaluation { public static FrameAnimationSessionEvaluation Running => new FrameAnimationSessionEvaluation( false, FrameClipEndBehavior.HoldLastFrame, FrameAnimationPlaybackError.None); public bool IsCompleted { get; } public FrameClipEndBehavior EndBehavior { get; } public FrameAnimationPlaybackError Error { get; } public bool IsFailed => Error.Code != FrameAnimationPlaybackErrorCode.None; private FrameAnimationSessionEvaluation( bool isCompleted, FrameClipEndBehavior endBehavior, FrameAnimationPlaybackError error) { IsCompleted = isCompleted; EndBehavior = endBehavior; Error = error; } public static FrameAnimationSessionEvaluation Completed(FrameClipEndBehavior endBehavior) { return new FrameAnimationSessionEvaluation(true, endBehavior, FrameAnimationPlaybackError.None); } public static FrameAnimationSessionEvaluation Failed( FrameAnimationPlaybackErrorCode code, string message) { return new FrameAnimationSessionEvaluation( false, FrameClipEndBehavior.HoldLastFrame, new FrameAnimationPlaybackError(code, message)); } } internal sealed class FrameAnimationPlaybackSession { private const double Epsilon = 0.000000001d; private readonly ResolvedPlaybackPlan plan; private int stepIndex; private int frameIndex; private double frameElapsedSeconds; private bool hasStarted; private bool hasCompleted; private bool hasCompletedFirstTerminalLoopCycle; public string PlayableId => plan.PlayableId; public string CurrentClipId => CurrentStep?.Clip != null ? CurrentStep.Clip.Id : string.Empty; public string CurrentNodeId => CurrentStep?.NodeId ?? string.Empty; public int CurrentFrameIndex => hasStarted && !hasCompleted ? frameIndex : -1; public FrameClip CurrentClip => CurrentStep?.Clip; internal bool HasCompletedFirstTerminalLoopCycle => hasCompletedFirstTerminalLoopCycle; internal FrameAnimationPlaybackSnapshot Snapshot => new FrameAnimationPlaybackSnapshot( hasStarted, hasCompleted, stepIndex, hasStarted ? frameIndex : -1, frameElapsedSeconds, CurrentStep); private ResolvedPlaybackStep CurrentStep => stepIndex >= 0 && stepIndex < plan.Steps.Count ? plan.Steps[stepIndex] : null; internal FrameAnimationPlaybackSession(ResolvedPlaybackPlan plan) { this.plan = plan ?? throw new ArgumentNullException(nameof(plan)); } public void Start(Action applySprite) { if (hasStarted) { return; } hasStarted = true; stepIndex = 0; frameIndex = 0; frameElapsedSeconds = 0d; ApplyCurrentFrame(applySprite); } internal void Restart(Action applySprite) { hasStarted = true; hasCompleted = false; hasCompletedFirstTerminalLoopCycle = false; stepIndex = 0; frameIndex = 0; frameElapsedSeconds = 0d; ApplyCurrentFrame(applySprite); } internal FrameAnimationSessionEvaluation Seek(double realSeconds, Action applySprite) { Restart(applySprite); return realSeconds <= 0d ? FrameAnimationSessionEvaluation.Running : Evaluate(realSeconds, 1f, applySprite); } public FrameAnimationSessionEvaluation Evaluate( double deltaTimeSeconds, float playerSpeed, Action applySprite) { if (!hasStarted || hasCompleted || deltaTimeSeconds <= 0d) { return FrameAnimationSessionEvaluation.Running; } var remainingRealSeconds = deltaTimeSeconds; while (remainingRealSeconds > Epsilon) { var step = CurrentStep; if (step?.Clip == null || frameIndex < 0 || frameIndex >= step.Clip.FrameCount) { hasCompleted = true; return FrameAnimationSessionEvaluation.Failed( FrameAnimationPlaybackErrorCode.InvalidPlayableData, "播放期间 Clip 或 Frame 数据失效。"); } var effectiveSpeed = playerSpeed * step.PlaybackSpeed; if (!FrameAnimationValueUtility.IsValidSpeed(effectiveSpeed)) { hasCompleted = true; return FrameAnimationSessionEvaluation.Failed( FrameAnimationPlaybackErrorCode.InvalidSpeed, "播放期间有效速度变为无效值。"); } if (effectiveSpeed == 0f) { return FrameAnimationSessionEvaluation.Running; } if (IsTerminalLoop(step) && frameIndex == 0 && frameElapsedSeconds <= Epsilon) { var cycleRealDuration = GetClipDurationSeconds(step.Clip) / effectiveSpeed; if (cycleRealDuration > Epsilon && remainingRealSeconds >= cycleRealDuration) { hasCompletedFirstTerminalLoopCycle = true; remainingRealSeconds %= cycleRealDuration; if (remainingRealSeconds <= Epsilon) { return FrameAnimationSessionEvaluation.Running; } } } var frame = step.Clip.Frames[frameIndex]; if (frame == null || frame.DurationMs <= 0) { hasCompleted = true; return FrameAnimationSessionEvaluation.Failed( FrameAnimationPlaybackErrorCode.InvalidPlayableData, "播放期间 Frame 数据失效或 durationMs 不再有效。"); } var frameDurationSeconds = frame.DurationMs / 1000d; var remainingAnimationSeconds = Math.Max(0d, frameDurationSeconds - frameElapsedSeconds); var realSecondsToBoundary = remainingAnimationSeconds / effectiveSpeed; if (remainingRealSeconds + Epsilon < realSecondsToBoundary) { frameElapsedSeconds += remainingRealSeconds * effectiveSpeed; return FrameAnimationSessionEvaluation.Running; } remainingRealSeconds = Math.Max(0d, remainingRealSeconds - realSecondsToBoundary); frameElapsedSeconds = 0d; var completion = AdvanceFrameOrStep(applySprite); if (completion.IsCompleted) { return completion; } } return FrameAnimationSessionEvaluation.Running; } private FrameAnimationSessionEvaluation AdvanceFrameOrStep(Action applySprite) { var step = CurrentStep; frameIndex++; if (frameIndex < step.Clip.FrameCount) { ApplyCurrentFrame(applySprite); return FrameAnimationSessionEvaluation.Running; } var isTerminalStep = stepIndex == plan.Steps.Count - 1; if (!isTerminalStep) { stepIndex++; frameIndex = 0; ApplyCurrentFrame(applySprite); return FrameAnimationSessionEvaluation.Running; } if (step.TerminalEndBehavior == FrameClipEndBehavior.Loop) { hasCompletedFirstTerminalLoopCycle = true; frameIndex = 0; ApplyCurrentFrame(applySprite); return FrameAnimationSessionEvaluation.Running; } frameIndex = step.Clip.FrameCount - 1; hasCompleted = true; return FrameAnimationSessionEvaluation.Completed(step.TerminalEndBehavior); } private bool IsTerminalLoop(ResolvedPlaybackStep step) { return stepIndex == plan.Steps.Count - 1 && step.TerminalEndBehavior == FrameClipEndBehavior.Loop; } private static double GetClipDurationSeconds(FrameClip clip) { return clip.TotalDurationMs / 1000d; } private void ApplyCurrentFrame(Action applySprite) { var step = CurrentStep; if (step?.Clip == null || frameIndex < 0 || frameIndex >= step.Clip.FrameCount) { return; } applySprite?.Invoke(step.Clip.Frames[frameIndex]?.Sprite); } } }