221 lines
8.0 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|