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

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;
}
}
}
}