Skip to content

Commit 8a901a5

Browse files
fix: networkvariable not synchronizing changes made during spawn or post-spawn. (#3878)
* fix Fixing the issue with the missing `OnSpawned` override within `NetworkVariable`. * test Adding a general test for scenarios like this one. * update Adding changelog entry. * style Adding comments for clarity of what `OnSpawned` and the call to `PostNetworkVariableWrite` is for. Removing trailing whitespaces. * refactor Just apply the changes to the NetworkVariable instance and not all NetworkVariables of the NetworkBehaviour. * style correcting the comment. * fix Very similar to NetworkList (we might collapse the OnSpawned logic into NetworkVariableBase. * test-fix OwnerModifiedTests was not properly spawning from the non-session owner client (it was spawning with ownership on the session owner side). NetworkVariableCollectionTests is showing an issue with changes for only the host instance and only within the TestDictionaryCollections (has to do with trying to add and then reverting vs the tracked changes). * test Narrowed down the issue to the changes tracked (i.e. added, removed, changed, unchanged) not matching when running a host but when comparing the actual dictionaries that all passes... * test Removing developer logging. * test - fix Fixed the issue in the `TestDictionaryCollections` test where clients that did not have the initial added target changes for server changes when running a host. Now, upon clients spawning players locally on the clients, the server write dictionary that is already populated by the host will have the added target changes injected during spawn to assure the changes match. (This is only for this specific test when running the host `TestFixture` pass. * style removing unused debug related script. * update Based on peer review, folding the same logic for spawn authority resetting dirty once spawned to assure no duplicate changes are sent.
1 parent b2e11c3 commit 8a901a5

File tree

8 files changed

+247
-55
lines changed

8 files changed

+247
-55
lines changed

com.unity.netcode.gameobjects/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
66

77
Additional documentation and release notes are available at [Multiplayer Documentation](https://docs-multiplayer.unity3d.com).
88

9+
## [2.8.2] - 2025-02-11
10+
11+
### Fixed
12+
13+
- Fixed issue where `NetworkVariable` was not properly synchronizing to changes made by the spawn and write authority during `OnNetworkSpawn` and `OnNetworkPostSpawn`. (#3878)
14+
15+
916
## [2.8.1] - 2025-01-23
1017

1118
### Fixed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -836,7 +836,7 @@ internal void NetworkPostSpawn()
836836
// all spawn related methods have been invoked.
837837
for (int i = 0; i < NetworkVariableFields.Count; i++)
838838
{
839-
NetworkVariableFields[i].OnSpawned();
839+
NetworkVariableFields[i].InternalOnSpawned();
840840
}
841841
}
842842

@@ -889,7 +889,7 @@ internal void InternalOnNetworkPreDespawn()
889889
// all spawn related methods have been invoked.
890890
for (int i = 0; i < NetworkVariableFields.Count; i++)
891891
{
892-
NetworkVariableFields[i].OnPreDespawn();
892+
NetworkVariableFields[i].InternalOnPreDespawn();
893893
}
894894
}
895895

com.unity.netcode.gameobjects/Runtime/NetworkVariable/Collections/NetworkList.cs

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -58,28 +58,6 @@ public NetworkList(IEnumerable<T> values = default,
5858
Dispose();
5959
}
6060

61-
internal override void OnSpawned()
62-
{
63-
// If the NetworkList is:
64-
// - Dirty
65-
// - State updates can be sent:
66-
// -- The instance has write permissions.
67-
// -- The last sent time plus the max send time period is less than the current time.
68-
// - User script has modified the list during spawn.
69-
// - This instance is on the spawn authority side.
70-
// When the NetworkObject is finished spawning (on the same frame), go ahead and reset
71-
// the dirty related properties and last sent time to prevent duplicate entries from
72-
// being sent (i.e. CreateObjectMessage will contain the changes so we don't need to
73-
// send a proceeding NetworkVariableDeltaMessage).
74-
if (IsDirty() && CanSend() && m_NetworkObject.IsSpawnAuthority)
75-
{
76-
UpdateLastSentTime();
77-
ResetDirty();
78-
SetDirty(false);
79-
}
80-
base.OnSpawned();
81-
}
82-
8361
/// <inheritdoc cref="NetworkVariable{T}.ResetDirty"/>
8462
public override void ResetDirty()
8563
{

com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariableBase.cs

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -140,27 +140,37 @@ public void Initialize(NetworkBehaviour networkBehaviour)
140140
}
141141
}
142142

143-
/// TODO-API: After further vetting and alignment on these, we might make them part of the public API.
144-
/// Could actually be like an interface that gets automatically registered for these kinds of notifications
145-
/// without having to be a NetworkBehaviour.
146-
#region OnSpawn and OnPreDespawn (ETC)
147-
148143
/// <summary>
149144
/// Invoked after the associated <see cref="NetworkBehaviour.OnNetworkPostSpawn"/> has been invoked.
150145
/// </summary>
151-
internal virtual void OnSpawned()
146+
internal void InternalOnSpawned()
152147
{
153-
148+
// If the NetworkVariableBase derived class is:
149+
// - On the spawn authority side.
150+
// - Dirty.
151+
// - State updates can be sent:
152+
// -- The instance has write permissions.
153+
// -- The last sent time plus the max send time period is less than the current time.
154+
// - User script has modified the list during spawn.
155+
// When the NetworkObject is finished spawning (on the same frame), go ahead and reset
156+
// the dirty related properties and last sent time to prevent duplicate updates from
157+
// being sent (i.e. CreateObjectMessage will contain the changes so we don't need to
158+
// send a proceeding NetworkVariableDeltaMessage).
159+
if (m_NetworkObject.IsSpawnAuthority && IsDirty() && CanWrite() && CanSend())
160+
{
161+
UpdateLastSentTime();
162+
ResetDirty();
163+
SetDirty(false);
164+
}
154165
}
155166

156167
/// <summary>
157168
/// Invoked after the associated <see cref="NetworkBehaviour.OnNetworkPreDespawn"/> has been invoked.
158169
/// </summary>
159-
internal virtual void OnPreDespawn()
170+
internal void InternalOnPreDespawn()
160171
{
161172

162173
}
163-
#endregion
164174

165175
/// <summary>
166176
/// Deinitialize is invoked when a NetworkObject is despawned.

com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariable/NetworkVariableCollectionsTests.cs

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -831,7 +831,7 @@ public IEnumerator TestDictionaryCollections()
831831
{
832832
// Server-side add same key and SerializableObject prior to being added to the owner side
833833
compDictionaryServer.ListCollectionOwner.Value.Add(newEntry.Item1, newEntry.Item2);
834-
// Checking if dirty on server side should revert back to origina known current dictionary state
834+
// Checking if dirty on server side should revert back to original known current dictionary state
835835
compDictionaryServer.ListCollectionOwner.IsDirty();
836836
yield return WaitForConditionOrTimeOut(() => compDictionaryServer.CompareTrackedChanges(ListTestHelperBase.Targets.Owner));
837837
AssertOnTimeout($"Server add to owner write collection property failed to restore on {className} {compDictionaryServer.name}! {compDictionaryServer.GetLog()}");
@@ -840,7 +840,6 @@ public IEnumerator TestDictionaryCollections()
840840
// Server-side add a completely new key and SerializableObject to to owner write permission property
841841
compDictionaryServer.ListCollectionOwner.Value.Add(GetNextKey(), SerializableObject.GetRandomObject());
842842
// Both should be overridden by the owner-side update
843-
844843
}
845844
VerboseDebug($"[{compDictionary.name}][Owner] Adding Key: {newEntry.Item1}");
846845
// Add key and SerializableObject to owner side
@@ -852,15 +851,17 @@ public IEnumerator TestDictionaryCollections()
852851
//////////////////////////////////
853852
// Server Add SerializableObject Entry
854853
newEntry = (GetNextKey(), SerializableObject.GetRandomObject());
854+
855855
// Only test restore on non-host clients (otherwise a host is both server and client/owner)
856856
if (!client.IsServer)
857857
{
858858
// Client-side add same key and SerializableObject to server write permission property
859859
compDictionary.ListCollectionServer.Value.Add(newEntry.Item1, newEntry.Item2);
860-
// Checking if dirty on client side should revert back to origina known current dictionary state
860+
// Checking if dirty on client side should revert back to original known current dictionary state
861861
compDictionary.ListCollectionServer.IsDirty();
862862
yield return WaitForConditionOrTimeOut(() => compDictionary.CompareTrackedChanges(ListTestHelperBase.Targets.Server));
863863
AssertOnTimeout($"Client-{client.LocalClientId} add to server write collection property failed to restore on {className} {compDictionary.name}! {compDictionary.GetLog()}");
864+
864865
// Client-side add the same key and SerializableObject to server write permission property (would throw key exists exception too if previous failed)
865866
compDictionary.ListCollectionServer.Value.Add(newEntry.Item1, newEntry.Item2);
866867
// Client-side add a completely new key and SerializableObject to to server write permission property
@@ -892,7 +893,6 @@ public IEnumerator TestDictionaryCollections()
892893

893894
yield return WaitForConditionOrTimeOut(() => compDictionaryServer.ValidateInstances());
894895
AssertOnTimeout($"[Server] Not all instances of client-{compDictionaryServer.OwnerClientId}'s {className} {compDictionaryServer.name} component match! {compDictionaryServer.GetLog()}");
895-
896896
ValidateClientsFlat(client);
897897
////////////////////////////////////
898898
// Owner Change SerializableObject Entry
@@ -915,7 +915,7 @@ public IEnumerator TestDictionaryCollections()
915915
{
916916
// Server-side update same key value prior to being updated to the owner side
917917
compDictionaryServer.ListCollectionOwner.Value[valueInt] = randomObject;
918-
// Checking if dirty on server side should revert back to origina known current dictionary state
918+
// Checking if dirty on server side should revert back to original known current dictionary state
919919
compDictionaryServer.ListCollectionOwner.IsDirty();
920920
yield return WaitForConditionOrTimeOut(() => compDictionaryServer.CompareTrackedChanges(ListTestHelperBase.Targets.Owner));
921921
AssertOnTimeout($"Server update collection entry value to local owner write collection property failed to restore on {className} {compDictionaryServer.name}! {compDictionaryServer.GetLog()}");
@@ -956,7 +956,7 @@ public IEnumerator TestDictionaryCollections()
956956
{
957957
// Owner-side update same key value prior to being updated to the server side
958958
compDictionary.ListCollectionServer.Value[valueInt] = randomObject;
959-
// Checking if dirty on owner side should revert back to origina known current dictionary state
959+
// Checking if dirty on owner side should revert back to original known current dictionary state
960960
compDictionary.ListCollectionServer.IsDirty();
961961
yield return WaitForConditionOrTimeOut(() => compDictionary.CompareTrackedChanges(ListTestHelperBase.Targets.Server));
962962
AssertOnTimeout($"Client-{client.LocalClientId} update collection entry value to local server write collection property failed to restore on {className} {compDictionary.name}! {compDictionary.GetLog()}");
@@ -1841,10 +1841,10 @@ private bool ChangesMatch(ulong clientId, Dictionary<DeltaTypes, Dictionary<int,
18411841
var deltaTypes = Enum.GetValues(typeof(DeltaTypes)).OfType<DeltaTypes>().ToList();
18421842
foreach (var deltaType in deltaTypes)
18431843
{
1844-
LogMessage($"Comparing {deltaType}:");
1844+
LogMessage($"[Comparing {deltaType}] Local: {local[deltaType].Count} | Other: {other[deltaType].Count}");
18451845
if (local[deltaType].Count != other[deltaType].Count)
18461846
{
1847-
LogMessage($"{deltaType}s count did not match!");
1847+
LogMessage($"[Client-{clientId}] Local {deltaType}s count of {local[deltaType].Count} did not match the other's count of {other[deltaType].Count}!");
18481848
return false;
18491849
}
18501850
if (!CompareDictionaries(clientId, local[deltaType], other[deltaType]))
@@ -1970,6 +1970,18 @@ public void TrackChanges(Targets target, Dictionary<int, SerializableObject> pre
19701970
contextTable[DeltaTypes.Removed] = whatWasRemoved;
19711971
contextTable[DeltaTypes.Changed] = whatChanged;
19721972
contextTable[DeltaTypes.UnChanged] = whatRemainedTheSame;
1973+
1974+
// Log all incoming changes when debug mode is enabled
1975+
if (!IsOwner && IsDebugMode)
1976+
{
1977+
LogMessage($"[{NetworkManager.name}][TrackChanges-> Client-{OwnerClientId}] Collection was updated!");
1978+
LogMessage($"Added: {whatWasAdded.Count} ");
1979+
LogMessage($"Removed: {whatWasRemoved.Count} ");
1980+
LogMessage($"Changed: {whatChanged.Count} ");
1981+
LogMessage($"UnChanged: {whatRemainedTheSame.Count} ");
1982+
UnityEngine.Debug.Log($"{GetLog()}");
1983+
LogStart();
1984+
}
19731985
}
19741986

19751987
public void OnServerListValuesChanged(Dictionary<int, SerializableObject> previous, Dictionary<int, SerializableObject> current)
@@ -2016,30 +2028,43 @@ public void ResetTrackedChanges()
20162028

20172029
protected override void OnNetworkPostSpawn()
20182030
{
2019-
TrackRelativeInstances();
20202031

2032+
TrackRelativeInstances();
20212033
ListCollectionServer.OnValueChanged += OnServerListValuesChanged;
20222034
ListCollectionOwner.OnValueChanged += OnOwnerListValuesChanged;
20232035

20242036
if (!IsDebugMode)
20252037
{
20262038
InitValues();
20272039
}
2040+
2041+
base.OnNetworkPostSpawn();
20282042
}
20292043

20302044
public void InitValues()
20312045
{
20322046
if (IsServer)
20332047
{
20342048
ListCollectionServer.Value = OnSetServerValues();
2035-
ListCollectionOwner.CheckDirtyState();
2049+
ListCollectionServer.CheckDirtyState();
20362050
}
20372051

20382052
if (IsOwner)
20392053
{
20402054
ListCollectionOwner.Value = OnSetOwnerValues();
20412055
ListCollectionOwner.CheckDirtyState();
20422056
}
2057+
2058+
// When running a host, the changes being tracked will not match because clients will be synchronized with changes
2059+
// already applied. This fixing this issue by injecting "added" server targeted changes during initialization on
2060+
// the connected clients' side.
2061+
if (!IsServer)
2062+
{
2063+
if (ListCollectionServer.Value.Count > 0 && NetworkVariableChanges[Targets.Server][DeltaTypes.Added].Count == 0)
2064+
{
2065+
TrackChanges(Targets.Server, new Dictionary<int, SerializableObject>(), ListCollectionServer.Value);
2066+
}
2067+
}
20432068
}
20442069

20452070
public override void OnNetworkDespawn()

0 commit comments

Comments
 (0)