Skip to content

Commit e9d9454

Browse files
fix: NetworkAnimator rising RTT issue (#2236)
* fix This fixes the issue where NetworkAnimator was not completely updated to the changes with AnimationMessage. NetworkAnimator will now only send states that have changed. Removed the check for AnimationMessage.AnimationStates being null as it will always be null when deserializing. Updated several comments for readability and clarity. * test adding additional checks to assure or AnimationStates list does not increase in size over time.
1 parent 9f44f65 commit e9d9454

File tree

3 files changed

+109
-88
lines changed

3 files changed

+109
-88
lines changed

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

Lines changed: 81 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ private void BuildDestinationToTransitionInfoTable()
227227
}
228228

229229
/// <summary>
230-
/// Creates the
230+
/// Creates the TransitionStateInfoList table
231231
/// </summary>
232232
private void BuildTransitionStateInfoList()
233233
{
@@ -305,7 +305,6 @@ public void OnBeforeSerialize()
305305

306306
internal struct AnimationState : INetworkSerializable
307307
{
308-
internal bool IsDirty;
309308
// Not to be serialized, used for processing the animation state
310309
internal bool HasBeenProcessed;
311310
internal int StateHash;
@@ -392,56 +391,41 @@ internal struct AnimationMessage : INetworkSerializable
392391
// Not to be serialized, used for processing the animation message
393392
internal bool HasBeenProcessed;
394393

395-
// state hash per layer. if non-zero, then Play() this animation, skipping transitions
394+
// This is preallocated/populated in OnNetworkSpawn for all instances in the event ownership or
395+
// authority changes. When serializing, IsDirtyCount determines how many AnimationState entries
396+
// should be serialized from the list. When deserializing the list is created and populated with
397+
// only the number of AnimationStates received which is dictated by the deserialized IsDirtyCount.
396398
internal List<AnimationState> AnimationStates;
397399

398-
/// <summary>
399-
/// Resets all AnimationStates' IsDirty flag
400-
/// </summary>
401-
internal void ClearDirty()
402-
{
403-
if (AnimationStates == null)
404-
{
405-
return;
406-
}
407-
for (int i = 0; i < AnimationStates.Count; i++)
408-
{
409-
var animationState = AnimationStates[i];
410-
animationState.IsDirty = false;
411-
AnimationStates[i] = animationState;
412-
}
413-
}
400+
// Used to determine how many AnimationState entries we are sending or receiving
401+
internal int IsDirtyCount;
414402

415403
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
416404
{
405+
var animationState = new AnimationState();
417406
if (serializer.IsReader)
418407
{
419-
if (AnimationStates == null)
420-
{
421-
AnimationStates = new List<AnimationState>();
422-
}
423-
else if (AnimationStates.Count > 0)
408+
AnimationStates = new List<AnimationState>();
409+
410+
serializer.SerializeValue(ref IsDirtyCount);
411+
// Since we create a new AnimationMessage when deserializing
412+
// we need to create new animation states for each incoming
413+
// AnimationState being updated
414+
for (int i = 0; i < IsDirtyCount; i++)
424415
{
425-
AnimationStates.Clear();
416+
animationState = new AnimationState();
417+
serializer.SerializeValue(ref animationState);
418+
AnimationStates.Add(animationState);
426419
}
427420
}
428-
var count = AnimationStates.Count;
429-
serializer.SerializeValue(ref count);
430-
431-
var animationState = new AnimationState();
432-
for (int i = 0; i < count; i++)
421+
else
433422
{
434-
if (serializer.IsWriter)
423+
// When writing, only send the counted dirty animation states
424+
serializer.SerializeValue(ref IsDirtyCount);
425+
for (int i = 0; i < IsDirtyCount; i++)
435426
{
436-
if (AnimationStates[i].IsDirty)
437-
{
438-
animationState = AnimationStates[i];
439-
}
440-
}
441-
serializer.SerializeNetworkSerializable(ref animationState);
442-
if (serializer.IsReader)
443-
{
444-
AnimationStates.Add(animationState);
427+
animationState = AnimationStates[i];
428+
serializer.SerializeNetworkSerializable(ref animationState);
445429
}
446430
}
447431
}
@@ -563,7 +547,17 @@ public override void OnDestroy()
563547
private List<int> m_ParametersToUpdate;
564548
private List<ulong> m_ClientSendList;
565549
private ClientRpcParams m_ClientRpcParams;
566-
private List<AnimationState> m_AnimationMessageStates;
550+
private AnimationMessage m_AnimationMessage;
551+
552+
/// <summary>
553+
/// Used for integration test to validate that the
554+
/// AnimationMessage.AnimationStates remains the same
555+
/// size as the layer count.
556+
/// </summary>
557+
internal AnimationMessage GetAnimationMessage()
558+
{
559+
return m_AnimationMessage;
560+
}
567561

568562
// Only used in Cleanup
569563
private NetworkManager m_CachedNetworkManager;
@@ -590,17 +584,19 @@ public override void OnNetworkSpawn()
590584
m_CachedNetworkManager = NetworkManager;
591585
NetworkManager.OnClientConnectedCallback += OnClientConnectedCallback;
592586
}
593-
594-
// !! Note !!
595-
// Do not clear this list. We re-use the AnimationState entries
596-
// initialized below
597-
m_AnimationMessageStates = new List<AnimationState>();
587+
// We initialize the m_AnimationMessage for all instances in the event that
588+
// ownership or authority changes during runtime.
589+
m_AnimationMessage = new AnimationMessage();
590+
m_AnimationMessage.AnimationStates = new List<AnimationState>();
598591

599592
// Store off our current layer weights and create our animation
600593
// state entries per layer.
601594
for (int layer = 0; layer < m_Animator.layerCount; layer++)
602595
{
603-
m_AnimationMessageStates.Add(new AnimationState());
596+
// We create an AnimationState per layer to preallocate the maximum
597+
// number of possible AnimationState changes we could send in one
598+
// AnimationMessage.
599+
m_AnimationMessage.AnimationStates.Add(new AnimationState());
604600
float layerWeightNow = m_Animator.GetLayerWeight(layer);
605601
if (layerWeightNow != m_LayerWeights[layer])
606602
{
@@ -677,19 +673,19 @@ internal void ServerSynchronizeNewPlayer(ulong playerId)
677673
}
678674
SendParametersUpdate(m_ClientRpcParams);
679675

680-
var animationMessage = new AnimationMessage
681-
{
682-
// Assign the existing m_AnimationMessageStates list
683-
AnimationStates = m_AnimationMessageStates
684-
};
676+
// Reset the dirty count before synchronizing the newly connected client with all layers
677+
m_AnimationMessage.IsDirtyCount = 0;
685678

686679
for (int layer = 0; layer < m_Animator.layerCount; layer++)
687680
{
688681
AnimatorStateInfo st = m_Animator.GetCurrentAnimatorStateInfo(layer);
689682
var stateHash = st.fullPathHash;
690683
var normalizedTime = st.normalizedTime;
691684
var isInTransition = m_Animator.IsInTransition(layer);
692-
var animMsg = m_AnimationMessageStates[layer];
685+
686+
// Grab one of the available AnimationState entries so we can fill it with the current
687+
// layer's animation state.
688+
var animationState = m_AnimationMessage.AnimationStates[layer];
693689

694690
// Synchronizing transitions with trigger conditions for late joining clients is now
695691
// handled by cross fading between the late joining client's current layer's AnimationState
@@ -723,25 +719,23 @@ internal void ServerSynchronizeNewPlayer(ulong playerId)
723719
var destinationInfo = m_DestinationStateToTransitioninfo[layer][nextState.shortNameHash];
724720
stateHash = destinationInfo.OriginatingState;
725721
// Set the destination state to cross fade to from the originating state
726-
animMsg.DestinationStateHash = destinationInfo.DestinationState;
722+
animationState.DestinationStateHash = destinationInfo.DestinationState;
727723
}
728724
}
729725
}
730726

731-
animMsg.Transition = isInTransition; // The only time this could be set to true
732-
animMsg.StateHash = stateHash; // When a transition, this is the originating/starting state
733-
animMsg.NormalizedTime = normalizedTime;
734-
animMsg.Layer = layer;
735-
animMsg.Weight = m_LayerWeights[layer];
736-
animMsg.IsDirty = true;
737-
m_AnimationMessageStates[layer] = animMsg;
738-
}
739-
if (animationMessage.AnimationStates.Count > 0)
740-
{
741-
// Server always send via client RPC
742-
SendAnimStateClientRpc(animationMessage, m_ClientRpcParams);
743-
animationMessage.ClearDirty();
727+
animationState.Transition = isInTransition; // The only time this could be set to true
728+
animationState.StateHash = stateHash; // When a transition, this is the originating/starting state
729+
animationState.NormalizedTime = normalizedTime;
730+
animationState.Layer = layer;
731+
animationState.Weight = m_LayerWeights[layer];
732+
733+
// Apply the changes
734+
m_AnimationMessage.AnimationStates[layer] = animationState;
744735
}
736+
// Send all animation states
737+
m_AnimationMessage.IsDirtyCount = m_Animator.layerCount;
738+
SendAnimStateClientRpc(m_AnimationMessage, m_ClientRpcParams);
745739
}
746740

747741
/// <summary>
@@ -779,13 +773,10 @@ internal void CheckForAnimatorChanges()
779773
int stateHash;
780774
float normalizedTime;
781775

782-
var animationMessage = new AnimationMessage
783-
{
784-
// Assign the existing m_AnimationMessageStates list
785-
AnimationStates = m_AnimationMessageStates
786-
};
776+
// Reset the dirty count before checking for AnimationState updates
777+
m_AnimationMessage.IsDirtyCount = 0;
787778

788-
// This sends updates only if a layer's AnimationState changes
779+
// This sends updates only if a layer's state has changed
789780
for (int layer = 0; layer < m_Animator.layerCount; layer++)
790781
{
791782
AnimatorStateInfo st = m_Animator.GetCurrentAnimatorStateInfo(layer);
@@ -797,31 +788,33 @@ internal void CheckForAnimatorChanges()
797788
continue;
798789
}
799790

800-
var animationState = new AnimationState
801-
{
802-
IsDirty = true,
803-
Transition = false, // Only used during synchronization
804-
StateHash = stateHash,
805-
NormalizedTime = normalizedTime,
806-
Layer = layer,
807-
Weight = m_LayerWeights[layer]
808-
};
791+
// If we made it here, then we need to synchronize this layer's animation state.
792+
// Get one of the preallocated AnimationState entries and populate it with the
793+
// current layer's state.
794+
var animationState = m_AnimationMessage.AnimationStates[m_AnimationMessage.IsDirtyCount];
795+
796+
animationState.Transition = false; // Only used during synchronization
797+
animationState.StateHash = stateHash;
798+
animationState.NormalizedTime = normalizedTime;
799+
animationState.Layer = layer;
800+
animationState.Weight = m_LayerWeights[layer];
809801

810-
animationMessage.AnimationStates.Add(animationState);
802+
// Apply the changes
803+
m_AnimationMessage.AnimationStates[m_AnimationMessage.IsDirtyCount] = animationState;
804+
m_AnimationMessage.IsDirtyCount++;
811805
}
812806

813-
// Make sure there is something to send
814-
if (animationMessage.AnimationStates.Count > 0)
807+
// Send an AnimationMessage only if there are dirty AnimationStates to send
808+
if (m_AnimationMessage.IsDirtyCount > 0)
815809
{
816810
if (!IsServer && IsOwner)
817811
{
818-
SendAnimStateServerRpc(animationMessage);
812+
SendAnimStateServerRpc(m_AnimationMessage);
819813
}
820814
else
821815
{
822-
SendAnimStateClientRpc(animationMessage);
816+
SendAnimStateClientRpc(m_AnimationMessage);
823817
}
824-
animationMessage.ClearDirty();
825818
}
826819
}
827820

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ private void Awake()
2929
m_NetworkAnimator = GetComponent<NetworkAnimator>();
3030
}
3131

32+
internal int GetAnimatorStateCount()
33+
{
34+
return m_NetworkAnimator.GetAnimationMessage().AnimationStates.Count;
35+
}
36+
3237
public override void OnNetworkSpawn()
3338
{
3439
if (IsTriggerTest)
@@ -117,6 +122,11 @@ public void SetTrigger(string name = "TestTrigger", bool monitorTrigger = false)
117122
}
118123
}
119124

125+
public void SetBool(string name, bool valueToSet)
126+
{
127+
m_Animator.SetBool(name, valueToSet);
128+
}
129+
120130
private System.Collections.IEnumerator TriggerMonitor(string triggerName)
121131
{
122132
var triggerStatus = m_Animator.GetBool(triggerName);

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ private bool WaitForClientsToInitialize()
239239
public IEnumerator TriggerUpdateTests([Values] OwnerShipMode ownerShipMode, [Values] AuthoritativeMode authoritativeMode)
240240
{
241241
CheckStateEnterCount.ResetTest();
242+
242243
VerboseDebug($" ++++++++++++++++++ Trigger Test [{TriggerTest.Iteration}][{ownerShipMode}] Starting ++++++++++++++++++ ");
243244
TriggerTest.IsVerboseDebug = m_EnableVerboseDebug;
244245
AnimatorTestHelper.IsTriggerTest = m_EnableVerboseDebug;
@@ -254,6 +255,10 @@ public IEnumerator TriggerUpdateTests([Values] OwnerShipMode ownerShipMode, [Val
254255
yield return WaitForConditionOrTimeOut(WaitForClientsToInitialize);
255256
AssertOnTimeout($"Timed out waiting for the client-side instance of {GetNetworkAnimatorName(authoritativeMode)} to be spawned!");
256257
var animatorTestHelper = ownerShipMode == OwnerShipMode.ClientOwner ? AnimatorTestHelper.ClientSideInstances[m_ClientNetworkManagers[0].LocalClientId] : AnimatorTestHelper.ServerSideInstance;
258+
var layerCount = animatorTestHelper.GetAnimator().layerCount;
259+
var animationStateCount = animatorTestHelper.GetAnimatorStateCount();
260+
261+
Assert.True(layerCount == animationStateCount, $"AnimationState count {animationStateCount} does not equal the layer count {layerCount}!");
257262
if (authoritativeMode == AuthoritativeMode.ServerAuth)
258263
{
259264
animatorTestHelper = AnimatorTestHelper.ServerSideInstance;
@@ -300,6 +305,19 @@ public IEnumerator TriggerUpdateTests([Values] OwnerShipMode ownerShipMode, [Val
300305
yield return WaitForConditionOrTimeOut(() => CheckStateEnterCount.AllStatesEnteredMatch(clientIdList));
301306
AssertOnTimeout($"Timed out waiting for all states entered to match!");
302307

308+
// Now, update some states for several seconds to assure the AnimationState count does not grow
309+
var waitForSeconds = new WaitForSeconds(0.25f);
310+
bool rotateToggle = true;
311+
for (int i = 0; i < 10; i++)
312+
{
313+
animatorTestHelper.SetBool("Rotate", rotateToggle);
314+
animatorTestHelper.SetTrigger("Pulse");
315+
animationStateCount = animatorTestHelper.GetAnimatorStateCount();
316+
Assert.True(layerCount == animationStateCount, $"AnimationState count {animationStateCount} does not equal the layer count {layerCount}!");
317+
yield return waitForSeconds;
318+
rotateToggle = !rotateToggle;
319+
}
320+
303321
AnimatorTestHelper.IsTriggerTest = false;
304322
VerboseDebug($" ------------------ Trigger Test [{TriggerTest.Iteration}][{ownerShipMode}] Stopping ------------------ ");
305323
}

0 commit comments

Comments
 (0)