syncInterval support

This commit is contained in:
mischa 2024-09-03 22:18:05 +02:00
parent 9688422d52
commit 9a9c8a6fdf
6 changed files with 44 additions and 35 deletions

View File

@ -52,7 +52,12 @@ public abstract class NetworkBehaviour : MonoBehaviour
[Tooltip("Time in seconds until next change is synchronized to the client. '0' means send immediately if changed. '0.5' means only send changes every 500ms.\n(This is for state synchronization like SyncVars, SyncLists, OnSerialize. Not for Cmds, Rpcs, etc.)")] [Tooltip("Time in seconds until next change is synchronized to the client. '0' means send immediately if changed. '0.5' means only send changes every 500ms.\n(This is for state synchronization like SyncVars, SyncLists, OnSerialize. Not for Cmds, Rpcs, etc.)")]
[Range(0, 2)] [Range(0, 2)]
[HideInInspector] public float syncInterval = 0; [HideInInspector] public float syncInterval = 0;
internal double lastSyncTime;
// for reliable, we only need the reliable last sync time.
// for unreliable, we need the reliable for baselines and unreliable for deltas.
// this way we can have syncInterval on unreliable components as well.
internal double lastSyncTimeReliable;
internal double lastSyncTimeUnreliable;
/// <summary>True if this object is on the server and has been spawned.</summary> /// <summary>True if this object is on the server and has been spawned.</summary>
// This is different from NetworkServer.active, which is true if the // This is different from NetworkServer.active, which is true if the
@ -230,29 +235,38 @@ public void SetSyncVarDirtyBit(ulong dirtyBit)
// true if syncInterval elapsed and any SyncVar or SyncObject is dirty // true if syncInterval elapsed and any SyncVar or SyncObject is dirty
// OR both bitmasks. != 0 if either was dirty. // OR both bitmasks. != 0 if either was dirty.
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsDirty() => public bool IsDirty() => // _RELIABLE
// check bits first. this is basically free. // check bits first. this is basically free.
(syncVarDirtyBits | syncObjectDirtyBits) != 0UL && (syncVarDirtyBits | syncObjectDirtyBits) != 0UL &&
// only check time if bits were dirty. this is more expensive. // only check time if bits were dirty. this is more expensive.
NetworkTime.localTime - lastSyncTime >= syncInterval; NetworkTime.localTime - lastSyncTimeReliable >= syncInterval;
// true if syncInterval elapsed and any SyncVar or SyncObject is dirty
// OR both bitmasks. != 0 if either was dirty.
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
bool IsDirty_BitsOnly() => public bool IsDirtyUnreliable() =>
(syncVarDirtyBits | syncObjectDirtyBits) != 0UL; // check bits first. this is basically free.
(syncVarDirtyBits | syncObjectDirtyBits) != 0UL &&
// only check time if bits were dirty. this is more expensive.
// -> for initial we check last reliable sync time.
// for delta we check last unreliable sync time.
NetworkTime.localTime - lastSyncTimeUnreliable >= syncInterval;
// convenience function to check if a component is dirty for the given // convenience function to check if a component is dirty for the given
// SyncMethod. // SyncMethod.
internal bool IsDirtyFor(SyncMethod method) internal bool IsDirtyFor(SyncMethod method)
{ {
// reliable: only if dirty bits were set and syncInterval elapsed // reliable: only if dirty bits were set and syncInterval elapsed.
if (method == SyncMethod.Reliable && syncMethod == SyncMethod.Reliable) if (method == SyncMethod.Reliable && syncMethod == SyncMethod.Reliable)
{ {
return IsDirty(); return IsDirty();
} }
// unreliable: if dirty bits were set (ignored syncInterval for tick aligned SyncVars) // unreliable: only if dirty bits were set and syncInterval elapsed.
// note that we chose 'syncInterval' support over 'tick aligned' SyncVars,
// because the bandwidth savings are worth it.
else if (method == SyncMethod.Unreliable && syncMethod == SyncMethod.Unreliable) else if (method == SyncMethod.Unreliable && syncMethod == SyncMethod.Unreliable)
{ {
return IsDirty_BitsOnly(); return IsDirtyUnreliable();
} }
return false; return false;
@ -261,9 +275,11 @@ internal bool IsDirtyFor(SyncMethod method)
/// <summary>Clears all the dirty bits that were set by SetSyncVarDirtyBit() (formally SetDirtyBits)</summary> /// <summary>Clears all the dirty bits that were set by SetSyncVarDirtyBit() (formally SetDirtyBits)</summary>
// automatically invoked when an update is sent for this object, but can // automatically invoked when an update is sent for this object, but can
// be called manually as well. // be called manually as well.
public void ClearAllDirtyBits() public void ClearAllDirtyBits(bool clearReliableSyncTime, bool clearUnreliableSyncTime)
{ {
lastSyncTime = NetworkTime.localTime; if (clearReliableSyncTime) lastSyncTimeReliable = NetworkTime.localTime;
if (clearUnreliableSyncTime) lastSyncTimeUnreliable = NetworkTime.localTime;
syncVarDirtyBits = 0L; syncVarDirtyBits = 0L;
syncObjectDirtyBits = 0L; syncObjectDirtyBits = 0L;

View File

@ -1027,7 +1027,7 @@ internal void SerializeServer(bool initialState, SyncMethod method, NetworkWrite
// otherwise if a player joins, we serialize monster, // otherwise if a player joins, we serialize monster,
// and shouldn't clear dirty bits not yet synced to // and shouldn't clear dirty bits not yet synced to
// other players. // other players.
if (!initialState) comp.ClearAllDirtyBits(); if (!initialState) comp.ClearAllDirtyBits(true, false);
} }
else if (method == SyncMethod.Unreliable) else if (method == SyncMethod.Unreliable)
{ {
@ -1036,11 +1036,9 @@ internal void SerializeServer(bool initialState, SyncMethod method, NetworkWrite
// and shouldn't clear dirty bits not yet synced to // and shouldn't clear dirty bits not yet synced to
// other players. // other players.
// //
// for delta: only clear for full syncs. // for delta: clear bits depending on if this was a
// delta syncs over unreliable may not be delivered, // reliable baseline or a unreliable delta sync.
// so we can only clear dirty bits for guaranteed to if (!initialState) comp.ClearAllDirtyBits(unreliableFullSendIntervalElapsed, !unreliableFullSendIntervalElapsed);
// be delivered full syncs.
if (!initialState && unreliableFullSendIntervalElapsed) comp.ClearAllDirtyBits();
} }
} }
} }
@ -1107,16 +1105,14 @@ internal void SerializeClient(SyncMethod method, NetworkWriter writer, bool unre
{ {
// for reliable: server knows initial. we only send deltas. // for reliable: server knows initial. we only send deltas.
// so always clear for deltas. // so always clear for deltas.
comp.ClearAllDirtyBits(); comp.ClearAllDirtyBits(true, false);
} }
else if (method == SyncMethod.Unreliable) else if (method == SyncMethod.Unreliable)
{ {
// for unreliable: server knows initial. we only send deltas. // for unreliable: server knows initial. we only send deltas.
// but only clear for full syncs. // for delta: clear bits depending on if this was a
// delta syncs over unreliable may not be delivered, // reliable baseline or a unreliable delta sync.
// so we can only clear dirty bits for guaranteed to comp.ClearAllDirtyBits(unreliableFullSendIntervalElapsed, !unreliableFullSendIntervalElapsed);
// be delivered full syncs.
if (unreliableFullSendIntervalElapsed) comp.ClearAllDirtyBits();
} }
} }
} }
@ -1284,7 +1280,7 @@ internal void ClearAllComponentsDirtyBits()
{ {
foreach (NetworkBehaviour comp in NetworkBehaviours) foreach (NetworkBehaviour comp in NetworkBehaviours)
{ {
comp.ClearAllDirtyBits(); comp.ClearAllDirtyBits(true, true);
} }
} }

View File

@ -2021,8 +2021,6 @@ static void BroadcastToConnection(NetworkConnectionToClient connection, bool unr
// 'Unreliable' sync: send Unreliable components over unreliable // 'Unreliable' sync: send Unreliable components over unreliable
// state is 'initial' for reliable baseline, and 'not initial' for unreliable deltas. // state is 'initial' for reliable baseline, and 'not initial' for unreliable deltas.
// note that syncInterval is always ignored for unreliable in order to have tick aligned [SyncVars].
// even if we pass SyncMethod.Reliable, it serializes with initialState=true.
serialization = SerializeForConnection(identity, connection, SyncMethod.Unreliable, unreliableFullSendIntervalElapsed); serialization = SerializeForConnection(identity, connection, SyncMethod.Unreliable, unreliableFullSendIntervalElapsed);
if (serialization != null) if (serialization != null)
{ {

View File

@ -104,9 +104,8 @@ protected void DrawDefaultSyncSettings()
if (syncDirection.enumValueIndex == (int)SyncDirection.ServerToClient) if (syncDirection.enumValueIndex == (int)SyncDirection.ServerToClient)
EditorGUILayout.PropertyField(serializedObject.FindProperty("syncMode")); EditorGUILayout.PropertyField(serializedObject.FindProperty("syncMode"));
// sync interval: only shown for reliable sync // sync interval
if (syncMethod.enumValueIndex == (int)SyncMethod.Reliable) EditorGUILayout.PropertyField(serializedObject.FindProperty("syncInterval"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("syncInterval"));
// apply // apply
serializedObject.ApplyModifiedProperties(); serializedObject.ApplyModifiedProperties();

View File

@ -88,12 +88,12 @@ public void IsDirty()
// changing a [SyncVar] should set it dirty // changing a [SyncVar] should set it dirty
++comp.health; ++comp.health;
Assert.That(comp.IsDirty(), Is.True); Assert.That(comp.IsDirty(), Is.True);
comp.ClearAllDirtyBits(); comp.ClearAllDirtyBits(true, true);
// changing a SyncCollection should set it dirty // changing a SyncCollection should set it dirty
comp.list.Add(42); comp.list.Add(42);
Assert.That(comp.IsDirty(), Is.True); Assert.That(comp.IsDirty(), Is.True);
comp.ClearAllDirtyBits(); comp.ClearAllDirtyBits(true, true);
// it should only be dirty after syncInterval elapsed // it should only be dirty after syncInterval elapsed
comp.syncInterval = float.MaxValue; comp.syncInterval = float.MaxValue;
@ -115,7 +115,7 @@ public void ClearAllDirtyBitsClearsSyncVarDirtyBits()
Assert.That(emptyBehaviour.IsDirty(), Is.True); Assert.That(emptyBehaviour.IsDirty(), Is.True);
// clear it // clear it
emptyBehaviour.ClearAllDirtyBits(); emptyBehaviour.ClearAllDirtyBits(true, true);
Assert.That(emptyBehaviour.IsDirty(), Is.False); Assert.That(emptyBehaviour.IsDirty(), Is.False);
} }
@ -134,7 +134,7 @@ public void ClearAllDirtyBitsClearsSyncObjectsDirtyBits()
Assert.That(comp.IsDirty, Is.True); Assert.That(comp.IsDirty, Is.True);
// clear bits should clear synclist bits too // clear bits should clear synclist bits too
comp.ClearAllDirtyBits(); comp.ClearAllDirtyBits(true, true);
Assert.That(comp.IsDirty, Is.False); Assert.That(comp.IsDirty, Is.False);
} }

View File

@ -115,7 +115,7 @@ public void TestSettingStruct()
player.guild = myGuild; player.guild = myGuild;
Assert.That(player.IsDirty(), "Setting struct should mark object as dirty"); Assert.That(player.IsDirty(), "Setting struct should mark object as dirty");
player.ClearAllDirtyBits(); player.ClearAllDirtyBits(true, true);
Assert.That(player.IsDirty(), Is.False, "ClearAllDirtyBits() should clear dirty flag"); Assert.That(player.IsDirty(), Is.False, "ClearAllDirtyBits() should clear dirty flag");
// clearing the guild should set dirty bit too // clearing the guild should set dirty bit too
@ -127,7 +127,7 @@ public void TestSettingStruct()
public void TestSyncIntervalAndClearAllComponents() public void TestSyncIntervalAndClearAllComponents()
{ {
CreateNetworked(out _, out _, out MockPlayer player); CreateNetworked(out _, out _, out MockPlayer player);
player.lastSyncTime = NetworkTime.localTime; player.lastSyncTimeReliable = NetworkTime.localTime;
// synchronize immediately // synchronize immediately
player.syncInterval = 1f; player.syncInterval = 1f;
@ -143,7 +143,7 @@ public void TestSyncIntervalAndClearAllComponents()
player.netIdentity.ClearAllComponentsDirtyBits(); player.netIdentity.ClearAllComponentsDirtyBits();
// set lastSyncTime far enough back to be ready for syncing // set lastSyncTime far enough back to be ready for syncing
player.lastSyncTime = NetworkTime.localTime - player.syncInterval; player.lastSyncTimeReliable = NetworkTime.localTime - player.syncInterval;
// should be dirty now // should be dirty now
Assert.That(player.IsDirty(), Is.False, "Sync interval met, should still not be dirty"); Assert.That(player.IsDirty(), Is.False, "Sync interval met, should still not be dirty");