Skip to content

Commit

Permalink
AnimationIndex tests
Browse files Browse the repository at this point in the history
  • Loading branch information
bdunderscore committed Nov 10, 2024
1 parent b3b21c3 commit d2966be
Show file tree
Hide file tree
Showing 9 changed files with 264 additions and 11 deletions.
2 changes: 1 addition & 1 deletion Editor/API/AnimatorServices/AnimationIndex.cs
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,8 @@ private IEnumerable<VirtualClip> EnumerateClips()
while (queue.Count > 0)
{
var node = queue.Dequeue();

node.RegisterCacheObserver(_invalidateAction);

if (!visited.Add(node))
{
continue;
Expand Down
2 changes: 2 additions & 0 deletions Editor/API/AnimatorServices/LayerPriority.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ namespace nadena.dev.ndmf.animator
{
public struct LayerPriority : IComparable<LayerPriority>
{
public static LayerPriority Default = new();

internal int Priority;

public LayerPriority(int priority)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ namespace nadena.dev.ndmf.animator
/// </summary>
public class VirtualAnimatorController : VirtualNode, ICommitable<AnimatorController>, IDisposable
{
private readonly CloneContext _context;
public string Name { get; set; }

private ImmutableDictionary<string, AnimatorControllerParameter> _parameters;
Expand All @@ -37,8 +38,9 @@ private struct LayerGroup
public List<VirtualLayer> Layers;
}

public VirtualAnimatorController(string name = "")
public VirtualAnimatorController(CloneContext context, string name = "")
{
_context = context;
Name = name;
Parameters = ImmutableDictionary<string, AnimatorControllerParameter>.Empty;
}
Expand All @@ -56,6 +58,16 @@ public void AddLayer(LayerPriority priority, VirtualLayer layer)
group.Layers.Add(layer);
}

public VirtualLayer AddLayer(LayerPriority priority, string name)
{
// implicitly creates state machine
var layer = VirtualLayer.Create(_context, name);

AddLayer(priority, layer);

return layer;
}

public IEnumerable<VirtualLayer> Layers
{
get { return _layers.Values.SelectMany(l => l.Layers); }
Expand Down
4 changes: 2 additions & 2 deletions Editor/API/AnimatorServices/VirtualObjects/VirtualLayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ public static VirtualLayer Clone(CloneContext context, AnimatorControllerLayer l
return clone;
}

public static VirtualLayer NewLayer(CloneContext context, string name = "(unnamed)")
public static VirtualLayer Create(CloneContext context, string name = "(unnamed)")
{
return new VirtualLayer(context, name);
}
Expand Down Expand Up @@ -131,7 +131,7 @@ private VirtualLayer(CloneContext context, string name)
SyncedLayerAffectsTiming = false;
SyncedLayerIndex = -1;

StateMachine = new VirtualStateMachine(name);
StateMachine = VirtualStateMachine.Create(context, name);
}

AnimatorControllerLayer ICommitable<AnimatorControllerLayer>.Prepare(CommitContext context)
Expand Down
22 changes: 20 additions & 2 deletions Editor/API/AnimatorServices/VirtualObjects/VirtualState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ public class VirtualState : VirtualNode, ICommitable<AnimatorState>, IDisposable
{
private AnimatorState _state;

public List<StateMachineBehaviour> Behaviours { get; set; }
private ImmutableList<StateMachineBehaviour> _behaviours;

public ImmutableList<StateMachineBehaviour> Behaviours
{
get => _behaviours;
set => _behaviours = I(value);
}

public static VirtualState Clone(
CloneContext context,
Expand All @@ -32,12 +38,24 @@ AnimatorState state
return new VirtualState(context, clonedState);
}

public static VirtualState Create(CloneContext _, string name = "unnamed")
{
return new VirtualState { Name = name };
}

private VirtualState()
{
_state = new AnimatorState();
Behaviours = ImmutableList<StateMachineBehaviour>.Empty;
Transitions = ImmutableList<VirtualStateTransition>.Empty;
}

private VirtualState(CloneContext context, AnimatorState clonedState)
{
_state = clonedState;

// TODO: Should we rewrite any internal properties of these StateMachineBehaviours?
Behaviours = _state.behaviours.Select(b => Object.Instantiate(b)).ToList();
Behaviours = _state.behaviours.Select(b => Object.Instantiate(b)).ToImmutableList();
context.DeferCall(() => { Transitions = _state.transitions.Select(context.Clone).ToImmutableList(); });
Motion = context.Clone(_state.motion);
}
Expand Down
32 changes: 28 additions & 4 deletions Editor/API/AnimatorServices/VirtualObjects/VirtualStateMachine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using JetBrains.Annotations;
using UnityEditor.Animations;
using UnityEngine;
using Object = UnityEngine.Object;
Expand All @@ -13,18 +14,18 @@ namespace nadena.dev.ndmf.animator
/// </summary>
public class VirtualStateMachine : VirtualNode, ICommitable<AnimatorStateMachine>, IDisposable
{
private readonly CloneContext _context;
private AnimatorStateMachine _stateMachine;

public static VirtualStateMachine Clone(CloneContext context, AnimatorStateMachine stateMachine)
{
if (stateMachine == null) return null;
if (context.TryGetValue(stateMachine, out VirtualStateMachine clone)) return clone;

var vsm = new VirtualStateMachine();
var vsm = new VirtualStateMachine(context, stateMachine.name);

context.DeferCall(() =>
{
vsm.Name = stateMachine.name;
vsm.AnyStatePosition = stateMachine.anyStatePosition;
vsm.AnyStateTransitions = stateMachine.anyStateTransitions
.Select(context.Clone).ToImmutableList();
Expand Down Expand Up @@ -52,10 +53,16 @@ public static VirtualStateMachine Clone(CloneContext context, AnimatorStateMachi
return vsm;
}

public VirtualStateMachine(string name = "")
public static VirtualStateMachine Create(CloneContext context, string name = "")
{
return new VirtualStateMachine(context, name);
}

private VirtualStateMachine(CloneContext context, string name = "")
{
_context = context;
_stateMachine = new AnimatorStateMachine();
_stateMachine.name = name;
Name = name;
AnyStatePosition = _stateMachine.anyStatePosition;
EntryPosition = _stateMachine.entryPosition;
ExitPosition = _stateMachine.exitPosition;
Expand Down Expand Up @@ -236,5 +243,22 @@ protected override IEnumerable<VirtualNode> _EnumerateChildren()
yield return transition;
}
}

public VirtualState AddState(string name, [CanBeNull] VirtualMotion motion = null, Vector3? position = null)
{
var state = VirtualState.Create(_context, name);

state.Motion = motion;
var childState = new VirtualChildState
{
State = state,
// TODO: Better automatic positioning
Position = position ?? Vector3.zero
};

States = States.Add(childState);

return state;
}
}
}
194 changes: 194 additions & 0 deletions UnitTests~/AnimationServices/AnimationIndexTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
using System.Collections.Generic;
using System.Linq;
using nadena.dev.ndmf.animator;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;

namespace UnitTests.AnimationServices
{
public class AnimationIndexTest
{
[Test]
public void TestBasicIndexing()
{
var context = new CloneContext(new GenericPlatformAnimatorBindings());
var controller = new VirtualAnimatorController(context, "test");
var layer = controller.AddLayer(LayerPriority.Default, "test");

var clip1 = VirtualClip.Create("c1");
var clip2 = VirtualClip.Create("c2");

layer.StateMachine.AddState("s1", motion: clip1);
layer.StateMachine.AddState("s2", motion: clip2);

var index = new AnimationIndex( new [] { controller });

// Verify the index starts empty. This also sets us up to test cache invalidation.
Assert.IsEmpty(index.GetClipsForObjectPath("x"));

var binding1 = EditorCurveBinding.FloatCurve("path1", typeof(Transform), "prop1");
var binding2 = EditorCurveBinding.FloatCurve("path2", typeof(Transform), "prop2");

clip1.SetFloatCurve(binding1, AnimationCurve.Constant(0, 1, 1));
clip1.SetFloatCurve(binding2, AnimationCurve.Constant(0, 1, 1));
clip2.SetFloatCurve(binding2, AnimationCurve.Constant(0, 1, 1));

var p1clips = index.GetClipsForObjectPath("path1").ToList();
var p2clips = index.GetClipsForObjectPath("path2").ToList();

Assert.AreEqual(1, p1clips.Count);
Assert.AreEqual(2, p2clips.Count);
Assert.AreEqual(clip1, p1clips[0]);
Assert.Contains(clip1, p2clips);
Assert.Contains(clip2, p2clips);

var b1clips = index.GetClipsForBinding(binding1).ToList();
var b2clips = index.GetClipsForBinding(binding2).ToList();

Assert.AreEqual(1, b1clips.Count);
Assert.AreEqual(2, b2clips.Count);

Assert.AreEqual(clip1, b1clips[0]);
Assert.Contains(clip1, b2clips);
Assert.Contains(clip2, b2clips);
}

[Test]
public void TestRewritePaths()
{
var context = new CloneContext(new GenericPlatformAnimatorBindings());
var controller = new VirtualAnimatorController(context, "test");
var layer = controller.AddLayer(LayerPriority.Default, "test");

var clip1 = VirtualClip.Create("c1");
var clip2 = VirtualClip.Create("c2");

layer.StateMachine.AddState("s1", motion: clip1);
layer.StateMachine.AddState("s2", motion: clip2);

var index = new AnimationIndex( new [] { controller });

var binding1 = EditorCurveBinding.FloatCurve("path1", typeof(Transform), "prop1");
var binding2 = EditorCurveBinding.FloatCurve("path2", typeof(Transform), "prop2");

clip1.SetFloatCurve(binding1, AnimationCurve.Constant(0, 1, 1));
clip1.SetFloatCurve(binding2, AnimationCurve.Constant(0, 1, 1));
clip2.SetFloatCurve(binding2, AnimationCurve.Constant(0, 1, 1));

index.RewritePaths(new Dictionary<string, string>
{
{ "path1", "path3" },
{ "path2", "path4" }
});

// Verify the clips were in fact rewritten
var clip1paths = clip1.GetFloatCurveBindings().Select(ecb => ecb.path).ToList();
var clip2paths = clip2.GetFloatCurveBindings().Select(ecb => ecb.path).ToList();

Assert.Contains("path3", clip1paths);
Assert.Contains("path4", clip1paths);
Assert.Contains("path4", clip2paths);

Assert.IsFalse(clip1paths.Contains("path1"));
Assert.IsFalse(clip1paths.Contains("path2"));
Assert.IsFalse(clip2paths.Contains("path2"));

// Verify the index was updated

var p1clips = index.GetClipsForObjectPath("path1").ToList();
var p2clips = index.GetClipsForObjectPath("path2").ToList();
var p3clips = index.GetClipsForObjectPath("path3").ToList();
var p4clips = index.GetClipsForObjectPath("path4").ToList();

Assert.IsEmpty(p1clips);
Assert.IsEmpty(p2clips);
Assert.AreEqual(1, p3clips.Count);
Assert.AreEqual(2, p4clips.Count);
Assert.AreEqual(clip1, p3clips[0]);
Assert.Contains(clip1, p4clips);
Assert.Contains(clip2, p4clips);

var b1clips = index.GetClipsForBinding(binding1).ToList();
var b2clips = index.GetClipsForBinding(binding2).ToList();

Assert.IsEmpty(b1clips);
Assert.IsEmpty(b2clips);
}

[Test]
public void TestEditClipsByBinding()
{
var context = new CloneContext(new GenericPlatformAnimatorBindings());
var controller = new VirtualAnimatorController(context, "test");
var layer = controller.AddLayer(LayerPriority.Default, "test");

var clip1 = VirtualClip.Create("c1");
var clip2 = VirtualClip.Create("c2");

layer.StateMachine.AddState("s1", motion: clip1);
layer.StateMachine.AddState("s2", motion: clip2);

var index = new AnimationIndex( new [] { controller });

var binding1 = EditorCurveBinding.FloatCurve("path1", typeof(Transform), "prop1");
var binding2 = EditorCurveBinding.FloatCurve("path2", typeof(Transform), "prop2");
var binding3 = EditorCurveBinding.FloatCurve("path3", typeof(Transform), "prop3");

clip1.SetFloatCurve(binding1, AnimationCurve.Constant(0, 1, 1));
clip1.SetFloatCurve(binding2, AnimationCurve.Constant(0, 1, 1));
clip2.SetFloatCurve(binding2, AnimationCurve.Constant(0, 1, 1));

List<VirtualClip> visited = new();
index.EditClipsByBinding(new [] { binding1 }, clip =>
{
visited.Add(clip);
clip.SetFloatCurve(binding1, AnimationCurve.Constant(0, 1, 2));
clip.SetFloatCurve(binding2, null);
clip.SetFloatCurve(binding3, AnimationCurve.Constant(0, 1, 2));
});

// Verify only the correct clips were visited
Assert.AreEqual(1, visited.Count);
Assert.AreEqual(clip1, visited[0]);

// Verify that we updated the index
var b2clips = index.GetClipsForBinding(binding2).ToList();
var b3clips = index.GetClipsForBinding(binding3).ToList();

Assert.AreEqual(1, b2clips.Count);
Assert.AreEqual(1, b3clips.Count);
Assert.AreEqual(clip2, b2clips[0]);
Assert.AreEqual(clip1, b3clips[0]);
}

[Test]
public void TestGraphLoops()
{
var context = new CloneContext(new GenericPlatformAnimatorBindings());
var controller = new VirtualAnimatorController(context, "test");
var layer = controller.AddLayer(LayerPriority.Default, "test");

var sm1 = VirtualStateMachine.Create(context, "sm1");
var sm2 = VirtualStateMachine.Create(context, "sm2");

layer.StateMachine.StateMachines = layer.StateMachine.StateMachines.Add(new ()
{
StateMachine = sm1
});
sm1.StateMachines = sm1.StateMachines.Add(new ()
{
StateMachine = sm2
});
sm2.StateMachines = sm2.StateMachines.Add(new ()
{
StateMachine = sm1
});

var index = new AnimationIndex( new [] { controller });

// Make sure we don't infinite loop
Assert.IsEmpty(index.GetClipsForObjectPath("x"));
}
}
}
3 changes: 3 additions & 0 deletions UnitTests~/AnimationServices/AnimationIndexTest.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion UnitTests~/AnimationServices/VirtualLayerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ public void PreservesStateMachine()
{
AssertPreserveProperty(
state => state.stateMachine = TrackObject(new AnimatorStateMachine() { name = "x" }),
state => state.StateMachine = new VirtualStateMachine() { Name = "x" },
state => state.StateMachine = VirtualStateMachine.Create(new CloneContext(new GenericPlatformAnimatorBindings()), "x"),
state => Assert.AreEqual("x", state.stateMachine.name),
state => Assert.AreEqual("x", state.StateMachine.Name)
);
Expand Down

0 comments on commit d2966be

Please sign in to comment.