Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75ece85309 | ||
|
|
278543cb8b | ||
|
|
9fdef73926 | ||
|
|
121e7f398c | ||
|
|
9c3c467046 | ||
|
|
69152748b5 | ||
|
|
8ac4a6705a | ||
|
|
a23b7376a0 | ||
|
|
1ccf81fe8a | ||
|
|
68d7be823d | ||
|
|
1c37b395da | ||
|
|
79999f7dfa | ||
|
|
090c9e1d86 | ||
|
|
452fcdf3fb |
@@ -1803,6 +1803,14 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
AddReadOnly(body, "ID", flow.Id);
|
||||
AddReadOnly(body, "Entry Node", flow.EntryNodeId);
|
||||
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("endBehaviorOverride"), "End Behavior");
|
||||
|
||||
@@ -2513,6 +2521,14 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
EditorGUILayout.PropertyField(property.FindPropertyRelative("entryNodeId"));
|
||||
}
|
||||
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"));
|
||||
if (property.FindPropertyRelative("hasEndBehaviorOverride").boolValue)
|
||||
{
|
||||
|
||||
@@ -1359,8 +1359,8 @@ MonoBehaviour:
|
||||
m_ShadowVolumeIntensityEnabled: 0
|
||||
m_ShadowVolumeIntensity: 0.75
|
||||
m_LocalBounds:
|
||||
m_Center: {x: 0.03540039, y: -0.06465006, z: 0}
|
||||
m_Extent: {x: 14.118, y: 5.56425, z: 0}
|
||||
m_Center: {x: 0.03154993, y: -0.058099985, z: 0}
|
||||
m_Extent: {x: 14.11525, y: 5.5638, z: 0}
|
||||
m_PointLightInnerAngle: 360
|
||||
m_PointLightOuterAngle: 360
|
||||
m_PointLightInnerRadius: 0
|
||||
@@ -2856,10 +2856,10 @@ SpriteRenderer:
|
||||
m_AutoUVMaxDistance: 0.5
|
||||
m_AutoUVMaxAngle: 89
|
||||
m_LightmapParameters: {fileID: 0}
|
||||
m_SortingLayerID: 0
|
||||
m_SortingLayer: 0
|
||||
m_SortingLayerID: 1016946085
|
||||
m_SortingLayer: 11
|
||||
m_SortingOrder: 0
|
||||
m_Sprite: {fileID: -2413806693520163455, guid: a86470a33a6bf42c4b3595704624658b, type: 3}
|
||||
m_Sprite: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_FlipX: 0
|
||||
m_FlipY: 0
|
||||
@@ -2867,7 +2867,7 @@ SpriteRenderer:
|
||||
m_Size: {x: 1, y: 1}
|
||||
m_AdaptiveModeThreshold: 0.5
|
||||
m_SpriteTileMode: 0
|
||||
m_WasSpriteAssigned: 1
|
||||
m_WasSpriteAssigned: 0
|
||||
m_MaskInteraction: 0
|
||||
m_SpriteSortPoint: 0
|
||||
--- !u!95 &7481572035279601973
|
||||
|
||||
+805
-805
File diff suppressed because it is too large
Load Diff
@@ -90,12 +90,15 @@ namespace AibisDream.FrameAnimation
|
||||
[SerializeField] private string id = string.Empty;
|
||||
[SerializeField] private string displayName = string.Empty;
|
||||
[SerializeField] private string entryNodeId = string.Empty;
|
||||
[SerializeField] private AnimationFlowAsyncCompletionMode asyncCompletionMode =
|
||||
AnimationFlowAsyncCompletionMode.CompleteOnTerminalLoopStart;
|
||||
[SerializeField] private bool hasEndBehaviorOverride;
|
||||
[SerializeField] private FrameClipEndBehavior endBehaviorOverride;
|
||||
|
||||
public string Id => id;
|
||||
public string DisplayName => displayName;
|
||||
public string EntryNodeId => entryNodeId;
|
||||
public AnimationFlowAsyncCompletionMode AsyncCompletionMode => asyncCompletionMode;
|
||||
public FrameClipEndBehavior? EndBehaviorOverride =>
|
||||
hasEndBehaviorOverride ? endBehaviorOverride : (FrameClipEndBehavior?)null;
|
||||
|
||||
@@ -107,11 +110,14 @@ namespace AibisDream.FrameAnimation
|
||||
string id,
|
||||
string displayName,
|
||||
string entryNodeId,
|
||||
FrameClipEndBehavior? endBehaviorOverride = null)
|
||||
FrameClipEndBehavior? endBehaviorOverride = null,
|
||||
AnimationFlowAsyncCompletionMode asyncCompletionMode =
|
||||
AnimationFlowAsyncCompletionMode.CompleteOnTerminalLoopStart)
|
||||
{
|
||||
this.id = id ?? string.Empty;
|
||||
this.displayName = displayName ?? this.id;
|
||||
this.entryNodeId = entryNodeId ?? string.Empty;
|
||||
this.asyncCompletionMode = asyncCompletionMode;
|
||||
SetEndBehaviorOverride(endBehaviorOverride);
|
||||
}
|
||||
|
||||
@@ -135,6 +141,11 @@ namespace AibisDream.FrameAnimation
|
||||
{
|
||||
entryNodeId = value ?? string.Empty;
|
||||
}
|
||||
|
||||
internal void SetAsyncCompletionMode(AnimationFlowAsyncCompletionMode value)
|
||||
{
|
||||
asyncCompletionMode = value;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
|
||||
@@ -5,6 +5,25 @@ using UnityEngine;
|
||||
|
||||
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
|
||||
{
|
||||
private readonly TaskCompletionSource<FrameAnimationPlaybackResult> completionSource =
|
||||
|
||||
@@ -97,6 +97,31 @@ namespace AibisDream.FrameAnimation
|
||||
public int CurrentFrameIndex => hasStarted && !hasCompleted ? frameIndex : -1;
|
||||
public FrameClip CurrentClip => CurrentStep?.Clip;
|
||||
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 =>
|
||||
new FrameAnimationPlaybackSnapshot(
|
||||
hasStarted,
|
||||
|
||||
@@ -138,18 +138,22 @@ namespace AibisDream.FrameAnimation
|
||||
return PlayInternal(playableId, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 等待当前播放请求完成首轮。非循环请求等待自然结束;终点为 Loop 的请求在
|
||||
/// 第一次回绕后结束等待,但播放请求本身保持活动并继续循环。
|
||||
/// </summary>
|
||||
public CustomYieldInstruction WaitForFirstPass(FrameAnimationPlaybackHandle playbackHandle)
|
||||
public FrameAnimationPlaybackAwaitable PlayAsync()
|
||||
{
|
||||
if (playbackHandle == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(playbackHandle));
|
||||
}
|
||||
var playableId = graph?.Settings?.DefaultPlayableId ?? string.Empty;
|
||||
return PlayAsyncInternal(playableId, default);
|
||||
}
|
||||
|
||||
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(
|
||||
@@ -404,6 +408,15 @@ namespace AibisDream.FrameAnimation
|
||||
return handle;
|
||||
}
|
||||
|
||||
private FrameAnimationPlaybackAwaitable PlayAsyncInternal(
|
||||
string playableId,
|
||||
FrameAnimationPlayOptions options)
|
||||
{
|
||||
return new FrameAnimationPlaybackAwaitable(
|
||||
this,
|
||||
PlayInternal(playableId, options));
|
||||
}
|
||||
|
||||
private bool TryCreateTarget(
|
||||
out IFrameAnimationTarget resolvedTarget,
|
||||
out FrameAnimationPlaybackError error)
|
||||
@@ -527,31 +540,15 @@ namespace AibisDream.FrameAnimation
|
||||
private Action<Sprite> ApplySpriteCallback =>
|
||||
applySpriteCallback ??= ApplySprite;
|
||||
|
||||
private bool ShouldWaitForFirstPass(FrameAnimationPlaybackHandle playbackHandle)
|
||||
internal bool ShouldWaitForAsyncCompletion(
|
||||
FrameAnimationPlaybackHandle playbackHandle)
|
||||
{
|
||||
if (playbackHandle.IsCompleted || activeHandle != playbackHandle || session == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return !session.HasCompletedFirstTerminalLoopCycle;
|
||||
}
|
||||
|
||||
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);
|
||||
return !session.HasReachedAsyncCompletionPoint;
|
||||
}
|
||||
|
||||
private void ApplyEndBehavior(FrameClipEndBehavior endBehavior)
|
||||
|
||||
@@ -28,12 +28,19 @@ namespace AibisDream.FrameAnimation
|
||||
public string PlayableId { get; }
|
||||
public bool IsFlow { 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;
|
||||
IsFlow = isFlow;
|
||||
Steps = steps;
|
||||
AsyncCompletionMode = asyncCompletionMode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,7 +285,11 @@ namespace AibisDream.FrameAnimation
|
||||
currentNodeId = edge.ToNodeId;
|
||||
}
|
||||
|
||||
plan = new ResolvedPlaybackPlan(flow.Id, true, steps);
|
||||
plan = new ResolvedPlaybackPlan(
|
||||
flow.Id,
|
||||
true,
|
||||
steps,
|
||||
flow.AsyncCompletionMode);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,12 @@ namespace AibisDream.FrameAnimation
|
||||
Always
|
||||
}
|
||||
|
||||
public enum AnimationFlowAsyncCompletionMode
|
||||
{
|
||||
CompleteOnTerminalLoopStart = 0,
|
||||
WaitForTerminalLoopFirstCycle = 1
|
||||
}
|
||||
|
||||
public enum FrameAnimationPlaybackState
|
||||
{
|
||||
Stopped,
|
||||
|
||||
@@ -468,13 +468,19 @@ namespace AibisDream.Kit
|
||||
|
||||
public void SetVolume(float normalizedVolume)
|
||||
{
|
||||
// 将 0~1 映射到 minDecibels ~ maxDecibels,再转换为线性幅度
|
||||
// 使用对数曲线匹配人耳听觉特性
|
||||
float db = Mathf.Lerp(minDecibels, maxDecibels, normalizedVolume);
|
||||
float linearVolume = Mathf.Pow(10f, db / 20f);
|
||||
normalizedVolume = Mathf.Clamp01(normalizedVolume);
|
||||
|
||||
var masterVca = RuntimeManager.GetBus("bus:/");
|
||||
masterVca.setVolume(linearVolume);
|
||||
// 最低档需要真正静音;有限的最小分贝值仍会保留可听的线性幅度。
|
||||
float linearVolume = 0f;
|
||||
if (normalizedVolume > 0f)
|
||||
{
|
||||
// 将非零音量映射到 minDecibels ~ maxDecibels,保留原有的对数听感曲线。
|
||||
float db = Mathf.Lerp(minDecibels, maxDecibels, normalizedVolume);
|
||||
linearVolume = Mathf.Pow(10f, db / 20f);
|
||||
}
|
||||
|
||||
var masterBus = RuntimeManager.GetBus("bus:/");
|
||||
masterBus.setVolume(linearVolume);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -95,12 +95,13 @@ namespace AibisDream
|
||||
|
||||
public override IEnumerator ChangeStateAsync(string stateName)
|
||||
{
|
||||
if (!TryPlay(stateName, false, out var handle))
|
||||
if (!TryPlayAsync(stateName, out var awaitable))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return _player.WaitForFirstPass(handle);
|
||||
yield return awaitable;
|
||||
var handle = awaitable.PlaybackHandle;
|
||||
if (handle.IsCompleted)
|
||||
{
|
||||
LogFailedResult(stateName, handle.Result);
|
||||
@@ -141,6 +142,50 @@ namespace AibisDream
|
||||
out FrameAnimationPlaybackHandle handle)
|
||||
{
|
||||
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)
|
||||
{
|
||||
Debug.LogError(
|
||||
@@ -157,17 +202,6 @@ namespace AibisDream
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ MonoBehaviour:
|
||||
m_EditorClassIdentifier:
|
||||
useHdrColorPickers: 0
|
||||
autoConfigureRenderPipeline: 1
|
||||
useImmediateModeInstancing: 0
|
||||
useImmediateModeInstancing: 1
|
||||
polylineDefaultPointsPerTurn: 64
|
||||
polylineBezierAngularSumAccuracy: 2
|
||||
pushPopStateInDrawCommands: 1
|
||||
@@ -37,6 +37,6 @@ MonoBehaviour:
|
||||
cylinderDivs: 080000000c000000200000004000000080000000
|
||||
capsuleDivs: 0200000003000000080000000400000005000000
|
||||
FRAG_OUTPUT_V4: 1
|
||||
LOCAL_ANTI_ALIASING_QUALITY: 2
|
||||
LOCAL_ANTI_ALIASING_QUALITY: 1
|
||||
QUAD_INTERPOLATION_QUALITY: 1
|
||||
NOOTS_ACROSS_SCREEN: 100
|
||||
|
||||
@@ -36,7 +36,8 @@ namespace AibisDream.FrameAnimation.Tests.EditMode
|
||||
"Flow",
|
||||
"Flow",
|
||||
node.InternalId,
|
||||
FrameClipEndBehavior.Clear);
|
||||
FrameClipEndBehavior.Clear,
|
||||
AnimationFlowAsyncCompletionMode.WaitForTerminalLoopFirstCycle);
|
||||
var graph = CreateGraph(
|
||||
new[] { clip },
|
||||
new[] { node },
|
||||
@@ -55,6 +56,9 @@ namespace AibisDream.FrameAnimation.Tests.EditMode
|
||||
Assert.That(
|
||||
clone.Flows[0].EndBehaviorOverride,
|
||||
Is.EqualTo(FrameClipEndBehavior.Clear));
|
||||
Assert.That(
|
||||
clone.Flows[0].AsyncCompletionMode,
|
||||
Is.EqualTo(AnimationFlowAsyncCompletionMode.WaitForTerminalLoopFirstCycle));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -126,6 +130,107 @@ namespace AibisDream.FrameAnimation.Tests.EditMode
|
||||
Assert.That(session.CurrentClipId, Is.EqualTo(idle.Id));
|
||||
Assert.That(session.CurrentNodeId, Is.EqualTo(idleNode.InternalId));
|
||||
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]
|
||||
|
||||
@@ -112,6 +112,151 @@ namespace AibisDream.FrameAnimation.Tests.PlayMode
|
||||
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]
|
||||
public void RestoreTerminalState_RebuildsSavedVisualState()
|
||||
{
|
||||
@@ -154,16 +299,60 @@ namespace AibisDream.FrameAnimation.Tests.PlayMode
|
||||
gameObject.AddComponent<SpriteRenderer>();
|
||||
var player = gameObject.AddComponent<FrameAnimationPlayer>();
|
||||
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;
|
||||
yield return null;
|
||||
|
||||
Assert.That(handle.Result.Reason,
|
||||
Is.EqualTo(FrameAnimationCompletionReason.Stopped));
|
||||
Assert.That(awaitable.keepWaiting, Is.False);
|
||||
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(
|
||||
string id,
|
||||
FrameClipEndBehavior endBehavior,
|
||||
@@ -186,15 +375,30 @@ namespace AibisDream.FrameAnimation.Tests.PlayMode
|
||||
|
||||
private FrameAnimationGraph CreateGraph(params FrameClip[] clips)
|
||||
{
|
||||
var graph = Track(ScriptableObject.CreateInstance<FrameAnimationGraph>());
|
||||
graph.Configure(
|
||||
"Graph",
|
||||
"Graph",
|
||||
return CreateGraph(
|
||||
clips,
|
||||
Array.Empty<AnimationNode>(),
|
||||
Array.Empty<AnimationEdge>(),
|
||||
Array.Empty<AnimationFlow>(),
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -91,9 +91,7 @@ peipei: 这次也是一样,<waitfor=0.3>我很努力了…<br>但<b>UF</b>画
|
||||
me: <b><color=\#FFB200>UF</color></b>…<waitfor=0.5>是什么<waitfor=0.1>绘图工具<waitfor=0.2>吗?<br><waitfor=0.5>画笔? #line:088f98f
|
||||
|
||||
<<hide_dialog>>
|
||||
<<wait 1.6>>
|
||||
<<change_actor_state_async D1S1_1a_Flow>>
|
||||
<<wait 0.5>>
|
||||
peipei: 啊对不起,<waitfor=0.1>医生…<waitfor=0.2><br>是我<waitfor=0.1>没有说清楚。 #line:0bea9ac
|
||||
peipei: 这是一个内置的<waitfor=0.1>绘图模块。<waitfor=0.3><br>…你看,<waitfor=0.2>我的眼睛。 #line:007d716
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ tags: event
|
||||
colorID: 8
|
||||
position: 480,-2840
|
||||
---
|
||||
<<if $gameStage=="手术" && $isPeipeiBE==false>>
|
||||
<<if $gameStage=="手术" && $global_isPeipeiBE==false>>
|
||||
<<jump 进行色彩回复后插入视觉>>
|
||||
<<endif>>
|
||||
<<jump 检查视觉>>
|
||||
|
||||
@@ -186,7 +186,7 @@ ChipModule: 已与附加模块建立连接! #line:0e44178
|
||||
<<wait 1.5>>
|
||||
<<endif>>
|
||||
<<hide_dialog>>
|
||||
<<if $gameStage == "手术"&&$isPeipeiBE==false>>
|
||||
<<if $gameStage == "手术"&&$global_isPeipeiBE==false>>
|
||||
Analysing: 检测到未识别的色彩晶圆…请进行校准。 #line:0d4afa0
|
||||
<<else>>
|
||||
<<play_sfx "event:/Scriptal/beep_warning">>
|
||||
|
||||
@@ -25,13 +25,13 @@ Graph 必须通过 `FrameAnimationGraphValidator` 校验。帧动画角色不会
|
||||
<<change_actor_state Idle 测试角色>>
|
||||
```
|
||||
|
||||
等待一次性动画播放完成,或等待 Loop playable 完成首轮:
|
||||
等待一次性动画播放完成,或按 Flow 配置等待 Loop:
|
||||
|
||||
```yarn
|
||||
<<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 displayName;
|
||||
string entryNodeId;
|
||||
AnimationFlowAsyncCompletionMode asyncCompletionMode;
|
||||
FrameClipEndBehavior? endBehaviorOverride;
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `entryNodeId` 保存入口 AnimationNode 的 `internalId`。
|
||||
- `asyncCompletionMode` 只控制终点为 Loop 时的 `PlayAsync` 等待边界:进入终点并显示首帧,或等待终点首轮回绕;零值默认采用前者。
|
||||
- `endBehaviorOverride` 是 Flow 到达终点时的可选结束行为覆盖。
|
||||
- Flow 包含的节点集合从 `entryNodeId` 沿全局边关系遍历得到,不单独保存 `includedNodeIds`。
|
||||
- 编辑器高亮、Flow 聚焦、校验和运行时使用同一套可达关系,避免人工维护的节点集合与实际连线不一致。
|
||||
- 多个 Flow 的可达范围可以重叠,因此可以自然共享 Idle 等公共节点。
|
||||
- 同一个 AnimationNode 不能作为多个 Flow 的入口,但不同 Flow 可以在后续路径中共享该节点。
|
||||
- Flow id 属于统一可播放命名空间,不能与任何 Clip 或其他 Flow id 重名。
|
||||
- 终点为非 Loop 时忽略 `asyncCompletionMode`,异步播放始终等待 Flow 自然结束。
|
||||
|
||||
#### 4.3.2 AnimationNode 数据
|
||||
|
||||
@@ -881,6 +884,7 @@ Flow 创建与入口管理:
|
||||
- Flow 只能通过“选中一个 Clip 节点 -> 从选中节点创建 Flow”建立。
|
||||
- 创建时必须且只能选中一个节点,编辑器自动将该节点写入 `entryNodeId`。
|
||||
- Flow id 默认取节点名称,但创建前允许修改;它必须与所有 Clip / Flow playable id 唯一。
|
||||
- Flow 属性区提供 `Async Completion` 配置,并说明它只影响终点为 Loop 的异步播放。
|
||||
- 同一个节点不能作为多个 Flow 的入口;节点已是入口时禁用创建命令并提供定位现有 Flow 的操作。
|
||||
- Flow 入口允许存在前驱节点;从该 Flow 播放时直接从入口开始,入口之前的节点不属于其可达范围。
|
||||
- 修改入口时,先选中目标节点并执行“设置为当前 Flow 入口”;若目标已是其他 Flow 入口则阻止修改。
|
||||
@@ -1215,6 +1219,15 @@ FrameAnimationPlaybackHandle : CustomYieldInstruction
|
||||
void RegisterCompleted(Action<FrameAnimationPlaybackResult> callback);
|
||||
}
|
||||
|
||||
FrameAnimationPlaybackAwaitable : CustomYieldInstruction
|
||||
{
|
||||
FrameAnimationPlaybackHandle PlaybackHandle { get; }
|
||||
}
|
||||
|
||||
FrameAnimationPlaybackAwaitable PlayAsync();
|
||||
FrameAnimationPlaybackAwaitable PlayAsync(string playableId);
|
||||
FrameAnimationPlaybackAwaitable PlayAsync(string playableId, FrameAnimationPlayOptions options);
|
||||
|
||||
FrameAnimationPlaybackResult
|
||||
{
|
||||
long requestId;
|
||||
@@ -1254,6 +1267,9 @@ FrameAnimationCompletionReason
|
||||
要求:
|
||||
|
||||
- Handle 可以直接用于 `yield return handle`。
|
||||
- `PlayAsync()` 调用时立即开始播放,返回值可直接用于 `yield return`;其 `PlaybackHandle` 仍表示底层播放请求的完整生命周期。
|
||||
- 非循环 playable 的异步等待随播放自然结束。直接 Loop Clip 固定等待第一轮;终点为 Loop 的 Flow 按 `AnimationFlow.AsyncCompletionMode` 在终点 Loop 首帧显示后或第一轮回绕后解除等待。
|
||||
- Awaitable 解除等待不等于底层 Loop 播放完成;Loop 的 `PlaybackHandle` 继续保持未完成,直到被替换、停止、禁用或销毁。
|
||||
- `WaitAsync()` 使用标准 `Task`,项目第一版不引入 UniTask 依赖。
|
||||
- `RegisterCompleted()` 是正式完成通知接口。请求尚未完成时登记回调;请求已经完成时立即用既有结果调用,避免同步失败或极短动画造成通知丢失。
|
||||
- 同一个播放请求只能从未完成状态结算一次;协程、Task 和回调必须观察到同一个结果。
|
||||
|
||||
+16
-6
@@ -86,6 +86,15 @@ Flow ID 就是 Yarn 中填写的动画名。建议使用有明确含义且不易
|
||||
3. 点击顶部 **Validate**,底部 **Validation** 中不能有 Error。
|
||||
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 调用
|
||||
|
||||
### 初始化角色
|
||||
@@ -163,17 +172,17 @@ hs: 戴上“实实”牌帽子,给你的头顶添件宝!
|
||||
|
||||
Yarn 会立即继续对白,Flow 则在后台按照连线依次播放。适合整段表演与对白同时发生的情况。
|
||||
|
||||
#### Flow:播放到终点首轮后再继续 Yarn
|
||||
#### Flow:按 Flow 配置等待后再继续 Yarn
|
||||
|
||||
```yarn
|
||||
<<change_actor_state_async 摘帽到伸手_Flow 火山>>
|
||||
// Flow 的前置动作和终点 Loop 首轮播放完后,才执行这里
|
||||
// 默认在前置动作结束、终点 Loop 首帧显示后执行这里
|
||||
```
|
||||
|
||||
`change_actor_state_async` 会从入口开始等待整条 Flow:
|
||||
|
||||
- 终点为非循环 Clip:等待所有节点自然播放结束;
|
||||
- `前置动作 → 终点 Loop` 的 Flow:等待前置动作和终点 Loop 的第一轮全部结束。
|
||||
- `前置动作 → 终点 Loop` 的 Flow:根据 **Async Completion**,在终点 Loop 开始时完成,或等待其第一轮结束。
|
||||
|
||||
如果终点是 `Loop`,命令返回后终点 Clip 仍会继续循环,直到被下一次状态切换替换。
|
||||
|
||||
@@ -184,7 +193,7 @@ Yarn 会立即继续对白,Flow 则在后台按照连线依次播放。适合
|
||||
| 单个 Clip | 不等待 | `<<change_actor_state ClipID 角色名>>` |
|
||||
| 单个 Clip | 等完整动画;Loop 等第一轮 | `<<change_actor_state_async ClipID 角色名>>` |
|
||||
| 一整条 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 不能重名。
|
||||
|
||||
@@ -212,7 +221,7 @@ Clip ID、Flow ID、角色名或槽位名中包含空格时,必须加英文双
|
||||
|
||||
hs: 医——生——救——我——!
|
||||
|
||||
// 调用 Flow:等“摘帽”播完,再等终点“伸手表情idle”完成第一轮
|
||||
// 调用 Flow:默认等“摘帽”播完,并显示终点“伸手表情idle”首帧
|
||||
<<change_actor_state_async 摘帽到伸手_Flow 火山>>
|
||||
|
||||
// Flow 返回后,终点的伸手 idle 仍在循环
|
||||
@@ -233,6 +242,7 @@ hs: 我……没有活干了。
|
||||
- 只有最后一个 Node 设置了结束行为吗?
|
||||
- 需要持续显示的 idle 是否设置为 `Loop`?
|
||||
- Flow ID 是否与已有 Clip / Flow 重名?
|
||||
- Flow 的 **Async Completion** 是否符合剧情节奏?
|
||||
- Yarn 中的 ID 与 Graph 完全一致,包括空格和大小写吗?
|
||||
- 需要等动画时是否用了 `change_actor_state_async`?
|
||||
- 顶部 **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 中途停住**
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ PlayerSettings:
|
||||
vulkanEnableLateAcquireNextImage: 0
|
||||
vulkanEnableCommandBufferRecycling: 1
|
||||
loadStoreDebugModeEnabled: 0
|
||||
bundleVersion: 0.6.2.3
|
||||
bundleVersion: 0.6.2.6
|
||||
preloadedAssets:
|
||||
- {fileID: 11400000, guid: 0fb2bc2476953fc43a74f0ab8879077c, type: 2}
|
||||
metroInputSource: 0
|
||||
|
||||
Reference in New Issue
Block a user