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; private SeekIndex seekIndex; 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 bool HasReachedAsyncCompletionPoint { get { if (!hasStarted || plan.Steps == null || plan.Steps.Count == 0) { return false; } var terminalStep = plan.Steps[plan.Steps.Count - 1]; if (terminalStep.TerminalEndBehavior != FrameClipEndBehavior.Loop) { return hasCompleted; } if (!plan.IsFlow || plan.AsyncCompletionMode == AnimationFlowAsyncCompletionMode.WaitForTerminalLoopFirstCycle) { return hasCompletedFirstTerminalLoopCycle; } return stepIndex == plan.Steps.Count - 1; } } 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) { if (!TryGetSeekIndex(out var index, out var error)) { hasStarted = true; hasCompleted = true; return FrameAnimationSessionEvaluation.Failed(error.Code, error.Message); } hasStarted = true; hasCompleted = false; hasCompletedFirstTerminalLoopCycle = false; var clampedSeconds = Math.Max(0d, realSeconds); var terminalStepIndex = index.Steps.Length - 1; var terminalStep = index.Steps[terminalStepIndex]; if (terminalStep.EndBehavior == FrameClipEndBehavior.Loop && clampedSeconds + Epsilon >= terminalStep.StartSeconds) { SeekTerminalLoop(terminalStepIndex, terminalStep, clampedSeconds); ApplyCurrentFrame(applySprite); return FrameAnimationSessionEvaluation.Running; } if (!double.IsPositiveInfinity(index.TotalDurationSeconds) && clampedSeconds + Epsilon >= index.TotalDurationSeconds) { stepIndex = terminalStepIndex; frameIndex = terminalStep.FrameEndSeconds.Length - 1; frameElapsedSeconds = 0d; hasCompleted = true; ApplyCurrentFrame(applySprite); return FrameAnimationSessionEvaluation.Completed(terminalStep.EndBehavior); } stepIndex = FindFirstBoundaryAfter(index.StepEndSeconds, clampedSeconds); if (stepIndex < 0 || stepIndex >= index.Steps.Length) { stepIndex = terminalStepIndex; } var seekStep = index.Steps[stepIndex]; SeekWithinStep(seekStep, clampedSeconds - seekStep.StartSeconds); ApplyCurrentFrame(applySprite); return FrameAnimationSessionEvaluation.Running; } 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 bool TryGetSeekIndex( out SeekIndex index, out FrameAnimationPlaybackError error) { if (seekIndex != null) { index = seekIndex; error = FrameAnimationPlaybackError.None; return true; } if (plan.Steps == null || plan.Steps.Count == 0) { index = null; error = new FrameAnimationPlaybackError( FrameAnimationPlaybackErrorCode.InvalidPlayableData, "播放计划不包含任何可播放 Step。"); return false; } var steps = new SeekStep[plan.Steps.Count]; var stepEndSeconds = new double[steps.Length]; var elapsedSeconds = 0d; for (var currentStepIndex = 0; currentStepIndex < steps.Length; currentStepIndex++) { var resolvedStep = plan.Steps[currentStepIndex]; if (resolvedStep?.Clip == null || resolvedStep.Clip.FrameCount <= 0) { index = null; error = new FrameAnimationPlaybackError( FrameAnimationPlaybackErrorCode.InvalidPlayableData, "播放期间 Clip 或 Frame 数据失效。"); return false; } if (!FrameAnimationValueUtility.IsValidSpeed(resolvedStep.PlaybackSpeed)) { index = null; error = new FrameAnimationPlaybackError( FrameAnimationPlaybackErrorCode.InvalidSpeed, "播放期间有效速度变为无效值。"); return false; } var frameEndSeconds = new double[resolvedStep.Clip.FrameCount]; var stepDurationSeconds = 0d; for (var currentFrameIndex = 0; currentFrameIndex < resolvedStep.Clip.FrameCount; currentFrameIndex++) { var frame = resolvedStep.Clip.Frames[currentFrameIndex]; if (frame == null || frame.DurationMs <= 0) { index = null; error = new FrameAnimationPlaybackError( FrameAnimationPlaybackErrorCode.InvalidPlayableData, "播放期间 Frame 数据失效或 durationMs 不再有效。"); return false; } if (resolvedStep.PlaybackSpeed == 0f) { frameEndSeconds[currentFrameIndex] = double.PositiveInfinity; } else { stepDurationSeconds += frame.DurationMs / 1000d / resolvedStep.PlaybackSpeed; frameEndSeconds[currentFrameIndex] = stepDurationSeconds; } } if (resolvedStep.PlaybackSpeed == 0f) { stepDurationSeconds = double.PositiveInfinity; } steps[currentStepIndex] = new SeekStep( elapsedSeconds, stepDurationSeconds, resolvedStep.PlaybackSpeed, resolvedStep.TerminalEndBehavior, frameEndSeconds); elapsedSeconds += stepDurationSeconds; stepEndSeconds[currentStepIndex] = elapsedSeconds; } seekIndex = new SeekIndex(steps, stepEndSeconds, elapsedSeconds); index = seekIndex; error = FrameAnimationPlaybackError.None; return true; } private void SeekTerminalLoop( int terminalStepIndex, SeekStep terminalStep, double realSeconds) { stepIndex = terminalStepIndex; if (terminalStep.PlaybackSpeed == 0f || double.IsPositiveInfinity(terminalStep.DurationSeconds)) { frameIndex = 0; frameElapsedSeconds = 0d; return; } var terminalElapsedSeconds = Math.Max(0d, realSeconds - terminalStep.StartSeconds); hasCompletedFirstTerminalLoopCycle = terminalElapsedSeconds + Epsilon >= terminalStep.DurationSeconds; var localSeconds = terminalElapsedSeconds % terminalStep.DurationSeconds; if (localSeconds + Epsilon >= terminalStep.DurationSeconds) { localSeconds = 0d; } SeekWithinStep(terminalStep, localSeconds); } private void SeekWithinStep(SeekStep seekStep, double localRealSeconds) { if (seekStep.PlaybackSpeed == 0f) { frameIndex = 0; frameElapsedSeconds = 0d; return; } var clampedLocalSeconds = Math.Max(0d, localRealSeconds); frameIndex = FindFirstBoundaryAfter(seekStep.FrameEndSeconds, clampedLocalSeconds); if (frameIndex < 0 || frameIndex >= seekStep.FrameEndSeconds.Length) { frameIndex = seekStep.FrameEndSeconds.Length - 1; } var frameStartRealSeconds = frameIndex == 0 ? 0d : seekStep.FrameEndSeconds[frameIndex - 1]; frameElapsedSeconds = Math.Max( 0d, (clampedLocalSeconds - frameStartRealSeconds) * seekStep.PlaybackSpeed); } private static int FindFirstBoundaryAfter(double[] boundaries, double seconds) { var low = 0; var high = boundaries.Length; var comparisonSeconds = seconds + Epsilon; while (low < high) { var middle = low + ((high - low) >> 1); if (boundaries[middle] > comparisonSeconds) { high = middle; } else { low = middle + 1; } } return low; } 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); } private sealed class SeekIndex { internal SeekStep[] Steps { get; } internal double[] StepEndSeconds { get; } internal double TotalDurationSeconds { get; } internal SeekIndex( SeekStep[] steps, double[] stepEndSeconds, double totalDurationSeconds) { Steps = steps; StepEndSeconds = stepEndSeconds; TotalDurationSeconds = totalDurationSeconds; } } private sealed class SeekStep { internal double StartSeconds { get; } internal double DurationSeconds { get; } internal float PlaybackSpeed { get; } internal FrameClipEndBehavior EndBehavior { get; } internal double[] FrameEndSeconds { get; } internal SeekStep( double startSeconds, double durationSeconds, float playbackSpeed, FrameClipEndBehavior endBehavior, double[] frameEndSeconds) { StartSeconds = startSeconds; DurationSeconds = durationSeconds; PlaybackSpeed = playbackSpeed; EndBehavior = endBehavior; FrameEndSeconds = frameEndSeconds; } } } }