diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkAnimator.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkAnimator.cs index bf275ab3c3..9542e790a5 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkAnimator.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkAnimator.cs @@ -323,7 +323,73 @@ internal class AnimatorParametersListContainer private readonly Dictionary m_ParameterToNameLookup = new Dictionary(); - private void ParseStateMachineStates(int layerIndex, ref AnimatorController animatorController, ref AnimatorStateMachine stateMachine) + private void AddTransitionStateInfo(int layerIndex, AnimatorState originatingState, AnimatorState destinationState, AnimatorStateTransition transition, AnimatorControllerParameter parameter, int transitionIndex) + { + var transitionInfo = new TransitionStateInfo() + { + Layer = layerIndex, + OriginatingState = originatingState.nameHash, + DestinationState = destinationState.nameHash, + TransitionDuration = transition.duration, + TriggerNameHash = parameter.nameHash, + TransitionIndex = transitionIndex + }; + TransitionStateInfoList.Add(transitionInfo); + } + + private void AddStateMachineDestinations(int layerIndex, AnimatorState originatingState, AnimatorStateMachine destinationStateMachine, AnimatorStateTransition transition, + AnimatorControllerParameter parameter, int transitionIndex, HashSet visitedStateMachines) + { + if (!visitedStateMachines.Add(destinationStateMachine)) + { + return; + } + + if (destinationStateMachine.defaultState != null) + { + AddTransitionStateInfo(layerIndex, originatingState, destinationStateMachine.defaultState, transition, parameter, transitionIndex); + } + + foreach (var entryTransition in destinationStateMachine.entryTransitions) + { + if (entryTransition.destinationState != null) + { + AddTransitionStateInfo(layerIndex, originatingState, entryTransition.destinationState, transition, parameter, transitionIndex); + } + else if (entryTransition.destinationStateMachine != null) + { + AddStateMachineDestinations(layerIndex, originatingState, entryTransition.destinationStateMachine, transition, parameter, transitionIndex, visitedStateMachines); + } + } + } + + private void AddExitTransitionStateInfo(int layerIndex, AnimatorState originatingState, AnimatorStateMachine exitingStateMachine, AnimatorStateTransition transition, + AnimatorControllerParameter parameter, int transitionIndex, List parentStateMachines, int parentIndex) + { + if (parentIndex < 0) + { + return; + } + + var parentStateMachine = parentStateMachines[parentIndex]; + foreach (var stateMachineTransition in parentStateMachine.GetStateMachineTransitions(exitingStateMachine)) + { + if (stateMachineTransition.destinationState != null) + { + AddTransitionStateInfo(layerIndex, originatingState, stateMachineTransition.destinationState, transition, parameter, transitionIndex); + } + else if (stateMachineTransition.destinationStateMachine != null) + { + AddStateMachineDestinations(layerIndex, originatingState, stateMachineTransition.destinationStateMachine, transition, parameter, transitionIndex, new HashSet()); + } + else if (stateMachineTransition.isExit) + { + AddExitTransitionStateInfo(layerIndex, originatingState, parentStateMachine, transition, parameter, transitionIndex, parentStateMachines, parentIndex - 1); + } + } + } + + private void ParseStateMachineStates(int layerIndex, ref AnimatorController animatorController, ref AnimatorStateMachine stateMachine, List parentStateMachines = null) { for (int y = 0; y < stateMachine.states.Length; y++) { @@ -355,25 +421,15 @@ private void ParseStateMachineStates(int layerIndex, ref AnimatorController anim { case AnimatorControllerParameterType.Trigger: { - if (transition.destinationStateMachine != null) + if (transition.destinationState != null) { - var destinationStateMachine = transition.destinationStateMachine; - ParseStateMachineStates(layerIndex, ref animatorController, ref destinationStateMachine); + AddTransitionStateInfo(layerIndex, animatorState, transition.destinationState, transition, parameter, z); } - else if (transition.destinationState != null) + else if (transition.isExit) { - var transitionInfo = new TransitionStateInfo() - { - Layer = layerIndex, - OriginatingState = animatorState.nameHash, - DestinationState = transition.destinationState.nameHash, - TransitionDuration = transition.duration, - TriggerNameHash = parameter.nameHash, - TransitionIndex = z - }; - TransitionStateInfoList.Add(transitionInfo); + AddExitTransitionStateInfo(layerIndex, animatorState, stateMachine, transition, parameter, z, parentStateMachines, parentStateMachines != null ? parentStateMachines.Count - 1 : -1); } - else + else if (transition.destinationStateMachine == null) { 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. " + $"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 } } } + + var childStateMachineParents = parentStateMachines != null ? new List(parentStateMachines) : new List(); + childStateMachineParents.Add(stateMachine); + foreach (var childStateMachine in stateMachine.stateMachines) + { + var nestedStateMachine = childStateMachine.stateMachine; + ParseStateMachineStates(layerIndex, ref animatorController, ref nestedStateMachine, childStateMachineParents); + } } /// diff --git a/com.unity.netcode.gameobjects/Tests/Editor/NetworkAnimatorEditorTests.cs b/com.unity.netcode.gameobjects/Tests/Editor/NetworkAnimatorEditorTests.cs new file mode 100644 index 0000000000..9cdcd449ec --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Editor/NetworkAnimatorEditorTests.cs @@ -0,0 +1,153 @@ +using NUnit.Framework; +using Unity.Netcode.Components; +using UnityEditor; +using UnityEditor.Animations; +using UnityEngine; + +namespace Unity.Netcode.GameObjects.EditorTests +{ + internal class NetworkAnimatorEditorTests + { + private const string k_TestControllerPath = "Assets/NetworkAnimatorConditionalExitTest.controller"; + + private GameObject m_GameObject; + + [TearDown] + public void TearDown() + { + if (m_GameObject != null) + { + Object.DestroyImmediate(m_GameObject); + } + + AssetDatabase.DeleteAsset(k_TestControllerPath); + } + + [Test] + public void ConditionalExitTransitionResolvesParentDestination() + { + var controller = AnimatorController.CreateAnimatorControllerAtPath(k_TestControllerPath); + controller.AddParameter("ExitSubMachine", AnimatorControllerParameterType.Trigger); + + var rootStateMachine = controller.layers[0].stateMachine; + var destinationState = rootStateMachine.AddState("State B"); + var subStateMachine = rootStateMachine.AddStateMachine("Sub SM"); + var originatingState = subStateMachine.AddState("State A"); + subStateMachine.defaultState = originatingState; + + rootStateMachine.AddEntryTransition(subStateMachine); + + var exitTransition = originatingState.AddExitTransition(); + exitTransition.hasExitTime = false; + exitTransition.duration = 0.25f; + exitTransition.AddCondition(AnimatorConditionMode.If, 0.0f, "ExitSubMachine"); + + var parentTransition = rootStateMachine.AddStateMachineTransition(subStateMachine, destinationState); + parentTransition.AddCondition(AnimatorConditionMode.If, 0.0f, "ExitSubMachine"); + AssetDatabase.SaveAssets(); + + var networkAnimator = CreateNetworkAnimator(controller, nameof(ConditionalExitTransitionResolvesParentDestination)); + networkAnimator.InvokeOnValidate(); + + Assert.That(networkAnimator.TransitionStateInfoList, Has.Count.EqualTo(1)); + var transitionInfo = networkAnimator.TransitionStateInfoList[0]; + Assert.That(transitionInfo.Layer, Is.Zero); + Assert.That(transitionInfo.OriginatingState, Is.EqualTo(originatingState.nameHash)); + Assert.That(transitionInfo.DestinationState, Is.EqualTo(destinationState.nameHash)); + Assert.That(transitionInfo.TransitionDuration, Is.EqualTo(exitTransition.duration)); + Assert.That(transitionInfo.TriggerNameHash, Is.EqualTo(Animator.StringToHash("ExitSubMachine"))); + } + + [Test] + public void NestedConditionalExitTransitionResolvesAncestorDestination() + { + var controller = AnimatorController.CreateAnimatorControllerAtPath(k_TestControllerPath); + controller.AddParameter("ExitNested", AnimatorControllerParameterType.Trigger); + + var rootStateMachine = controller.layers[0].stateMachine; + var destinationState = rootStateMachine.AddState("State B"); + var outerStateMachine = rootStateMachine.AddStateMachine("Outer SM"); + var innerStateMachine = outerStateMachine.AddStateMachine("Inner SM"); + var originatingState = innerStateMachine.AddState("State A"); + innerStateMachine.defaultState = originatingState; + + rootStateMachine.AddEntryTransition(outerStateMachine); + outerStateMachine.AddEntryTransition(innerStateMachine); + + var exitTransition = originatingState.AddExitTransition(); + exitTransition.hasExitTime = false; + exitTransition.duration = 0.5f; + exitTransition.AddCondition(AnimatorConditionMode.If, 0.0f, "ExitNested"); + + var exitInnerTransition = outerStateMachine.AddStateMachineExitTransition(innerStateMachine); + exitInnerTransition.AddCondition(AnimatorConditionMode.If, 0.0f, "ExitNested"); + var exitOuterTransition = rootStateMachine.AddStateMachineTransition(outerStateMachine, destinationState); + exitOuterTransition.AddCondition(AnimatorConditionMode.If, 0.0f, "ExitNested"); + AssetDatabase.SaveAssets(); + + var networkAnimator = CreateNetworkAnimator(controller, nameof(NestedConditionalExitTransitionResolvesAncestorDestination)); + networkAnimator.InvokeOnValidate(); + + Assert.That(networkAnimator.TransitionStateInfoList, Has.Count.EqualTo(1)); + var transitionInfo = networkAnimator.TransitionStateInfoList[0]; + Assert.That(transitionInfo.OriginatingState, Is.EqualTo(originatingState.nameHash)); + Assert.That(transitionInfo.DestinationState, Is.EqualTo(destinationState.nameHash)); + Assert.That(transitionInfo.TransitionDuration, Is.EqualTo(exitTransition.duration)); + } + + [Test] + public void ConditionalExitTransitionResolvesDestinationStateMachineStates() + { + var controller = AnimatorController.CreateAnimatorControllerAtPath(k_TestControllerPath); + controller.AddParameter("ExitSubMachine", AnimatorControllerParameterType.Trigger); + + var rootStateMachine = controller.layers[0].stateMachine; + var sourceStateMachine = rootStateMachine.AddStateMachine("Source SM"); + var originatingState = sourceStateMachine.AddState("State A"); + sourceStateMachine.defaultState = originatingState; + rootStateMachine.AddEntryTransition(sourceStateMachine); + + var destinationStateMachine = rootStateMachine.AddStateMachine("Destination SM"); + var defaultDestinationState = destinationStateMachine.AddState("Default State"); + var entryDestinationState = destinationStateMachine.AddState("Entry State"); + destinationStateMachine.defaultState = defaultDestinationState; + destinationStateMachine.AddEntryTransition(entryDestinationState); + + var exitTransition = originatingState.AddExitTransition(); + exitTransition.hasExitTime = false; + exitTransition.duration = 0.25f; + exitTransition.AddCondition(AnimatorConditionMode.If, 0.0f, "ExitSubMachine"); + + var parentTransition = rootStateMachine.AddStateMachineTransition(sourceStateMachine, destinationStateMachine); + parentTransition.AddCondition(AnimatorConditionMode.If, 0.0f, "ExitSubMachine"); + AssetDatabase.SaveAssets(); + + var networkAnimator = CreateNetworkAnimator(controller, nameof(ConditionalExitTransitionResolvesDestinationStateMachineStates)); + networkAnimator.InvokeOnValidate(); + + Assert.That(networkAnimator.TransitionStateInfoList, Has.Count.EqualTo(2)); + Assert.That(networkAnimator.TransitionStateInfoList.Exists(entry => entry.DestinationState == defaultDestinationState.nameHash), Is.True); + Assert.That(networkAnimator.TransitionStateInfoList.Exists(entry => entry.DestinationState == entryDestinationState.nameHash), Is.True); + } + + private TestNetworkAnimator CreateNetworkAnimator(AnimatorController controller, string name) + { + m_GameObject = new GameObject(name); + m_GameObject.AddComponent(); + var animator = m_GameObject.AddComponent(); + animator.runtimeAnimatorController = controller; + var networkAnimator = m_GameObject.AddComponent(); + networkAnimator.Animator = animator; + networkAnimator.AnimatorParameterEntries = new NetworkAnimator.AnimatorParametersListContainer(); + return networkAnimator; + } + + private class TestNetworkAnimator : NetworkAnimator + { + public void InvokeOnValidate() + { + base.OnValidate(); + } + } + } +} diff --git a/com.unity.netcode.gameobjects/Tests/Editor/NetworkAnimatorEditorTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Editor/NetworkAnimatorEditorTests.cs.meta new file mode 100644 index 0000000000..8ff8606f4c --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Editor/NetworkAnimatorEditorTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7df48e8f74684d8b8d0c650fdfb9fa1a +timeCreated: 1790424000