Skip to content
2 changes: 2 additions & 0 deletions com.unity.netcode.gameobjects/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ Additional documentation and release notes are available at [Multiplayer Documen

### Fixed

- Fixed issue where `NetworkAnimator` was not properly detecting and synchronizing cross fade initiated transitions. (#2481)
- Fixed issue where `NetworkAnimator` was not properly synchronizing animation state updates. (#2481)
- 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)
- Fixed registry of public `NetworkVariable`s in derived `NetworkBehaviour`s (#2423)
- Fixed issue where runtime association of `Animator` properties to `AnimationCurve`s would cause `NetworkAnimator` to attempt to update those changes. (#2416)
Expand Down
334 changes: 165 additions & 169 deletions com.unity.netcode.gameobjects/Components/NetworkAnimator.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -941,7 +941,11 @@ public void Dispose()
}
}
QueuedSceneJobs.Clear();
Object.Destroy(CoroutineRunner.gameObject);
if (CoroutineRunner != null && CoroutineRunner.gameObject != null)
{
Object.Destroy(CoroutineRunner.gameObject);
}

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,24 @@ private float GetLayerWeight(int layer)
return m_Animator.GetLayerWeight(layer);
}

[ServerRpc]
private void TestCrossFadeServerRpc()
{
m_Animator.CrossFade("CrossFadeState", 0.25f, 0);
}

private void TestCrossFade()
{
if (!IsServer && m_IsServerAuthoritative)
{
TestCrossFadeServerRpc();
}
else
{
m_Animator.CrossFade("CrossFadeState", 0.25f, 0);
}
}

private void LateUpdate()
{

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

DisplayTestIntValueIfChanged();

if (Input.GetKeyDown(KeyCode.G))
{
TestCrossFade();
}

// Rotates the cube
if (Input.GetKeyDown(KeyCode.C))
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System.Collections.Generic;
using Unity.Netcode;
using UnityEngine;

/// <summary>
/// This StateMachineBehaviour is used to detect an <see cref="Animator.CrossFade"/> initiated transition
/// for integration test purposes.
/// </summary>
public class CrossFadeTransitionDetect : StateMachineBehaviour
{
public static Dictionary<ulong, Dictionary<int, AnimatorStateInfo>> StatesEntered = new Dictionary<ulong, Dictionary<int, AnimatorStateInfo>>();
public static bool IsVerboseDebug;

public static string CurrentTargetStateName { get; private set; }
public static int CurrentTargetStateHash { get; private set; }

public static List<ulong> ClientIds = new List<ulong>();

public static void ResetTest()
{
ClientIds.Clear();
StatesEntered.Clear();
IsVerboseDebug = false;
}

private void Log(string logMessage)
{
if (!IsVerboseDebug)
{
return;
}
Debug.Log($"[CrossFadeDetect] {logMessage}");
}

public static bool AllClientsTransitioned()
{
foreach (var clientId in ClientIds)
{
if (!StatesEntered.ContainsKey(clientId))
{
return false;
}

if (!StatesEntered[clientId].ContainsKey(CurrentTargetStateHash))
{
return false;
}
}
return true;
}

public static void SetTargetAnimationState(string animationStateName)
{
CurrentTargetStateName = animationStateName;
CurrentTargetStateHash = Animator.StringToHash(animationStateName);
}

public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
if (stateInfo.shortNameHash != CurrentTargetStateHash)
{
Log($"[Ignoring State][Layer-{layerIndex}] Incoming: ({stateInfo.fullPathHash}) | Targeting: ({CurrentTargetStateHash})");
return;
}

var networkObject = animator.GetComponent<NetworkObject>();
if (networkObject == null || networkObject.NetworkManager == null || !networkObject.IsSpawned)
{
return;
}

var clientId = networkObject.NetworkManager.LocalClientId;
if (!StatesEntered.ContainsKey(clientId))
{
StatesEntered.Add(clientId, new Dictionary<int, AnimatorStateInfo>());
}

if (!StatesEntered[clientId].ContainsKey(stateInfo.shortNameHash))
{
StatesEntered[clientId].Add(stateInfo.shortNameHash, stateInfo);
}
else
{
StatesEntered[clientId][stateInfo.shortNameHash] = stateInfo;
}

Log($"[{layerIndex}][STATE-ENTER][{clientId}] {networkObject.NetworkManager.name} entered state {stateInfo.shortNameHash}!");
}
}

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

Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,34 @@ AnimatorState:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1102 &-7324519211837832008
AnimatorState:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: CrossFadeState
m_Speed: 1
m_CycleOffset: 0
m_Transitions:
- {fileID: -5899436739107315318}
m_StateMachineBehaviours:
- {fileID: 8360333217518347423}
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 7400000, guid: bd13c1363af7aaf4db0ffb085ac89d77, type: 2}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1101 &-6396453490711135124
AnimatorStateTransition:
m_ObjectHideFlags: 1
Expand Down Expand Up @@ -238,6 +266,28 @@ AnimatorStateTransition:
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &-5899436739107315318
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: -1676030328622575462}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.75
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1102 &-5552815716159021554
AnimatorState:
serializedVersion: 6
Expand Down Expand Up @@ -629,6 +679,9 @@ AnimatorStateMachine:
- serializedVersion: 1
m_State: {fileID: -1603678049383302394}
m_Position: {x: 440, y: 190, z: 0}
- serializedVersion: 1
m_State: {fileID: -7324519211837832008}
m_Position: {x: 570, y: -140, z: 0}
m_ChildStateMachines: []
m_AnyStateTransitions: []
m_EntryTransitions: []
Expand Down Expand Up @@ -1266,3 +1319,15 @@ AnimatorStateMachine:
m_ExitPosition: {x: 800, y: 120, z: 0}
m_ParentStateMachinePosition: {x: 800, y: 20, z: 0}
m_DefaultState: {fileID: 5205197960406981613}
--- !u!114 &8360333217518347423
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 53314cafa8e073c40b0b35dd25485c42, type: 3}
m_Name:
m_EditorClassIdentifier:
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,13 @@ public void SetTrigger(string name = "TestTrigger", bool monitorTrigger = false)
}
}

public const string TargetCrossFadeState = "CrossFadeState";

public void TestCrossFade()
{
m_Animator.CrossFade(TargetCrossFadeState, 0.25f, 0);
}

public void SetBool(string name, bool valueToSet)
{
m_Animator.SetBool(name, valueToSet);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,76 @@ private bool WaitForClientsToInitialize()
return true;
}

private bool AllClientsTransitioningAnyState()
{
foreach (var networkManager in m_ClientNetworkManagers)
{
var clientId = networkManager.LocalClientId;
if (!AnimatorTestHelper.ClientSideInstances.ContainsKey(clientId))
{
return false;
}
var animator = AnimatorTestHelper.ClientSideInstances[clientId].GetComponent<Animator>();
if (!animator.isInitialized)
{
return false;
}
var transitionInfo = animator.GetAnimatorTransitionInfo(0);
if (!transitionInfo.anyState)
{
return false;
}
VerboseDebug($"{networkManager.name} transitioning from AnyState or CrossFade.");
}
return true;
}

/// <summary>
/// Verifies that cross fading is synchronized with currently connected clients
/// </summary>
[UnityTest]
public IEnumerator CrossFadeTransitionTests([Values] OwnerShipMode ownerShipMode, [Values] AuthoritativeMode authoritativeMode)
{
CrossFadeTransitionDetect.ResetTest();
CrossFadeTransitionDetect.SetTargetAnimationState(AnimatorTestHelper.TargetCrossFadeState);
VerboseDebug($" ++++++++++++++++++ Cross Fade Transition Test [{ownerShipMode}] Starting ++++++++++++++++++ ");
CrossFadeTransitionDetect.IsVerboseDebug = m_EnableVerboseDebug;

// Spawn our test animator object
var objectInstance = SpawnPrefab(ownerShipMode == OwnerShipMode.ClientOwner, authoritativeMode);

// Wait for it to spawn server-side
var success = WaitForConditionOrTimeOutWithTimeTravel(() => AnimatorTestHelper.ServerSideInstance != null);
Assert.True(success, $"Timed out waiting for the server-side instance of {GetNetworkAnimatorName(authoritativeMode)} to be spawned!");

// Wait for it to spawn client-side
success = WaitForConditionOrTimeOutWithTimeTravel(WaitForClientsToInitialize);
Assert.True(success, $"Timed out waiting for the client-side instance of {GetNetworkAnimatorName(authoritativeMode)} to be spawned!");
var animatorTestHelper = ownerShipMode == OwnerShipMode.ClientOwner ? AnimatorTestHelper.ClientSideInstances[m_ClientNetworkManagers[0].LocalClientId] : AnimatorTestHelper.ServerSideInstance;
var layerCount = animatorTestHelper.GetAnimator().layerCount;

var animationStateCount = animatorTestHelper.GetAnimatorStateCount();
Assert.True(layerCount == animationStateCount, $"AnimationState count {animationStateCount} does not equal the layer count {layerCount}!");

if (authoritativeMode == AuthoritativeMode.ServerAuth)
{
animatorTestHelper = AnimatorTestHelper.ServerSideInstance;
}

CrossFadeTransitionDetect.ClientIds.Add(m_ServerNetworkManager.LocalClientId);
foreach (var client in m_ClientNetworkManagers)
{
CrossFadeTransitionDetect.ClientIds.Add(client.LocalClientId);
}

animatorTestHelper.TestCrossFade();

// Verify the host and all clients performed a cross fade transition
yield return WaitForConditionOrTimeOut(CrossFadeTransitionDetect.AllClientsTransitioned);
AssertOnTimeout($"Timed out waiting for all clients to transition from synchronized cross fade!");
}


/// <summary>
/// Verifies that triggers are synchronized with currently connected clients
/// </summary>
Expand Down Expand Up @@ -331,8 +401,6 @@ public IEnumerator TriggerUpdateTests([Values] OwnerShipMode ownerShipMode, [Val
// Verify we only entered each state once
success = WaitForConditionOrTimeOutWithTimeTravel(() => CheckStateEnterCount.AllStatesEnteredMatch(clientIdList));
Assert.True(success, $"Timed out waiting for all states entered to match!");
// Since the com.unity.netcode.components does not allow test project to access its internals
// during runtime, this is only used when running test runner from within the editor

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