fix: 火山表情修复
This commit is contained in:
@@ -290,7 +290,7 @@ MonoBehaviour:
|
||||
frameName: "\u706B\u5C71\u50CF\u7D20\u7248 29.aseprite"
|
||||
sourceIndex: 29
|
||||
speed: 1
|
||||
defaultEndBehavior: 1
|
||||
defaultEndBehavior: 0
|
||||
hasImportInfo: 1
|
||||
importInfo:
|
||||
importSourceId: d329feaf92a945f885e419c05824cd56
|
||||
|
||||
@@ -8477,13 +8477,13 @@ RectTransform:
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 834154072}
|
||||
- {fileID: 3100101002}
|
||||
- {fileID: 300534481}
|
||||
- {fileID: 2135943277}
|
||||
- {fileID: 1546742660}
|
||||
- {fileID: 923998313}
|
||||
- {fileID: 1867622714}
|
||||
- {fileID: 3100100002}
|
||||
- {fileID: 3100101002}
|
||||
m_Father: {fileID: 1110722291}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
|
||||
@@ -88,12 +88,14 @@ namespace AibisDream.FrameAnimation
|
||||
private double frameElapsedSeconds;
|
||||
private bool hasStarted;
|
||||
private bool hasCompleted;
|
||||
private bool hasCompletedFirstTerminalLoopCycle;
|
||||
|
||||
public string PlayableId => plan.PlayableId;
|
||||
public string CurrentClipId => CurrentStep?.Clip != null ? CurrentStep.Clip.Id : string.Empty;
|
||||
public string CurrentNodeId => CurrentStep?.NodeId ?? string.Empty;
|
||||
public int CurrentFrameIndex => hasStarted && !hasCompleted ? frameIndex : -1;
|
||||
public FrameClip CurrentClip => CurrentStep?.Clip;
|
||||
internal bool HasCompletedFirstTerminalLoopCycle => hasCompletedFirstTerminalLoopCycle;
|
||||
internal FrameAnimationPlaybackSnapshot Snapshot =>
|
||||
new FrameAnimationPlaybackSnapshot(
|
||||
hasStarted,
|
||||
@@ -129,6 +131,7 @@ namespace AibisDream.FrameAnimation
|
||||
{
|
||||
hasStarted = true;
|
||||
hasCompleted = false;
|
||||
hasCompletedFirstTerminalLoopCycle = false;
|
||||
stepIndex = 0;
|
||||
frameIndex = 0;
|
||||
frameElapsedSeconds = 0d;
|
||||
@@ -186,6 +189,7 @@ namespace AibisDream.FrameAnimation
|
||||
var cycleRealDuration = GetClipDurationSeconds(step.Clip) / effectiveSpeed;
|
||||
if (cycleRealDuration > Epsilon && remainingRealSeconds >= cycleRealDuration)
|
||||
{
|
||||
hasCompletedFirstTerminalLoopCycle = true;
|
||||
remainingRealSeconds %= cycleRealDuration;
|
||||
if (remainingRealSeconds <= Epsilon)
|
||||
{
|
||||
@@ -247,6 +251,7 @@ namespace AibisDream.FrameAnimation
|
||||
|
||||
if (step.TerminalEndBehavior == FrameClipEndBehavior.Loop)
|
||||
{
|
||||
hasCompletedFirstTerminalLoopCycle = true;
|
||||
frameIndex = 0;
|
||||
ApplyCurrentFrame(applySprite);
|
||||
return FrameAnimationSessionEvaluation.Running;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
@@ -136,6 +137,20 @@ namespace AibisDream.FrameAnimation
|
||||
return PlayInternal(playableId, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 等待当前播放请求完成首轮。非循环请求等待自然结束;终点为 Loop 的请求在
|
||||
/// 第一次回绕后结束等待,但播放请求本身保持活动并继续循环。
|
||||
/// </summary>
|
||||
public CustomYieldInstruction WaitForFirstPass(FrameAnimationPlaybackHandle playbackHandle)
|
||||
{
|
||||
if (playbackHandle == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(playbackHandle));
|
||||
}
|
||||
|
||||
return new FirstPassYieldInstruction(this, playbackHandle);
|
||||
}
|
||||
|
||||
public bool TryBindGraph(
|
||||
FrameAnimationGraph value,
|
||||
out FrameAnimationPlaybackError error)
|
||||
@@ -499,6 +514,33 @@ namespace AibisDream.FrameAnimation
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShouldWaitForFirstPass(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);
|
||||
}
|
||||
|
||||
private void ApplyEndBehavior(FrameClipEndBehavior endBehavior)
|
||||
{
|
||||
switch (endBehavior)
|
||||
|
||||
@@ -100,8 +100,11 @@ namespace AibisDream
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return handle;
|
||||
LogFailedResult(stateName, handle.Result);
|
||||
yield return _player.WaitForFirstPass(handle);
|
||||
if (handle.IsCompleted)
|
||||
{
|
||||
LogFailedResult(stateName, handle.Result);
|
||||
}
|
||||
}
|
||||
|
||||
public override ActorEntrySnapshotDto CaptureEntry()
|
||||
|
||||
@@ -114,6 +114,223 @@ namespace AibisDream.FrameAnimation.Tests.PlayMode
|
||||
Assert.That(player.CurrentPlayableId, Is.EqualTo(second.Id));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WaitForFirstPass_DirectLoopReleasesAfterFirstWrapAndKeepsPlaying()
|
||||
{
|
||||
var first = CreateSprite(Color.red);
|
||||
var second = CreateSprite(Color.blue);
|
||||
var clip = CreateClip("Loop", FrameClipEndBehavior.Loop, first, second);
|
||||
var graph = CreateDirectGraph(clip);
|
||||
var gameObject = Track(new GameObject("Wait Direct Loop"));
|
||||
var renderer = gameObject.AddComponent<SpriteRenderer>();
|
||||
var player = gameObject.AddComponent<FrameAnimationPlayer>();
|
||||
player.ConfigureForAuthoring(graph, false, 1f);
|
||||
|
||||
var handle = player.Play(clip.Id);
|
||||
var wait = player.WaitForFirstPass(handle);
|
||||
|
||||
Assert.That(wait.keepWaiting, Is.True);
|
||||
player.EvaluateForTests(0.1d);
|
||||
Assert.That(wait.keepWaiting, Is.True);
|
||||
|
||||
player.EvaluateForTests(0.1d);
|
||||
|
||||
Assert.That(wait.keepWaiting, Is.False);
|
||||
Assert.That(handle.IsCompleted, Is.False);
|
||||
Assert.That(player.State, Is.EqualTo(FrameAnimationPlaybackState.Playing));
|
||||
Assert.That(player.CurrentFrameIndex, Is.EqualTo(0));
|
||||
Assert.That(renderer.sprite, Is.SameAs(first));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WaitForFirstPass_FlowWaitsForIntroAndOneTerminalLoopCycle()
|
||||
{
|
||||
var introClip = CreateClip(
|
||||
"Intro",
|
||||
FrameClipEndBehavior.HoldLastFrame,
|
||||
CreateSprite(Color.red));
|
||||
var loopFirst = CreateSprite(Color.green);
|
||||
var loopClip = CreateClip(
|
||||
"Loop",
|
||||
FrameClipEndBehavior.Loop,
|
||||
loopFirst,
|
||||
CreateSprite(Color.blue));
|
||||
var introNode = new AnimationNode(introClip.Id);
|
||||
var loopNode = new AnimationNode(loopClip.Id);
|
||||
var edge = new AnimationEdge(introNode.InternalId, loopNode.InternalId);
|
||||
var flow = new AnimationFlow("Flow", "Flow", introNode.InternalId);
|
||||
var graph = Track(ScriptableObject.CreateInstance<FrameAnimationGraph>());
|
||||
graph.Configure(
|
||||
"Graph",
|
||||
"Graph",
|
||||
new[] { introClip, loopClip },
|
||||
new[] { introNode, loopNode },
|
||||
new[] { edge },
|
||||
new[] { flow },
|
||||
flow.Id);
|
||||
var gameObject = Track(new GameObject("Wait Flow Loop"));
|
||||
var renderer = gameObject.AddComponent<SpriteRenderer>();
|
||||
var player = gameObject.AddComponent<FrameAnimationPlayer>();
|
||||
player.ConfigureForAuthoring(graph, false, 1f);
|
||||
|
||||
var handle = player.Play(flow.Id);
|
||||
var wait = player.WaitForFirstPass(handle);
|
||||
|
||||
player.EvaluateForTests(0.1d);
|
||||
Assert.That(player.CurrentClipId, Is.EqualTo(loopClip.Id));
|
||||
Assert.That(wait.keepWaiting, Is.True);
|
||||
|
||||
player.EvaluateForTests(0.2d);
|
||||
|
||||
Assert.That(wait.keepWaiting, Is.False);
|
||||
Assert.That(handle.IsCompleted, Is.False);
|
||||
Assert.That(player.State, Is.EqualTo(FrameAnimationPlaybackState.Playing));
|
||||
Assert.That(player.CurrentFrameIndex, Is.EqualTo(0));
|
||||
Assert.That(renderer.sprite, Is.SameAs(loopFirst));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WaitForFirstPass_LargeDeltaCrossesIntroAndTerminalLoopCycle()
|
||||
{
|
||||
var introClip = CreateClip(
|
||||
"Intro",
|
||||
FrameClipEndBehavior.HoldLastFrame,
|
||||
CreateSprite(Color.red));
|
||||
var loopClip = CreateClip(
|
||||
"Loop",
|
||||
FrameClipEndBehavior.Loop,
|
||||
CreateSprite(Color.green),
|
||||
CreateSprite(Color.blue));
|
||||
var introNode = new AnimationNode(introClip.Id);
|
||||
var loopNode = new AnimationNode(loopClip.Id);
|
||||
var graph = Track(ScriptableObject.CreateInstance<FrameAnimationGraph>());
|
||||
graph.Configure(
|
||||
"Graph",
|
||||
"Graph",
|
||||
new[] { introClip, loopClip },
|
||||
new[] { introNode, loopNode },
|
||||
new[] { new AnimationEdge(introNode.InternalId, loopNode.InternalId) },
|
||||
new[] { new AnimationFlow("Flow", "Flow", introNode.InternalId) },
|
||||
"Flow");
|
||||
var gameObject = Track(new GameObject("Wait Large Delta"));
|
||||
gameObject.AddComponent<SpriteRenderer>();
|
||||
var player = gameObject.AddComponent<FrameAnimationPlayer>();
|
||||
player.ConfigureForAuthoring(graph, false, 1f);
|
||||
|
||||
var handle = player.Play("Flow");
|
||||
var wait = player.WaitForFirstPass(handle);
|
||||
player.EvaluateForTests(1.35d);
|
||||
|
||||
Assert.That(wait.keepWaiting, Is.False);
|
||||
Assert.That(handle.IsCompleted, Is.False);
|
||||
Assert.That(player.State, Is.EqualTo(FrameAnimationPlaybackState.Playing));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WaitForFirstPass_NonLoopMirrorsNaturalCompletion()
|
||||
{
|
||||
var clip = CreateClip(
|
||||
"Once",
|
||||
FrameClipEndBehavior.HoldLastFrame,
|
||||
CreateSprite(Color.red),
|
||||
CreateSprite(Color.blue));
|
||||
var graph = CreateDirectGraph(clip);
|
||||
var gameObject = Track(new GameObject("Wait Once"));
|
||||
gameObject.AddComponent<SpriteRenderer>();
|
||||
var player = gameObject.AddComponent<FrameAnimationPlayer>();
|
||||
player.ConfigureForAuthoring(graph, false, 1f);
|
||||
|
||||
var handle = player.Play(clip.Id);
|
||||
var wait = player.WaitForFirstPass(handle);
|
||||
player.EvaluateForTests(0.2d);
|
||||
|
||||
Assert.That(wait.keepWaiting, Is.False);
|
||||
Assert.That(handle.Result.Reason, Is.EqualTo(FrameAnimationCompletionReason.Completed));
|
||||
Assert.That(player.State, Is.EqualTo(FrameAnimationPlaybackState.Stopped));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WaitForFirstPass_ReleasesWhenRequestIsReplacedOrStopped()
|
||||
{
|
||||
var first = CreateClip("First", FrameClipEndBehavior.Loop, CreateSprite(Color.red));
|
||||
var second = CreateClip("Second", FrameClipEndBehavior.Loop, CreateSprite(Color.blue));
|
||||
var graph = Track(ScriptableObject.CreateInstance<FrameAnimationGraph>());
|
||||
graph.Configure(
|
||||
"Graph",
|
||||
"Graph",
|
||||
new[] { first, second },
|
||||
Array.Empty<AnimationNode>(),
|
||||
Array.Empty<AnimationEdge>(),
|
||||
Array.Empty<AnimationFlow>(),
|
||||
first.Id);
|
||||
var gameObject = Track(new GameObject("Wait Termination"));
|
||||
gameObject.AddComponent<SpriteRenderer>();
|
||||
var player = gameObject.AddComponent<FrameAnimationPlayer>();
|
||||
player.ConfigureForAuthoring(graph, false, 1f);
|
||||
|
||||
var replacedHandle = player.Play(first.Id);
|
||||
var replacedWait = player.WaitForFirstPass(replacedHandle);
|
||||
var stoppedHandle = player.Play(second.Id);
|
||||
var stoppedWait = player.WaitForFirstPass(stoppedHandle);
|
||||
|
||||
Assert.That(replacedWait.keepWaiting, Is.False);
|
||||
Assert.That(replacedHandle.Result.Reason, Is.EqualTo(FrameAnimationCompletionReason.Replaced));
|
||||
Assert.That(stoppedWait.keepWaiting, Is.True);
|
||||
|
||||
player.Stop();
|
||||
|
||||
Assert.That(stoppedWait.keepWaiting, Is.False);
|
||||
Assert.That(stoppedHandle.Result.Reason, Is.EqualTo(FrameAnimationCompletionReason.Stopped));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WaitForFirstPass_PauseAndZeroSpeedDoNotReleaseEarly()
|
||||
{
|
||||
var clip = CreateClip(
|
||||
"Loop",
|
||||
FrameClipEndBehavior.Loop,
|
||||
CreateSprite(Color.red),
|
||||
CreateSprite(Color.blue));
|
||||
var graph = CreateDirectGraph(clip);
|
||||
var gameObject = Track(new GameObject("Wait Paused"));
|
||||
gameObject.AddComponent<SpriteRenderer>();
|
||||
var player = gameObject.AddComponent<FrameAnimationPlayer>();
|
||||
player.ConfigureForAuthoring(graph, false, 0f);
|
||||
|
||||
var handle = player.Play(clip.Id);
|
||||
var wait = player.WaitForFirstPass(handle);
|
||||
player.EvaluateForTests(1d);
|
||||
Assert.That(wait.keepWaiting, Is.True);
|
||||
|
||||
player.SetSpeed(1f);
|
||||
player.Pause();
|
||||
player.EvaluateForTests(1d);
|
||||
Assert.That(wait.keepWaiting, Is.True);
|
||||
|
||||
player.Resume();
|
||||
player.EvaluateForTests(0.2d);
|
||||
Assert.That(wait.keepWaiting, Is.False);
|
||||
Assert.That(handle.IsCompleted, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WaitForFirstPass_FailedRequestDoesNotWait()
|
||||
{
|
||||
var clip = CreateClip("Loop", FrameClipEndBehavior.Loop, CreateSprite(Color.white));
|
||||
var graph = CreateDirectGraph(clip);
|
||||
var gameObject = Track(new GameObject("Wait Failed"));
|
||||
gameObject.AddComponent<SpriteRenderer>();
|
||||
var player = gameObject.AddComponent<FrameAnimationPlayer>();
|
||||
player.ConfigureForAuthoring(graph, false, 1f);
|
||||
|
||||
var handle = player.Play("Missing");
|
||||
var wait = player.WaitForFirstPass(handle);
|
||||
|
||||
Assert.That(wait.keepWaiting, Is.False);
|
||||
Assert.That(handle.Result.Reason, Is.EqualTo(FrameAnimationCompletionReason.Failed));
|
||||
Assert.That(handle.Result.Error.Code, Is.EqualTo(FrameAnimationPlaybackErrorCode.PlayableNotFound));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FailedTargetProbeDoesNotDiscardCurrentTargetOrRequest()
|
||||
{
|
||||
|
||||
@@ -125,8 +125,7 @@ hs: 嘿嘿… #line:066c9f4
|
||||
hs: 真的不想打扰你…可是可惜…我可能等不到晚上了。 #line:005bb59
|
||||
|
||||
me: 怎…怎么了?年审的原因吗? #line:092cb78
|
||||
<<change_actor_state "扣头切屏特效" 火山>>
|
||||
<<wait 1>>
|
||||
<<change_actor_state_async "扣头切屏特效" 火山>>
|
||||
<<change_actor_state "扣头表情idle" 火山>>
|
||||
//俩人一时都没说话。 #line:0755544
|
||||
<<wait 2>>
|
||||
@@ -177,7 +176,7 @@ me: 你能哪怕一秒钟不要再说这些笑话了么…你今天是怎么回
|
||||
<<wait 1>>
|
||||
// 尴尬的笑声
|
||||
<<wait 1.5>>
|
||||
<<change_actor_state "扣头切屏特效" 火山>>
|
||||
<<change_actor_state_async "扣头切屏特效" 火山>>
|
||||
<<change_actor_state "扣头表情idle" 火山>>
|
||||
<<wait 1>>
|
||||
hs: 啊…哈哈哈。冷笑话已经不时*帽*了吗。看来我确实真是土老 *帽* 了。 #line:0aacf26
|
||||
|
||||
@@ -25,13 +25,13 @@ Graph 必须通过 `FrameAnimationGraphValidator` 校验。帧动画角色不会
|
||||
<<change_actor_state Idle 测试角色>>
|
||||
```
|
||||
|
||||
等待一次性动画播放完成:
|
||||
等待一次性动画播放完成,或等待 Loop playable 完成首轮:
|
||||
|
||||
```yarn
|
||||
<<change_actor_state_async Intro 测试角色>>
|
||||
```
|
||||
|
||||
`init_actor` 会等待 Prefab 和 Graph 加载完成,因此紧随其后的状态切换可以安全执行。Loop playable 的 Handle 不会自然完成,Loop 必须使用 `change_actor_state`,不要使用异步命令。
|
||||
`init_actor` 会等待 Prefab 和 Graph 加载完成,因此紧随其后的状态切换可以安全执行。`change_actor_state_async` 播放 Loop playable 时,会在直接 Loop Clip 完成第一轮后继续执行 Yarn;对于“前置节点 → 终点 Loop”的 Flow,则等待前置节点和终点 Loop 第一轮全部完成。命令返回后动画仍会继续循环,直到被新状态替换或主动停止。无需等待首轮时仍可使用 `change_actor_state` 立即继续 Yarn。
|
||||
|
||||
## 存档行为
|
||||
|
||||
|
||||
Reference in New Issue
Block a user