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; } } }