feat: 帧动画Timeline问题处理

This commit is contained in:
2026-08-02 02:22:46 +08:00
parent 20a458ec78
commit 64c7245d9e
12 changed files with 2411 additions and 1247 deletions
@@ -89,6 +89,7 @@ namespace AibisDream.FrameAnimation
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;
@@ -140,10 +141,49 @@ namespace AibisDream.FrameAnimation
internal FrameAnimationSessionEvaluation Seek(double realSeconds, Action<Sprite> applySprite)
{
Restart(applySprite);
return realSeconds <= 0d
? FrameAnimationSessionEvaluation.Running
: Evaluate(realSeconds, 1f, 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(
@@ -273,6 +313,169 @@ namespace AibisDream.FrameAnimation
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;
@@ -283,5 +486,45 @@ namespace AibisDream.FrameAnimation
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;
}
}
}
}
@@ -18,6 +18,7 @@ namespace AibisDream.FrameAnimation
private FrameAnimationPlaybackSession session;
private FrameAnimationPlaybackHandle activeHandle;
private FrameAnimationPlaybackState state = FrameAnimationPlaybackState.Stopped;
private Action<Sprite> applySpriteCallback;
private bool _skipInitialPlayOnEnable;
private bool hasStartedPlayback;
@@ -100,7 +101,7 @@ namespace AibisDream.FrameAnimation
return;
}
var evaluation = session.Evaluate(Time.deltaTime, speed, ApplySprite);
var evaluation = session.Evaluate(Time.deltaTime, speed, ApplySpriteCallback);
if (evaluation.IsFailed)
{
FailActive(evaluation.Error.Code, evaluation.Error.Message);
@@ -255,7 +256,7 @@ namespace AibisDream.FrameAnimation
session = new FrameAnimationPlaybackSession(plan);
activeHandle = handle;
state = FrameAnimationPlaybackState.Playing;
session.Start(ApplySprite);
session.Start(ApplySpriteCallback);
return handle;
}
@@ -332,7 +333,7 @@ namespace AibisDream.FrameAnimation
return;
}
var evaluation = session.Evaluate(deltaTimeSeconds, speed, ApplySprite);
var evaluation = session.Evaluate(deltaTimeSeconds, speed, ApplySpriteCallback);
if (evaluation.IsFailed)
{
FailActive(evaluation.Error.Code, evaluation.Error.Message);
@@ -399,7 +400,7 @@ namespace AibisDream.FrameAnimation
state = FrameAnimationPlaybackState.Playing;
target = resolvedTarget;
target.Enabled = true;
session.Start(ApplySprite);
session.Start(ApplySpriteCallback);
return handle;
}
@@ -517,12 +518,15 @@ namespace AibisDream.FrameAnimation
private void ApplySprite(Sprite sprite)
{
if (target != null && target.IsValid)
if (target != null && target.IsValid && target.Sprite != sprite)
{
target.Sprite = sprite;
}
}
private Action<Sprite> ApplySpriteCallback =>
applySpriteCallback ??= ApplySprite;
private bool ShouldWaitForFirstPass(FrameAnimationPlaybackHandle playbackHandle)
{
if (playbackHandle.IsCompleted || activeHandle != playbackHandle || session == null)
@@ -559,12 +563,15 @@ namespace AibisDream.FrameAnimation
case FrameClipEndBehavior.Clear:
if (target != null && target.IsValid)
{
target.Enabled = true;
target.Sprite = null;
if (!target.Enabled)
{
target.Enabled = true;
}
ApplySprite(null);
}
break;
case FrameClipEndBehavior.HideTarget:
if (target != null && target.IsValid)
if (target != null && target.IsValid && target.Enabled)
{
target.Enabled = false;
}
@@ -579,11 +586,17 @@ namespace AibisDream.FrameAnimation
case FrameAnimationStopMode.HoldCurrentFrame:
break;
case FrameAnimationStopMode.Clear:
target.Enabled = true;
target.Sprite = null;
if (!target.Enabled)
{
target.Enabled = true;
}
ApplySprite(null);
break;
case FrameAnimationStopMode.HideTarget:
target.Enabled = false;
if (target.Enabled)
{
target.Enabled = false;
}
break;
}
}
@@ -17,6 +17,7 @@ namespace AibisDream.FrameAnimation
private FrameAnimationPlaybackSession session;
private FrameAnimationPlaybackHandle activeHandle;
private FrameAnimationPlaybackState state = FrameAnimationPlaybackState.Stopped;
private Action<Sprite> applySpriteCallback;
private bool skipInitialPlayOnEnable;
private bool timelineControlled;
private FrameClip timelineClip;
@@ -130,7 +131,7 @@ namespace AibisDream.FrameAnimation
session = new FrameAnimationPlaybackSession(plan);
activeHandle = handle;
state = FrameAnimationPlaybackState.Playing;
session.Start(ApplySprite);
session.Start(ApplySpriteCallback);
return handle;
}
@@ -251,12 +252,14 @@ namespace AibisDream.FrameAnimation
timelineClip = value;
session = new FrameAnimationPlaybackSession(plan);
session.Start(ApplySprite);
}
target.Enabled = true;
if (!target.Enabled)
{
target.Enabled = true;
}
state = FrameAnimationPlaybackState.Playing;
var evaluation = session.Seek(Math.Max(0d, timeSeconds), ApplySprite);
var evaluation = session.Seek(Math.Max(0d, timeSeconds), ApplySpriteCallback);
if (evaluation.IsFailed)
{
session = null;
@@ -299,6 +302,13 @@ namespace AibisDream.FrameAnimation
return SampleTimeline(value, Math.Max(0d, duration - 0.000001d));
}
if (value.DefaultEndBehavior == FrameClipEndBehavior.Clear ||
value.DefaultEndBehavior == FrameClipEndBehavior.HideTarget)
{
ApplyEndBehavior(value.DefaultEndBehavior);
return FrameAnimationPlaybackError.None;
}
var sampleError = SampleTimeline(value, GetEffectiveClipDurationSeconds(value));
if (sampleError.Code == FrameAnimationPlaybackErrorCode.None)
{
@@ -310,8 +320,11 @@ namespace AibisDream.FrameAnimation
internal void RestoreTimelineBaseline()
{
if (!timelineControlled || target == null || !target.IsValid) return;
target.Sprite = timelineBaselineSprite;
target.Enabled = timelineBaselineEnabled;
ApplySprite(timelineBaselineSprite);
if (target.Enabled != timelineBaselineEnabled)
{
target.Enabled = timelineBaselineEnabled;
}
session = null;
timelineClip = null;
state = FrameAnimationPlaybackState.Stopped;
@@ -364,7 +377,7 @@ namespace AibisDream.FrameAnimation
return;
}
var evaluation = session.Evaluate(deltaTimeSeconds, speed, ApplySprite);
var evaluation = session.Evaluate(deltaTimeSeconds, speed, ApplySpriteCallback);
if (evaluation.IsFailed) FailActive(evaluation.Error.Code, evaluation.Error.Message);
else if (evaluation.IsCompleted) CompleteNaturally(evaluation.EndBehavior);
}
@@ -386,7 +399,7 @@ namespace AibisDream.FrameAnimation
state = FrameAnimationPlaybackState.Playing;
target = resolvedTarget;
target.Enabled = true;
session.Start(ApplySprite);
session.Start(ApplySpriteCallback);
return handle;
}
@@ -525,9 +538,15 @@ namespace AibisDream.FrameAnimation
private void ApplySprite(Sprite sprite)
{
if (target != null && target.IsValid) target.Sprite = sprite;
if (target != null && target.IsValid && target.Sprite != sprite)
{
target.Sprite = sprite;
}
}
private Action<Sprite> ApplySpriteCallback =>
applySpriteCallback ??= ApplySprite;
private bool ShouldWaitForFirstPass(FrameAnimationPlaybackHandle playbackHandle)
{
return !playbackHandle.IsCompleted && activeHandle == playbackHandle && session != null &&
@@ -556,12 +575,18 @@ namespace AibisDream.FrameAnimation
case FrameClipEndBehavior.Clear:
if (target != null && target.IsValid)
{
target.Enabled = true;
target.Sprite = null;
if (!target.Enabled)
{
target.Enabled = true;
}
ApplySprite(null);
}
break;
case FrameClipEndBehavior.HideTarget:
if (target != null && target.IsValid) target.Enabled = false;
if (target != null && target.IsValid && target.Enabled)
{
target.Enabled = false;
}
break;
}
}
@@ -571,11 +596,17 @@ namespace AibisDream.FrameAnimation
switch (mode)
{
case FrameAnimationStopMode.Clear:
target.Enabled = true;
target.Sprite = null;
if (!target.Enabled)
{
target.Enabled = true;
}
ApplySprite(null);
break;
case FrameAnimationStopMode.HideTarget:
target.Enabled = false;
if (target.Enabled)
{
target.Enabled = false;
}
break;
}
}
@@ -103,7 +103,6 @@ namespace AibisDream.FrameAnimation.Timeline
var clipInfo = clips[activeIndex];
terminalClip = clipInfo.Asset?.FrameClip;
var localTime = playable.GetInput(activeIndex).GetTime();
localTime = NormalizeLoopTime(terminalClip, localTime);
ReportError(currentPlayer.SampleTimeline(terminalClip, localTime), currentPlayer);
return;
}
@@ -177,21 +176,6 @@ namespace AibisDream.FrameAnimation.Timeline
return selected;
}
private static double NormalizeLoopTime(FrameClip clip, double localTime)
{
if (clip == null || clip.DefaultEndBehavior != FrameClipEndBehavior.Loop)
{
return Math.Max(0d, localTime);
}
var duration = FrameAnimationTimelineClip.GetEffectiveDuration(clip);
if (duration <= 0d)
{
return 0d;
}
return Math.Max(0d, localTime) % duration;
}
private void ReleaseCurrentPlayer()
{
if (currentPlayer != null)