Skip to content

Commit dc58e42

Browse files
authored
fix: OnValueChanged not being triggered on collections when the collection changes back to the previous value (#3539)
https://jira.unity3d.com/browse/MTTB-1421 fixes: #3534 ## Changelog - Fixed: When calling `CheckDirtyState(true)` on a `NetworkVariable` collection, `OnValueChanged` now triggers on the authority if the authority has changed the collection and then changed it back to the initial state in the same frame. This matches the behaviour of non-collection NetworkVariables. ## Testing and Documentation - Includes unit tests. - Includes integration tests. ## Backport Backported by #3544
1 parent 6b9f9b5 commit dc58e42

File tree

6 files changed

+779
-514
lines changed

6 files changed

+779
-514
lines changed

com.unity.netcode.gameobjects/CHANGELOG.md

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

1717
### Fixed
1818

19+
- Fixed ensuring OnValueChanged callback is still triggered on the authority when a collection changes and then reverts to the previous value in the same frame. (#3539)
1920
- Fixed synchronizing the destroyGameObject parameter to clients for InScenePlaced network objects. (#3514)
2021
- Fixed distributed authority related issue where enabling the `NetworkObject.DestroyWithScene` would cause errors when a destroying non-authority instances due to loading (single mode) or unloading scene events. (#3500)
2122

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

Lines changed: 47 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public override void OnInitialize()
5757
base.OnInitialize();
5858

5959
m_HasPreviousValue = true;
60-
NetworkVariableSerialization<T>.Duplicate(m_InternalValue, ref m_InternalOriginalValue);
60+
NetworkVariableSerialization<T>.Duplicate(m_InternalValue, ref m_LastInternalValue);
6161
NetworkVariableSerialization<T>.Duplicate(m_InternalValue, ref m_PreviousValue);
6262
}
6363

@@ -73,7 +73,7 @@ public NetworkVariable(T value = default,
7373
: base(readPerm, writePerm)
7474
{
7575
m_InternalValue = value;
76-
m_InternalOriginalValue = default;
76+
m_LastInternalValue = default;
7777
// Since we start with IsDirty = true, this doesn't need to be duplicated
7878
// right away. It won't get read until after ResetDirty() is called, and
7979
// the duplicate will be made there. Avoiding calling
@@ -92,25 +92,45 @@ public void Reset(T value = default)
9292
if (m_NetworkBehaviour == null || m_NetworkBehaviour != null && !m_NetworkBehaviour.NetworkObject.IsSpawned)
9393
{
9494
m_InternalValue = value;
95-
NetworkVariableSerialization<T>.Duplicate(m_InternalValue, ref m_InternalOriginalValue);
95+
NetworkVariableSerialization<T>.Duplicate(m_InternalValue, ref m_LastInternalValue);
9696
m_PreviousValue = default;
9797
}
9898
}
9999

100100
/// <summary>
101-
/// The internal value of the NetworkVariable
101+
/// The current internal value of the NetworkVariable.
102102
/// </summary>
103+
/// <remarks>
104+
/// When using collections, this InternalValue can be updated directly without going through the <see cref="NetworkVariable{T}.Value"/> setter.
105+
/// </remarks>
103106
[SerializeField]
104107
private protected T m_InternalValue;
105108

106-
// The introduction of standard .NET collections caused an issue with permissions since there is no way to detect changes in the
107-
// collection without doing a full comparison. While this approach does consume more memory per collection instance, it is the
108-
// lowest risk approach to resolving the issue where a client with no write permissions could make changes to a collection locally
109-
// which can cause a myriad of issues.
110-
private protected T m_InternalOriginalValue;
109+
/// <summary>
110+
/// The last valid/authorized value of the network variable.
111+
/// </summary>
112+
/// <remarks>
113+
/// The introduction of standard .NET collections caused an issue with permissions since there is no way to detect changes in the
114+
/// collection without doing a full comparison. While this approach does consume more memory per collection instance, it is the
115+
/// lowest risk approach to resolving the issue where a client with no write permissions could make changes to a collection locally
116+
/// which can cause a myriad of issues.
117+
/// </remarks>
118+
private protected T m_LastInternalValue;
111119

120+
/// <summary>
121+
/// The most recent value that was synchronized over the network.
122+
/// Synchronized over the network at the end of the frame in which the <see cref="NetworkVariable{T}"/> was marked dirty.
123+
/// </summary>
124+
/// <remarks>
125+
/// Only contains the value synchronized over the network at the end of the last frame.
126+
/// All in-between changes on the authority are tracked by <see cref="m_LastInternalValue"/>.
127+
/// </remarks>
112128
private protected T m_PreviousValue;
113129

130+
/// <summary>
131+
/// Whether this network variable has had changes synchronized over the network.
132+
/// Indicates whether <see cref="m_PreviousValue"/> is populated and valid.
133+
/// </summary>
114134
private bool m_HasPreviousValue;
115135
private bool m_IsDisposed;
116136

@@ -139,7 +159,7 @@ public virtual T Value
139159
{
140160
T previousValue = m_InternalValue;
141161
m_InternalValue = value;
142-
NetworkVariableSerialization<T>.Duplicate(m_InternalValue, ref m_InternalOriginalValue);
162+
NetworkVariableSerialization<T>.Duplicate(m_InternalValue, ref m_LastInternalValue);
143163
SetDirty(true);
144164
m_IsDisposed = false;
145165
OnValueChanged?.Invoke(previousValue, m_InternalValue);
@@ -165,20 +185,21 @@ public bool CheckDirtyState(bool forceCheck = false)
165185
if (CannotWrite)
166186
{
167187
// If modifications are detected, then revert back to the last known current value
168-
if (!NetworkVariableSerialization<T>.AreEqual(ref m_InternalValue, ref m_InternalOriginalValue))
188+
if (!NetworkVariableSerialization<T>.AreEqual(ref m_InternalValue, ref m_LastInternalValue))
169189
{
170-
NetworkVariableSerialization<T>.Duplicate(m_InternalOriginalValue, ref m_InternalValue);
190+
NetworkVariableSerialization<T>.Duplicate(m_LastInternalValue, ref m_InternalValue);
171191
}
172192
return false;
173193
}
174194

175-
// Compare the previous with the current if not dirty or forcing a check.
176-
if ((!isDirty || forceCheck) && !NetworkVariableSerialization<T>.AreEqual(ref m_PreviousValue, ref m_InternalValue))
195+
// Compare the last internal value with the current value if not dirty or forcing a check.
196+
if ((!isDirty || forceCheck) && !NetworkVariableSerialization<T>.AreEqual(ref m_LastInternalValue, ref m_InternalValue))
177197
{
178198
SetDirty(true);
179-
OnValueChanged?.Invoke(m_PreviousValue, m_InternalValue);
199+
OnValueChanged?.Invoke(m_LastInternalValue, m_InternalValue);
180200
m_IsDisposed = false;
181201
isDirty = true;
202+
NetworkVariableSerialization<T>.Duplicate(m_InternalValue, ref m_LastInternalValue);
182203
}
183204
return isDirty;
184205
}
@@ -212,11 +233,11 @@ public override void Dispose()
212233
m_InternalValue = default;
213234

214235
// Dispose the internal original value
215-
if (m_InternalOriginalValue is IDisposable internalOriginalValueDisposable)
236+
if (m_LastInternalValue is IDisposable internalOriginalValueDisposable)
216237
{
217238
internalOriginalValueDisposable.Dispose();
218239
}
219-
m_InternalOriginalValue = default;
240+
m_LastInternalValue = default;
220241

221242
// Dispose the previous value if there is one
222243
if (m_HasPreviousValue && m_PreviousValue is IDisposable previousValueDisposable)
@@ -245,9 +266,9 @@ public override bool IsDirty()
245266
{
246267
// If the client does not have write permissions but the internal value is determined to be locally modified and we are applying updates, then we should revert
247268
// to the original collection value prior to applying updates (primarily for collections).
248-
if (!NetworkUpdaterCheck && CannotWrite && !NetworkVariableSerialization<T>.AreEqual(ref m_InternalValue, ref m_InternalOriginalValue))
269+
if (!NetworkUpdaterCheck && CannotWrite && !NetworkVariableSerialization<T>.AreEqual(ref m_InternalValue, ref m_LastInternalValue))
249270
{
250-
NetworkVariableSerialization<T>.Duplicate(m_InternalOriginalValue, ref m_InternalValue);
271+
NetworkVariableSerialization<T>.Duplicate(m_LastInternalValue, ref m_InternalValue);
251272
return true;
252273
}
253274
// For most cases we can use the dirty flag.
@@ -284,7 +305,7 @@ public override void ResetDirty()
284305
m_HasPreviousValue = true;
285306
NetworkVariableSerialization<T>.Duplicate(m_InternalValue, ref m_PreviousValue);
286307
// Once updated, assure the original current value is updated for future comparison purposes
287-
NetworkVariableSerialization<T>.Duplicate(m_InternalValue, ref m_InternalOriginalValue);
308+
NetworkVariableSerialization<T>.Duplicate(m_InternalValue, ref m_LastInternalValue);
288309
}
289310
base.ResetDirty();
290311
}
@@ -307,9 +328,9 @@ public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta)
307328
{
308329
// If the client does not have write permissions but the internal value is determined to be locally modified and we are applying updates, then we should revert
309330
// to the original collection value prior to applying updates (primarily for collections).
310-
if (CannotWrite && !NetworkVariableSerialization<T>.AreEqual(ref m_InternalOriginalValue, ref m_InternalValue))
331+
if (CannotWrite && !NetworkVariableSerialization<T>.AreEqual(ref m_LastInternalValue, ref m_InternalValue))
311332
{
312-
NetworkVariableSerialization<T>.Duplicate(m_InternalOriginalValue, ref m_InternalValue);
333+
NetworkVariableSerialization<T>.Duplicate(m_LastInternalValue, ref m_InternalValue);
313334
}
314335

315336
NetworkVariableSerialization<T>.ReadDelta(reader, ref m_InternalValue);
@@ -341,17 +362,17 @@ internal override void PostDeltaRead()
341362
m_HasPreviousValue = true;
342363
NetworkVariableSerialization<T>.Duplicate(m_InternalValue, ref m_PreviousValue);
343364
// Once updated, assure the original current value is updated for future comparison purposes
344-
NetworkVariableSerialization<T>.Duplicate(m_InternalValue, ref m_InternalOriginalValue);
365+
NetworkVariableSerialization<T>.Duplicate(m_InternalValue, ref m_LastInternalValue);
345366
}
346367

347368
/// <inheritdoc />
348369
public override void ReadField(FastBufferReader reader)
349370
{
350371
// If the client does not have write permissions but the internal value is determined to be locally modified and we are applying updates, then we should revert
351372
// to the original collection value prior to applying updates (primarily for collections).
352-
if (CannotWrite && !NetworkVariableSerialization<T>.AreEqual(ref m_InternalOriginalValue, ref m_InternalValue))
373+
if (CannotWrite && !NetworkVariableSerialization<T>.AreEqual(ref m_LastInternalValue, ref m_InternalValue))
353374
{
354-
NetworkVariableSerialization<T>.Duplicate(m_InternalOriginalValue, ref m_InternalValue);
375+
NetworkVariableSerialization<T>.Duplicate(m_LastInternalValue, ref m_InternalValue);
355376
}
356377

357378
NetworkVariableSerialization<T>.Read(reader, ref m_InternalValue);
@@ -363,7 +384,7 @@ public override void ReadField(FastBufferReader reader)
363384
NetworkVariableSerialization<T>.Duplicate(m_InternalValue, ref m_PreviousValue);
364385

365386
// Once updated, assure the original current value is updated for future comparison purposes
366-
NetworkVariableSerialization<T>.Duplicate(m_InternalValue, ref m_InternalOriginalValue);
387+
NetworkVariableSerialization<T>.Duplicate(m_InternalValue, ref m_LastInternalValue);
367388
}
368389

369390
/// <inheritdoc />

0 commit comments

Comments
 (0)