fix: 火山表情修复

This commit is contained in:
2026-07-19 23:43:53 +08:00
parent a6d541383b
commit f444978717
8 changed files with 275 additions and 9 deletions
@@ -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
+1 -1
View File
@@ -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()
{
+2 -3
View File
@@ -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
+2 -2
View File
@@ -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
## 存档行为