531 lines
20 KiB
C#
531 lines
20 KiB
C#
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 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<Sprite> applySprite)
|
|
{
|
|
if (hasStarted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
hasStarted = true;
|
|
stepIndex = 0;
|
|
frameIndex = 0;
|
|
frameElapsedSeconds = 0d;
|
|
ApplyCurrentFrame(applySprite);
|
|
}
|
|
|
|
internal void Restart(Action<Sprite> applySprite)
|
|
{
|
|
hasStarted = true;
|
|
hasCompleted = false;
|
|
hasCompletedFirstTerminalLoopCycle = false;
|
|
stepIndex = 0;
|
|
frameIndex = 0;
|
|
frameElapsedSeconds = 0d;
|
|
ApplyCurrentFrame(applySprite);
|
|
}
|
|
|
|
internal FrameAnimationSessionEvaluation Seek(double realSeconds, Action<Sprite> 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<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)
|
|
{
|
|
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<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)
|
|
{
|
|
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<Sprite> 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;
|
|
}
|
|
}
|
|
}
|
|
}
|