using System;
using System.Collections;
using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
namespace AibisDream.FrameAnimation.Tests.PlayMode
{
///
/// 帧动画组件的最小运行时集成测试。
/// 时间推进细节由 EditMode 的 PlaybackSession 契约覆盖。
///
public sealed class FrameAnimationPlayerTests
{
private readonly List createdObjects =
new List();
[TearDown]
public void TearDown()
{
for (var index = createdObjects.Count - 1; index >= 0; index--)
{
if (createdObjects[index] != null)
UnityEngine.Object.DestroyImmediate(createdObjects[index]);
}
createdObjects.Clear();
}
[Test]
public void GraphPlayer_RendersAndAppliesTerminalBehavior()
{
var first = CreateSprite(Color.red);
var second = CreateSprite(Color.blue);
var clip = CreateClip(
"Clip",
FrameClipEndBehavior.Clear,
first,
second);
var graph = CreateGraph(clip);
var gameObject = Track(new GameObject("Graph Player"));
var renderer = gameObject.AddComponent();
var player = gameObject.AddComponent();
player.ConfigureForAuthoring(graph, false, 1f);
var handle = player.Play(clip.Id);
Assert.That(renderer.sprite, Is.SameAs(first));
player.EvaluateForTests(0.2d);
Assert.That(handle.Result.Reason,
Is.EqualTo(FrameAnimationCompletionReason.Completed));
Assert.That(renderer.sprite, Is.Null);
Assert.That(renderer.enabled, Is.True);
}
[Test]
public void StandalonePlayer_PlaysConfiguredClipWithoutGraph()
{
var first = CreateSprite(Color.magenta);
var second = CreateSprite(Color.cyan);
var clip = CreateClip(
"Standalone",
FrameClipEndBehavior.HoldLastFrame,
first,
second);
var gameObject = Track(new GameObject("Standalone Player"));
var renderer = gameObject.AddComponent();
var player = gameObject.AddComponent();
player.ConfigureForAuthoring(clip, false, 1f);
var handle = player.Play();
Assert.That(player.CurrentClip, Is.SameAs(clip));
player.EvaluateForTests(0.2d);
Assert.That(handle.Result.Reason,
Is.EqualTo(FrameAnimationCompletionReason.Completed));
Assert.That(renderer.sprite, Is.SameAs(second));
}
[Test]
public void PlayRequest_FailureKeepsActiveAndSuccessReplacesIt()
{
var first = CreateClip(
"First",
FrameClipEndBehavior.Loop,
CreateSprite(Color.white));
var second = CreateClip(
"Second",
FrameClipEndBehavior.Loop,
CreateSprite(Color.black));
var graph = CreateGraph(first, second);
var gameObject = Track(new GameObject("Request Arbitration"));
gameObject.AddComponent();
var player = gameObject.AddComponent();
player.ConfigureForAuthoring(graph, false, 1f);
var active = player.Play(first.Id);
var failed = player.Play("Missing");
Assert.That(failed.Result.Error.Code,
Is.EqualTo(FrameAnimationPlaybackErrorCode.PlayableNotFound));
Assert.That(active.IsCompleted, Is.False);
Assert.That(player.CurrentPlayableId, Is.EqualTo(first.Id));
var replacement = player.Play(second.Id);
Assert.That(active.Result.Reason,
Is.EqualTo(FrameAnimationCompletionReason.Replaced));
Assert.That(replacement.IsCompleted, Is.False);
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();
var completeOnStartPlayer = completeOnStartObject.AddComponent();
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();
var waitForCyclePlayer = waitForCycleObject.AddComponent();
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(),
new[] { flow },
flow.Id);
var gameObject = Track(new GameObject("Single Loop Flow"));
var renderer = gameObject.AddComponent();
var player = gameObject.AddComponent();
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();
var player = gameObject.AddComponent();
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();
var player = gameObject.AddComponent();
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();
var player = gameObject.AddComponent();
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()
{
var terminalSprite = CreateSprite(Color.green);
var hold = CreateClip(
"Hold",
FrameClipEndBehavior.HoldLastFrame,
CreateSprite(Color.red),
terminalSprite);
var hidden = CreateClip(
"Hidden",
FrameClipEndBehavior.HideTarget,
CreateSprite(Color.blue));
var graph = CreateGraph(hold, hidden);
var gameObject = Track(new GameObject("Restore Terminal State"));
var renderer = gameObject.AddComponent();
var player = gameObject.AddComponent();
player.ConfigureForAuthoring(graph, false, 1f);
var holdResult = player.RestoreTerminalState(hold.Id);
Assert.That(holdResult.Result.Reason,
Is.EqualTo(FrameAnimationCompletionReason.Completed));
Assert.That(renderer.sprite, Is.SameAs(terminalSprite));
Assert.That(renderer.enabled, Is.True);
player.RestoreTerminalState(hidden.Id);
Assert.That(renderer.enabled, Is.False);
}
[UnityTest]
public IEnumerator Disable_SettlesActiveRequestAsStopped()
{
var clip = CreateClip(
"Loop",
FrameClipEndBehavior.Loop,
CreateSprite(Color.white));
var gameObject = Track(new GameObject("Disable Player"));
gameObject.AddComponent();
var player = gameObject.AddComponent();
player.ConfigureForAuthoring(CreateGraph(clip), false, 1f);
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();
var player = gameObject.AddComponent();
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,
params Sprite[] sprites)
{
var clip = Track(ScriptableObject.CreateInstance());
var frames = new List();
for (var index = 0; index < sprites.Length; index++)
{
frames.Add(new FrameAnimationFrame(
sprites[index],
100,
$"frame_{index}",
index));
}
clip.Configure(id, id, frames, 1f, endBehavior);
return clip;
}
private FrameAnimationGraph CreateGraph(params FrameClip[] clips)
{
return CreateGraph(
clips,
Array.Empty(),
Array.Empty(),
Array.Empty(),
clips[0].Id);
}
private FrameAnimationGraph CreateGraph(
FrameClip[] clips,
AnimationNode[] nodes,
AnimationEdge[] edges,
AnimationFlow[] flows,
string defaultPlayableId)
{
var graph = Track(ScriptableObject.CreateInstance());
graph.Configure(
"Graph",
"Graph",
clips,
nodes,
edges,
flows,
defaultPlayableId);
return graph;
}
private Sprite CreateSprite(Color color)
{
var texture = Track(new Texture2D(2, 2));
texture.SetPixels(new[] { color, color, color, color });
texture.Apply();
return Track(Sprite.Create(
texture,
new Rect(0, 0, 2, 2),
new Vector2(0.5f, 0.5f)));
}
private T Track(T value) where T : UnityEngine.Object
{
createdObjects.Add(value);
return value;
}
}
}