259 lines
11 KiB
C#
259 lines
11 KiB
C#
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<UnityEngine.Object> createdObjects = new List<UnityEngine.Object>();
|
|
|
|
[TearDown]
|
|
public void TearDown()
|
|
{
|
|
foreach (var createdObject in createdObjects.Where(item => item != null))
|
|
{
|
|
UnityEngine.Object.DestroyImmediate(createdObject);
|
|
}
|
|
createdObjects.Clear();
|
|
}
|
|
|
|
[Test]
|
|
public void DataModel_SerializesOptionalOverridesAndKeepsInternalIds()
|
|
{
|
|
var clip = CreateClip("Idle", FrameClipEndBehavior.Loop, 1f, 100, 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("Graph", new[] { clip }, new[] { node }, Array.Empty<AnimationEdge>(), new[] { flow }, flow.Id);
|
|
var clone = Track(ScriptableObject.CreateInstance<FrameAnimationGraph>());
|
|
|
|
var json = EditorJsonUtility.ToJson(graph);
|
|
EditorJsonUtility.FromJsonOverwrite(json, 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 Validator_ReportsDuplicatePlayableAndInvalidGraphStructure()
|
|
{
|
|
var first = CreateClip("Same", FrameClipEndBehavior.HoldLastFrame, 1f, 100);
|
|
var second = CreateClip("Same", FrameClipEndBehavior.HoldLastFrame, 1f);
|
|
var node = new AnimationNode("MissingClip", internalId: "invalid-id");
|
|
var flow = new AnimationFlow("Same", "Same Flow", node.InternalId);
|
|
var graph = CreateGraph(
|
|
"Invalid",
|
|
new[] { first, second },
|
|
new[] { node },
|
|
Array.Empty<AnimationEdge>(),
|
|
new[] { flow },
|
|
"Missing");
|
|
|
|
var report = FrameAnimationGraphValidator.Validate(graph);
|
|
|
|
Assert.That(report.HasErrors, Is.True);
|
|
Assert.That(report.Issues.Any(issue => issue.Code == FrameAnimationValidationCode.PlayableIdDuplicate), Is.True);
|
|
Assert.That(report.Issues.Any(issue => issue.Code == FrameAnimationValidationCode.ClipFramesEmpty), Is.True);
|
|
Assert.That(report.Issues.Any(issue => issue.Code == FrameAnimationValidationCode.InternalIdInvalid), Is.True);
|
|
Assert.That(report.Issues.Any(issue => issue.Code == FrameAnimationValidationCode.NodeClipMissing), Is.True);
|
|
Assert.That(report.Issues.Any(issue => issue.Code == FrameAnimationValidationCode.DefaultPlayableInvalid), Is.True);
|
|
}
|
|
|
|
[Test]
|
|
public void Resolver_RejectsCycleAndMultipleSuccessors()
|
|
{
|
|
var clip = CreateClip("Clip", FrameClipEndBehavior.HoldLastFrame, 1f, 100);
|
|
var a = new AnimationNode(clip.Id);
|
|
var b = new AnimationNode(clip.Id);
|
|
var c = new AnimationNode(clip.Id);
|
|
var flow = new AnimationFlow("Flow", "Flow", a.InternalId);
|
|
var graph = CreateGraph(
|
|
"Graph",
|
|
new[] { clip },
|
|
new[] { a, b, c },
|
|
new[]
|
|
{
|
|
new AnimationEdge(a.InternalId, b.InternalId),
|
|
new AnimationEdge(a.InternalId, c.InternalId),
|
|
new AnimationEdge(b.InternalId, a.InternalId)
|
|
},
|
|
new[] { flow },
|
|
flow.Id);
|
|
|
|
var resolved = FrameAnimationResolver.TryResolve(
|
|
graph,
|
|
flow.Id,
|
|
default,
|
|
out _,
|
|
out var error);
|
|
|
|
Assert.That(resolved, Is.False);
|
|
Assert.That(error.Code, Is.EqualTo(FrameAnimationPlaybackErrorCode.InvalidPlayableData));
|
|
}
|
|
|
|
[Test]
|
|
public void Session_LargeDeltaCrossesFlowAndLoopsTerminalClip()
|
|
{
|
|
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(
|
|
"Graph",
|
|
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 Session_ExactBoundaryAndRemainingTimeAdvanceDeterministically()
|
|
{
|
|
var clip = CreateClip("Clip", FrameClipEndBehavior.HoldLastFrame, 1f, 100, 200, 300);
|
|
var graph = CreateGraph(
|
|
"Graph",
|
|
new[] { clip },
|
|
Array.Empty<AnimationNode>(),
|
|
Array.Empty<AnimationEdge>(),
|
|
Array.Empty<AnimationFlow>(),
|
|
clip.Id);
|
|
Assert.That(FrameAnimationResolver.TryResolve(graph, clip.Id, default, out var plan, out _), Is.True);
|
|
var session = new FrameAnimationPlaybackSession(plan);
|
|
session.Start(_ => { });
|
|
|
|
session.Evaluate(0.1d, 1f, _ => { });
|
|
Assert.That(session.CurrentFrameIndex, Is.EqualTo(1));
|
|
session.Evaluate(0.25d, 1f, _ => { });
|
|
Assert.That(session.CurrentFrameIndex, Is.EqualTo(2));
|
|
var completion = session.Evaluate(0.25d, 1f, _ => { });
|
|
|
|
Assert.That(completion.IsCompleted, Is.True);
|
|
Assert.That(completion.EndBehavior, Is.EqualTo(FrameClipEndBehavior.HoldLastFrame));
|
|
}
|
|
|
|
[Test]
|
|
public void Session_UsesNodeSpeedInsteadOfClipSpeedAndKeepsPlayerMultiplier()
|
|
{
|
|
var clip = CreateClip("Clip", FrameClipEndBehavior.HoldLastFrame, 0.5f, 100, 100);
|
|
var node = new AnimationNode(clip.Id, speedOverride: 2f);
|
|
var flow = new AnimationFlow("Flow", "Flow", node.InternalId);
|
|
var graph = CreateGraph(
|
|
"Graph",
|
|
new[] { clip },
|
|
new[] { node },
|
|
Array.Empty<AnimationEdge>(),
|
|
new[] { flow },
|
|
flow.Id);
|
|
Assert.That(FrameAnimationResolver.TryResolve(graph, flow.Id, default, out var plan, out _), Is.True);
|
|
var session = new FrameAnimationPlaybackSession(plan);
|
|
session.Start(_ => { });
|
|
|
|
session.Evaluate(0.025d, 2f, _ => { });
|
|
|
|
Assert.That(session.CurrentFrameIndex, Is.EqualTo(1));
|
|
}
|
|
|
|
[Test]
|
|
public void Resolver_EndBehaviorPrecedenceIsNodeThenRequestThenFlowThenClip()
|
|
{
|
|
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("Graph", new[] { clip }, new[] { node }, Array.Empty<AnimationEdge>(), 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 Handle_CompletesOnceAndLateObserversReceiveSameResult()
|
|
{
|
|
var handle = new FrameAnimationPlaybackHandle(42);
|
|
var callbackCount = 0;
|
|
FrameAnimationPlaybackResult observed = default;
|
|
handle.RegisterCompleted(result =>
|
|
{
|
|
callbackCount++;
|
|
observed = result;
|
|
});
|
|
Assert.Throws<InvalidOperationException>(() => _ = handle.Result);
|
|
|
|
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(observed.RequestId, Is.EqualTo(42));
|
|
Assert.That(handle.WaitAsync().Result.RequestId, Is.EqualTo(42));
|
|
}
|
|
|
|
private FrameClip CreateClip(
|
|
string id,
|
|
FrameClipEndBehavior endBehavior,
|
|
float speed,
|
|
params int[] durations)
|
|
{
|
|
var clip = Track(ScriptableObject.CreateInstance<FrameClip>());
|
|
var frames = durations.Select((duration, index) =>
|
|
new FrameAnimationFrame(null, duration, $"frame_{index}", index));
|
|
clip.Configure(id, id, frames, speed, endBehavior);
|
|
return clip;
|
|
}
|
|
|
|
private FrameAnimationGraph CreateGraph(
|
|
string id,
|
|
IEnumerable<FrameClip> clips,
|
|
IEnumerable<AnimationNode> nodes,
|
|
IEnumerable<AnimationEdge> edges,
|
|
IEnumerable<AnimationFlow> flows,
|
|
string defaultPlayableId)
|
|
{
|
|
var graph = Track(ScriptableObject.CreateInstance<FrameAnimationGraph>());
|
|
graph.Configure(id, id, clips, nodes, edges, flows, defaultPlayableId);
|
|
return graph;
|
|
}
|
|
|
|
private T Track<T>(T value) where T : UnityEngine.Object
|
|
{
|
|
createdObjects.Add(value);
|
|
return value;
|
|
}
|
|
}
|
|
}
|