Skip to content

Commit b9fe0d2

Browse files
authored
feat: Added support for client anticipation in NetworkVariables and NetworkTransform and support for throttling functionality in NetworkVariables (#2820)
1 parent 1fa39e1 commit b9fe0d2

35 files changed

+2584
-25
lines changed

com.unity.netcode.gameobjects/CHANGELOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ Additional documentation and release notes are available at [Multiplayer Documen
99
## [Unreleased]
1010

1111
### Added
12+
-Added AnticipatedNetworkVariable<T>, which adds support for client anticipation of NetworkVariable values, allowing for more responsive gameplay (#2820)
13+
- Added AnticipatedNetworkTransform, which adds support for client anticipation of NetworkTransforms (#2820)
14+
- Added NetworkVariableBase.ExceedsDirtinessThreshold to allow network variables to throttle updates by only sending updates when the difference between the current and previous values exceeds a threshold. (This is exposed in NetworkVariable<T> with the callback NetworkVariable<T>.CheckExceedsDirtinessThreshold) (#2820)
15+
- Added NetworkVariableUpdateTraits, which add additional throttling support: MinSecondsBetweenUpdates will prevent the NetworkVariable from sending updates more often than the specified time period (even if it exceeds the dirtiness threshold), while MaxSecondsBetweenUpdates will force a dirty NetworkVariable to send an update after the specified time period even if it has not yet exceeded the dirtiness threshold. (#2820)
16+
- Added virtual method NetworkVariableBase.OnInitialize() which can be used by NetworkVariable subclasses to add initialization code (#2820)
17+
- Added virtual method NetworkVariableBase.Update(), which is called once per frame to support behaviors such as interpolation between an anticipated value and an authoritative one. (#2820)
18+
- Added NetworkTime.TickWithPartial, which represents the current tick as a double that includes the fractional/partial tick value. (#2820)
19+
- Added NetworkTickSystem.AnticipationTick, which can be helpful with implementation of client anticipation. This value represents the tick the current local client was at at the beginning of the most recent network round trip, which enables it to correlate server update ticks with the client tick that may have triggered them. (#2820)
1220

1321
### Fixed
1422

com.unity.netcode.gameobjects/Components/AnticipatedNetworkTransform.cs

+500
Large diffs are not rendered by default.

com.unity.netcode.gameobjects/Components/AnticipatedNetworkTransform.cs.meta

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

com.unity.netcode.gameobjects/Components/NetworkTransform.cs

+16-2
Original file line numberDiff line numberDiff line change
@@ -2047,10 +2047,15 @@ private bool ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState netw
20472047
return isDirty;
20482048
}
20492049

2050+
protected virtual void OnTransformUpdated()
2051+
{
2052+
2053+
}
2054+
20502055
/// <summary>
20512056
/// Applies the authoritative state to the transform
20522057
/// </summary>
2053-
private void ApplyAuthoritativeState()
2058+
protected internal void ApplyAuthoritativeState()
20542059
{
20552060
var networkState = m_LocalAuthoritativeNetworkState;
20562061
// The m_CurrentPosition, m_CurrentRotation, and m_CurrentScale values are continually updated
@@ -2221,6 +2226,7 @@ private void ApplyAuthoritativeState()
22212226
}
22222227
transform.localScale = m_CurrentScale;
22232228
}
2229+
OnTransformUpdated();
22242230
}
22252231

22262232
/// <summary>
@@ -2418,6 +2424,7 @@ private void ApplyTeleportingState(NetworkTransformState newState)
24182424
{
24192425
AddLogEntry(ref newState, NetworkObject.OwnerClientId);
24202426
}
2427+
OnTransformUpdated();
24212428
}
24222429

24232430
/// <summary>
@@ -2586,6 +2593,11 @@ protected virtual void OnNetworkTransformStateUpdated(ref NetworkTransformState
25862593

25872594
}
25882595

2596+
protected virtual void OnBeforeUpdateTransformState()
2597+
{
2598+
2599+
}
2600+
25892601
private NetworkTransformState m_OldState = new NetworkTransformState();
25902602

25912603
/// <summary>
@@ -2609,6 +2621,8 @@ private void OnNetworkStateChanged(NetworkTransformState oldState, NetworkTransf
26092621
// Get the time when this new state was sent
26102622
newState.SentTime = new NetworkTime(m_CachedNetworkManager.NetworkConfig.TickRate, newState.NetworkTick).Time;
26112623

2624+
OnBeforeUpdateTransformState();
2625+
26122626
// Apply the new state
26132627
ApplyUpdatedState(newState);
26142628

@@ -3315,7 +3329,7 @@ private static void RegisterForTickUpdate(NetworkTransform networkTransform)
33153329

33163330
/// <summary>
33173331
/// If a NetworkTransformTickRegistration exists for the NetworkManager instance, then this will
3318-
/// remove the NetworkTransform instance from the single tick update entry point.
3332+
/// remove the NetworkTransform instance from the single tick update entry point.
33193333
/// </summary>
33203334
/// <param name="networkTransform"></param>
33213335
private static void DeregisterForTickUpdate(NetworkTransform networkTransform)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using Unity.Netcode.Components;
2+
using UnityEditor;
3+
4+
namespace Unity.Netcode.Editor
5+
{
6+
/// <summary>
7+
/// The <see cref="CustomEditor"/> for <see cref="AnticipatedNetworkTransform"/>
8+
/// </summary>
9+
[CustomEditor(typeof(AnticipatedNetworkTransform), true)]
10+
public class AnticipatedNetworkTransformEditor : NetworkTransformEditor
11+
{
12+
public override bool HideInterpolateValue => true;
13+
}
14+
}

com.unity.netcode.gameobjects/Editor/AnticipatedNetworkTransformEditor.cs.meta

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs

+1
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,7 @@ private void CreateNetworkVariableTypeInitializers(AssemblyDefinition assembly,
408408
}
409409
else
410410
{
411+
m_Diagnostics.AddError($"{type}: Managed type in NetworkVariable must implement IEquatable<{type}>");
411412
equalityMethod = new GenericInstanceMethod(m_NetworkVariableSerializationTypes_InitializeEqualityChecker_ManagedClassEquals_MethodRef);
412413
}
413414

com.unity.netcode.gameobjects/Editor/NetworkTransformEditor.cs

+7-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ public class NetworkTransformEditor : UnityEditor.Editor
3737
private static GUIContent s_RotationLabel = EditorGUIUtility.TrTextContent("Rotation");
3838
private static GUIContent s_ScaleLabel = EditorGUIUtility.TrTextContent("Scale");
3939

40+
public virtual bool HideInterpolateValue => false;
41+
4042
/// <inheritdoc/>
4143
public void OnEnable()
4244
{
@@ -137,7 +139,11 @@ public override void OnInspectorGUI()
137139
EditorGUILayout.Space();
138140
EditorGUILayout.LabelField("Configurations", EditorStyles.boldLabel);
139141
EditorGUILayout.PropertyField(m_InLocalSpaceProperty);
140-
EditorGUILayout.PropertyField(m_InterpolateProperty);
142+
if (!HideInterpolateValue)
143+
{
144+
EditorGUILayout.PropertyField(m_InterpolateProperty);
145+
}
146+
141147
EditorGUILayout.PropertyField(m_SlerpPosition);
142148
EditorGUILayout.PropertyField(m_UseQuaternionSynchronization);
143149
if (m_UseQuaternionSynchronization.boolValue)

com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs

+9
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,15 @@ internal void DisconnectEventHandler(ulong transportClientId)
510510
// as the client ID is no longer valid.
511511
NetworkManager.Shutdown(true);
512512
}
513+
514+
if (NetworkManager.IsServer)
515+
{
516+
MessageManager.ClientDisconnected(clientId);
517+
}
518+
else
519+
{
520+
MessageManager.ClientDisconnected(NetworkManager.ServerClientId);
521+
}
513522
#if DEVELOPMENT_BUILD || UNITY_EDITOR
514523
s_TransportDisconnect.End();
515524
#endif

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

+38-8
Original file line numberDiff line numberDiff line change
@@ -815,19 +815,35 @@ internal void PostNetworkVariableWrite(bool forced = false)
815815
// during OnNetworkSpawn has been sent and needs to be cleared
816816
for (int i = 0; i < NetworkVariableFields.Count; i++)
817817
{
818-
NetworkVariableFields[i].ResetDirty();
818+
var networkVariable = NetworkVariableFields[i];
819+
if (networkVariable.IsDirty())
820+
{
821+
if (networkVariable.CanSend())
822+
{
823+
networkVariable.UpdateLastSentTime();
824+
networkVariable.ResetDirty();
825+
networkVariable.SetDirty(false);
826+
}
827+
}
819828
}
820829
}
821830
else
822831
{
823832
// mark any variables we wrote as no longer dirty
824833
for (int i = 0; i < NetworkVariableIndexesToReset.Count; i++)
825834
{
826-
NetworkVariableFields[NetworkVariableIndexesToReset[i]].ResetDirty();
835+
var networkVariable = NetworkVariableFields[NetworkVariableIndexesToReset[i]];
836+
if (networkVariable.IsDirty())
837+
{
838+
if (networkVariable.CanSend())
839+
{
840+
networkVariable.UpdateLastSentTime();
841+
networkVariable.ResetDirty();
842+
networkVariable.SetDirty(false);
843+
}
844+
}
827845
}
828846
}
829-
830-
MarkVariablesDirty(false);
831847
}
832848

833849
internal void PreVariableUpdate()
@@ -836,7 +852,6 @@ internal void PreVariableUpdate()
836852
{
837853
InitializeVariables();
838854
}
839-
840855
PreNetworkVariableWrite();
841856
}
842857

@@ -863,7 +878,10 @@ private void NetworkVariableUpdate(ulong targetClientId, int behaviourIndex)
863878
var networkVariable = NetworkVariableFields[k];
864879
if (networkVariable.IsDirty() && networkVariable.CanClientRead(targetClientId))
865880
{
866-
shouldSend = true;
881+
if (networkVariable.CanSend())
882+
{
883+
shouldSend = true;
884+
}
867885
break;
868886
}
869887
}
@@ -904,9 +922,16 @@ private bool CouldHaveDirtyNetworkVariables()
904922
// TODO: There should be a better way by reading one dirty variable vs. 'n'
905923
for (int i = 0; i < NetworkVariableFields.Count; i++)
906924
{
907-
if (NetworkVariableFields[i].IsDirty())
925+
var networkVariable = NetworkVariableFields[i];
926+
if (networkVariable.IsDirty())
908927
{
909-
return true;
928+
if (networkVariable.CanSend())
929+
{
930+
return true;
931+
}
932+
// If it's dirty but can't be sent yet, we have to keep monitoring it until one of the
933+
// conditions blocking its send changes.
934+
NetworkManager.BehaviourUpdater.AddForUpdate(NetworkObject);
910935
}
911936
}
912937

@@ -1063,6 +1088,11 @@ protected virtual void OnSynchronize<T>(ref BufferSerializer<T> serializer) wher
10631088

10641089
}
10651090

1091+
public virtual void OnReanticipate(double lastRoundTripTime)
1092+
{
1093+
1094+
}
1095+
10661096
/// <summary>
10671097
/// The relative client identifier targeted for the serialization of this <see cref="NetworkBehaviour"/> instance.
10681098
/// </summary>

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

+5-1
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@ public class NetworkBehaviourUpdater
1111
private NetworkManager m_NetworkManager;
1212
private NetworkConnectionManager m_ConnectionManager;
1313
private HashSet<NetworkObject> m_DirtyNetworkObjects = new HashSet<NetworkObject>();
14+
private HashSet<NetworkObject> m_PendingDirtyNetworkObjects = new HashSet<NetworkObject>();
1415

1516
#if DEVELOPMENT_BUILD || UNITY_EDITOR
1617
private ProfilerMarker m_NetworkBehaviourUpdate = new ProfilerMarker($"{nameof(NetworkBehaviour)}.{nameof(NetworkBehaviourUpdate)}");
1718
#endif
1819

1920
internal void AddForUpdate(NetworkObject networkObject)
2021
{
21-
m_DirtyNetworkObjects.Add(networkObject);
22+
m_PendingDirtyNetworkObjects.Add(networkObject);
2223
}
2324

2425
internal void NetworkBehaviourUpdate()
@@ -28,6 +29,9 @@ internal void NetworkBehaviourUpdate()
2829
#endif
2930
try
3031
{
32+
m_DirtyNetworkObjects.UnionWith(m_PendingDirtyNetworkObjects);
33+
m_PendingDirtyNetworkObjects.Clear();
34+
3135
// NetworkObject references can become null, when hidden or despawned. Once NUll, there is no point
3236
// trying to process them, even if they were previously marked as dirty.
3337
m_DirtyNetworkObjects.RemoveWhere((sobj) => sobj == null);

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

+33
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,25 @@ public void NetworkUpdate(NetworkUpdateStage updateStage)
4545

4646
DeferredMessageManager.ProcessTriggers(IDeferredNetworkMessageManager.TriggerType.OnNextFrame, 0);
4747

48+
AnticipationSystem.SetupForUpdate();
4849
MessageManager.ProcessIncomingMessageQueue();
4950
MessageManager.CleanupDisconnectedClients();
51+
52+
AnticipationSystem.ProcessReanticipation();
5053
}
5154
break;
5255
case NetworkUpdateStage.PreUpdate:
5356
{
5457
NetworkTimeSystem.UpdateTime();
58+
AnticipationSystem.Update();
5559
}
5660
break;
61+
case NetworkUpdateStage.PostScriptLateUpdate:
62+
63+
AnticipationSystem.Sync();
64+
AnticipationSystem.SetupForRender();
65+
break;
66+
5767
case NetworkUpdateStage.PostLateUpdate:
5868
{
5969
// This should be invoked just prior to the MessageManager processes its outbound queue.
@@ -274,6 +284,25 @@ public event Action OnTransportFailure
274284
remove => ConnectionManager.OnTransportFailure -= value;
275285
}
276286

287+
public delegate void ReanticipateDelegate(double lastRoundTripTime);
288+
289+
/// <summary>
290+
/// This callback is called after all individual OnReanticipate calls on AnticipatedNetworkVariable
291+
/// and AnticipatedNetworkTransform values have been invoked. The first parameter is a hash set of
292+
/// all the variables that have been changed on this frame (you can detect a particular variable by
293+
/// checking if the set contains it), while the second parameter is a set of all anticipated network
294+
/// transforms that have been changed. Both are passed as their base class type.
295+
///
296+
/// The third parameter is the local time corresponding to the current authoritative server state
297+
/// (i.e., to determine the amount of time that needs to be re-simulated, you will use
298+
/// NetworkManager.LocalTime.Time - authorityTime).
299+
/// </summary>
300+
public event ReanticipateDelegate OnReanticipate
301+
{
302+
add => AnticipationSystem.OnReanticipate += value;
303+
remove => AnticipationSystem.OnReanticipate -= value;
304+
}
305+
277306
/// <summary>
278307
/// The callback to invoke during connection approval. Allows client code to decide whether or not to allow incoming client connection
279308
/// </summary>
@@ -518,6 +547,8 @@ public NetworkPrefabHandler PrefabHandler
518547
/// </summary>
519548
public NetworkTickSystem NetworkTickSystem { get; private set; }
520549

550+
internal AnticipationSystem AnticipationSystem { get; private set; }
551+
521552
/// <summary>
522553
/// Used for time mocking in tests
523554
/// </summary>
@@ -813,6 +844,7 @@ internal void Initialize(bool server)
813844

814845
this.RegisterNetworkUpdate(NetworkUpdateStage.EarlyUpdate);
815846
this.RegisterNetworkUpdate(NetworkUpdateStage.PreUpdate);
847+
this.RegisterNetworkUpdate(NetworkUpdateStage.PostScriptLateUpdate);
816848
this.RegisterNetworkUpdate(NetworkUpdateStage.PostLateUpdate);
817849

818850
// ComponentFactory needs to set its defaults next
@@ -845,6 +877,7 @@ internal void Initialize(bool server)
845877
// The remaining systems can then be initialized
846878
NetworkTimeSystem = server ? NetworkTimeSystem.ServerTimeSystem() : new NetworkTimeSystem(1.0 / NetworkConfig.TickRate);
847879
NetworkTickSystem = NetworkTimeSystem.Initialize(this);
880+
AnticipationSystem = new AnticipationSystem(this);
848881

849882
// Create spawn manager instance
850883
SpawnManager = new NetworkSpawnManager(this);

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

+21
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,14 @@ public enum NetworkUpdateStage : byte
5454
/// </summary>
5555
PreLateUpdate = 6,
5656
/// <summary>
57+
/// Updated after Monobehaviour.LateUpdate, but BEFORE rendering
58+
/// </summary>
59+
// Yes, these numbers are out of order due to backward compatibility requirements.
60+
// The enum values are listed in the order they will be called.
61+
PostScriptLateUpdate = 8,
62+
/// <summary>
5763
/// Updated after the Monobehaviour.LateUpdate for all components is invoked
64+
/// and all rendering is complete
5865
/// </summary>
5966
PostLateUpdate = 7
6067
}
@@ -258,6 +265,18 @@ public static PlayerLoopSystem CreateLoopSystem()
258265
}
259266
}
260267

268+
internal struct NetworkPostScriptLateUpdate
269+
{
270+
public static PlayerLoopSystem CreateLoopSystem()
271+
{
272+
return new PlayerLoopSystem
273+
{
274+
type = typeof(NetworkPostScriptLateUpdate),
275+
updateDelegate = () => RunNetworkUpdateStage(NetworkUpdateStage.PostScriptLateUpdate)
276+
};
277+
}
278+
}
279+
261280
internal struct NetworkPostLateUpdate
262281
{
263282
public static PlayerLoopSystem CreateLoopSystem()
@@ -399,6 +418,7 @@ internal static void RegisterLoopSystems()
399418
else if (currentSystem.type == typeof(PreLateUpdate))
400419
{
401420
TryAddLoopSystem(ref currentSystem, NetworkPreLateUpdate.CreateLoopSystem(), typeof(PreLateUpdate.ScriptRunBehaviourLateUpdate), LoopSystemPosition.Before);
421+
TryAddLoopSystem(ref currentSystem, NetworkPostScriptLateUpdate.CreateLoopSystem(), typeof(PreLateUpdate.ScriptRunBehaviourLateUpdate), LoopSystemPosition.After);
402422
}
403423
else if (currentSystem.type == typeof(PostLateUpdate))
404424
{
@@ -440,6 +460,7 @@ internal static void UnregisterLoopSystems()
440460
else if (currentSystem.type == typeof(PreLateUpdate))
441461
{
442462
TryRemoveLoopSystem(ref currentSystem, typeof(NetworkPreLateUpdate));
463+
TryRemoveLoopSystem(ref currentSystem, typeof(NetworkPostScriptLateUpdate));
443464
}
444465
else if (currentSystem.type == typeof(PostLateUpdate))
445466
{

0 commit comments

Comments
 (0)