feat: 帧动画Timeline问题处理
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user