feat: FlowAsync调整
This commit is contained in:
@@ -1803,6 +1803,14 @@ namespace AibisDream.FrameAnimation.Editor
|
|||||||
AddReadOnly(body, "ID", flow.Id);
|
AddReadOnly(body, "ID", flow.Id);
|
||||||
AddReadOnly(body, "Entry Node", flow.EntryNodeId);
|
AddReadOnly(body, "Entry Node", flow.EntryNodeId);
|
||||||
AddBoundProperty(body, serialized, property.FindPropertyRelative("displayName"), "Display Name");
|
AddBoundProperty(body, serialized, property.FindPropertyRelative("displayName"), "Display Name");
|
||||||
|
AddBoundProperty(
|
||||||
|
body,
|
||||||
|
serialized,
|
||||||
|
property.FindPropertyRelative("asyncCompletionMode"),
|
||||||
|
"Async Completion");
|
||||||
|
body.Add(CreateCallout(
|
||||||
|
"Only affects async playback when the resolved terminal clip loops.",
|
||||||
|
"fa-callout--info"));
|
||||||
AddBoundProperty(body, serialized, property.FindPropertyRelative("hasEndBehaviorOverride"), "Override End Behavior");
|
AddBoundProperty(body, serialized, property.FindPropertyRelative("hasEndBehaviorOverride"), "Override End Behavior");
|
||||||
AddBoundProperty(body, serialized, property.FindPropertyRelative("endBehaviorOverride"), "End Behavior");
|
AddBoundProperty(body, serialized, property.FindPropertyRelative("endBehaviorOverride"), "End Behavior");
|
||||||
|
|
||||||
@@ -2513,6 +2521,14 @@ namespace AibisDream.FrameAnimation.Editor
|
|||||||
EditorGUILayout.PropertyField(property.FindPropertyRelative("entryNodeId"));
|
EditorGUILayout.PropertyField(property.FindPropertyRelative("entryNodeId"));
|
||||||
}
|
}
|
||||||
EditorGUILayout.PropertyField(property.FindPropertyRelative("displayName"));
|
EditorGUILayout.PropertyField(property.FindPropertyRelative("displayName"));
|
||||||
|
EditorGUILayout.PropertyField(
|
||||||
|
property.FindPropertyRelative("asyncCompletionMode"),
|
||||||
|
new GUIContent(
|
||||||
|
"Async Completion",
|
||||||
|
"Only affects async playback when the resolved terminal clip loops."));
|
||||||
|
EditorGUILayout.HelpBox(
|
||||||
|
"Async Completion only affects a Flow whose resolved terminal clip loops.",
|
||||||
|
MessageType.Info);
|
||||||
EditorGUILayout.PropertyField(property.FindPropertyRelative("hasEndBehaviorOverride"));
|
EditorGUILayout.PropertyField(property.FindPropertyRelative("hasEndBehaviorOverride"));
|
||||||
if (property.FindPropertyRelative("hasEndBehaviorOverride").boolValue)
|
if (property.FindPropertyRelative("hasEndBehaviorOverride").boolValue)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -90,12 +90,15 @@ namespace AibisDream.FrameAnimation
|
|||||||
[SerializeField] private string id = string.Empty;
|
[SerializeField] private string id = string.Empty;
|
||||||
[SerializeField] private string displayName = string.Empty;
|
[SerializeField] private string displayName = string.Empty;
|
||||||
[SerializeField] private string entryNodeId = string.Empty;
|
[SerializeField] private string entryNodeId = string.Empty;
|
||||||
|
[SerializeField] private AnimationFlowAsyncCompletionMode asyncCompletionMode =
|
||||||
|
AnimationFlowAsyncCompletionMode.CompleteOnTerminalLoopStart;
|
||||||
[SerializeField] private bool hasEndBehaviorOverride;
|
[SerializeField] private bool hasEndBehaviorOverride;
|
||||||
[SerializeField] private FrameClipEndBehavior endBehaviorOverride;
|
[SerializeField] private FrameClipEndBehavior endBehaviorOverride;
|
||||||
|
|
||||||
public string Id => id;
|
public string Id => id;
|
||||||
public string DisplayName => displayName;
|
public string DisplayName => displayName;
|
||||||
public string EntryNodeId => entryNodeId;
|
public string EntryNodeId => entryNodeId;
|
||||||
|
public AnimationFlowAsyncCompletionMode AsyncCompletionMode => asyncCompletionMode;
|
||||||
public FrameClipEndBehavior? EndBehaviorOverride =>
|
public FrameClipEndBehavior? EndBehaviorOverride =>
|
||||||
hasEndBehaviorOverride ? endBehaviorOverride : (FrameClipEndBehavior?)null;
|
hasEndBehaviorOverride ? endBehaviorOverride : (FrameClipEndBehavior?)null;
|
||||||
|
|
||||||
@@ -107,11 +110,14 @@ namespace AibisDream.FrameAnimation
|
|||||||
string id,
|
string id,
|
||||||
string displayName,
|
string displayName,
|
||||||
string entryNodeId,
|
string entryNodeId,
|
||||||
FrameClipEndBehavior? endBehaviorOverride = null)
|
FrameClipEndBehavior? endBehaviorOverride = null,
|
||||||
|
AnimationFlowAsyncCompletionMode asyncCompletionMode =
|
||||||
|
AnimationFlowAsyncCompletionMode.CompleteOnTerminalLoopStart)
|
||||||
{
|
{
|
||||||
this.id = id ?? string.Empty;
|
this.id = id ?? string.Empty;
|
||||||
this.displayName = displayName ?? this.id;
|
this.displayName = displayName ?? this.id;
|
||||||
this.entryNodeId = entryNodeId ?? string.Empty;
|
this.entryNodeId = entryNodeId ?? string.Empty;
|
||||||
|
this.asyncCompletionMode = asyncCompletionMode;
|
||||||
SetEndBehaviorOverride(endBehaviorOverride);
|
SetEndBehaviorOverride(endBehaviorOverride);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,6 +141,11 @@ namespace AibisDream.FrameAnimation
|
|||||||
{
|
{
|
||||||
entryNodeId = value ?? string.Empty;
|
entryNodeId = value ?? string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void SetAsyncCompletionMode(AnimationFlowAsyncCompletionMode value)
|
||||||
|
{
|
||||||
|
asyncCompletionMode = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
|
|||||||
@@ -5,6 +5,25 @@ using UnityEngine;
|
|||||||
|
|
||||||
namespace AibisDream.FrameAnimation
|
namespace AibisDream.FrameAnimation
|
||||||
{
|
{
|
||||||
|
public sealed class FrameAnimationPlaybackAwaitable : CustomYieldInstruction
|
||||||
|
{
|
||||||
|
private readonly FrameAnimationPlayer player;
|
||||||
|
|
||||||
|
public FrameAnimationPlaybackHandle PlaybackHandle { get; }
|
||||||
|
|
||||||
|
internal FrameAnimationPlaybackAwaitable(
|
||||||
|
FrameAnimationPlayer player,
|
||||||
|
FrameAnimationPlaybackHandle playbackHandle)
|
||||||
|
{
|
||||||
|
this.player = player;
|
||||||
|
PlaybackHandle = playbackHandle ??
|
||||||
|
throw new ArgumentNullException(nameof(playbackHandle));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool keepWaiting =>
|
||||||
|
player != null && player.ShouldWaitForAsyncCompletion(PlaybackHandle);
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class FrameAnimationPlaybackHandle : CustomYieldInstruction
|
public sealed class FrameAnimationPlaybackHandle : CustomYieldInstruction
|
||||||
{
|
{
|
||||||
private readonly TaskCompletionSource<FrameAnimationPlaybackResult> completionSource =
|
private readonly TaskCompletionSource<FrameAnimationPlaybackResult> completionSource =
|
||||||
|
|||||||
@@ -97,6 +97,31 @@ namespace AibisDream.FrameAnimation
|
|||||||
public int CurrentFrameIndex => hasStarted && !hasCompleted ? frameIndex : -1;
|
public int CurrentFrameIndex => hasStarted && !hasCompleted ? frameIndex : -1;
|
||||||
public FrameClip CurrentClip => CurrentStep?.Clip;
|
public FrameClip CurrentClip => CurrentStep?.Clip;
|
||||||
internal bool HasCompletedFirstTerminalLoopCycle => hasCompletedFirstTerminalLoopCycle;
|
internal bool HasCompletedFirstTerminalLoopCycle => hasCompletedFirstTerminalLoopCycle;
|
||||||
|
internal bool HasReachedAsyncCompletionPoint
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!hasStarted || plan.Steps == null || plan.Steps.Count == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var terminalStep = plan.Steps[plan.Steps.Count - 1];
|
||||||
|
if (terminalStep.TerminalEndBehavior != FrameClipEndBehavior.Loop)
|
||||||
|
{
|
||||||
|
return hasCompleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!plan.IsFlow ||
|
||||||
|
plan.AsyncCompletionMode ==
|
||||||
|
AnimationFlowAsyncCompletionMode.WaitForTerminalLoopFirstCycle)
|
||||||
|
{
|
||||||
|
return hasCompletedFirstTerminalLoopCycle;
|
||||||
|
}
|
||||||
|
|
||||||
|
return stepIndex == plan.Steps.Count - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
internal FrameAnimationPlaybackSnapshot Snapshot =>
|
internal FrameAnimationPlaybackSnapshot Snapshot =>
|
||||||
new FrameAnimationPlaybackSnapshot(
|
new FrameAnimationPlaybackSnapshot(
|
||||||
hasStarted,
|
hasStarted,
|
||||||
|
|||||||
@@ -138,18 +138,22 @@ namespace AibisDream.FrameAnimation
|
|||||||
return PlayInternal(playableId, options);
|
return PlayInternal(playableId, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public FrameAnimationPlaybackAwaitable PlayAsync()
|
||||||
/// 等待当前播放请求完成首轮。非循环请求等待自然结束;终点为 Loop 的请求在
|
|
||||||
/// 第一次回绕后结束等待,但播放请求本身保持活动并继续循环。
|
|
||||||
/// </summary>
|
|
||||||
public CustomYieldInstruction WaitForFirstPass(FrameAnimationPlaybackHandle playbackHandle)
|
|
||||||
{
|
{
|
||||||
if (playbackHandle == null)
|
var playableId = graph?.Settings?.DefaultPlayableId ?? string.Empty;
|
||||||
{
|
return PlayAsyncInternal(playableId, default);
|
||||||
throw new ArgumentNullException(nameof(playbackHandle));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new FirstPassYieldInstruction(this, playbackHandle);
|
public FrameAnimationPlaybackAwaitable PlayAsync(string playableId)
|
||||||
|
{
|
||||||
|
return PlayAsyncInternal(playableId, default);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FrameAnimationPlaybackAwaitable PlayAsync(
|
||||||
|
string playableId,
|
||||||
|
FrameAnimationPlayOptions options)
|
||||||
|
{
|
||||||
|
return PlayAsyncInternal(playableId, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryBindGraph(
|
public bool TryBindGraph(
|
||||||
@@ -404,6 +408,15 @@ namespace AibisDream.FrameAnimation
|
|||||||
return handle;
|
return handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private FrameAnimationPlaybackAwaitable PlayAsyncInternal(
|
||||||
|
string playableId,
|
||||||
|
FrameAnimationPlayOptions options)
|
||||||
|
{
|
||||||
|
return new FrameAnimationPlaybackAwaitable(
|
||||||
|
this,
|
||||||
|
PlayInternal(playableId, options));
|
||||||
|
}
|
||||||
|
|
||||||
private bool TryCreateTarget(
|
private bool TryCreateTarget(
|
||||||
out IFrameAnimationTarget resolvedTarget,
|
out IFrameAnimationTarget resolvedTarget,
|
||||||
out FrameAnimationPlaybackError error)
|
out FrameAnimationPlaybackError error)
|
||||||
@@ -527,31 +540,15 @@ namespace AibisDream.FrameAnimation
|
|||||||
private Action<Sprite> ApplySpriteCallback =>
|
private Action<Sprite> ApplySpriteCallback =>
|
||||||
applySpriteCallback ??= ApplySprite;
|
applySpriteCallback ??= ApplySprite;
|
||||||
|
|
||||||
private bool ShouldWaitForFirstPass(FrameAnimationPlaybackHandle playbackHandle)
|
internal bool ShouldWaitForAsyncCompletion(
|
||||||
|
FrameAnimationPlaybackHandle playbackHandle)
|
||||||
{
|
{
|
||||||
if (playbackHandle.IsCompleted || activeHandle != playbackHandle || session == null)
|
if (playbackHandle.IsCompleted || activeHandle != playbackHandle || session == null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return !session.HasCompletedFirstTerminalLoopCycle;
|
return !session.HasReachedAsyncCompletionPoint;
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class FirstPassYieldInstruction : CustomYieldInstruction
|
|
||||||
{
|
|
||||||
private readonly FrameAnimationPlayer player;
|
|
||||||
private readonly FrameAnimationPlaybackHandle playbackHandle;
|
|
||||||
|
|
||||||
public FirstPassYieldInstruction(
|
|
||||||
FrameAnimationPlayer player,
|
|
||||||
FrameAnimationPlaybackHandle playbackHandle)
|
|
||||||
{
|
|
||||||
this.player = player;
|
|
||||||
this.playbackHandle = playbackHandle;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool keepWaiting =>
|
|
||||||
player != null && player.ShouldWaitForFirstPass(playbackHandle);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyEndBehavior(FrameClipEndBehavior endBehavior)
|
private void ApplyEndBehavior(FrameClipEndBehavior endBehavior)
|
||||||
|
|||||||
@@ -28,12 +28,19 @@ namespace AibisDream.FrameAnimation
|
|||||||
public string PlayableId { get; }
|
public string PlayableId { get; }
|
||||||
public bool IsFlow { get; }
|
public bool IsFlow { get; }
|
||||||
public IReadOnlyList<ResolvedPlaybackStep> Steps { get; }
|
public IReadOnlyList<ResolvedPlaybackStep> Steps { get; }
|
||||||
|
public AnimationFlowAsyncCompletionMode AsyncCompletionMode { get; }
|
||||||
|
|
||||||
public ResolvedPlaybackPlan(string playableId, bool isFlow, IReadOnlyList<ResolvedPlaybackStep> steps)
|
public ResolvedPlaybackPlan(
|
||||||
|
string playableId,
|
||||||
|
bool isFlow,
|
||||||
|
IReadOnlyList<ResolvedPlaybackStep> steps,
|
||||||
|
AnimationFlowAsyncCompletionMode asyncCompletionMode =
|
||||||
|
AnimationFlowAsyncCompletionMode.WaitForTerminalLoopFirstCycle)
|
||||||
{
|
{
|
||||||
PlayableId = playableId;
|
PlayableId = playableId;
|
||||||
IsFlow = isFlow;
|
IsFlow = isFlow;
|
||||||
Steps = steps;
|
Steps = steps;
|
||||||
|
AsyncCompletionMode = asyncCompletionMode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,7 +285,11 @@ namespace AibisDream.FrameAnimation
|
|||||||
currentNodeId = edge.ToNodeId;
|
currentNodeId = edge.ToNodeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
plan = new ResolvedPlaybackPlan(flow.Id, true, steps);
|
plan = new ResolvedPlaybackPlan(
|
||||||
|
flow.Id,
|
||||||
|
true,
|
||||||
|
steps,
|
||||||
|
flow.AsyncCompletionMode);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ namespace AibisDream.FrameAnimation
|
|||||||
Always
|
Always
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum AnimationFlowAsyncCompletionMode
|
||||||
|
{
|
||||||
|
CompleteOnTerminalLoopStart = 0,
|
||||||
|
WaitForTerminalLoopFirstCycle = 1
|
||||||
|
}
|
||||||
|
|
||||||
public enum FrameAnimationPlaybackState
|
public enum FrameAnimationPlaybackState
|
||||||
{
|
{
|
||||||
Stopped,
|
Stopped,
|
||||||
|
|||||||
@@ -95,12 +95,13 @@ namespace AibisDream
|
|||||||
|
|
||||||
public override IEnumerator ChangeStateAsync(string stateName)
|
public override IEnumerator ChangeStateAsync(string stateName)
|
||||||
{
|
{
|
||||||
if (!TryPlay(stateName, false, out var handle))
|
if (!TryPlayAsync(stateName, out var awaitable))
|
||||||
{
|
{
|
||||||
yield break;
|
yield break;
|
||||||
}
|
}
|
||||||
|
|
||||||
yield return _player.WaitForFirstPass(handle);
|
yield return awaitable;
|
||||||
|
var handle = awaitable.PlaybackHandle;
|
||||||
if (handle.IsCompleted)
|
if (handle.IsCompleted)
|
||||||
{
|
{
|
||||||
LogFailedResult(stateName, handle.Result);
|
LogFailedResult(stateName, handle.Result);
|
||||||
@@ -141,6 +142,50 @@ namespace AibisDream
|
|||||||
out FrameAnimationPlaybackHandle handle)
|
out FrameAnimationPlaybackHandle handle)
|
||||||
{
|
{
|
||||||
handle = null;
|
handle = null;
|
||||||
|
if (!CanPlay(stateName))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
handle = restoreTerminalState
|
||||||
|
? _player.RestoreTerminalState(stateName)
|
||||||
|
: _player.Play(stateName);
|
||||||
|
if (handle.IsCompleted &&
|
||||||
|
handle.Result.Reason == FrameAnimationCompletionReason.Failed)
|
||||||
|
{
|
||||||
|
LogFailedResult(stateName, handle.Result);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentStateName = stateName;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryPlayAsync(
|
||||||
|
string stateName,
|
||||||
|
out FrameAnimationPlaybackAwaitable awaitable)
|
||||||
|
{
|
||||||
|
awaitable = null;
|
||||||
|
if (!CanPlay(stateName))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
awaitable = _player.PlayAsync(stateName);
|
||||||
|
var handle = awaitable.PlaybackHandle;
|
||||||
|
if (handle.IsCompleted &&
|
||||||
|
handle.Result.Reason == FrameAnimationCompletionReason.Failed)
|
||||||
|
{
|
||||||
|
LogFailedResult(stateName, handle.Result);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentStateName = stateName;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanPlay(string stateName)
|
||||||
|
{
|
||||||
if (!_isReady || _player == null)
|
if (!_isReady || _player == null)
|
||||||
{
|
{
|
||||||
Debug.LogError(
|
Debug.LogError(
|
||||||
@@ -157,17 +202,6 @@ namespace AibisDream
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
handle = restoreTerminalState
|
|
||||||
? _player.RestoreTerminalState(stateName)
|
|
||||||
: _player.Play(stateName);
|
|
||||||
if (handle.IsCompleted &&
|
|
||||||
handle.Result.Reason == FrameAnimationCompletionReason.Failed)
|
|
||||||
{
|
|
||||||
LogFailedResult(stateName, handle.Result);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_currentStateName = stateName;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ namespace AibisDream.FrameAnimation.Tests.EditMode
|
|||||||
"Flow",
|
"Flow",
|
||||||
"Flow",
|
"Flow",
|
||||||
node.InternalId,
|
node.InternalId,
|
||||||
FrameClipEndBehavior.Clear);
|
FrameClipEndBehavior.Clear,
|
||||||
|
AnimationFlowAsyncCompletionMode.WaitForTerminalLoopFirstCycle);
|
||||||
var graph = CreateGraph(
|
var graph = CreateGraph(
|
||||||
new[] { clip },
|
new[] { clip },
|
||||||
new[] { node },
|
new[] { node },
|
||||||
@@ -55,6 +56,9 @@ namespace AibisDream.FrameAnimation.Tests.EditMode
|
|||||||
Assert.That(
|
Assert.That(
|
||||||
clone.Flows[0].EndBehaviorOverride,
|
clone.Flows[0].EndBehaviorOverride,
|
||||||
Is.EqualTo(FrameClipEndBehavior.Clear));
|
Is.EqualTo(FrameClipEndBehavior.Clear));
|
||||||
|
Assert.That(
|
||||||
|
clone.Flows[0].AsyncCompletionMode,
|
||||||
|
Is.EqualTo(AnimationFlowAsyncCompletionMode.WaitForTerminalLoopFirstCycle));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -126,6 +130,107 @@ namespace AibisDream.FrameAnimation.Tests.EditMode
|
|||||||
Assert.That(session.CurrentClipId, Is.EqualTo(idle.Id));
|
Assert.That(session.CurrentClipId, Is.EqualTo(idle.Id));
|
||||||
Assert.That(session.CurrentNodeId, Is.EqualTo(idleNode.InternalId));
|
Assert.That(session.CurrentNodeId, Is.EqualTo(idleNode.InternalId));
|
||||||
Assert.That(session.CurrentFrameIndex, Is.EqualTo(2));
|
Assert.That(session.CurrentFrameIndex, Is.EqualTo(2));
|
||||||
|
Assert.That(
|
||||||
|
plan.AsyncCompletionMode,
|
||||||
|
Is.EqualTo(AnimationFlowAsyncCompletionMode.CompleteOnTerminalLoopStart));
|
||||||
|
Assert.That(session.HasReachedAsyncCompletionPoint, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void PlaybackSession_FlowAsyncCompletionHonorsTerminalLoopPolicy()
|
||||||
|
{
|
||||||
|
var intro = CreateClip("Intro", FrameClipEndBehavior.HoldLastFrame, 1f, 100);
|
||||||
|
var idle = CreateClip("Idle", FrameClipEndBehavior.Loop, 1f, 100);
|
||||||
|
|
||||||
|
var completeOnStartPlan = new ResolvedPlaybackPlan(
|
||||||
|
"CompleteOnStart",
|
||||||
|
true,
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
new ResolvedPlaybackStep(
|
||||||
|
intro,
|
||||||
|
"intro-node",
|
||||||
|
1f,
|
||||||
|
FrameClipEndBehavior.HoldLastFrame),
|
||||||
|
new ResolvedPlaybackStep(
|
||||||
|
idle,
|
||||||
|
"idle-node",
|
||||||
|
1f,
|
||||||
|
FrameClipEndBehavior.Loop)
|
||||||
|
},
|
||||||
|
AnimationFlowAsyncCompletionMode.CompleteOnTerminalLoopStart);
|
||||||
|
var completeOnStartSession = new FrameAnimationPlaybackSession(completeOnStartPlan);
|
||||||
|
completeOnStartSession.Start(_ => { });
|
||||||
|
|
||||||
|
Assert.That(completeOnStartSession.HasReachedAsyncCompletionPoint, Is.False);
|
||||||
|
completeOnStartSession.Evaluate(0.1d, 1f, _ => { });
|
||||||
|
Assert.That(completeOnStartSession.CurrentClipId, Is.EqualTo(idle.Id));
|
||||||
|
Assert.That(completeOnStartSession.CurrentFrameIndex, Is.Zero);
|
||||||
|
Assert.That(completeOnStartSession.HasReachedAsyncCompletionPoint, Is.True);
|
||||||
|
|
||||||
|
var waitForCyclePlan = new ResolvedPlaybackPlan(
|
||||||
|
"WaitForCycle",
|
||||||
|
true,
|
||||||
|
completeOnStartPlan.Steps,
|
||||||
|
AnimationFlowAsyncCompletionMode.WaitForTerminalLoopFirstCycle);
|
||||||
|
var waitForCycleSession = new FrameAnimationPlaybackSession(waitForCyclePlan);
|
||||||
|
waitForCycleSession.Start(_ => { });
|
||||||
|
waitForCycleSession.Evaluate(0.1d, 1f, _ => { });
|
||||||
|
|
||||||
|
Assert.That(waitForCycleSession.CurrentClipId, Is.EqualTo(idle.Id));
|
||||||
|
Assert.That(waitForCycleSession.HasReachedAsyncCompletionPoint, Is.False);
|
||||||
|
waitForCycleSession.Evaluate(0.1d, 1f, _ => { });
|
||||||
|
Assert.That(waitForCycleSession.HasCompletedFirstTerminalLoopCycle, Is.True);
|
||||||
|
Assert.That(waitForCycleSession.HasReachedAsyncCompletionPoint, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void PlaybackSession_DirectLoopClipAlwaysWaitsForFirstCycle()
|
||||||
|
{
|
||||||
|
var loop = CreateClip("Loop", FrameClipEndBehavior.Loop, 1f, 100);
|
||||||
|
Assert.That(
|
||||||
|
FrameAnimationResolver.TryResolveClip(
|
||||||
|
loop,
|
||||||
|
default,
|
||||||
|
out var plan,
|
||||||
|
out var error),
|
||||||
|
Is.True,
|
||||||
|
error.ToString());
|
||||||
|
var session = new FrameAnimationPlaybackSession(plan);
|
||||||
|
session.Start(_ => { });
|
||||||
|
|
||||||
|
Assert.That(session.HasReachedAsyncCompletionPoint, Is.False);
|
||||||
|
session.Evaluate(0.1d, 1f, _ => { });
|
||||||
|
Assert.That(session.HasReachedAsyncCompletionPoint, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void PlaybackSession_FiniteFlowCompletesNaturallyForEitherPolicy()
|
||||||
|
{
|
||||||
|
var clip = CreateClip("Finite", FrameClipEndBehavior.HoldLastFrame, 1f, 100);
|
||||||
|
foreach (AnimationFlowAsyncCompletionMode mode in Enum.GetValues(
|
||||||
|
typeof(AnimationFlowAsyncCompletionMode)))
|
||||||
|
{
|
||||||
|
var plan = new ResolvedPlaybackPlan(
|
||||||
|
$"Finite-{mode}",
|
||||||
|
true,
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
new ResolvedPlaybackStep(
|
||||||
|
clip,
|
||||||
|
"finite-node",
|
||||||
|
1f,
|
||||||
|
FrameClipEndBehavior.HoldLastFrame)
|
||||||
|
},
|
||||||
|
mode);
|
||||||
|
var session = new FrameAnimationPlaybackSession(plan);
|
||||||
|
session.Start(_ => { });
|
||||||
|
|
||||||
|
Assert.That(session.HasReachedAsyncCompletionPoint, Is.False);
|
||||||
|
var evaluation = session.Evaluate(0.1d, 1f, _ => { });
|
||||||
|
Assert.That(evaluation.IsCompleted, Is.True);
|
||||||
|
Assert.That(session.HasReachedAsyncCompletionPoint, Is.True);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|||||||
@@ -112,6 +112,151 @@ namespace AibisDream.FrameAnimation.Tests.PlayMode
|
|||||||
Assert.That(player.CurrentPlayableId, Is.EqualTo(second.Id));
|
Assert.That(player.CurrentPlayableId, Is.EqualTo(second.Id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void PlayAsync_FlowCompletesAtConfiguredTerminalLoopBoundary()
|
||||||
|
{
|
||||||
|
var introSprite = CreateSprite(Color.red);
|
||||||
|
var idleSprite = CreateSprite(Color.green);
|
||||||
|
var intro = CreateClip("Intro", FrameClipEndBehavior.HoldLastFrame, introSprite);
|
||||||
|
var idle = CreateClip("Idle", FrameClipEndBehavior.Loop, idleSprite);
|
||||||
|
|
||||||
|
var completeOnStartObject = Track(new GameObject("Complete On Loop Start"));
|
||||||
|
var completeOnStartRenderer = completeOnStartObject.AddComponent<SpriteRenderer>();
|
||||||
|
var completeOnStartPlayer = completeOnStartObject.AddComponent<FrameAnimationPlayer>();
|
||||||
|
var completeOnStartGraph = CreateFlowGraph(
|
||||||
|
intro,
|
||||||
|
idle,
|
||||||
|
AnimationFlowAsyncCompletionMode.CompleteOnTerminalLoopStart);
|
||||||
|
completeOnStartPlayer.ConfigureForAuthoring(completeOnStartGraph, false, 1f);
|
||||||
|
|
||||||
|
var completeOnStart = completeOnStartPlayer.PlayAsync("Flow");
|
||||||
|
Assert.That(completeOnStart.keepWaiting, Is.True);
|
||||||
|
Assert.That(completeOnStartRenderer.sprite, Is.SameAs(introSprite));
|
||||||
|
|
||||||
|
completeOnStartPlayer.EvaluateForTests(0.1d);
|
||||||
|
|
||||||
|
Assert.That(completeOnStartRenderer.sprite, Is.SameAs(idleSprite));
|
||||||
|
Assert.That(completeOnStart.keepWaiting, Is.False);
|
||||||
|
Assert.That(completeOnStart.PlaybackHandle.IsCompleted, Is.False);
|
||||||
|
Assert.That(completeOnStartPlayer.State, Is.EqualTo(FrameAnimationPlaybackState.Playing));
|
||||||
|
|
||||||
|
var waitForCycleObject = Track(new GameObject("Wait For Loop Cycle"));
|
||||||
|
waitForCycleObject.AddComponent<SpriteRenderer>();
|
||||||
|
var waitForCyclePlayer = waitForCycleObject.AddComponent<FrameAnimationPlayer>();
|
||||||
|
var waitForCycleGraph = CreateFlowGraph(
|
||||||
|
intro,
|
||||||
|
idle,
|
||||||
|
AnimationFlowAsyncCompletionMode.WaitForTerminalLoopFirstCycle);
|
||||||
|
waitForCyclePlayer.ConfigureForAuthoring(waitForCycleGraph, false, 1f);
|
||||||
|
|
||||||
|
var waitForCycle = waitForCyclePlayer.PlayAsync("Flow");
|
||||||
|
waitForCyclePlayer.EvaluateForTests(0.1d);
|
||||||
|
Assert.That(waitForCycle.keepWaiting, Is.True);
|
||||||
|
|
||||||
|
waitForCyclePlayer.EvaluateForTests(0.1d);
|
||||||
|
Assert.That(waitForCycle.keepWaiting, Is.False);
|
||||||
|
Assert.That(waitForCycle.PlaybackHandle.IsCompleted, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void PlayAsync_SingleNodeLoopFlowCompletesAfterApplyingFirstFrame()
|
||||||
|
{
|
||||||
|
var sprite = CreateSprite(Color.yellow);
|
||||||
|
var loop = CreateClip("Loop", FrameClipEndBehavior.Loop, sprite);
|
||||||
|
var node = new AnimationNode(loop.Id);
|
||||||
|
var flow = new AnimationFlow("Flow", "Flow", node.InternalId);
|
||||||
|
var graph = CreateGraph(
|
||||||
|
new[] { loop },
|
||||||
|
new[] { node },
|
||||||
|
Array.Empty<AnimationEdge>(),
|
||||||
|
new[] { flow },
|
||||||
|
flow.Id);
|
||||||
|
var gameObject = Track(new GameObject("Single Loop Flow"));
|
||||||
|
var renderer = gameObject.AddComponent<SpriteRenderer>();
|
||||||
|
var player = gameObject.AddComponent<FrameAnimationPlayer>();
|
||||||
|
player.ConfigureForAuthoring(graph, false, 1f);
|
||||||
|
|
||||||
|
var awaitable = player.PlayAsync(flow.Id);
|
||||||
|
|
||||||
|
Assert.That(renderer.sprite, Is.SameAs(sprite));
|
||||||
|
Assert.That(awaitable.keepWaiting, Is.False);
|
||||||
|
Assert.That(awaitable.PlaybackHandle.IsCompleted, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void PlayAsync_DirectLoopClipWaitsForFirstCycle()
|
||||||
|
{
|
||||||
|
var loop = CreateClip(
|
||||||
|
"Loop",
|
||||||
|
FrameClipEndBehavior.Loop,
|
||||||
|
CreateSprite(Color.white));
|
||||||
|
var gameObject = Track(new GameObject("Direct Loop Clip"));
|
||||||
|
gameObject.AddComponent<SpriteRenderer>();
|
||||||
|
var player = gameObject.AddComponent<FrameAnimationPlayer>();
|
||||||
|
player.ConfigureForAuthoring(CreateGraph(loop), false, 1f);
|
||||||
|
|
||||||
|
var awaitable = player.PlayAsync(loop.Id);
|
||||||
|
Assert.That(awaitable.keepWaiting, Is.True);
|
||||||
|
|
||||||
|
player.EvaluateForTests(0.1d);
|
||||||
|
|
||||||
|
Assert.That(awaitable.keepWaiting, Is.False);
|
||||||
|
Assert.That(awaitable.PlaybackHandle.IsCompleted, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator PlayAsync_AwaitableCanBeYieldedDirectly()
|
||||||
|
{
|
||||||
|
var clip = CreateClip(
|
||||||
|
"Finite",
|
||||||
|
FrameClipEndBehavior.HoldLastFrame,
|
||||||
|
CreateSprite(Color.cyan));
|
||||||
|
var gameObject = Track(new GameObject("Yield Async Player"));
|
||||||
|
gameObject.AddComponent<SpriteRenderer>();
|
||||||
|
var player = gameObject.AddComponent<FrameAnimationPlayer>();
|
||||||
|
player.ConfigureForAuthoring(CreateGraph(clip), false, 1f);
|
||||||
|
|
||||||
|
var awaitable = player.PlayAsync(clip.Id);
|
||||||
|
yield return awaitable;
|
||||||
|
|
||||||
|
Assert.That(awaitable.keepWaiting, Is.False);
|
||||||
|
Assert.That(
|
||||||
|
awaitable.PlaybackHandle.Result.Reason,
|
||||||
|
Is.EqualTo(FrameAnimationCompletionReason.Completed));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void PlayAsync_FailureAndReplacementReleaseAwaitable()
|
||||||
|
{
|
||||||
|
var first = CreateClip(
|
||||||
|
"First",
|
||||||
|
FrameClipEndBehavior.Loop,
|
||||||
|
CreateSprite(Color.white));
|
||||||
|
var second = CreateClip(
|
||||||
|
"Second",
|
||||||
|
FrameClipEndBehavior.Loop,
|
||||||
|
CreateSprite(Color.black));
|
||||||
|
var gameObject = Track(new GameObject("Async Arbitration"));
|
||||||
|
gameObject.AddComponent<SpriteRenderer>();
|
||||||
|
var player = gameObject.AddComponent<FrameAnimationPlayer>();
|
||||||
|
player.ConfigureForAuthoring(CreateGraph(first, second), false, 1f);
|
||||||
|
|
||||||
|
var failed = player.PlayAsync("Missing");
|
||||||
|
Assert.That(failed.keepWaiting, Is.False);
|
||||||
|
Assert.That(
|
||||||
|
failed.PlaybackHandle.Result.Error.Code,
|
||||||
|
Is.EqualTo(FrameAnimationPlaybackErrorCode.PlayableNotFound));
|
||||||
|
|
||||||
|
var active = player.PlayAsync(first.Id);
|
||||||
|
Assert.That(active.keepWaiting, Is.True);
|
||||||
|
player.Play(second.Id);
|
||||||
|
|
||||||
|
Assert.That(active.keepWaiting, Is.False);
|
||||||
|
Assert.That(
|
||||||
|
active.PlaybackHandle.Result.Reason,
|
||||||
|
Is.EqualTo(FrameAnimationCompletionReason.Replaced));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void RestoreTerminalState_RebuildsSavedVisualState()
|
public void RestoreTerminalState_RebuildsSavedVisualState()
|
||||||
{
|
{
|
||||||
@@ -154,16 +299,60 @@ namespace AibisDream.FrameAnimation.Tests.PlayMode
|
|||||||
gameObject.AddComponent<SpriteRenderer>();
|
gameObject.AddComponent<SpriteRenderer>();
|
||||||
var player = gameObject.AddComponent<FrameAnimationPlayer>();
|
var player = gameObject.AddComponent<FrameAnimationPlayer>();
|
||||||
player.ConfigureForAuthoring(CreateGraph(clip), false, 1f);
|
player.ConfigureForAuthoring(CreateGraph(clip), false, 1f);
|
||||||
var handle = player.Play(clip.Id);
|
var awaitable = player.PlayAsync(clip.Id);
|
||||||
|
var handle = awaitable.PlaybackHandle;
|
||||||
|
|
||||||
player.enabled = false;
|
player.enabled = false;
|
||||||
yield return null;
|
yield return null;
|
||||||
|
|
||||||
Assert.That(handle.Result.Reason,
|
Assert.That(handle.Result.Reason,
|
||||||
Is.EqualTo(FrameAnimationCompletionReason.Stopped));
|
Is.EqualTo(FrameAnimationCompletionReason.Stopped));
|
||||||
|
Assert.That(awaitable.keepWaiting, Is.False);
|
||||||
Assert.That(player.State, Is.EqualTo(FrameAnimationPlaybackState.Stopped));
|
Assert.That(player.State, Is.EqualTo(FrameAnimationPlaybackState.Stopped));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void PlayAsync_StopReleasesAwaitableAsStopped()
|
||||||
|
{
|
||||||
|
var clip = CreateClip(
|
||||||
|
"Loop",
|
||||||
|
FrameClipEndBehavior.Loop,
|
||||||
|
CreateSprite(Color.white));
|
||||||
|
var gameObject = Track(new GameObject("Stop Async Player"));
|
||||||
|
gameObject.AddComponent<SpriteRenderer>();
|
||||||
|
var player = gameObject.AddComponent<FrameAnimationPlayer>();
|
||||||
|
player.ConfigureForAuthoring(CreateGraph(clip), false, 1f);
|
||||||
|
var awaitable = player.PlayAsync(clip.Id);
|
||||||
|
|
||||||
|
player.Stop();
|
||||||
|
|
||||||
|
Assert.That(awaitable.keepWaiting, Is.False);
|
||||||
|
Assert.That(
|
||||||
|
awaitable.PlaybackHandle.Result.Reason,
|
||||||
|
Is.EqualTo(FrameAnimationCompletionReason.Stopped));
|
||||||
|
}
|
||||||
|
|
||||||
|
private FrameAnimationGraph CreateFlowGraph(
|
||||||
|
FrameClip intro,
|
||||||
|
FrameClip terminal,
|
||||||
|
AnimationFlowAsyncCompletionMode asyncCompletionMode)
|
||||||
|
{
|
||||||
|
var introNode = new AnimationNode(intro.Id);
|
||||||
|
var terminalNode = new AnimationNode(terminal.Id);
|
||||||
|
var flow = new AnimationFlow(
|
||||||
|
"Flow",
|
||||||
|
"Flow",
|
||||||
|
introNode.InternalId,
|
||||||
|
null,
|
||||||
|
asyncCompletionMode);
|
||||||
|
return CreateGraph(
|
||||||
|
new[] { intro, terminal },
|
||||||
|
new[] { introNode, terminalNode },
|
||||||
|
new[] { new AnimationEdge(introNode.InternalId, terminalNode.InternalId) },
|
||||||
|
new[] { flow },
|
||||||
|
flow.Id);
|
||||||
|
}
|
||||||
|
|
||||||
private FrameClip CreateClip(
|
private FrameClip CreateClip(
|
||||||
string id,
|
string id,
|
||||||
FrameClipEndBehavior endBehavior,
|
FrameClipEndBehavior endBehavior,
|
||||||
@@ -186,15 +375,30 @@ namespace AibisDream.FrameAnimation.Tests.PlayMode
|
|||||||
|
|
||||||
private FrameAnimationGraph CreateGraph(params FrameClip[] clips)
|
private FrameAnimationGraph CreateGraph(params FrameClip[] clips)
|
||||||
{
|
{
|
||||||
var graph = Track(ScriptableObject.CreateInstance<FrameAnimationGraph>());
|
return CreateGraph(
|
||||||
graph.Configure(
|
|
||||||
"Graph",
|
|
||||||
"Graph",
|
|
||||||
clips,
|
clips,
|
||||||
Array.Empty<AnimationNode>(),
|
Array.Empty<AnimationNode>(),
|
||||||
Array.Empty<AnimationEdge>(),
|
Array.Empty<AnimationEdge>(),
|
||||||
Array.Empty<AnimationFlow>(),
|
Array.Empty<AnimationFlow>(),
|
||||||
clips[0].Id);
|
clips[0].Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private FrameAnimationGraph CreateGraph(
|
||||||
|
FrameClip[] clips,
|
||||||
|
AnimationNode[] nodes,
|
||||||
|
AnimationEdge[] edges,
|
||||||
|
AnimationFlow[] flows,
|
||||||
|
string defaultPlayableId)
|
||||||
|
{
|
||||||
|
var graph = Track(ScriptableObject.CreateInstance<FrameAnimationGraph>());
|
||||||
|
graph.Configure(
|
||||||
|
"Graph",
|
||||||
|
"Graph",
|
||||||
|
clips,
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
flows,
|
||||||
|
defaultPlayableId);
|
||||||
return graph;
|
return graph;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,13 +25,13 @@ Graph 必须通过 `FrameAnimationGraphValidator` 校验。帧动画角色不会
|
|||||||
<<change_actor_state Idle 测试角色>>
|
<<change_actor_state Idle 测试角色>>
|
||||||
```
|
```
|
||||||
|
|
||||||
等待一次性动画播放完成,或等待 Loop playable 完成首轮:
|
等待一次性动画播放完成,或按 Flow 配置等待 Loop:
|
||||||
|
|
||||||
```yarn
|
```yarn
|
||||||
<<change_actor_state_async Intro 测试角色>>
|
<<change_actor_state_async Intro 测试角色>>
|
||||||
```
|
```
|
||||||
|
|
||||||
`init_actor` 会等待 Prefab 和 Graph 加载完成,因此紧随其后的状态切换可以安全执行。`change_actor_state_async` 播放 Loop playable 时,会在直接 Loop Clip 完成第一轮后继续执行 Yarn;对于“前置节点 → 终点 Loop”的 Flow,则等待前置节点和终点 Loop 第一轮全部完成。命令返回后动画仍会继续循环,直到被新状态替换或主动停止。无需等待首轮时仍可使用 `change_actor_state` 立即继续 Yarn。
|
`init_actor` 会等待 Prefab 和 Graph 加载完成,因此紧随其后的状态切换可以安全执行。直接异步播放 Loop Clip 时,完成第一轮后继续执行 Yarn。对于“前置节点 → 终点 Loop”的 Flow,等待时机由 Flow 的 **Async Completion** 决定:默认在前置节点结束、终点 Loop 首帧显示后继续;也可以配置为等待终点 Loop 完整播放第一轮。命令返回后动画仍会继续循环,直到被新状态替换或主动停止。完全无需等待时仍可使用 `change_actor_state` 立即继续 Yarn。
|
||||||
|
|
||||||
## 存档行为
|
## 存档行为
|
||||||
|
|
||||||
|
|||||||
@@ -447,18 +447,21 @@ FrameAnimationGraphEditorData
|
|||||||
string id;
|
string id;
|
||||||
string displayName;
|
string displayName;
|
||||||
string entryNodeId;
|
string entryNodeId;
|
||||||
|
AnimationFlowAsyncCompletionMode asyncCompletionMode;
|
||||||
FrameClipEndBehavior? endBehaviorOverride;
|
FrameClipEndBehavior? endBehaviorOverride;
|
||||||
```
|
```
|
||||||
|
|
||||||
说明:
|
说明:
|
||||||
|
|
||||||
- `entryNodeId` 保存入口 AnimationNode 的 `internalId`。
|
- `entryNodeId` 保存入口 AnimationNode 的 `internalId`。
|
||||||
|
- `asyncCompletionMode` 只控制终点为 Loop 时的 `PlayAsync` 等待边界:进入终点并显示首帧,或等待终点首轮回绕;零值默认采用前者。
|
||||||
- `endBehaviorOverride` 是 Flow 到达终点时的可选结束行为覆盖。
|
- `endBehaviorOverride` 是 Flow 到达终点时的可选结束行为覆盖。
|
||||||
- Flow 包含的节点集合从 `entryNodeId` 沿全局边关系遍历得到,不单独保存 `includedNodeIds`。
|
- Flow 包含的节点集合从 `entryNodeId` 沿全局边关系遍历得到,不单独保存 `includedNodeIds`。
|
||||||
- 编辑器高亮、Flow 聚焦、校验和运行时使用同一套可达关系,避免人工维护的节点集合与实际连线不一致。
|
- 编辑器高亮、Flow 聚焦、校验和运行时使用同一套可达关系,避免人工维护的节点集合与实际连线不一致。
|
||||||
- 多个 Flow 的可达范围可以重叠,因此可以自然共享 Idle 等公共节点。
|
- 多个 Flow 的可达范围可以重叠,因此可以自然共享 Idle 等公共节点。
|
||||||
- 同一个 AnimationNode 不能作为多个 Flow 的入口,但不同 Flow 可以在后续路径中共享该节点。
|
- 同一个 AnimationNode 不能作为多个 Flow 的入口,但不同 Flow 可以在后续路径中共享该节点。
|
||||||
- Flow id 属于统一可播放命名空间,不能与任何 Clip 或其他 Flow id 重名。
|
- Flow id 属于统一可播放命名空间,不能与任何 Clip 或其他 Flow id 重名。
|
||||||
|
- 终点为非 Loop 时忽略 `asyncCompletionMode`,异步播放始终等待 Flow 自然结束。
|
||||||
|
|
||||||
#### 4.3.2 AnimationNode 数据
|
#### 4.3.2 AnimationNode 数据
|
||||||
|
|
||||||
@@ -881,6 +884,7 @@ Flow 创建与入口管理:
|
|||||||
- Flow 只能通过“选中一个 Clip 节点 -> 从选中节点创建 Flow”建立。
|
- Flow 只能通过“选中一个 Clip 节点 -> 从选中节点创建 Flow”建立。
|
||||||
- 创建时必须且只能选中一个节点,编辑器自动将该节点写入 `entryNodeId`。
|
- 创建时必须且只能选中一个节点,编辑器自动将该节点写入 `entryNodeId`。
|
||||||
- Flow id 默认取节点名称,但创建前允许修改;它必须与所有 Clip / Flow playable id 唯一。
|
- Flow id 默认取节点名称,但创建前允许修改;它必须与所有 Clip / Flow playable id 唯一。
|
||||||
|
- Flow 属性区提供 `Async Completion` 配置,并说明它只影响终点为 Loop 的异步播放。
|
||||||
- 同一个节点不能作为多个 Flow 的入口;节点已是入口时禁用创建命令并提供定位现有 Flow 的操作。
|
- 同一个节点不能作为多个 Flow 的入口;节点已是入口时禁用创建命令并提供定位现有 Flow 的操作。
|
||||||
- Flow 入口允许存在前驱节点;从该 Flow 播放时直接从入口开始,入口之前的节点不属于其可达范围。
|
- Flow 入口允许存在前驱节点;从该 Flow 播放时直接从入口开始,入口之前的节点不属于其可达范围。
|
||||||
- 修改入口时,先选中目标节点并执行“设置为当前 Flow 入口”;若目标已是其他 Flow 入口则阻止修改。
|
- 修改入口时,先选中目标节点并执行“设置为当前 Flow 入口”;若目标已是其他 Flow 入口则阻止修改。
|
||||||
@@ -1215,6 +1219,15 @@ FrameAnimationPlaybackHandle : CustomYieldInstruction
|
|||||||
void RegisterCompleted(Action<FrameAnimationPlaybackResult> callback);
|
void RegisterCompleted(Action<FrameAnimationPlaybackResult> callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FrameAnimationPlaybackAwaitable : CustomYieldInstruction
|
||||||
|
{
|
||||||
|
FrameAnimationPlaybackHandle PlaybackHandle { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
FrameAnimationPlaybackAwaitable PlayAsync();
|
||||||
|
FrameAnimationPlaybackAwaitable PlayAsync(string playableId);
|
||||||
|
FrameAnimationPlaybackAwaitable PlayAsync(string playableId, FrameAnimationPlayOptions options);
|
||||||
|
|
||||||
FrameAnimationPlaybackResult
|
FrameAnimationPlaybackResult
|
||||||
{
|
{
|
||||||
long requestId;
|
long requestId;
|
||||||
@@ -1254,6 +1267,9 @@ FrameAnimationCompletionReason
|
|||||||
要求:
|
要求:
|
||||||
|
|
||||||
- Handle 可以直接用于 `yield return handle`。
|
- Handle 可以直接用于 `yield return handle`。
|
||||||
|
- `PlayAsync()` 调用时立即开始播放,返回值可直接用于 `yield return`;其 `PlaybackHandle` 仍表示底层播放请求的完整生命周期。
|
||||||
|
- 非循环 playable 的异步等待随播放自然结束。直接 Loop Clip 固定等待第一轮;终点为 Loop 的 Flow 按 `AnimationFlow.AsyncCompletionMode` 在终点 Loop 首帧显示后或第一轮回绕后解除等待。
|
||||||
|
- Awaitable 解除等待不等于底层 Loop 播放完成;Loop 的 `PlaybackHandle` 继续保持未完成,直到被替换、停止、禁用或销毁。
|
||||||
- `WaitAsync()` 使用标准 `Task`,项目第一版不引入 UniTask 依赖。
|
- `WaitAsync()` 使用标准 `Task`,项目第一版不引入 UniTask 依赖。
|
||||||
- `RegisterCompleted()` 是正式完成通知接口。请求尚未完成时登记回调;请求已经完成时立即用既有结果调用,避免同步失败或极短动画造成通知丢失。
|
- `RegisterCompleted()` 是正式完成通知接口。请求尚未完成时登记回调;请求已经完成时立即用既有结果调用,避免同步失败或极短动画造成通知丢失。
|
||||||
- 同一个播放请求只能从未完成状态结算一次;协程、Task 和回调必须观察到同一个结果。
|
- 同一个播放请求只能从未完成状态结算一次;协程、Task 和回调必须观察到同一个结果。
|
||||||
|
|||||||
+16
-6
@@ -86,6 +86,15 @@ Flow ID 就是 Yarn 中填写的动画名。建议使用有明确含义且不易
|
|||||||
3. 点击顶部 **Validate**,底部 **Validation** 中不能有 Error。
|
3. 点击顶部 **Validate**,底部 **Validation** 中不能有 Error。
|
||||||
4. 点击顶部 **Save** 保存。
|
4. 点击顶部 **Save** 保存。
|
||||||
|
|
||||||
|
### Flow 的异步完成配置
|
||||||
|
|
||||||
|
选择 Flow 后,可以在右侧 **Animation Flow** 区域设置 **Async Completion**。该配置只在 Flow 的终点为 `Loop` 时生效:
|
||||||
|
|
||||||
|
- `Complete On Terminal Loop Start`:默认值。所有前置节点播放完成并显示终点 Loop 第一帧后,异步调用继续执行;终点 Loop 在后台持续循环。
|
||||||
|
- `Wait For Terminal Loop First Cycle`:等待前置节点和终点 Loop 第一轮全部播放完成后,异步调用继续执行。
|
||||||
|
|
||||||
|
终点不是 `Loop` 时,两种配置行为相同,都会等待整条 Flow 自然结束。
|
||||||
|
|
||||||
## 4. Yarn 调用
|
## 4. Yarn 调用
|
||||||
|
|
||||||
### 初始化角色
|
### 初始化角色
|
||||||
@@ -163,17 +172,17 @@ hs: 戴上“实实”牌帽子,给你的头顶添件宝!
|
|||||||
|
|
||||||
Yarn 会立即继续对白,Flow 则在后台按照连线依次播放。适合整段表演与对白同时发生的情况。
|
Yarn 会立即继续对白,Flow 则在后台按照连线依次播放。适合整段表演与对白同时发生的情况。
|
||||||
|
|
||||||
#### Flow:播放到终点首轮后再继续 Yarn
|
#### Flow:按 Flow 配置等待后再继续 Yarn
|
||||||
|
|
||||||
```yarn
|
```yarn
|
||||||
<<change_actor_state_async 摘帽到伸手_Flow 火山>>
|
<<change_actor_state_async 摘帽到伸手_Flow 火山>>
|
||||||
// Flow 的前置动作和终点 Loop 首轮播放完后,才执行这里
|
// 默认在前置动作结束、终点 Loop 首帧显示后执行这里
|
||||||
```
|
```
|
||||||
|
|
||||||
`change_actor_state_async` 会从入口开始等待整条 Flow:
|
`change_actor_state_async` 会从入口开始等待整条 Flow:
|
||||||
|
|
||||||
- 终点为非循环 Clip:等待所有节点自然播放结束;
|
- 终点为非循环 Clip:等待所有节点自然播放结束;
|
||||||
- `前置动作 → 终点 Loop` 的 Flow:等待前置动作和终点 Loop 的第一轮全部结束。
|
- `前置动作 → 终点 Loop` 的 Flow:根据 **Async Completion**,在终点 Loop 开始时完成,或等待其第一轮结束。
|
||||||
|
|
||||||
如果终点是 `Loop`,命令返回后终点 Clip 仍会继续循环,直到被下一次状态切换替换。
|
如果终点是 `Loop`,命令返回后终点 Clip 仍会继续循环,直到被下一次状态切换替换。
|
||||||
|
|
||||||
@@ -184,7 +193,7 @@ Yarn 会立即继续对白,Flow 则在后台按照连线依次播放。适合
|
|||||||
| 单个 Clip | 不等待 | `<<change_actor_state ClipID 角色名>>` |
|
| 单个 Clip | 不等待 | `<<change_actor_state ClipID 角色名>>` |
|
||||||
| 单个 Clip | 等完整动画;Loop 等第一轮 | `<<change_actor_state_async ClipID 角色名>>` |
|
| 单个 Clip | 等完整动画;Loop 等第一轮 | `<<change_actor_state_async ClipID 角色名>>` |
|
||||||
| 一整条 Flow | 不等待 | `<<change_actor_state FlowID 角色名>>` |
|
| 一整条 Flow | 不等待 | `<<change_actor_state FlowID 角色名>>` |
|
||||||
| 一整条 Flow | 等到终点首轮完成 | `<<change_actor_state_async FlowID 角色名>>` |
|
| 一整条 Flow | 按 Flow 的 Async Completion 等待 | `<<change_actor_state_async FlowID 角色名>>` |
|
||||||
|
|
||||||
命令本身不需要标明目标是 Clip 还是 Flow。系统会用 ID 在当前角色的 Graph 中查找;因此 Clip ID 和 Flow ID 不能重名。
|
命令本身不需要标明目标是 Clip 还是 Flow。系统会用 ID 在当前角色的 Graph 中查找;因此 Clip ID 和 Flow ID 不能重名。
|
||||||
|
|
||||||
@@ -212,7 +221,7 @@ Clip ID、Flow ID、角色名或槽位名中包含空格时,必须加英文双
|
|||||||
|
|
||||||
hs: 医——生——救——我——!
|
hs: 医——生——救——我——!
|
||||||
|
|
||||||
// 调用 Flow:等“摘帽”播完,再等终点“伸手表情idle”完成第一轮
|
// 调用 Flow:默认等“摘帽”播完,并显示终点“伸手表情idle”首帧
|
||||||
<<change_actor_state_async 摘帽到伸手_Flow 火山>>
|
<<change_actor_state_async 摘帽到伸手_Flow 火山>>
|
||||||
|
|
||||||
// Flow 返回后,终点的伸手 idle 仍在循环
|
// Flow 返回后,终点的伸手 idle 仍在循环
|
||||||
@@ -233,6 +242,7 @@ hs: 我……没有活干了。
|
|||||||
- 只有最后一个 Node 设置了结束行为吗?
|
- 只有最后一个 Node 设置了结束行为吗?
|
||||||
- 需要持续显示的 idle 是否设置为 `Loop`?
|
- 需要持续显示的 idle 是否设置为 `Loop`?
|
||||||
- Flow ID 是否与已有 Clip / Flow 重名?
|
- Flow ID 是否与已有 Clip / Flow 重名?
|
||||||
|
- Flow 的 **Async Completion** 是否符合剧情节奏?
|
||||||
- Yarn 中的 ID 与 Graph 完全一致,包括空格和大小写吗?
|
- Yarn 中的 ID 与 Graph 完全一致,包括空格和大小写吗?
|
||||||
- 需要等动画时是否用了 `change_actor_state_async`?
|
- 需要等动画时是否用了 `change_actor_state_async`?
|
||||||
- 顶部 **Validate** 是否无 Error?
|
- 顶部 **Validate** 是否无 Error?
|
||||||
@@ -254,7 +264,7 @@ Yarn 中传入的是 Clip ID 或 Flow ID,不是资源文件名、Graph 名或
|
|||||||
|
|
||||||
**循环动画导致剧情无法继续**
|
**循环动画导致剧情无法继续**
|
||||||
|
|
||||||
使用 `change_actor_state_async` 等待 Loop 时,只会等待第一轮,不会无限阻塞。如果仍未继续,先运行 **Validate** 检查 Flow 路径和结尾配置。
|
直接异步播放 Loop Clip 时只等待第一轮。异步播放 Flow 时,默认进入终点 Loop 即继续,也可以通过 **Async Completion** 配置为等待终点 Loop 第一轮。如果仍未继续,先检查是否还停在前置节点、有效速度是否为 0,并运行 **Validate** 检查 Flow 路径和结尾配置。
|
||||||
|
|
||||||
**想让 Flow 中途停住**
|
**想让 Flow 中途停住**
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user