-
Notifications
You must be signed in to change notification settings - Fork 459
Expand file tree
/
Copy pathNetworkVariable.cs
More file actions
390 lines (350 loc) · 18.2 KB
/
NetworkVariable.cs
File metadata and controls
390 lines (350 loc) · 18.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
using System;
using UnityEngine;
namespace Unity.Netcode
{
/// <summary>
/// A variable that can be synchronized over the network.
/// </summary>
/// <typeparam name="T">the unmanaged type for <see cref="NetworkVariable{T}"/> </typeparam>
[Serializable]
[GenerateSerializationForGenericParameter(0)]
public class NetworkVariable<T> : NetworkVariableBase
{
/// <summary>
/// Delegate type for value changed event
/// </summary>
/// <param name="previousValue">The value before the change</param>
/// <param name="newValue">The new value</param>
public delegate void OnValueChangedDelegate(T previousValue, T newValue);
/// <summary>
/// The callback to be invoked when the value gets changed
/// </summary>
public OnValueChangedDelegate OnValueChanged;
/// <summary>
/// Delegate that determines if the difference between two values exceeds a threshold for network synchronization
/// </summary>
/// <param name="previousValue">The previous value to compare against</param>
/// <param name="newValue">The new value to compare</param>
/// <returns>True if the difference exceeds the threshold and should be synchronized, false otherwise</returns>
public delegate bool CheckExceedsDirtinessThresholdDelegate(in T previousValue, in T newValue);
/// <summary>
/// Delegate instance for checking if value changes exceed the dirtiness threshold
/// </summary>
public CheckExceedsDirtinessThresholdDelegate CheckExceedsDirtinessThreshold;
/// <summary>
/// Determines if the current value has changed enough from its previous value to warrant network synchronization
/// </summary>
/// <returns>True if the value should be synchronized, false otherwise</returns>
public override bool ExceedsDirtinessThreshold()
{
if (CheckExceedsDirtinessThreshold != null && m_HasPreviousValue)
{
return CheckExceedsDirtinessThreshold(m_PreviousValue, m_InternalValue);
}
return true;
}
/// <summary>
/// Initializes the NetworkVariable by setting up initial and previous values
/// </summary>
public override void OnInitialize()
{
base.OnInitialize();
m_HasPreviousValue = true;
NetworkVariableSerialization<T>.Duplicate(m_InternalValue, ref m_InternalOriginalValue);
NetworkVariableSerialization<T>.Duplicate(m_InternalValue, ref m_PreviousValue);
}
/// <summary>
/// Constructor for <see cref="NetworkVariable{T}"/>
/// </summary>
/// <param name="value">initial value set that is of type T</param>
/// <param name="readPerm">the <see cref="NetworkVariableReadPermission"/> for this <see cref="NetworkVariable{T}"/></param>
/// <param name="writePerm">the <see cref="NetworkVariableWritePermission"/> for this <see cref="NetworkVariable{T}"/></param>
public NetworkVariable(T value = default,
NetworkVariableReadPermission readPerm = DefaultReadPerm,
NetworkVariableWritePermission writePerm = DefaultWritePerm)
: base(readPerm, writePerm)
{
m_InternalValue = value;
m_InternalOriginalValue = default;
// Since we start with IsDirty = true, this doesn't need to be duplicated
// right away. It won't get read until after ResetDirty() is called, and
// the duplicate will be made there. Avoiding calling
// NetworkVariableSerialization<T>.Duplicate() is important because calling
// it in the constructor might not give users enough time to set the
// DuplicateValue callback if they're using UserNetworkVariableSerialization
m_PreviousValue = default;
}
/// <summary>
/// Resets the NetworkVariable when the associated NetworkObject is not spawned
/// </summary>
/// <param name="value">the value to reset the NetworkVariable to (if none specified it resets to the default)</param>
public void Reset(T value = default)
{
if (m_NetworkBehaviour == null || m_NetworkBehaviour != null && !m_NetworkBehaviour.NetworkObject.IsSpawned)
{
m_InternalValue = value;
NetworkVariableSerialization<T>.Duplicate(m_InternalValue, ref m_InternalOriginalValue);
m_PreviousValue = default;
}
}
/// <summary>
/// The internal value of the NetworkVariable
/// </summary>
[SerializeField]
private protected T m_InternalValue;
// The introduction of standard .NET collections caused an issue with permissions since there is no way to detect changes in the
// collection without doing a full comparison. While this approach does consume more memory per collection instance, it is the
// lowest risk approach to resolving the issue where a client with no write permissions could make changes to a collection locally
// which can cause a myriad of issues.
private protected T m_InternalOriginalValue;
private protected T m_PreviousValue;
private bool m_HasPreviousValue;
private bool m_IsDisposed;
/// <summary>
/// The value of the NetworkVariable container
/// </summary>
/// <remarks>
/// When assigning collections to <see cref="Value"/>, unless it is a completely new collection this will not
/// detect any deltas with most managed collection classes since assignment of one collection value to another
/// is actually just a reference to the collection itself. <br />
/// To detect deltas in a collection, you should invoke <see cref="CheckDirtyState"/> after making modifications to the collection.
/// </remarks>
public virtual T Value
{
get => m_InternalValue;
set
{
if (CannotWrite)
{
LogWritePermissionError();
return;
}
// Compare the Value being applied to the current value
if (!NetworkVariableSerialization<T>.AreEqual(ref m_InternalValue, ref value))
{
T previousValue = m_InternalValue;
m_InternalValue = value;
NetworkVariableSerialization<T>.Duplicate(m_InternalValue, ref m_InternalOriginalValue);
SetDirty(true);
m_IsDisposed = false;
OnValueChanged?.Invoke(previousValue, m_InternalValue);
}
}
}
/// <summary>
/// Invoke this method to check if a collection's items are dirty.
/// The default behavior is to exit early if the <see cref="NetworkVariable{T}"/> is already dirty.
/// </summary>
/// <remarks>
/// This is to be used as a way to check if a <see cref="NetworkVariable{T}"/> containing a managed collection has any changees to the collection items.<br />
/// If you invoked this when a collection is dirty, it will not trigger the <see cref="OnValueChanged"/> unless you set forceCheck param to true. <br />
/// </remarks>
/// <param name="forceCheck"> when true, this check will force a full item collection check even if the NetworkVariable is already dirty</param>
/// <returns>True if the variable is dirty and needs synchronization, false otherwise</returns>
public bool CheckDirtyState(bool forceCheck = false)
{
var isDirty = base.IsDirty();
// A client without permissions invoking this method should only check to assure the current value is equal to the last known current value
if (CannotWrite)
{
// If modifications are detected, then revert back to the last known current value
if (!NetworkVariableSerialization<T>.AreEqual(ref m_InternalValue, ref m_InternalOriginalValue))
{
NetworkVariableSerialization<T>.Duplicate(m_InternalOriginalValue, ref m_InternalValue);
}
return false;
}
// Compare the previous with the current if not dirty or forcing a check.
if ((!isDirty || forceCheck) && !NetworkVariableSerialization<T>.AreEqual(ref m_PreviousValue, ref m_InternalValue))
{
SetDirty(true);
OnValueChanged?.Invoke(m_PreviousValue, m_InternalValue);
m_IsDisposed = false;
isDirty = true;
}
return isDirty;
}
/// <inheritdoc/>
internal override void OnCheckIsDirtyState()
{
CheckDirtyState();
base.OnCheckIsDirtyState();
}
internal ref T RefValue()
{
return ref m_InternalValue;
}
/// <inheritdoc/>
public override void Dispose()
{
if (m_IsDisposed)
{
return;
}
m_IsDisposed = true;
// Dispose the internal value
if (m_InternalValue is IDisposable internalValueDisposable)
{
internalValueDisposable.Dispose();
}
m_InternalValue = default;
// Dispose the internal original value
if (m_InternalOriginalValue is IDisposable internalOriginalValueDisposable)
{
internalOriginalValueDisposable.Dispose();
}
m_InternalOriginalValue = default;
// Dispose the previous value if there is one
if (m_HasPreviousValue && m_PreviousValue is IDisposable previousValueDisposable)
{
m_HasPreviousValue = false;
previousValueDisposable.Dispose();
}
m_PreviousValue = default;
base.Dispose();
}
/// <summary>
/// Finalizer that ensures proper cleanup of resources
/// </summary>
~NetworkVariable()
{
Dispose();
}
/// <summary>
/// Gets Whether or not the container is dirty
/// </summary>
/// <returns>Whether or not the container is dirty</returns>
public override bool IsDirty()
{
// 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
// to the original collection value prior to applying updates (primarily for collections).
if (!NetworkUpdaterCheck && CannotWrite && !NetworkVariableSerialization<T>.AreEqual(ref m_InternalValue, ref m_InternalOriginalValue))
{
NetworkVariableSerialization<T>.Duplicate(m_InternalOriginalValue, ref m_InternalValue);
return true;
}
// For most cases we can use the dirty flag.
// This doesn't work for cases where we're wrapping more complex types
// like INetworkSerializable, NativeList, NativeArray, etc.
// Changes to the values in those types don't call the Value.set method,
// so we can't catch those changes and need to compare the current value
// against the previous one.
if (base.IsDirty())
{
return true;
}
var dirty = !NetworkVariableSerialization<T>.AreEqual(ref m_PreviousValue, ref m_InternalValue);
// Cache the dirty value so we don't perform this again if we already know we're dirty
// Unfortunately we can't cache the NOT dirty state, because that might change
// in between to checks... but the DIRTY state won't change until ResetDirty()
// is called.
SetDirty(dirty);
return dirty;
}
/// <summary>
/// Resets the dirty state and marks the variable as synced / clean
/// </summary>
public override void ResetDirty()
{
// Resetting the dirty value declares that the current value is not dirty
// Therefore, we set the m_PreviousValue field to a duplicate of the current
// field, so that our next dirty check is made against the current "not dirty"
// value.
if (IsDirty())
{
m_HasPreviousValue = true;
NetworkVariableSerialization<T>.Duplicate(m_InternalValue, ref m_PreviousValue);
// Once updated, assure the original current value is updated for future comparison purposes
NetworkVariableSerialization<T>.Duplicate(m_InternalValue, ref m_InternalOriginalValue);
}
base.ResetDirty();
}
/// <summary>
/// Writes the variable to the writer
/// </summary>
/// <param name="writer">The stream to write the value to</param>
public override void WriteDelta(FastBufferWriter writer)
{
NetworkVariableSerialization<T>.WriteDelta(writer, ref m_InternalValue, ref m_PreviousValue);
}
/// <summary>
/// Reads value from the reader and applies it
/// </summary>
/// <param name="reader">The stream to read the value from</param>
/// <param name="keepDirtyDelta">Whether or not the container should keep the dirty delta, or mark the delta as consumed</param>
public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta)
{
// 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
// to the original collection value prior to applying updates (primarily for collections).
if (CannotWrite && !NetworkVariableSerialization<T>.AreEqual(ref m_InternalOriginalValue, ref m_InternalValue))
{
NetworkVariableSerialization<T>.Duplicate(m_InternalOriginalValue, ref m_InternalValue);
}
NetworkVariableSerialization<T>.ReadDelta(reader, ref m_InternalValue);
// keepDirtyDelta marks a variable received as dirty and causes the server to send the value to clients
// In a prefect world, whether a variable was A) modified locally or B) received and needs retransmit
// would be stored in different fields
// LEGACY NOTE: This is only to handle NetworkVariableDeltaMessage Version 0 connections. The updated
// NetworkVariableDeltaMessage no longer uses this approach.
if (keepDirtyDelta)
{
SetDirty(true);
}
OnValueChanged?.Invoke(m_PreviousValue, m_InternalValue);
}
/// <summary>
/// This should be always invoked (client & server) to assure the previous values are set
/// !! IMPORTANT !!
/// When a server forwards delta updates to connected clients, it needs to preserve the previous dirty value(s)
/// until it is done serializing all valid NetworkVariable field deltas (relative to each client). This is invoked
/// after it is done forwarding the deltas at the end of the <see cref="NetworkVariableDeltaMessage.Handle(ref NetworkContext)"/> method.
/// </summary>
internal override void PostDeltaRead()
{
// In order to get managed collections to properly have a previous and current value, we have to
// duplicate the collection at this point before making any modifications to the current.
m_HasPreviousValue = true;
NetworkVariableSerialization<T>.Duplicate(m_InternalValue, ref m_PreviousValue);
// Once updated, assure the original current value is updated for future comparison purposes
NetworkVariableSerialization<T>.Duplicate(m_InternalValue, ref m_InternalOriginalValue);
}
/// <inheritdoc />
public override void ReadField(FastBufferReader reader)
{
// 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
// to the original collection value prior to applying updates (primarily for collections).
if (CannotWrite && !NetworkVariableSerialization<T>.AreEqual(ref m_InternalOriginalValue, ref m_InternalValue))
{
NetworkVariableSerialization<T>.Duplicate(m_InternalOriginalValue, ref m_InternalValue);
}
NetworkVariableSerialization<T>.Read(reader, ref m_InternalValue);
// In order to get managed collections to properly have a previous and current value, we have to
// duplicate the collection at this point before making any modifications to the current.
// We duplicate the final value after the read (for ReadField ONLY) so the previous value is at par
// with the current value (since this is only invoked when initially synchronizing).
m_HasPreviousValue = true;
NetworkVariableSerialization<T>.Duplicate(m_InternalValue, ref m_PreviousValue);
// Once updated, assure the original current value is updated for future comparison purposes
NetworkVariableSerialization<T>.Duplicate(m_InternalValue, ref m_InternalOriginalValue);
}
/// <inheritdoc />
public override void WriteField(FastBufferWriter writer)
{
NetworkVariableSerialization<T>.Write(writer, ref m_InternalValue);
}
internal override void WriteFieldSynchronization(FastBufferWriter writer)
{
// If we have a pending update, then synchronize the client with the previously known
// value since the updated version will be sent on the next tick or next time it is
// set to be updated
if (base.IsDirty() && m_HasPreviousValue)
{
NetworkVariableSerialization<T>.Write(writer, ref m_PreviousValue);
}
else
{
base.WriteFieldSynchronization(writer);
}
}
}
}