using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; using UnityEditor; using UnityEngine; namespace AibisDream.FrameAnimation.Tests.EditMode { /// /// 帧动画运行时模型的稳定契约。编辑器窗口与具体布局不在此处测试。 /// public sealed class FrameAnimationCoreTests { private readonly List createdObjects = new List(); [TearDown] public void TearDown() { foreach (var createdObject in createdObjects.Where(item => item != null)) UnityEngine.Object.DestroyImmediate(createdObject); createdObjects.Clear(); } [Test] public void GraphSerialization_PreservesRuntimeIdentityAndOverrides() { var clip = CreateClip("Idle", FrameClipEndBehavior.Loop, 1f, 100); var node = new AnimationNode( clip.Id, endBehaviorOverride: FrameClipEndBehavior.HideTarget, speedOverride: 2f); var flow = new AnimationFlow( "Flow", "Flow", node.InternalId, FrameClipEndBehavior.Clear); var graph = CreateGraph( new[] { clip }, new[] { node }, Array.Empty(), new[] { flow }, flow.Id); var clone = Track(ScriptableObject.CreateInstance()); EditorJsonUtility.FromJsonOverwrite(EditorJsonUtility.ToJson(graph), clone); Assert.That(clone.Nodes[0].InternalId, Is.EqualTo(node.InternalId)); Assert.That( clone.Nodes[0].EndBehaviorOverride, Is.EqualTo(FrameClipEndBehavior.HideTarget)); Assert.That(clone.Nodes[0].SpeedOverride, Is.EqualTo(2f)); Assert.That( clone.Flows[0].EndBehaviorOverride, Is.EqualTo(FrameClipEndBehavior.Clear)); } [Test] public void InvalidTopology_IsRejectedBeforePlayback() { var clip = CreateClip("Clip", FrameClipEndBehavior.HoldLastFrame, 1f, 100); var first = new AnimationNode(clip.Id); var second = new AnimationNode(clip.Id); var third = new AnimationNode(clip.Id); var flow = new AnimationFlow("Flow", "Flow", first.InternalId); var graph = CreateGraph( new[] { clip }, new[] { first, second, third }, new[] { new AnimationEdge(first.InternalId, second.InternalId), new AnimationEdge(first.InternalId, third.InternalId), new AnimationEdge(second.InternalId, first.InternalId) }, new[] { flow }, flow.Id); var report = FrameAnimationGraphValidator.Validate(graph); var resolved = FrameAnimationResolver.TryResolve( graph, flow.Id, default, out _, out var error); Assert.That(report.HasErrors, Is.True); Assert.That(resolved, Is.False); Assert.That( error.Code, Is.EqualTo(FrameAnimationPlaybackErrorCode.InvalidPlayableData)); } [Test] public void PlaybackSession_CrossesFlowAndLoopsTerminalClip() { var intro = CreateClip("Intro", FrameClipEndBehavior.HoldLastFrame, 1f, 100, 100); var idle = CreateClip("Idle", FrameClipEndBehavior.HoldLastFrame, 1f, 100, 100, 100); var introNode = new AnimationNode(intro.Id); var idleNode = new AnimationNode( idle.Id, endBehaviorOverride: FrameClipEndBehavior.Loop); var flow = new AnimationFlow("IntroToIdle", "Intro To Idle", introNode.InternalId); var graph = CreateGraph( new[] { intro, idle }, new[] { introNode, idleNode }, new[] { new AnimationEdge(introNode.InternalId, idleNode.InternalId) }, new[] { flow }, flow.Id); Assert.That( FrameAnimationResolver.TryResolve( graph, flow.Id, default, out var plan, out var error), Is.True, error.ToString()); var session = new FrameAnimationPlaybackSession(plan); session.Start(_ => { }); var evaluation = session.Evaluate(1.05d, 1f, _ => { }); Assert.That(evaluation.IsCompleted, Is.False); Assert.That(session.CurrentClipId, Is.EqualTo(idle.Id)); Assert.That(session.CurrentNodeId, Is.EqualTo(idleNode.InternalId)); Assert.That(session.CurrentFrameIndex, Is.EqualTo(2)); } [Test] public void EndBehavior_NodeOverrideHasHighestPrecedence() { var clip = CreateClip("Clip", FrameClipEndBehavior.HoldLastFrame, 1f, 100); var node = new AnimationNode( clip.Id, endBehaviorOverride: FrameClipEndBehavior.HideTarget); var flow = new AnimationFlow( "Flow", "Flow", node.InternalId, FrameClipEndBehavior.Clear); var graph = CreateGraph( new[] { clip }, new[] { node }, Array.Empty(), new[] { flow }, flow.Id); Assert.That( FrameAnimationResolver.TryResolve( graph, flow.Id, new FrameAnimationPlayOptions(FrameClipEndBehavior.Loop), out var plan, out _), Is.True); Assert.That( plan.Steps[0].TerminalEndBehavior, Is.EqualTo(FrameClipEndBehavior.HideTarget)); } [Test] public void PlaybackHandle_CompletesExactlyOnce() { var handle = new FrameAnimationPlaybackHandle(42); var callbackCount = 0; handle.RegisterCompleted(_ => callbackCount++); var completion = new FrameAnimationPlaybackResult( 42, "Clip", FrameAnimationCompletionReason.Completed, FrameAnimationPlaybackError.None); Assert.That(handle.TryComplete(completion), Is.True); Assert.That(handle.TryComplete(completion), Is.False); handle.RegisterCompleted(_ => callbackCount++); Assert.That(callbackCount, Is.EqualTo(2)); Assert.That(handle.Result.RequestId, Is.EqualTo(42)); } private FrameClip CreateClip( string id, FrameClipEndBehavior endBehavior, float speed, params int[] durations) { var clip = Track(ScriptableObject.CreateInstance()); clip.Configure( id, id, durations.Select((duration, index) => new FrameAnimationFrame(null, duration, $"frame_{index}", index)), speed, endBehavior); return clip; } private FrameAnimationGraph CreateGraph( IEnumerable clips, IEnumerable nodes, IEnumerable edges, IEnumerable flows, string defaultPlayableId) { var graph = Track(ScriptableObject.CreateInstance()); graph.Configure( "Graph", "Graph", clips, nodes, edges, flows, defaultPlayableId); return graph; } private T Track(T value) where T : UnityEngine.Object { createdObjects.Add(value); return value; } } }