Skip to content

Commit fc9f032

Browse files
fix: late-joining client cross-fades when not on the originating state
A late-joining client starts in its default state, so requiring the originating state left it stuck there. Adds a runtime test that joins mid-transition from a non-default state.
1 parent 96e3a00 commit fc9f032

4 files changed

Lines changed: 126 additions & 2 deletions

File tree

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1572,8 +1572,9 @@ private void UpdateAnimationState(AnimationState animationState)
15721572
{
15731573
if (TryGetTransitionInfo(animationState.Layer, animationState.DestinationStateHash, animationState.StateHash, out var transitionInfo))
15741574
{
1575-
// Make sure we are on the originating/starting state we are going to cross-fade into
1576-
if (currentState.shortNameHash == animationState.StateHash)
1575+
// A late-joining client starts in its default state, which may not be the originating state,
1576+
// so it cross-fades from whatever state it is in unless it is already mid-transition
1577+
if (currentState.shortNameHash == animationState.StateHash || !m_Animator.IsInTransition(animationState.Layer))
15771578
{
15781579
// Cross-fade from the current to the destination state for the transitions duration while starting at the server's current normalized time of the transition
15791580
m_Animator.CrossFade(transitionInfo.DestinationState, transitionInfo.TransitionDuration, transitionInfo.Layer, 0.0f, animationState.NormalizedTime);
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#if COM_UNITY_MODULES_ANIMATION && UNITY_EDITOR
2+
using System.Collections;
3+
using System.Text;
4+
using Unity.Netcode.Components;
5+
using Unity.Netcode.TestHelpers.Runtime;
6+
using UnityEditor;
7+
using UnityEditor.Animations;
8+
using UnityEngine;
9+
using UnityEngine.TestTools;
10+
11+
namespace Unity.Netcode.RuntimeTests
12+
{
13+
/// <summary>
14+
/// Verifies a client that joins while the authority is mid-transition from a state other than
15+
/// the controller's default state still cross-fades into the transition's destination state.
16+
/// </summary>
17+
internal class NetworkAnimatorLateJoinTransitionTests : NetcodeIntegrationTest
18+
{
19+
private const string k_ControllerPath = "Assets/NetworkAnimatorLateJoinTransitionTest.controller";
20+
private const string k_TriggerName = "ToB";
21+
22+
protected override int NumberOfClients => 1;
23+
24+
private AnimatorController m_Controller;
25+
private AnimatorState m_DefaultState;
26+
private AnimatorState m_SourceState;
27+
private AnimatorState m_DestinationState;
28+
private GameObject m_Prefab;
29+
30+
protected override void OnServerAndClientsCreated()
31+
{
32+
m_Controller = AnimatorController.CreateAnimatorControllerAtPath(k_ControllerPath);
33+
m_Controller.AddParameter(k_TriggerName, AnimatorControllerParameterType.Trigger);
34+
35+
var rootStateMachine = m_Controller.layers[0].stateMachine;
36+
m_DefaultState = rootStateMachine.AddState("State A");
37+
m_SourceState = rootStateMachine.AddState("State C");
38+
m_DestinationState = rootStateMachine.AddState("State B");
39+
rootStateMachine.defaultState = m_DefaultState;
40+
41+
// Both states transition into the same destination, and the transitions are long enough to still be running when the client joins
42+
foreach (var state in new[] { m_DefaultState, m_SourceState })
43+
{
44+
var transition = state.AddTransition(m_DestinationState);
45+
transition.hasExitTime = false;
46+
transition.duration = 10.0f;
47+
transition.AddCondition(AnimatorConditionMode.If, 0.0f, k_TriggerName);
48+
}
49+
AssetDatabase.SaveAssets();
50+
51+
m_Prefab = CreateNetworkObjectPrefab("Animated");
52+
var animator = m_Prefab.AddComponent<Animator>();
53+
animator.runtimeAnimatorController = m_Controller;
54+
var networkAnimator = m_Prefab.AddComponent<TestNetworkAnimator>();
55+
networkAnimator.Animator = animator;
56+
networkAnimator.AnimatorParameterEntries = new NetworkAnimator.AnimatorParametersListContainer();
57+
networkAnimator.InvokeOnValidate();
58+
networkAnimator.OnAfterDeserialize();
59+
60+
base.OnServerAndClientsCreated();
61+
}
62+
63+
protected override IEnumerator OnTearDown()
64+
{
65+
AssetDatabase.DeleteAsset(k_ControllerPath);
66+
yield return base.OnTearDown();
67+
}
68+
69+
[UnityTest]
70+
public IEnumerator LateJoinFromNonDefaultStateCrossFadesToDestination()
71+
{
72+
var authority = GetAuthorityNetworkManager();
73+
var authorityInstance = SpawnObject(m_Prefab, authority).GetComponent<NetworkObject>();
74+
var authorityAnimator = authorityInstance.GetComponent<Animator>();
75+
76+
authorityAnimator.Play(m_SourceState.nameHash, 0);
77+
yield return WaitForConditionOrTimeOut(_ => authorityAnimator.GetCurrentAnimatorStateInfo(0).shortNameHash == m_SourceState.nameHash);
78+
AssertOnTimeout($"Authority did not enter {m_SourceState.name}!");
79+
80+
authorityAnimator.SetTrigger(k_TriggerName);
81+
yield return WaitForConditionOrTimeOut(_ => authorityAnimator.IsInTransition(0) &&
82+
authorityAnimator.GetNextAnimatorStateInfo(0).shortNameHash == m_DestinationState.nameHash);
83+
AssertOnTimeout($"Authority did not start transitioning from {m_SourceState.name} to {m_DestinationState.name}!");
84+
85+
yield return CreateAndStartNewClient();
86+
var lateJoinClient = m_ClientNetworkManagers[m_ClientNetworkManagers.Length - 1];
87+
88+
yield return WaitForConditionOrTimeOut(_ => lateJoinClient.SpawnManager.SpawnedObjects.ContainsKey(authorityInstance.NetworkObjectId));
89+
AssertOnTimeout($"Late-joining client did not spawn {authorityInstance.name}!");
90+
var clientAnimator = lateJoinClient.SpawnManager.SpawnedObjects[authorityInstance.NetworkObjectId].GetComponent<Animator>();
91+
92+
yield return WaitForConditionOrTimeOut(errorLog => ReachingDestination(clientAnimator, errorLog));
93+
AssertOnTimeout($"Late-joining client did not cross-fade to {m_DestinationState.name}!");
94+
}
95+
96+
private bool ReachingDestination(Animator animator, StringBuilder errorLog)
97+
{
98+
var current = animator.GetCurrentAnimatorStateInfo(0).shortNameHash;
99+
if (current == m_DestinationState.nameHash || (animator.IsInTransition(0) && animator.GetNextAnimatorStateInfo(0).shortNameHash == m_DestinationState.nameHash))
100+
{
101+
return true;
102+
}
103+
errorLog.Append($"Client current state hash {current}, in transition: {animator.IsInTransition(0)}");
104+
return false;
105+
}
106+
107+
private class TestNetworkAnimator : NetworkAnimator
108+
{
109+
public void InvokeOnValidate()
110+
{
111+
base.OnValidate();
112+
}
113+
}
114+
}
115+
}
116+
#endif

‎com.unity.netcode.gameobjects/Tests/Runtime/Components/NetworkAnimatorLateJoinTransitionTests.cs.meta‎

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

‎com.unity.netcode.gameobjects/Tests/Runtime/Unity.Netcode.Runtime.Tests.asmdef‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@
4545
"expression": "1.0.0-pre.7",
4646
"define": "MULTIPLAYER_TOOLS_1_0_0_PRE_7"
4747
},
48+
{
49+
"name": "com.unity.modules.animation",
50+
"expression": "",
51+
"define": "COM_UNITY_MODULES_ANIMATION"
52+
},
4853
{
4954
"name": "com.unity.modules.physics",
5055
"expression": "",

0 commit comments

Comments
 (0)