Skip to content

Commit 4ebfb5f

Browse files
fix: NetworkAnimator not detecting and synchronizing cross fade initiated transitions (#2481)
* fix For client authoritative, the server now actually sends any pending animation updates to any non-authoritative clients. Trigger not updating properly on server when server authoritative but was the owner sending the trigger update. Improve detection of the type of animation state change. This fixes the issue where cross fades were not being handled by NetworkAnimator. This is a first pass fix and the serialization could use an overhaul to reduce bandwidth costs. * update Making sure we don't toggle between transition and crossfade. Packing values for AnimationState Updating the manual test used to get cross fading to AnimationStates without transitions working. Making the serialization of the two bools one byte instead of two bytes. * test Adding some cross fade tests to the NetworkAnimatorTests. Also fixed an issue with the coroutineRunner being destroyed before the IntegrationTestSceneHandler was. This is the final test for cross fade initiated transitions and whether it is detected and synchronized or not.
1 parent cfb3148 commit 4ebfb5f

File tree

9 files changed

+437
-172
lines changed

9 files changed

+437
-172
lines changed

com.unity.netcode.gameobjects/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ Additional documentation and release notes are available at [Multiplayer Documen
4141

4242
### Fixed
4343

44+
- Fixed issue where `NetworkAnimator` was not properly detecting and synchronizing cross fade initiated transitions. (#2481)
45+
- Fixed issue where `NetworkAnimator` was not properly synchronizing animation state updates. (#2481)
4446
- Fixed an issue where Named Message Handlers could remove themselves causing an exception when the metrics tried to access the name of the message.(#2426)
4547
- Fixed registry of public `NetworkVariable`s in derived `NetworkBehaviour`s (#2423)
4648
- Fixed issue where runtime association of `Animator` properties to `AnimationCurve`s would cause `NetworkAnimator` to attempt to update those changes. (#2416)

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

Lines changed: 165 additions & 169 deletions
Large diffs are not rendered by default.

com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestSceneHandler.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -941,7 +941,11 @@ public void Dispose()
941941
}
942942
}
943943
QueuedSceneJobs.Clear();
944-
Object.Destroy(CoroutineRunner.gameObject);
944+
if (CoroutineRunner != null && CoroutineRunner.gameObject != null)
945+
{
946+
Object.Destroy(CoroutineRunner.gameObject);
947+
}
948+
945949
}
946950
}
947951
}

testproject/Assets/Tests/Manual/NetworkAnimatorTests/AnimatedCubeController.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,24 @@ private float GetLayerWeight(int layer)
189189
return m_Animator.GetLayerWeight(layer);
190190
}
191191

192+
[ServerRpc]
193+
private void TestCrossFadeServerRpc()
194+
{
195+
m_Animator.CrossFade("CrossFadeState", 0.25f, 0);
196+
}
197+
198+
private void TestCrossFade()
199+
{
200+
if (!IsServer && m_IsServerAuthoritative)
201+
{
202+
TestCrossFadeServerRpc();
203+
}
204+
else
205+
{
206+
m_Animator.CrossFade("CrossFadeState", 0.25f, 0);
207+
}
208+
}
209+
192210
private void LateUpdate()
193211
{
194212

@@ -209,6 +227,11 @@ private void LateUpdate()
209227

210228
DisplayTestIntValueIfChanged();
211229

230+
if (Input.GetKeyDown(KeyCode.G))
231+
{
232+
TestCrossFade();
233+
}
234+
212235
// Rotates the cube
213236
if (Input.GetKeyDown(KeyCode.C))
214237
{
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
using System.Collections.Generic;
2+
using Unity.Netcode;
3+
using UnityEngine;
4+
5+
/// <summary>
6+
/// This StateMachineBehaviour is used to detect an <see cref="Animator.CrossFade"/> initiated transition
7+
/// for integration test purposes.
8+
/// </summary>
9+
public class CrossFadeTransitionDetect : StateMachineBehaviour
10+
{
11+
public static Dictionary<ulong, Dictionary<int, AnimatorStateInfo>> StatesEntered = new Dictionary<ulong, Dictionary<int, AnimatorStateInfo>>();
12+
public static bool IsVerboseDebug;
13+
14+
public static string CurrentTargetStateName { get; private set; }
15+
public static int CurrentTargetStateHash { get; private set; }
16+
17+
public static List<ulong> ClientIds = new List<ulong>();
18+
19+
public static void ResetTest()
20+
{
21+
ClientIds.Clear();
22+
StatesEntered.Clear();
23+
IsVerboseDebug = false;
24+
}
25+
26+
private void Log(string logMessage)
27+
{
28+
if (!IsVerboseDebug)
29+
{
30+
return;
31+
}
32+
Debug.Log($"[CrossFadeDetect] {logMessage}");
33+
}
34+
35+
public static bool AllClientsTransitioned()
36+
{
37+
foreach (var clientId in ClientIds)
38+
{
39+
if (!StatesEntered.ContainsKey(clientId))
40+
{
41+
return false;
42+
}
43+
44+
if (!StatesEntered[clientId].ContainsKey(CurrentTargetStateHash))
45+
{
46+
return false;
47+
}
48+
}
49+
return true;
50+
}
51+
52+
public static void SetTargetAnimationState(string animationStateName)
53+
{
54+
CurrentTargetStateName = animationStateName;
55+
CurrentTargetStateHash = Animator.StringToHash(animationStateName);
56+
}
57+
58+
public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
59+
{
60+
if (stateInfo.shortNameHash != CurrentTargetStateHash)
61+
{
62+
Log($"[Ignoring State][Layer-{layerIndex}] Incoming: ({stateInfo.fullPathHash}) | Targeting: ({CurrentTargetStateHash})");
63+
return;
64+
}
65+
66+
var networkObject = animator.GetComponent<NetworkObject>();
67+
if (networkObject == null || networkObject.NetworkManager == null || !networkObject.IsSpawned)
68+
{
69+
return;
70+
}
71+
72+
var clientId = networkObject.NetworkManager.LocalClientId;
73+
if (!StatesEntered.ContainsKey(clientId))
74+
{
75+
StatesEntered.Add(clientId, new Dictionary<int, AnimatorStateInfo>());
76+
}
77+
78+
if (!StatesEntered[clientId].ContainsKey(stateInfo.shortNameHash))
79+
{
80+
StatesEntered[clientId].Add(stateInfo.shortNameHash, stateInfo);
81+
}
82+
else
83+
{
84+
StatesEntered[clientId][stateInfo.shortNameHash] = stateInfo;
85+
}
86+
87+
Log($"[{layerIndex}][STATE-ENTER][{clientId}] {networkObject.NetworkManager.name} entered state {stateInfo.shortNameHash}!");
88+
}
89+
}

testproject/Assets/Tests/Manual/NetworkAnimatorTests/CrossFadeTransitionDetect.cs.meta

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

testproject/Assets/Tests/Manual/NetworkAnimatorTests/CubeAnimatorController.controller

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,34 @@ AnimatorState:
188188
m_MirrorParameter:
189189
m_CycleOffsetParameter:
190190
m_TimeParameter:
191+
--- !u!1102 &-7324519211837832008
192+
AnimatorState:
193+
serializedVersion: 6
194+
m_ObjectHideFlags: 1
195+
m_CorrespondingSourceObject: {fileID: 0}
196+
m_PrefabInstance: {fileID: 0}
197+
m_PrefabAsset: {fileID: 0}
198+
m_Name: CrossFadeState
199+
m_Speed: 1
200+
m_CycleOffset: 0
201+
m_Transitions:
202+
- {fileID: -5899436739107315318}
203+
m_StateMachineBehaviours:
204+
- {fileID: 8360333217518347423}
205+
m_Position: {x: 50, y: 50, z: 0}
206+
m_IKOnFeet: 0
207+
m_WriteDefaultValues: 1
208+
m_Mirror: 0
209+
m_SpeedParameterActive: 0
210+
m_MirrorParameterActive: 0
211+
m_CycleOffsetParameterActive: 0
212+
m_TimeParameterActive: 0
213+
m_Motion: {fileID: 7400000, guid: bd13c1363af7aaf4db0ffb085ac89d77, type: 2}
214+
m_Tag:
215+
m_SpeedParameter:
216+
m_MirrorParameter:
217+
m_CycleOffsetParameter:
218+
m_TimeParameter:
191219
--- !u!1101 &-6396453490711135124
192220
AnimatorStateTransition:
193221
m_ObjectHideFlags: 1
@@ -238,6 +266,28 @@ AnimatorStateTransition:
238266
m_InterruptionSource: 0
239267
m_OrderedInterruption: 1
240268
m_CanTransitionToSelf: 1
269+
--- !u!1101 &-5899436739107315318
270+
AnimatorStateTransition:
271+
m_ObjectHideFlags: 1
272+
m_CorrespondingSourceObject: {fileID: 0}
273+
m_PrefabInstance: {fileID: 0}
274+
m_PrefabAsset: {fileID: 0}
275+
m_Name:
276+
m_Conditions: []
277+
m_DstStateMachine: {fileID: 0}
278+
m_DstState: {fileID: -1676030328622575462}
279+
m_Solo: 0
280+
m_Mute: 0
281+
m_IsExit: 0
282+
serializedVersion: 3
283+
m_TransitionDuration: 0.25
284+
m_TransitionOffset: 0
285+
m_ExitTime: 0.75
286+
m_HasExitTime: 1
287+
m_HasFixedDuration: 1
288+
m_InterruptionSource: 0
289+
m_OrderedInterruption: 1
290+
m_CanTransitionToSelf: 1
241291
--- !u!1102 &-5552815716159021554
242292
AnimatorState:
243293
serializedVersion: 6
@@ -629,6 +679,9 @@ AnimatorStateMachine:
629679
- serializedVersion: 1
630680
m_State: {fileID: -1603678049383302394}
631681
m_Position: {x: 440, y: 190, z: 0}
682+
- serializedVersion: 1
683+
m_State: {fileID: -7324519211837832008}
684+
m_Position: {x: 570, y: -140, z: 0}
632685
m_ChildStateMachines: []
633686
m_AnyStateTransitions: []
634687
m_EntryTransitions: []
@@ -1266,3 +1319,15 @@ AnimatorStateMachine:
12661319
m_ExitPosition: {x: 800, y: 120, z: 0}
12671320
m_ParentStateMachinePosition: {x: 800, y: 20, z: 0}
12681321
m_DefaultState: {fileID: 5205197960406981613}
1322+
--- !u!114 &8360333217518347423
1323+
MonoBehaviour:
1324+
m_ObjectHideFlags: 1
1325+
m_CorrespondingSourceObject: {fileID: 0}
1326+
m_PrefabInstance: {fileID: 0}
1327+
m_PrefabAsset: {fileID: 0}
1328+
m_GameObject: {fileID: 0}
1329+
m_Enabled: 1
1330+
m_EditorHideFlags: 0
1331+
m_Script: {fileID: 11500000, guid: 53314cafa8e073c40b0b35dd25485c42, type: 3}
1332+
m_Name:
1333+
m_EditorClassIdentifier:

testproject/Assets/Tests/Runtime/Animation/AnimatorTestHelper.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,13 @@ public void SetTrigger(string name = "TestTrigger", bool monitorTrigger = false)
122122
}
123123
}
124124

125+
public const string TargetCrossFadeState = "CrossFadeState";
126+
127+
public void TestCrossFade()
128+
{
129+
m_Animator.CrossFade(TargetCrossFadeState, 0.25f, 0);
130+
}
131+
125132
public void SetBool(string name, bool valueToSet)
126133
{
127134
m_Animator.SetBool(name, valueToSet);

testproject/Assets/Tests/Runtime/Animation/NetworkAnimatorTests.cs

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,76 @@ private bool WaitForClientsToInitialize()
257257
return true;
258258
}
259259

260+
private bool AllClientsTransitioningAnyState()
261+
{
262+
foreach (var networkManager in m_ClientNetworkManagers)
263+
{
264+
var clientId = networkManager.LocalClientId;
265+
if (!AnimatorTestHelper.ClientSideInstances.ContainsKey(clientId))
266+
{
267+
return false;
268+
}
269+
var animator = AnimatorTestHelper.ClientSideInstances[clientId].GetComponent<Animator>();
270+
if (!animator.isInitialized)
271+
{
272+
return false;
273+
}
274+
var transitionInfo = animator.GetAnimatorTransitionInfo(0);
275+
if (!transitionInfo.anyState)
276+
{
277+
return false;
278+
}
279+
VerboseDebug($"{networkManager.name} transitioning from AnyState or CrossFade.");
280+
}
281+
return true;
282+
}
283+
284+
/// <summary>
285+
/// Verifies that cross fading is synchronized with currently connected clients
286+
/// </summary>
287+
[UnityTest]
288+
public IEnumerator CrossFadeTransitionTests([Values] OwnerShipMode ownerShipMode, [Values] AuthoritativeMode authoritativeMode)
289+
{
290+
CrossFadeTransitionDetect.ResetTest();
291+
CrossFadeTransitionDetect.SetTargetAnimationState(AnimatorTestHelper.TargetCrossFadeState);
292+
VerboseDebug($" ++++++++++++++++++ Cross Fade Transition Test [{ownerShipMode}] Starting ++++++++++++++++++ ");
293+
CrossFadeTransitionDetect.IsVerboseDebug = m_EnableVerboseDebug;
294+
295+
// Spawn our test animator object
296+
var objectInstance = SpawnPrefab(ownerShipMode == OwnerShipMode.ClientOwner, authoritativeMode);
297+
298+
// Wait for it to spawn server-side
299+
var success = WaitForConditionOrTimeOutWithTimeTravel(() => AnimatorTestHelper.ServerSideInstance != null);
300+
Assert.True(success, $"Timed out waiting for the server-side instance of {GetNetworkAnimatorName(authoritativeMode)} to be spawned!");
301+
302+
// Wait for it to spawn client-side
303+
success = WaitForConditionOrTimeOutWithTimeTravel(WaitForClientsToInitialize);
304+
Assert.True(success, $"Timed out waiting for the client-side instance of {GetNetworkAnimatorName(authoritativeMode)} to be spawned!");
305+
var animatorTestHelper = ownerShipMode == OwnerShipMode.ClientOwner ? AnimatorTestHelper.ClientSideInstances[m_ClientNetworkManagers[0].LocalClientId] : AnimatorTestHelper.ServerSideInstance;
306+
var layerCount = animatorTestHelper.GetAnimator().layerCount;
307+
308+
var animationStateCount = animatorTestHelper.GetAnimatorStateCount();
309+
Assert.True(layerCount == animationStateCount, $"AnimationState count {animationStateCount} does not equal the layer count {layerCount}!");
310+
311+
if (authoritativeMode == AuthoritativeMode.ServerAuth)
312+
{
313+
animatorTestHelper = AnimatorTestHelper.ServerSideInstance;
314+
}
315+
316+
CrossFadeTransitionDetect.ClientIds.Add(m_ServerNetworkManager.LocalClientId);
317+
foreach (var client in m_ClientNetworkManagers)
318+
{
319+
CrossFadeTransitionDetect.ClientIds.Add(client.LocalClientId);
320+
}
321+
322+
animatorTestHelper.TestCrossFade();
323+
324+
// Verify the host and all clients performed a cross fade transition
325+
yield return WaitForConditionOrTimeOut(CrossFadeTransitionDetect.AllClientsTransitioned);
326+
AssertOnTimeout($"Timed out waiting for all clients to transition from synchronized cross fade!");
327+
}
328+
329+
260330
/// <summary>
261331
/// Verifies that triggers are synchronized with currently connected clients
262332
/// </summary>
@@ -331,8 +401,6 @@ public IEnumerator TriggerUpdateTests([Values] OwnerShipMode ownerShipMode, [Val
331401
// Verify we only entered each state once
332402
success = WaitForConditionOrTimeOutWithTimeTravel(() => CheckStateEnterCount.AllStatesEnteredMatch(clientIdList));
333403
Assert.True(success, $"Timed out waiting for all states entered to match!");
334-
// Since the com.unity.netcode.components does not allow test project to access its internals
335-
// during runtime, this is only used when running test runner from within the editor
336404

337405
// Now, update some states for several seconds to assure the AnimationState count does not grow
338406
var waitForSeconds = new WaitForSeconds(0.25f);

0 commit comments

Comments
 (0)