Skip to content

Commit 6a16349

Browse files
fix: NetworkVariables with NetworkVariableUpdateTraits can cause other NetworkVariables to drop changes (up-port) (#3466)
This PR resolves an issue where a `NetworkBehaviour` with multiple `NetworkVariables` could drop changes if one of the `NetworkVariables` has unique `NetworkVariableUpdateTraits` set, is dirty, but is not ready to send. Based on user @khyperia's submission #3462. ## Changelog - Fixed: issue where `NetworkVariable`s on a `NetworkBehaviour` could fail to synchronize changes if one has `NetworkVariableUpdateTraits` set and is dirty but is not ready to send. ## Testing and Documentation - Includes integration test `NetworkVariableTraitsTests.WhenNonTraitsIsDirtyButTraitsIsNotReadyToSend`. - No documentation changes or additions were necessary. ## Backport This is an up-port of #3465 --------- Co-authored-by: khyperia <[email protected]>
1 parent 8a72d00 commit 6a16349

File tree

3 files changed

+136
-93
lines changed

3 files changed

+136
-93
lines changed

com.unity.netcode.gameobjects/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Additional documentation and release notes are available at [Multiplayer Documen
1414

1515
### Fixed
1616

17+
- Fixed issue where `NetworkVariable`s on a `NetworkBehaviour` could fail to synchronize changes if one has `NetworkVariableUpdateTraits` set and is dirty but is not ready to send. (#3466)
1718
- Fixed issues with the `NetworkBehaviour` and `NetworkVariable` length safety checks. (#3405)
1819
- Fixed memory leaks when domain reload is disabled. (#3427)
1920
- Fixed an exception being thrown when unregistering a custom message handler from within the registered callback. (#3417)

com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1094,8 +1094,8 @@ internal void NetworkVariableUpdate(ulong targetClientId, bool forceSend = false
10941094
if (networkVariable.CanSend())
10951095
{
10961096
shouldSend = true;
1097+
break;
10971098
}
1098-
break;
10991099
}
11001100
}
11011101
// All of this is just to prevent the DA Host from re-sending a NetworkVariable update it received from the client owner
Lines changed: 134 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,138 +1,180 @@
1+
using System.Collections;
2+
using System.Text;
13
using NUnit.Framework;
24
using Unity.Netcode.TestHelpers.Runtime;
35
using UnityEngine;
4-
using Object = UnityEngine.Object;
6+
using UnityEngine.TestTools;
57

68
namespace Unity.Netcode.RuntimeTests
79
{
810
internal class NetworkVariableTraitsComponent : NetworkBehaviour
911
{
1012
public NetworkVariable<float> TheVariable = new NetworkVariable<float>();
13+
public NetworkVariable<float> AnotherVariable = new NetworkVariable<float>();
1114
}
1215

16+
[TestFixture(HostOrServer.Host)]
17+
[TestFixture(HostOrServer.DAHost)]
1318
internal class NetworkVariableTraitsTests : NetcodeIntegrationTest
1419
{
15-
protected override int NumberOfClients => 2;
20+
protected override int NumberOfClients => 3;
1621

17-
protected override bool m_EnableTimeTravel => true;
18-
protected override bool m_SetupIsACoroutine => false;
19-
protected override bool m_TearDownIsACoroutine => false;
22+
private StringBuilder m_ErrorLog = new StringBuilder();
23+
24+
public NetworkVariableTraitsTests(HostOrServer hostOrServer) : base(hostOrServer) { }
2025

2126
protected override void OnPlayerPrefabGameObjectCreated()
2227
{
2328
m_PlayerPrefab.AddComponent<NetworkVariableTraitsComponent>();
2429
}
2530

26-
public NetworkVariableTraitsComponent GetTestComponent()
31+
public NetworkVariableTraitsComponent GetAuthorityComponent()
2732
{
28-
return m_ClientNetworkManagers[0].LocalClient.PlayerObject.GetComponent<NetworkVariableTraitsComponent>();
33+
return GetAuthorityNetworkManager().LocalClient.PlayerObject.GetComponent<NetworkVariableTraitsComponent>();
2934
}
3035

31-
public NetworkVariableTraitsComponent GetServerComponent()
36+
private bool AllAuthorityInstanceValuesMatch(float firstValue, float secondValue = 0.0f)
3237
{
33-
foreach (var obj in Object.FindObjectsByType<NetworkVariableTraitsComponent>(FindObjectsSortMode.None))
38+
m_ErrorLog.Clear();
39+
var authorityComponent = GetAuthorityComponent();
40+
if (authorityComponent.TheVariable.Value != firstValue || authorityComponent.AnotherVariable.Value != secondValue)
41+
{
42+
m_ErrorLog.Append($"[Client-{authorityComponent.OwnerClientId}][{authorityComponent.name}] Authority values did not match ({firstValue} | {secondValue})! " +
43+
$"TheVariable: {authorityComponent.TheVariable.Value} | AnotherVariable: {authorityComponent.AnotherVariable.Value}");
44+
return false;
45+
}
46+
foreach (var client in m_ClientNetworkManagers)
3447
{
35-
if (obj.NetworkManager == m_ServerNetworkManager && obj.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId)
48+
if (client.LocalClient.IsSessionOwner)
49+
{
50+
continue;
51+
}
52+
if (!client.SpawnManager.SpawnedObjects.ContainsKey(authorityComponent.NetworkObjectId))
53+
{
54+
m_ErrorLog.Append($"Failed to find {authorityComponent.name} instance on Client-{client.LocalClientId}!");
55+
return false;
56+
}
57+
var testComponent = client.SpawnManager.SpawnedObjects[authorityComponent.NetworkObjectId].GetComponent<NetworkVariableTraitsComponent>();
58+
if (testComponent.TheVariable.Value != firstValue || testComponent.AnotherVariable.Value != secondValue)
3659
{
37-
return obj;
60+
m_ErrorLog.Append($"[Client-{client.LocalClientId}][{testComponent.name}] Authority values did not match ({firstValue} | {secondValue})! " +
61+
$"TheVariable: {testComponent.TheVariable.Value} | AnotherVariable: {testComponent.AnotherVariable.Value}");
62+
return false;
3863
}
3964
}
40-
41-
return null;
65+
return true;
4266
}
4367

44-
[Test]
45-
public void WhenNewValueIsLessThanThreshold_VariableIsNotSerialized()
68+
[UnityTest]
69+
public IEnumerator WhenNewValueIsLessThanThreshold_VariableIsNotSerialized()
4670
{
47-
var serverComponent = GetServerComponent();
48-
var testComponent = GetTestComponent();
49-
serverComponent.TheVariable.CheckExceedsDirtinessThreshold = (in float value, in float newValue) => Mathf.Abs(newValue - value) >= 0.1;
50-
51-
serverComponent.TheVariable.Value = 0.05f;
52-
53-
TimeTravel(2, 120);
54-
55-
Assert.AreEqual(0.05f, serverComponent.TheVariable.Value); ;
56-
Assert.AreEqual(0, testComponent.TheVariable.Value); ;
71+
var authorityComponent = GetAuthorityComponent();
72+
authorityComponent.TheVariable.CheckExceedsDirtinessThreshold = (in float value, in float newValue) => Mathf.Abs(newValue - value) >= 0.1;
73+
74+
var timeoutHelper = new TimeoutHelper(1.0f);
75+
var newValue = 0.05f;
76+
authorityComponent.TheVariable.Value = newValue;
77+
yield return WaitForConditionOrTimeOut(() => AllAuthorityInstanceValuesMatch(newValue), timeoutHelper);
78+
Assert.True(timeoutHelper.TimedOut, $"Non-authority instances recieved changes when they should not have!");
5779
}
58-
[Test]
59-
public void WhenNewValueIsGreaterThanThreshold_VariableIsSerialized()
60-
{
61-
var serverComponent = GetServerComponent();
62-
var testComponent = GetTestComponent();
63-
serverComponent.TheVariable.CheckExceedsDirtinessThreshold = (in float value, in float newValue) => Mathf.Abs(newValue - value) >= 0.1;
6480

65-
serverComponent.TheVariable.Value = 0.15f;
66-
67-
TimeTravel(2, 120);
68-
69-
Assert.AreEqual(0.15f, serverComponent.TheVariable.Value); ;
70-
Assert.AreEqual(0.15f, testComponent.TheVariable.Value); ;
81+
[UnityTest]
82+
public IEnumerator WhenNewValueIsGreaterThanThreshold_VariableIsSerialized()
83+
{
84+
var authorityComponent = GetAuthorityComponent();
85+
authorityComponent.TheVariable.CheckExceedsDirtinessThreshold = (in float value, in float newValue) => Mathf.Abs(newValue - value) >= 0.1;
86+
87+
var timeoutHelper = new TimeoutHelper(1.0f);
88+
var newValue = 0.15f;
89+
authorityComponent.TheVariable.Value = newValue;
90+
yield return WaitForConditionOrTimeOut(() => AllAuthorityInstanceValuesMatch(newValue), timeoutHelper);
91+
AssertOnTimeout($"{m_ErrorLog}", timeoutHelper);
7192
}
7293

73-
[Test]
74-
public void WhenNewValueIsLessThanThresholdButMaxTimeHasPassed_VariableIsSerialized()
94+
[UnityTest]
95+
public IEnumerator WhenNewValueIsLessThanThresholdButMaxTimeHasPassed_VariableIsSerialized()
7596
{
76-
var serverComponent = GetServerComponent();
77-
var testComponent = GetTestComponent();
78-
serverComponent.TheVariable.CheckExceedsDirtinessThreshold = (in float value, in float newValue) => Mathf.Abs(newValue - value) >= 0.1;
79-
serverComponent.TheVariable.SetUpdateTraits(new NetworkVariableUpdateTraits { MaxSecondsBetweenUpdates = 2 });
80-
serverComponent.TheVariable.LastUpdateSent = m_ServerNetworkManager.NetworkTimeSystem.LocalTime;
81-
82-
serverComponent.TheVariable.Value = 0.05f;
83-
84-
TimeTravel(1 / 60f * 119, 119);
85-
86-
Assert.AreEqual(0.05f, serverComponent.TheVariable.Value); ;
87-
Assert.AreEqual(0, testComponent.TheVariable.Value); ;
88-
89-
TimeTravel(1 / 60f * 4, 4);
90-
91-
Assert.AreEqual(0.05f, serverComponent.TheVariable.Value); ;
92-
Assert.AreEqual(0.05f, testComponent.TheVariable.Value); ;
97+
var authorityComponent = GetAuthorityComponent();
98+
authorityComponent.TheVariable.CheckExceedsDirtinessThreshold = (in float value, in float newValue) => Mathf.Abs(newValue - value) >= 0.1;
99+
authorityComponent.TheVariable.SetUpdateTraits(new NetworkVariableUpdateTraits { MaxSecondsBetweenUpdates = 1.0f });
100+
authorityComponent.TheVariable.LastUpdateSent = authorityComponent.NetworkManager.NetworkTimeSystem.LocalTime;
101+
102+
var timeoutHelper = new TimeoutHelper(0.62f);
103+
var newValue = 0.05f;
104+
authorityComponent.TheVariable.Value = newValue;
105+
// We expect a timeout for this condition
106+
yield return WaitForConditionOrTimeOut(() => AllAuthorityInstanceValuesMatch(newValue), timeoutHelper);
107+
Assert.True(timeoutHelper.TimedOut, $"Non-authority instances recieved changes when they should not have!");
108+
109+
// Now we expect this to not timeout
110+
yield return WaitForConditionOrTimeOut(() => AllAuthorityInstanceValuesMatch(newValue), timeoutHelper);
111+
AssertOnTimeout($"{m_ErrorLog}", timeoutHelper);
93112
}
94113

95-
[Test]
96-
public void WhenNewValueIsGreaterThanThresholdButMinTimeHasNotPassed_VariableIsNotSerialized()
114+
[UnityTest]
115+
public IEnumerator WhenNewValueIsGreaterThanThresholdButMinTimeHasNotPassed_VariableIsNotSerialized()
97116
{
98-
var serverComponent = GetServerComponent();
99-
var testComponent = GetTestComponent();
100-
serverComponent.TheVariable.CheckExceedsDirtinessThreshold = (in float value, in float newValue) => Mathf.Abs(newValue - value) >= 0.1;
101-
serverComponent.TheVariable.SetUpdateTraits(new NetworkVariableUpdateTraits { MinSecondsBetweenUpdates = 2 });
102-
serverComponent.TheVariable.LastUpdateSent = m_ServerNetworkManager.NetworkTimeSystem.LocalTime;
103-
104-
serverComponent.TheVariable.Value = 0.15f;
105-
106-
TimeTravel(1 / 60f * 119, 119);
107-
108-
Assert.AreEqual(0.15f, serverComponent.TheVariable.Value); ;
109-
Assert.AreEqual(0, testComponent.TheVariable.Value); ;
110-
111-
TimeTravel(1 / 60f * 4, 4);
112-
113-
Assert.AreEqual(0.15f, serverComponent.TheVariable.Value); ;
114-
Assert.AreEqual(0.15f, testComponent.TheVariable.Value); ;
117+
var authorityComponent = GetAuthorityComponent();
118+
authorityComponent.TheVariable.CheckExceedsDirtinessThreshold = (in float value, in float newValue) => Mathf.Abs(newValue - value) >= 0.1;
119+
authorityComponent.TheVariable.SetUpdateTraits(new NetworkVariableUpdateTraits { MinSecondsBetweenUpdates = 1 });
120+
authorityComponent.TheVariable.LastUpdateSent = authorityComponent.NetworkManager.NetworkTimeSystem.LocalTime;
121+
122+
var timeoutHelper = new TimeoutHelper(0.62f);
123+
var newValue = 0.15f;
124+
authorityComponent.TheVariable.Value = newValue;
125+
// We expect a timeout for this condition
126+
yield return WaitForConditionOrTimeOut(() => AllAuthorityInstanceValuesMatch(newValue), timeoutHelper);
127+
Assert.True(timeoutHelper.TimedOut, $"Non-authority instances recieved changes when they should not have!");
128+
129+
// Now we expect this to not timeout
130+
yield return WaitForConditionOrTimeOut(() => AllAuthorityInstanceValuesMatch(newValue), timeoutHelper);
131+
AssertOnTimeout($"{m_ErrorLog}", timeoutHelper);
115132
}
116133

117-
[Test]
118-
public void WhenNoThresholdIsSetButMinTimeHasNotPassed_VariableIsNotSerialized()
134+
[UnityTest]
135+
public IEnumerator WhenNoThresholdIsSetButMinTimeHasNotPassed_VariableIsNotSerialized()
119136
{
120-
var serverComponent = GetServerComponent();
121-
var testComponent = GetTestComponent();
122-
serverComponent.TheVariable.SetUpdateTraits(new NetworkVariableUpdateTraits { MinSecondsBetweenUpdates = 2 });
123-
serverComponent.TheVariable.LastUpdateSent = m_ServerNetworkManager.NetworkTimeSystem.LocalTime;
124-
125-
serverComponent.TheVariable.Value = 0.15f;
126-
127-
TimeTravel(1 / 60f * 119, 119);
128-
129-
Assert.AreEqual(0.15f, serverComponent.TheVariable.Value); ;
130-
Assert.AreEqual(0, testComponent.TheVariable.Value); ;
131-
132-
TimeTravel(1 / 60f * 4, 4);
137+
var authorityComponent = GetAuthorityComponent();
138+
authorityComponent.TheVariable.SetUpdateTraits(new NetworkVariableUpdateTraits { MinSecondsBetweenUpdates = 1 });
139+
authorityComponent.TheVariable.LastUpdateSent = authorityComponent.NetworkManager.NetworkTimeSystem.LocalTime;
140+
141+
var timeoutHelper = new TimeoutHelper(0.62f);
142+
var newValue = 0.15f;
143+
authorityComponent.TheVariable.Value = newValue;
144+
// We expect a timeout for this condition
145+
yield return WaitForConditionOrTimeOut(() => AllAuthorityInstanceValuesMatch(newValue), timeoutHelper);
146+
Assert.True(timeoutHelper.TimedOut, $"Non-authority instances recieved changes when they should not have!");
147+
148+
// Now we expect this to not timeout
149+
yield return WaitForConditionOrTimeOut(() => AllAuthorityInstanceValuesMatch(newValue), timeoutHelper);
150+
AssertOnTimeout($"{m_ErrorLog}", timeoutHelper);
151+
}
133152

134-
Assert.AreEqual(0.15f, serverComponent.TheVariable.Value); ;
135-
Assert.AreEqual(0.15f, testComponent.TheVariable.Value); ;
153+
/// <summary>
154+
/// Integration test to validate that a <see cref="NetworkVariable{T}"/> with <see cref="NetworkVariableUpdateTraits"/>
155+
/// does not cause other <see cref="NetworkVariable{T}"/>s to miss an update when they are dirty but the one with
156+
/// traits is not ready to send an update.
157+
/// </summary>
158+
[UnityTest]
159+
public IEnumerator WhenNonTraitsIsDirtyButTraitsIsNotReadyToSend()
160+
{
161+
var authorityComponent = GetAuthorityComponent();
162+
authorityComponent.TheVariable.SetUpdateTraits(new NetworkVariableUpdateTraits { MinSecondsBetweenUpdates = 1 });
163+
authorityComponent.TheVariable.LastUpdateSent = authorityComponent.NetworkManager.NetworkTimeSystem.LocalTime;
164+
165+
var timeoutHelper = new TimeoutHelper(0.62f);
166+
var firstValue = 0.15f;
167+
var secondValue = 0.15f;
168+
authorityComponent.TheVariable.Value = firstValue;
169+
// We expect a timeout for this condition
170+
yield return WaitForConditionOrTimeOut(() => AllAuthorityInstanceValuesMatch(firstValue, secondValue), timeoutHelper);
171+
Assert.True(timeoutHelper.TimedOut, $"Non-authority instances recieved changes when they should not have!");
172+
173+
secondValue = 1.5f;
174+
authorityComponent.AnotherVariable.Value = secondValue;
175+
// Now we expect this to not timeout
176+
yield return WaitForConditionOrTimeOut(() => AllAuthorityInstanceValuesMatch(firstValue, secondValue), timeoutHelper);
177+
AssertOnTimeout($"{m_ErrorLog}", timeoutHelper);
136178
}
137179
}
138180
}

0 commit comments

Comments
 (0)