Files
aibis-dream/Assets/Scripts/FrameAnimation/Runtime/FrameAnimationPlaybackSession.cs
T

221 lines
8.0 KiB
C#

using System;
using UnityEngine;
namespace AibisDream.FrameAnimation
{
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;
public string PlayableId => plan.PlayableId;
public string CurrentClipId => CurrentStep?.Clip != null ? CurrentStep.Clip.Id : string.Empty;
public string CurrentNodeId => plan.IsFlow && CurrentStep != null ? CurrentStep.NodeId : string.Empty;
public int CurrentFrameIndex => hasStarted && !hasCompleted ? frameIndex : -1;
public FrameClip CurrentClip => CurrentStep?.Clip;
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<Sprite> applySprite)
{
if (hasStarted)
{
return;
}
hasStarted = true;
stepIndex = 0;
frameIndex = 0;
frameElapsedSeconds = 0d;
ApplyCurrentFrame(applySprite);
}
public FrameAnimationSessionEvaluation Evaluate(
double deltaTimeSeconds,
float playerSpeed,
Action<Sprite> 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)
{
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<Sprite> 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)
{
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<Sprite> applySprite)
{
var step = CurrentStep;
if (step?.Clip == null || frameIndex < 0 || frameIndex >= step.Clip.FrameCount)
{
return;
}
applySprite?.Invoke(step.Clip.Frames[frameIndex]?.Sprite);
}
}
}