Skip to content

Commit 92acdad

Browse files
committed
Fix NetworkAnimator conditional exit transitions
1 parent 341d9dc commit 92acdad

3 files changed

Lines changed: 236 additions & 16 deletions

File tree

‎com.unity.netcode.gameobjects/Runtime/Components/NetworkAnimator.cs‎

Lines changed: 80 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,73 @@ internal class AnimatorParametersListContainer
323323

324324
private readonly Dictionary<int, AnimatorControllerParameter> m_ParameterToNameLookup = new Dictionary<int, AnimatorControllerParameter>();
325325

326-
private void ParseStateMachineStates(int layerIndex, ref AnimatorController animatorController, ref AnimatorStateMachine stateMachine)
326+
private void AddTransitionStateInfo(int layerIndex, AnimatorState originatingState, AnimatorState destinationState, AnimatorStateTransition transition, AnimatorControllerParameter parameter, int transitionIndex)
327+
{
328+
var transitionInfo = new TransitionStateInfo()
329+
{
330+
Layer = layerIndex,
331+
OriginatingState = originatingState.nameHash,
332+
DestinationState = destinationState.nameHash,
333+
TransitionDuration = transition.duration,
334+
TriggerNameHash = parameter.nameHash,
335+
TransitionIndex = transitionIndex
336+
};
337+
TransitionStateInfoList.Add(transitionInfo);
338+
}
339+
340+
private void AddStateMachineDestinations(int layerIndex, AnimatorState originatingState, AnimatorStateMachine destinationStateMachine, AnimatorStateTransition transition,
341+
AnimatorControllerParameter parameter, int transitionIndex, HashSet<AnimatorStateMachine> visitedStateMachines)
342+
{
343+
if (!visitedStateMachines.Add(destinationStateMachine))
344+
{
345+
return;
346+
}
347+
348+
if (destinationStateMachine.defaultState != null)
349+
{
350+
AddTransitionStateInfo(layerIndex, originatingState, destinationStateMachine.defaultState, transition, parameter, transitionIndex);
351+
}
352+
353+
foreach (var entryTransition in destinationStateMachine.entryTransitions)
354+
{
355+
if (entryTransition.destinationState != null)
356+
{
357+
AddTransitionStateInfo(layerIndex, originatingState, entryTransition.destinationState, transition, parameter, transitionIndex);
358+
}
359+
else if (entryTransition.destinationStateMachine != null)
360+
{
361+
AddStateMachineDestinations(layerIndex, originatingState, entryTransition.destinationStateMachine, transition, parameter, transitionIndex, visitedStateMachines);
362+
}
363+
}
364+
}
365+
366+
private void AddExitTransitionStateInfo(int layerIndex, AnimatorState originatingState, AnimatorStateMachine exitingStateMachine, AnimatorStateTransition transition,
367+
AnimatorControllerParameter parameter, int transitionIndex, List<AnimatorStateMachine> parentStateMachines, int parentIndex)
368+
{
369+
if (parentIndex < 0)
370+
{
371+
return;
372+
}
373+
374+
var parentStateMachine = parentStateMachines[parentIndex];
375+
foreach (var stateMachineTransition in parentStateMachine.GetStateMachineTransitions(exitingStateMachine))
376+
{
377+
if (stateMachineTransition.destinationState != null)
378+
{
379+
AddTransitionStateInfo(layerIndex, originatingState, stateMachineTransition.destinationState, transition, parameter, transitionIndex);
380+
}
381+
else if (stateMachineTransition.destinationStateMachine != null)
382+
{
383+
AddStateMachineDestinations(layerIndex, originatingState, stateMachineTransition.destinationStateMachine, transition, parameter, transitionIndex, new HashSet<AnimatorStateMachine>());
384+
}
385+
else if (stateMachineTransition.isExit)
386+
{
387+
AddExitTransitionStateInfo(layerIndex, originatingState, parentStateMachine, transition, parameter, transitionIndex, parentStateMachines, parentIndex - 1);
388+
}
389+
}
390+
}
391+
392+
private void ParseStateMachineStates(int layerIndex, ref AnimatorController animatorController, ref AnimatorStateMachine stateMachine, List<AnimatorStateMachine> parentStateMachines = null)
327393
{
328394
for (int y = 0; y < stateMachine.states.Length; y++)
329395
{
@@ -355,25 +421,15 @@ private void ParseStateMachineStates(int layerIndex, ref AnimatorController anim
355421
{
356422
case AnimatorControllerParameterType.Trigger:
357423
{
358-
if (transition.destinationStateMachine != null)
424+
if (transition.destinationState != null)
359425
{
360-
var destinationStateMachine = transition.destinationStateMachine;
361-
ParseStateMachineStates(layerIndex, ref animatorController, ref destinationStateMachine);
426+
AddTransitionStateInfo(layerIndex, animatorState, transition.destinationState, transition, parameter, z);
362427
}
363-
else if (transition.destinationState != null)
428+
else if (transition.isExit)
364429
{
365-
var transitionInfo = new TransitionStateInfo()
366-
{
367-
Layer = layerIndex,
368-
OriginatingState = animatorState.nameHash,
369-
DestinationState = transition.destinationState.nameHash,
370-
TransitionDuration = transition.duration,
371-
TriggerNameHash = parameter.nameHash,
372-
TransitionIndex = z
373-
};
374-
TransitionStateInfoList.Add(transitionInfo);
430+
AddExitTransitionStateInfo(layerIndex, animatorState, stateMachine, transition, parameter, z, parentStateMachines, parentStateMachines != null ? parentStateMachines.Count - 1 : -1);
375431
}
376-
else
432+
else if (transition.destinationStateMachine == null)
377433
{
378434
Debug.LogError($"[{name}][Conditional Transition for {animatorState.name}] Conditional triggered transition has neither a DestinationState nor a DestinationStateMachine! This transition is not likely to synchronize properly. " +
379435
$"Please file a GitHub issue about this error with details about your Animator's setup.");
@@ -387,6 +443,14 @@ private void ParseStateMachineStates(int layerIndex, ref AnimatorController anim
387443
}
388444
}
389445
}
446+
447+
var childStateMachineParents = parentStateMachines != null ? new List<AnimatorStateMachine>(parentStateMachines) : new List<AnimatorStateMachine>();
448+
childStateMachineParents.Add(stateMachine);
449+
foreach (var childStateMachine in stateMachine.stateMachines)
450+
{
451+
var nestedStateMachine = childStateMachine.stateMachine;
452+
ParseStateMachineStates(layerIndex, ref animatorController, ref nestedStateMachine, childStateMachineParents);
453+
}
390454
}
391455

392456
/// <summary>
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
using NUnit.Framework;
2+
using Unity.Netcode.Components;
3+
using UnityEditor;
4+
using UnityEditor.Animations;
5+
using UnityEngine;
6+
7+
namespace Unity.Netcode.GameObjects.EditorTests
8+
{
9+
internal class NetworkAnimatorEditorTests
10+
{
11+
private const string k_TestControllerPath = "Assets/NetworkAnimatorConditionalExitTest.controller";
12+
13+
private GameObject m_GameObject;
14+
15+
[TearDown]
16+
public void TearDown()
17+
{
18+
if (m_GameObject != null)
19+
{
20+
Object.DestroyImmediate(m_GameObject);
21+
}
22+
23+
AssetDatabase.DeleteAsset(k_TestControllerPath);
24+
}
25+
26+
[Test]
27+
public void ConditionalExitTransitionResolvesParentDestination()
28+
{
29+
var controller = AnimatorController.CreateAnimatorControllerAtPath(k_TestControllerPath);
30+
controller.AddParameter("ExitSubMachine", AnimatorControllerParameterType.Trigger);
31+
32+
var rootStateMachine = controller.layers[0].stateMachine;
33+
var destinationState = rootStateMachine.AddState("State B");
34+
var subStateMachine = rootStateMachine.AddStateMachine("Sub SM");
35+
var originatingState = subStateMachine.AddState("State A");
36+
subStateMachine.defaultState = originatingState;
37+
38+
rootStateMachine.AddEntryTransition(subStateMachine);
39+
40+
var exitTransition = originatingState.AddExitTransition();
41+
exitTransition.hasExitTime = false;
42+
exitTransition.duration = 0.25f;
43+
exitTransition.AddCondition(AnimatorConditionMode.If, 0.0f, "ExitSubMachine");
44+
45+
var parentTransition = rootStateMachine.AddStateMachineTransition(subStateMachine, destinationState);
46+
parentTransition.AddCondition(AnimatorConditionMode.If, 0.0f, "ExitSubMachine");
47+
AssetDatabase.SaveAssets();
48+
49+
var networkAnimator = CreateNetworkAnimator(controller, nameof(ConditionalExitTransitionResolvesParentDestination));
50+
networkAnimator.InvokeOnValidate();
51+
52+
Assert.That(networkAnimator.TransitionStateInfoList, Has.Count.EqualTo(1));
53+
var transitionInfo = networkAnimator.TransitionStateInfoList[0];
54+
Assert.That(transitionInfo.Layer, Is.Zero);
55+
Assert.That(transitionInfo.OriginatingState, Is.EqualTo(originatingState.nameHash));
56+
Assert.That(transitionInfo.DestinationState, Is.EqualTo(destinationState.nameHash));
57+
Assert.That(transitionInfo.TransitionDuration, Is.EqualTo(exitTransition.duration));
58+
Assert.That(transitionInfo.TriggerNameHash, Is.EqualTo(Animator.StringToHash("ExitSubMachine")));
59+
}
60+
61+
[Test]
62+
public void NestedConditionalExitTransitionResolvesAncestorDestination()
63+
{
64+
var controller = AnimatorController.CreateAnimatorControllerAtPath(k_TestControllerPath);
65+
controller.AddParameter("ExitNested", AnimatorControllerParameterType.Trigger);
66+
67+
var rootStateMachine = controller.layers[0].stateMachine;
68+
var destinationState = rootStateMachine.AddState("State B");
69+
var outerStateMachine = rootStateMachine.AddStateMachine("Outer SM");
70+
var innerStateMachine = outerStateMachine.AddStateMachine("Inner SM");
71+
var originatingState = innerStateMachine.AddState("State A");
72+
innerStateMachine.defaultState = originatingState;
73+
74+
rootStateMachine.AddEntryTransition(outerStateMachine);
75+
outerStateMachine.AddEntryTransition(innerStateMachine);
76+
77+
var exitTransition = originatingState.AddExitTransition();
78+
exitTransition.hasExitTime = false;
79+
exitTransition.duration = 0.5f;
80+
exitTransition.AddCondition(AnimatorConditionMode.If, 0.0f, "ExitNested");
81+
82+
var exitInnerTransition = outerStateMachine.AddStateMachineExitTransition(innerStateMachine);
83+
exitInnerTransition.AddCondition(AnimatorConditionMode.If, 0.0f, "ExitNested");
84+
var exitOuterTransition = rootStateMachine.AddStateMachineTransition(outerStateMachine, destinationState);
85+
exitOuterTransition.AddCondition(AnimatorConditionMode.If, 0.0f, "ExitNested");
86+
AssetDatabase.SaveAssets();
87+
88+
var networkAnimator = CreateNetworkAnimator(controller, nameof(NestedConditionalExitTransitionResolvesAncestorDestination));
89+
networkAnimator.InvokeOnValidate();
90+
91+
Assert.That(networkAnimator.TransitionStateInfoList, Has.Count.EqualTo(1));
92+
var transitionInfo = networkAnimator.TransitionStateInfoList[0];
93+
Assert.That(transitionInfo.OriginatingState, Is.EqualTo(originatingState.nameHash));
94+
Assert.That(transitionInfo.DestinationState, Is.EqualTo(destinationState.nameHash));
95+
Assert.That(transitionInfo.TransitionDuration, Is.EqualTo(exitTransition.duration));
96+
}
97+
98+
[Test]
99+
public void ConditionalExitTransitionResolvesDestinationStateMachineStates()
100+
{
101+
var controller = AnimatorController.CreateAnimatorControllerAtPath(k_TestControllerPath);
102+
controller.AddParameter("ExitSubMachine", AnimatorControllerParameterType.Trigger);
103+
104+
var rootStateMachine = controller.layers[0].stateMachine;
105+
var sourceStateMachine = rootStateMachine.AddStateMachine("Source SM");
106+
var originatingState = sourceStateMachine.AddState("State A");
107+
sourceStateMachine.defaultState = originatingState;
108+
rootStateMachine.AddEntryTransition(sourceStateMachine);
109+
110+
var destinationStateMachine = rootStateMachine.AddStateMachine("Destination SM");
111+
var defaultDestinationState = destinationStateMachine.AddState("Default State");
112+
var entryDestinationState = destinationStateMachine.AddState("Entry State");
113+
destinationStateMachine.defaultState = defaultDestinationState;
114+
destinationStateMachine.AddEntryTransition(entryDestinationState);
115+
116+
var exitTransition = originatingState.AddExitTransition();
117+
exitTransition.hasExitTime = false;
118+
exitTransition.duration = 0.25f;
119+
exitTransition.AddCondition(AnimatorConditionMode.If, 0.0f, "ExitSubMachine");
120+
121+
var parentTransition = rootStateMachine.AddStateMachineTransition(sourceStateMachine, destinationStateMachine);
122+
parentTransition.AddCondition(AnimatorConditionMode.If, 0.0f, "ExitSubMachine");
123+
AssetDatabase.SaveAssets();
124+
125+
var networkAnimator = CreateNetworkAnimator(controller, nameof(ConditionalExitTransitionResolvesDestinationStateMachineStates));
126+
networkAnimator.InvokeOnValidate();
127+
128+
Assert.That(networkAnimator.TransitionStateInfoList, Has.Count.EqualTo(2));
129+
Assert.That(networkAnimator.TransitionStateInfoList.Exists(entry => entry.DestinationState == defaultDestinationState.nameHash), Is.True);
130+
Assert.That(networkAnimator.TransitionStateInfoList.Exists(entry => entry.DestinationState == entryDestinationState.nameHash), Is.True);
131+
}
132+
133+
private TestNetworkAnimator CreateNetworkAnimator(AnimatorController controller, string name)
134+
{
135+
m_GameObject = new GameObject(name);
136+
m_GameObject.AddComponent<NetworkObject>();
137+
var animator = m_GameObject.AddComponent<Animator>();
138+
animator.runtimeAnimatorController = controller;
139+
var networkAnimator = m_GameObject.AddComponent<TestNetworkAnimator>();
140+
networkAnimator.Animator = animator;
141+
networkAnimator.AnimatorParameterEntries = new NetworkAnimator.AnimatorParametersListContainer();
142+
return networkAnimator;
143+
}
144+
145+
private class TestNetworkAnimator : NetworkAnimator
146+
{
147+
public void InvokeOnValidate()
148+
{
149+
base.OnValidate();
150+
}
151+
}
152+
}
153+
}

‎com.unity.netcode.gameobjects/Tests/Editor/NetworkAnimatorEditorTests.cs.meta‎

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)