Skip to content

Commit 3ea55cd

Browse files
authored
feat!: implement tick-based synchronized NetworkVar (#482)
BREAKING CHANGE: NetworkedVar ReadField and ReadDelta functions now take a local and remote tick as parameters. This should only affect implementation of internal types like NetworkedVarDictionary.
1 parent 18c1a0a commit 3ea55cd

11 files changed

Lines changed: 209 additions & 39 deletions

File tree

com.unity.multiplayer.mlapi/Runtime/Core/NetworkedBehaviour.cs

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,11 @@ public bool HasNetworkedObject
366366
internal bool networkedStartInvoked = false;
367367
internal bool internalNetworkedStartInvoked = false;
368368
/// <summary>
369+
/// Stores the network tick at the NetworkedBehaviourUpdate time
370+
/// This allows sending NetworkedVars not more often than once per network tick, regardless of the update rate
371+
/// </summary>
372+
public static ushort currentTick { get; private set; }
373+
/// <summary>
369374
/// Gets called when message handlers are ready to be registered and the networking is setup
370375
/// </summary>
371376
public virtual void NetworkStart()
@@ -521,6 +526,14 @@ internal void InitializeVars()
521526

522527
internal static void NetworkedBehaviourUpdate()
523528
{
529+
// Don't NetworkedBehaviourUpdate more than once per network tick
530+
ushort tick = NetworkingManager.Singleton.networkTickSystem.GetTick();
531+
if (tick == currentTick)
532+
{
533+
return;
534+
}
535+
currentTick = tick;
536+
524537
#if DEVELOPMENT_BUILD || UNITY_EDITOR
525538
s_NetworkedBehaviourUpdate.Begin();
526539
#endif
@@ -574,7 +587,6 @@ internal static void NetworkedBehaviourUpdate()
574587
}
575588
}
576589
}
577-
578590
}
579591
finally
580592
{
@@ -615,8 +627,8 @@ internal void VarUpdate(ulong clientId)
615627

616628
private void NetworkedVarUpdate(ulong clientId)
617629
{
618-
if (!CouldHaveDirtyNetworkedVars())
619-
return;
630+
if (!CouldHaveDirtyNetworkedVars())
631+
return;
620632

621633
for (int j = 0; j < channelMappedNetworkedVarIndexes.Count; j++)
622634
{
@@ -627,6 +639,10 @@ private void NetworkedVarUpdate(ulong clientId)
627639
writer.WriteUInt64Packed(NetworkId);
628640
writer.WriteUInt16Packed(NetworkedObject.GetOrderIndex(this));
629641

642+
// Write the current tick frame
643+
// todo: this is currently done per channel, per tick. The snapshot system might improve on this
644+
writer.WriteUInt16Packed(currentTick);
645+
630646
bool writtenAny = false;
631647
for (int k = 0; k < networkedVarFields.Count; k++)
632648
{
@@ -716,6 +732,9 @@ internal static void HandleNetworkedVarDeltas(List<INetworkedVar> networkedVarLi
716732
{
717733
using (PooledBitReader reader = PooledBitReader.Get(stream))
718734
{
735+
// read the remote network tick at which this variable was written.
736+
ushort remoteTick = reader.ReadUInt16Packed();
737+
719738
for (int i = 0; i < networkedVarList.Count; i++)
720739
{
721740
ushort varSize = 0;
@@ -766,10 +785,15 @@ internal static void HandleNetworkedVarDeltas(List<INetworkedVar> networkedVarLi
766785
}
767786
}
768787

788+
// read the local network tick at which this variable was written.
789+
// if this var was updated from our machine, this local tick will be locally valid
790+
ushort localTick = reader.ReadUInt16Packed();
791+
769792
long readStartPos = stream.Position;
770793

771-
networkedVarList[i].ReadDelta(stream, IsServer);
794+
networkedVarList[i].ReadDelta(stream, IsServer, localTick, remoteTick);
772795
PerformanceDataManager.Increment(ProfilerConstants.NumberNetworkVarsReceived);
796+
773797
ProfilerStatManager.networkVarsRcvd.Record();
774798

775799
if (NetworkingManager.Singleton.NetworkConfig.EnsureNetworkedVarLengthSafety)
@@ -836,8 +860,9 @@ internal static void HandleNetworkedVarUpdate(List<INetworkedVar> networkedVarLi
836860

837861
long readStartPos = stream.Position;
838862

839-
networkedVarList[i].ReadField(stream);
863+
networkedVarList[i].ReadField(stream, NetworkTickSystem.k_NoTick, NetworkTickSystem.k_NoTick);
840864
PerformanceDataManager.Increment(ProfilerConstants.NumberNetworkVarsReceived);
865+
841866
ProfilerStatManager.networkVarsRcvd.Record();
842867

843868
if (NetworkingManager.Singleton.NetworkConfig.EnsureNetworkedVarLengthSafety)
@@ -934,7 +959,7 @@ internal static void SetNetworkedVarData(List<INetworkedVar> networkedVarList, S
934959

935960
long readStartPos = stream.Position;
936961

937-
networkedVarList[j].ReadField(stream);
962+
networkedVarList[j].ReadField(stream, NetworkTickSystem.k_NoTick, NetworkTickSystem.k_NoTick);
938963

939964
if (NetworkingManager.Singleton.NetworkConfig.EnsureNetworkedVarLengthSafety)
940965
{

com.unity.multiplayer.mlapi/Runtime/NetworkedVar/Collections/NetworkedDictionary.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ public Channel GetChannel()
9191
}
9292

9393
/// <inheritdoc />
94-
public void ReadDelta(Stream stream, bool keepDirtyDelta)
94+
public void ReadDelta(Stream stream, bool keepDirtyDelta, ushort localTick, ushort remoteTick)
9595
{
9696
using (PooledBitReader reader = PooledBitReader.Get(stream))
9797
{
@@ -239,7 +239,7 @@ public void ReadDelta(Stream stream, bool keepDirtyDelta)
239239
}
240240

241241
/// <inheritdoc />
242-
public void ReadField(Stream stream)
242+
public void ReadField(Stream stream, ushort localTick, ushort remoteTick)
243243
{
244244
using (PooledBitReader reader = PooledBitReader.Get(stream))
245245
{

com.unity.multiplayer.mlapi/Runtime/NetworkedVar/Collections/NetworkedList.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ public void WriteField(Stream stream)
203203
}
204204

205205
/// <inheritdoc />
206-
public void ReadField(Stream stream)
206+
public void ReadField(Stream stream, ushort localTick, ushort remoteTick)
207207
{
208208
using (PooledBitReader reader = PooledBitReader.Get(stream))
209209
{
@@ -217,7 +217,7 @@ public void ReadField(Stream stream)
217217
}
218218

219219
/// <inheritdoc />
220-
public void ReadDelta(Stream stream, bool keepDirtyDelta)
220+
public void ReadDelta(Stream stream, bool keepDirtyDelta, ushort localTick, ushort remoteTick)
221221
{
222222
using (PooledBitReader reader = PooledBitReader.Get(stream))
223223
{

com.unity.multiplayer.mlapi/Runtime/NetworkedVar/Collections/NetworkedSet.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ public void WriteField(Stream stream)
188188
}
189189

190190
/// <inheritdoc />
191-
public void ReadField(Stream stream)
191+
public void ReadField(Stream stream, ushort localTick, ushort remoteTick)
192192
{
193193
using (PooledBitReader reader = PooledBitReader.Get(stream))
194194
{
@@ -203,7 +203,7 @@ public void ReadField(Stream stream)
203203
}
204204

205205
/// <inheritdoc />
206-
public void ReadDelta(Stream stream, bool keepDirtyDelta)
206+
public void ReadDelta(Stream stream, bool keepDirtyDelta, ushort localTick, ushort remoteTick)
207207
{
208208
using (PooledBitReader reader = PooledBitReader.Get(stream))
209209
{

com.unity.multiplayer.mlapi/Runtime/NetworkedVar/INetworkedVar.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,17 @@ public interface INetworkedVar
4848
/// Reads the complete state from the reader and applies it
4949
/// </summary>
5050
/// <param name="stream">The stream to read the state from</param>
51-
void ReadField(Stream stream);
51+
/// <param name="localTick">The local network tick at which this var was written, on the machine it was written </param>
52+
/// <param name="remoteTick">The remote network tick at which this var was sent by the host </param>
53+
void ReadField(Stream stream, ushort localTick, ushort remoteTick);
5254
/// <summary>
5355
/// Reads delta from the reader and applies them to the internal value
5456
/// </summary>
5557
/// <param name="stream">The stream to read the delta from</param>
5658
/// <param name="keepDirtyDelta">Whether or not the delta should be kept as dirty or consumed</param>
57-
void ReadDelta(Stream stream, bool keepDirtyDelta);
59+
/// <param name="localTick">The local network tick at which this var was written, on the machine it was written </param>
60+
/// <param name="remoteTick">The remote network tick at which this var was sent by the host </param>
61+
void ReadDelta(Stream stream, bool keepDirtyDelta, ushort localTick, ushort remoteTick);
5862
/// <summary>
5963
/// Sets NetworkedBehaviour the container belongs to.
6064
/// </summary>

com.unity.multiplayer.mlapi/Runtime/NetworkedVar/NetworkedVar.cs

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using UnityEngine;
33
using System.IO;
44
using System;
5+
using MLAPI.Logging;
56
using MLAPI.Serialization.Pooled;
67
using MLAPI.Transports;
78

@@ -22,9 +23,13 @@ public class NetworkedVar<T> : INetworkedVar
2223
/// </summary>
2324
public readonly NetworkedVarSettings Settings = new NetworkedVarSettings();
2425
/// <summary>
25-
/// Gets the last time the variable was synced
26+
/// The last time the variable was written to locally
2627
/// </summary>
27-
public float LastSyncedTime { get; internal set; }
28+
public ushort LocalTick { get; internal set; }
29+
/// <summary>
30+
/// The last time the variable was written to remotely. Uses the remote timescale
31+
/// </summary>
32+
public ushort RemoteTick { get; internal set; }
2833
/// <summary>
2934
/// Delegate type for value changed event
3035
/// </summary>
@@ -89,6 +94,10 @@ public T Value
8994
{
9095
if (!EqualityComparer<T>.Default.Equals(InternalValue, value))
9196
{
97+
// Setter is assumed to be called locally, by game code.
98+
// When used by the host, it is its responsibility to set the RemoteTick
99+
RemoteTick = NetworkTickSystem.k_NoTick;
100+
92101
isDirty = true;
93102
T previousValue = InternalValue;
94103
InternalValue = value;
@@ -102,17 +111,12 @@ public T Value
102111
public void ResetDirty()
103112
{
104113
isDirty = false;
105-
LastSyncedTime = NetworkingManager.Singleton.NetworkTime;
106114
}
107115

108116
/// <inheritdoc />
109117
public bool IsDirty()
110118
{
111-
if (!isDirty) return false;
112-
if (Settings.SendTickrate == 0) return true;
113-
if (Settings.SendTickrate < 0) return false;
114-
if (NetworkingManager.Singleton.NetworkTime - LastSyncedTime >= (1f / Settings.SendTickrate)) return true;
115-
return false;
119+
return isDirty;
116120
}
117121

118122
/// <inheritdoc />
@@ -139,7 +143,18 @@ public bool CanClientRead(ulong clientId)
139143
/// Writes the variable to the writer
140144
/// </summary>
141145
/// <param name="stream">The stream to write the value to</param>
142-
public void WriteDelta(Stream stream) => WriteField(stream); //The NetworkedVar is built for simple data types and has no delta.
146+
public void WriteDelta(Stream stream)
147+
{
148+
using (PooledBitWriter writer = PooledBitWriter.Get(stream))
149+
{
150+
// write the network tick at which this NetworkedVar was modified remotely
151+
// this will allow lag-compensation
152+
// todo: this is currently only done on delta updates. Consider whether it should be done in WriteField
153+
writer.WriteUInt16Packed(RemoteTick);
154+
}
155+
156+
WriteField(stream);
157+
}
143158

144159
/// <inheritdoc />
145160
public bool CanClientWrite(ulong clientId)
@@ -167,8 +182,13 @@ public bool CanClientWrite(ulong clientId)
167182
/// </summary>
168183
/// <param name="stream">The stream to read the value from</param>
169184
/// <param name="keepDirtyDelta">Whether or not the container should keep the dirty delta, or mark the delta as consumed</param>
170-
public void ReadDelta(Stream stream, bool keepDirtyDelta)
185+
public void ReadDelta(Stream stream, bool keepDirtyDelta, ushort localTick, ushort remoteTick)
171186
{
187+
// todo: This allows the host-returned value to be set back to an old value
188+
// this will need to be adjusted to check if we're have a most recent value
189+
LocalTick = localTick;
190+
RemoteTick = remoteTick;
191+
172192
using (PooledBitReader reader = PooledBitReader.Get(stream))
173193
{
174194
T previousValue = InternalValue;
@@ -188,14 +208,16 @@ public void SetNetworkedBehaviour(NetworkedBehaviour behaviour)
188208
}
189209

190210
/// <inheritdoc />
191-
public void ReadField(Stream stream)
211+
public void ReadField(Stream stream, ushort localTick, ushort remoteTick)
192212
{
193-
ReadDelta(stream, false);
213+
ReadDelta(stream, false, localTick, remoteTick);
194214
}
195215

196216
/// <inheritdoc />
197217
public void WriteField(Stream stream)
198218
{
219+
// Store the local tick at which this NetworkedVar was modified
220+
LocalTick = NetworkedBehaviour.currentTick;
199221
using (PooledBitWriter writer = PooledBitWriter.Get(stream))
200222
{
201223
writer.WriteObjectPacked(InternalValue); //BOX

testproject/Assets/Prefabs/Cube.prefab

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ GameObject:
2020
- component: {fileID: -745482209883575862}
2121
- component: {fileID: -7468455824255952951}
2222
- component: {fileID: -4978466230159947418}
23+
- component: {fileID: 2690316626396496521}
2324
m_Layer: 0
2425
m_Name: Cube
2526
m_TagString: Target
@@ -170,7 +171,7 @@ MonoBehaviour:
170171
m_Script: {fileID: 11500000, guid: 3e34656ebae784afca7d1f7f6dc18580, type: 3}
171172
m_Name:
172173
m_EditorClassIdentifier:
173-
range: 10
174+
m_Range: 10
174175
--- !u!120 &-745482209883575862
175176
LineRenderer:
176177
m_ObjectHideFlags: 0
@@ -328,3 +329,15 @@ MonoBehaviour:
328329
m_Script: {fileID: 11500000, guid: 9548116c10df1486ea12b7329b77c5cf, type: 3}
329330
m_Name:
330331
m_EditorClassIdentifier:
332+
--- !u!114 &2690316626396496521
333+
MonoBehaviour:
334+
m_ObjectHideFlags: 0
335+
m_CorrespondingSourceObject: {fileID: 0}
336+
m_PrefabInstance: {fileID: 0}
337+
m_PrefabAsset: {fileID: 0}
338+
m_GameObject: {fileID: 8685790303553767886}
339+
m_Enabled: 1
340+
m_EditorHideFlags: 0
341+
m_Script: {fileID: 11500000, guid: 962d0654df408407d8055453c9020f2b, type: 3}
342+
m_Name:
343+
m_EditorClassIdentifier:

testproject/Assets/Scripts/SyncTransform.cs

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using MLAPI.Logging;
21
using MLAPI.NetworkedVar;
32
using UnityEngine;
43

@@ -50,7 +49,6 @@ void SyncPosChanged(Vector3 before, Vector3 after)
5049
{
5150
gameObject.transform.position = after;
5251
}
53-
//Debug.Log("[1] received position from " + before + " to " + after);
5452
}
5553
}
5654

@@ -68,7 +66,6 @@ void SyncRotChanged(Quaternion before, Quaternion after)
6866
{
6967
gameObject.transform.rotation = after;
7068
}
71-
//Debug.Log("[2] received rotation from " + before + " to " + after);
7269
}
7370
}
7471

@@ -118,10 +115,6 @@ void FixedUpdate()
118115
{
119116
gameObject.transform.position = m_PosStore[1];
120117
}
121-
122-
var after = gameObject.transform.position;
123-
124-
//Debug.Log("[3] Updated position from " + before + " to " + after);
125118
}
126119

127120
if (m_RotTimes[0] >= 0.0 && m_RotTimes[1] >= 0.0)
@@ -142,10 +135,6 @@ void FixedUpdate()
142135
{
143136
gameObject.transform.rotation = m_RotStore[1];
144137
}
145-
146-
var after = gameObject.transform.rotation;
147-
148-
//Debug.Log("[4] Updated rotation from " + before + " to " + after);
149138
}
150139
}
151140
}

testproject/Assets/Scripts/Testing.meta

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)