NetworkServer.dirtyObjects; NetworkBehaviour.SetDirty -> NetworkIdentity.OnBecameDirty

This commit is contained in:
mischa 2023-08-03 12:46:45 +08:00
parent 9d291a9d89
commit 0b484d0830
6 changed files with 170 additions and 60 deletions

View File

@ -155,10 +155,18 @@ protected void SetSyncVarHookGuard(ulong dirtyBit, bool value)
syncVarHookGuard &= ~dirtyBit; syncVarHookGuard &= ~dirtyBit;
} }
// callback for both SyncObject and SyncVar dirty bit setters.
// called once it becomes dirty, not called again while already dirty.
// we only want to follow the .netIdentity memory indirection once.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
void OnBecameDirty() => netIdentity.OnBecameDirty();
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
void SetSyncObjectDirtyBit(ulong dirtyBit) void SetSyncObjectDirtyBit(ulong dirtyBit)
{ {
bool clean = syncObjectDirtyBits == 0;
syncObjectDirtyBits |= dirtyBit; syncObjectDirtyBits |= dirtyBit;
if (clean) OnBecameDirty();
} }
/// <summary>Set as dirty so that it's synced to clients again.</summary> /// <summary>Set as dirty so that it's synced to clients again.</summary>
@ -166,7 +174,9 @@ void SetSyncObjectDirtyBit(ulong dirtyBit)
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetSyncVarDirtyBit(ulong dirtyBit) public void SetSyncVarDirtyBit(ulong dirtyBit)
{ {
bool clean = syncObjectDirtyBits == 0;
syncVarDirtyBits |= dirtyBit; syncVarDirtyBits |= dirtyBit;
if (clean) OnBecameDirty();
} }
/// <summary>Set as dirty to trigger OnSerialize & send. Dirty bits are cleared after the send.</summary> /// <summary>Set as dirty to trigger OnSerialize & send. Dirty bits are cleared after the send.</summary>
@ -190,6 +200,12 @@ public bool IsDirty() =>
// 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 - lastSyncTime >= syncInterval;
// check only dirty bits, ignoring sync interval.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsDirtyPending() =>
// check bits first. this is basically free.
(syncVarDirtyBits | syncObjectDirtyBits) != 0UL;
/// <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.

View File

@ -806,19 +806,57 @@ internal void OnStopLocalPlayer()
} }
} }
// NetworkBehaviour OnBecameDirty calls NetworkIdentity callback with index
bool addedToDirtySpawned = false;
internal void OnBecameDirty()
{
// ensure either isServer or isClient are set.
// ensures tests are obvious. without proper setup, it should throw.
if (!isClient && !isServer)
Debug.LogWarning("NetworkIdentity.OnBecameDirty(): neither isClient nor isServer are true. Improper setup?");
if (isServer)
{
// only add to dirty spawned once.
// don't run the insertion twice.
if (!addedToDirtySpawned)
{
// insert into server dirty objects if not inserted yet
// TODO keep a bool so we don't insert all the time?
// only add if observed.
// otherwise no point in adding + iterating from broadcast.
if (observers.Count > 0)
{
NetworkServer.dirtySpawned.Add(this);
addedToDirtySpawned = true;
}
}
}
}
// build dirty mask for server owner & observers (= all dirty components). // build dirty mask for server owner & observers (= all dirty components).
// faster to do it in one iteration instead of iterating separately. // faster to do it in one iteration instead of iterating separately.
(ulong, ulong) ServerDirtyMasks(bool initialState) (ulong, ulong, ulong) ServerDirtyMasks(bool initialState)
{ {
ulong ownerMask = 0; ulong ownerMask = 0;
ulong observerMask = 0; ulong observerMask = 0;
// are any dirty but not ready to be sent yet?
// we need to know this because then we don't remove
// the NetworkIdentity from dirtyObjects just yet.
// otherwise if we remove before it was synced, we would miss a sync.
ulong dirtyPending = 0;
NetworkBehaviour[] components = NetworkBehaviours; NetworkBehaviour[] components = NetworkBehaviours;
for (int i = 0; i < components.Length; ++i) for (int i = 0; i < components.Length; ++i)
{ {
NetworkBehaviour component = components[i]; NetworkBehaviour component = components[i];
bool dirty = component.IsDirty(); bool dirty = component.IsDirty();
bool pending = !dirty && component.IsDirtyPending();
ulong nthBit = (1u << i); ulong nthBit = (1u << i);
// owner needs to be considered for both SyncModes, because // owner needs to be considered for both SyncModes, because
@ -828,7 +866,10 @@ internal void OnStopLocalPlayer()
// for delta, only for ServerToClient and only if dirty. // for delta, only for ServerToClient and only if dirty.
// ClientToServer comes from the owner client. // ClientToServer comes from the owner client.
if (initialState || (component.syncDirection == SyncDirection.ServerToClient && dirty)) if (initialState || (component.syncDirection == SyncDirection.ServerToClient && dirty))
{
ownerMask |= nthBit; ownerMask |= nthBit;
if (pending) dirtyPending |= nthBit;
}
// observers need to be considered only in Observers mode // observers need to be considered only in Observers mode
// //
@ -837,10 +878,13 @@ internal void OnStopLocalPlayer()
// SyncDirection is irrelevant, as both are broadcast to // SyncDirection is irrelevant, as both are broadcast to
// observers which aren't the owner. // observers which aren't the owner.
if (component.syncMode == SyncMode.Observers && (initialState || dirty)) if (component.syncMode == SyncMode.Observers && (initialState || dirty))
{
observerMask |= nthBit; observerMask |= nthBit;
if (pending) dirtyPending |= nthBit;
}
} }
return (ownerMask, observerMask); return (ownerMask, observerMask, dirtyPending);
} }
// build dirty mask for client. // build dirty mask for client.
@ -885,7 +929,7 @@ internal static bool IsDirty(ulong mask, int index)
// check ownerWritten/observersWritten to know if anything was written // check ownerWritten/observersWritten to know if anything was written
// We pass dirtyComponentsMask into this function so that we can check // We pass dirtyComponentsMask into this function so that we can check
// if any Components are dirty before creating writers // if any Components are dirty before creating writers
internal void SerializeServer(bool initialState, NetworkWriter ownerWriter, NetworkWriter observersWriter) internal void SerializeServer(bool initialState, NetworkWriter ownerWriter, NetworkWriter observersWriter, out bool pendingDirty)
{ {
// ensure NetworkBehaviours are valid before usage // ensure NetworkBehaviours are valid before usage
ValidateComponents(); ValidateComponents();
@ -898,7 +942,7 @@ internal void SerializeServer(bool initialState, NetworkWriter ownerWriter, Netw
// instead of writing a 1 byte index per component, // instead of writing a 1 byte index per component,
// we limit components to 64 bits and write one ulong instead. // we limit components to 64 bits and write one ulong instead.
// the ulong is also varint compressed for minimum bandwidth. // the ulong is also varint compressed for minimum bandwidth.
(ulong ownerMask, ulong observerMask) = ServerDirtyMasks(initialState); (ulong ownerMask, ulong observerMask, ulong pendingMask) = ServerDirtyMasks(initialState);
// if nothing dirty, then don't even write the mask. // if nothing dirty, then don't even write the mask.
// otherwise, every unchanged object would send a 1 byte dirty mask! // otherwise, every unchanged object would send a 1 byte dirty mask!
@ -955,6 +999,16 @@ internal void SerializeServer(bool initialState, NetworkWriter ownerWriter, Netw
} }
} }
} }
// are any dirty but not ready to be sent yet?
// we need to know this because then we don't remove
// the NetworkIdentity from dirtyObjects just yet.
// otherwise if we remove before it was synced, we would miss a sync.
pendingDirty = pendingMask != 0;
// if none are still pending, this will be removed from dirtyObjects.
// in that case, clear our flag (the flag is only for performance).
if (!pendingDirty) addedToDirtySpawned = false;
} }
// serialize components into writer on the client. // serialize components into writer on the client.

View File

@ -48,6 +48,20 @@ public static partial class NetworkServer
public static readonly Dictionary<uint, NetworkIdentity> spawned = public static readonly Dictionary<uint, NetworkIdentity> spawned =
new Dictionary<uint, NetworkIdentity>(); new Dictionary<uint, NetworkIdentity>();
// all spawned which are dirty (= need broadcasting).
//
// note that some dirty objects may not be ready for broadcasting,
// because we add them independent of syncInterval.
//
// otherwise each NetworkBehaviour would need an Update() to wait until
// syncInterval is elapsed, which is more expansive then simply adding
// a few false positives here.
//
// NetworkIdentity adds itself to dirtySpawned exactly once.
// we can safely use a List<T> here, faster than a Dictionary with enumerators.
internal static readonly List<NetworkIdentity> dirtySpawned =
new List<NetworkIdentity>();
/// <summary>Single player mode can use dontListen to not accept incoming connections</summary> /// <summary>Single player mode can use dontListen to not accept incoming connections</summary>
// see also: https://github.com/vis2k/Mirror/pull/2595 // see also: https://github.com/vis2k/Mirror/pull/2595
public static bool dontListen; public static bool dontListen;
@ -1213,7 +1227,7 @@ static ArraySegment<byte> CreateSpawnMessagePayload(bool isOwner, NetworkIdentit
// serialize all components with initialState = true // serialize all components with initialState = true
// (can be null if has none) // (can be null if has none)
identity.SerializeServer(true, ownerWriter, observersWriter); identity.SerializeServer(true, ownerWriter, observersWriter, out _);
// convert to ArraySegment to avoid reader allocations // convert to ArraySegment to avoid reader allocations
// if nothing was written, .ToArraySegment returns an empty segment. // if nothing was written, .ToArraySegment returns an empty segment.
@ -1627,7 +1641,6 @@ public static void RebuildObservers(NetworkIdentity identity, bool initialize)
// broadcasting //////////////////////////////////////////////////////// // broadcasting ////////////////////////////////////////////////////////
// helper function to get the right serialization for a connection // helper function to get the right serialization for a connection
static NetworkWriter SerializeForConnection( static NetworkWriter SerializeForConnection(
NetworkConnectionToClient connection,
bool owned, bool owned,
NetworkWriter ownerWriter, NetworkWriter ownerWriter,
NetworkWriter observersWriter) NetworkWriter observersWriter)
@ -1652,47 +1665,7 @@ static NetworkWriter SerializeForConnection(
return null; return null;
} }
static void BroadcastIdentity( static void BroadcastDirtySpawned()
NetworkIdentity identity,
NetworkWriterPooled ownerWriter,
NetworkWriterPooled observersWriter)
{
// only serialize if it has any observers
// TODO only set dirty if has observers? would be easiest.
if (identity.observers.Count > 0)
{
// serialize for owner & observers
ownerWriter.Position = 0;
observersWriter.Position = 0;
identity.SerializeServer(false, ownerWriter, observersWriter);
// broadcast to each observer connection
foreach (NetworkConnectionToClient connection in identity.observers.Values)
{
// has this connection joined the world yet?
if (connection.isReady)
{
// is this entity owned by this connection?
bool owned = identity.connectionToClient == connection;
// get serialization for this entity viewed by this connection
// (if anything was serialized this time)
NetworkWriter serialization = SerializeForConnection(connection, owned, ownerWriter, observersWriter);
if (serialization != null)
{
EntityStateMessage message = new EntityStateMessage
{
netId = identity.netId,
payload = serialization.ToArraySegment()
};
connection.Send(message);
}
}
}
}
}
static void BroadcastSpawned()
{ {
// PULL-Broadcasting vs. PUSH-Broadcasting: // PULL-Broadcasting vs. PUSH-Broadcasting:
// //
@ -1715,21 +1688,73 @@ static void BroadcastSpawned()
observersWriter = NetworkWriterPool.Get()) observersWriter = NetworkWriterPool.Get())
{ {
// let's use push broadcasting to prepare for dirtyObjects. // let's use push broadcasting to prepare for dirtyObjects.
foreach (NetworkIdentity identity in spawned.Values) // only iterate NetworkIdentities which we know to be dirty.
// for example, in an MMO we don't need to iterate NPCs,
// item drops, idle monsters etc. every Broadcast.
for (int i = 0; i < dirtySpawned.Count; ++i)
{ {
NetworkIdentity identity = dirtySpawned[i];
// make sure it's not null or destroyed. // make sure it's not null or destroyed.
// (which can happen if someone uses // (which can happen if someone uses
// GameObject.Destroy instead of // GameObject.Destroy instead of
// NetworkServer.Destroy) // NetworkServer.Destroy)
if (identity != null) if (identity != null)
{ {
BroadcastIdentity(identity, ownerWriter, observersWriter); // only serialize if it has any observers
// TODO only set dirty if has observers? would be easiest.
if (identity.observers.Count > 0)
{
// serialize for owner & observers
ownerWriter.Position = 0;
observersWriter.Position = 0;
identity.SerializeServer(false, ownerWriter, observersWriter, out bool pendingDirty);
// broadcast to each observer connection
foreach (NetworkConnectionToClient connection in identity.observers.Values)
{
// has this connection joined the world yet?
if (connection.isReady)
{
// is this entity owned by this connection?
bool owned = identity.connectionToClient == connection;
// get serialization for this entity viewed by this connection
// (if anything was serialized this time)
NetworkWriter serialization = SerializeForConnection(owned, ownerWriter, observersWriter);
if (serialization != null)
{
EntityStateMessage message = new EntityStateMessage
{
netId = identity.netId,
payload = serialization.ToArraySegment()
};
connection.Send(message);
}
}
}
// if there are no more dirty components pending,
// then remove this in place
if (!pendingDirty)
{
// List.RemoveAt(i) is O(N).
// instead, use O(1) swap-remove from Rust.
// dirtySpawned.RemoveAt(i);
dirtySpawned.SwapRemove(i);
// the last element was moved to 'i'.
// count was reduced by 1.
// our for-int loop checks .Count, nothing more to do here.
}
}
} }
// spawned list should have no null entries because we // spawned list should have no null entries because we
// always call Remove in OnObjectDestroy everywhere. // always call Remove in OnObjectDestroy everywhere.
// if it does have null then someone used // if it does have null then someone used
// GameObject.Destroy instead of NetworkServer.Destroy. // GameObject.Destroy instead of NetworkServer.Destroy.
else Debug.LogWarning($"Found 'null' entry in spawned. Please call NetworkServer.Destroy to destroy networked objects. Don't use GameObject.Destroy."); else Debug.LogWarning($"Found 'null' entry in dirtySpawned. Please call NetworkServer.Destroy to destroy networked objects. Don't use GameObject.Destroy.");
} }
} }
} }
@ -1800,7 +1825,7 @@ static void Broadcast()
connections.Values.CopyTo(connectionsCopy); connections.Values.CopyTo(connectionsCopy);
// broadcast spawned entities // broadcast spawned entities
BroadcastSpawned(); BroadcastDirtySpawned();
// flush all connection's batched messages // flush all connection's batched messages
FlushConnections(); FlushConnections();

View File

@ -60,5 +60,20 @@ public static bool TryDequeue<T>(this Queue<T> source, out T element)
return false; return false;
} }
#endif #endif
// List.RemoveAt is O(N).
// implement Rust's swap-remove O(1) removal technique.
public static void SwapRemove<T>(this List<T> list, int index)
{
// we can only swap if we have at least two elements
if (list.Count >= 2)
{
// copy last element to index
list[index] = list[list.Count - 1];
}
// remove last element
list.RemoveAt(list.Count - 1);
}
} }
} }

View File

@ -50,7 +50,7 @@ public void SerializeAndDeserializeAll()
serverObserversComp.value = 42; serverObserversComp.value = 42;
// serialize server object // serialize server object
serverIdentity.SerializeServer(true, ownerWriter, observersWriter); serverIdentity.SerializeServer(true, ownerWriter, observersWriter, out _);
// deserialize client object with OWNER payload // deserialize client object with OWNER payload
NetworkReader reader = new NetworkReader(ownerWriter.ToArray()); NetworkReader reader = new NetworkReader(ownerWriter.ToArray());
@ -96,7 +96,7 @@ public void SerializationException()
// serialize server object // serialize server object
// should work even if compExc throws an exception. // should work even if compExc throws an exception.
// error log because of the exception is expected. // error log because of the exception is expected.
serverIdentity.SerializeServer(true, ownerWriter, observersWriter); serverIdentity.SerializeServer(true, ownerWriter, observersWriter, out _);
// deserialize client object with OWNER payload // deserialize client object with OWNER payload
// should work even if compExc throws an exception // should work even if compExc throws an exception
@ -187,7 +187,7 @@ public void SerializationMismatch()
serverComp.value = "42"; serverComp.value = "42";
// serialize server object // serialize server object
serverIdentity.SerializeServer(true, ownerWriter, observersWriter); serverIdentity.SerializeServer(true, ownerWriter, observersWriter, out _);
// deserialize on client // deserialize on client
// ignore warning log because of serialization mismatch // ignore warning log because of serialization mismatch
@ -219,7 +219,7 @@ public void SerializeServer_NotInitial_NotDirty_WritesNothing()
// serialize server object. // serialize server object.
// 'initial' would write everything. // 'initial' would write everything.
// instead, try 'not initial' with 0 dirty bits // instead, try 'not initial' with 0 dirty bits
serverIdentity.SerializeServer(false, ownerWriter, observersWriter); serverIdentity.SerializeServer(false, ownerWriter, observersWriter, out _);
Assert.That(ownerWriter.Position, Is.EqualTo(0)); Assert.That(ownerWriter.Position, Is.EqualTo(0));
Assert.That(observersWriter.Position, Is.EqualTo(0)); Assert.That(observersWriter.Position, Is.EqualTo(0));
} }
@ -285,7 +285,7 @@ public void SerializeServer_OwnerMode_ClientToServer()
comp.SetValue(11); // modify with helper function to avoid #3525 comp.SetValue(11); // modify with helper function to avoid #3525
// initial: should still write for owner // initial: should still write for owner
identity.SerializeServer(true, ownerWriter, observersWriter); identity.SerializeServer(true, ownerWriter, observersWriter, out _);
Debug.Log("initial ownerWriter: " + ownerWriter); Debug.Log("initial ownerWriter: " + ownerWriter);
Debug.Log("initial observerWriter: " + observersWriter); Debug.Log("initial observerWriter: " + observersWriter);
Assert.That(ownerWriter.Position, Is.GreaterThan(0)); Assert.That(ownerWriter.Position, Is.GreaterThan(0));
@ -295,7 +295,7 @@ public void SerializeServer_OwnerMode_ClientToServer()
comp.SetValue(22); // modify with helper function to avoid #3525 comp.SetValue(22); // modify with helper function to avoid #3525
ownerWriter.Position = 0; ownerWriter.Position = 0;
observersWriter.Position = 0; observersWriter.Position = 0;
identity.SerializeServer(false, ownerWriter, observersWriter); identity.SerializeServer(false, ownerWriter, observersWriter, out _);
Debug.Log("delta ownerWriter: " + ownerWriter); Debug.Log("delta ownerWriter: " + ownerWriter);
Debug.Log("delta observersWriter: " + observersWriter); Debug.Log("delta observersWriter: " + observersWriter);
Assert.That(ownerWriter.Position, Is.EqualTo(0)); Assert.That(ownerWriter.Position, Is.EqualTo(0));
@ -323,7 +323,7 @@ public void SerializeServer_ObserversMode_ClientToServer()
comp.SetValue(11); // modify with helper function to avoid #3525 comp.SetValue(11); // modify with helper function to avoid #3525
// initial: should write something for owner and observers // initial: should write something for owner and observers
identity.SerializeServer(true, ownerWriter, observersWriter); identity.SerializeServer(true, ownerWriter, observersWriter, out _);
Debug.Log("initial ownerWriter: " + ownerWriter); Debug.Log("initial ownerWriter: " + ownerWriter);
Debug.Log("initial observerWriter: " + observersWriter); Debug.Log("initial observerWriter: " + observersWriter);
Assert.That(ownerWriter.Position, Is.GreaterThan(0)); Assert.That(ownerWriter.Position, Is.GreaterThan(0));
@ -333,7 +333,7 @@ public void SerializeServer_ObserversMode_ClientToServer()
comp.SetValue(22); // modify with helper function to avoid #3525 comp.SetValue(22); // modify with helper function to avoid #3525
ownerWriter.Position = 0; ownerWriter.Position = 0;
observersWriter.Position = 0; observersWriter.Position = 0;
identity.SerializeServer(false, ownerWriter, observersWriter); identity.SerializeServer(false, ownerWriter, observersWriter, out _);
Debug.Log("delta ownerWriter: " + ownerWriter); Debug.Log("delta ownerWriter: " + ownerWriter);
Debug.Log("delta observersWriter: " + observersWriter); Debug.Log("delta observersWriter: " + observersWriter);
Assert.That(ownerWriter.Position, Is.EqualTo(0)); Assert.That(ownerWriter.Position, Is.EqualTo(0));

View File

@ -404,7 +404,7 @@ public void TestSyncingAbstractNetworkBehaviour()
NetworkWriter ownerWriter = new NetworkWriter(); NetworkWriter ownerWriter = new NetworkWriter();
// not really used in this Test // not really used in this Test
NetworkWriter observersWriter = new NetworkWriter(); NetworkWriter observersWriter = new NetworkWriter();
serverIdentity.SerializeServer(true, ownerWriter, observersWriter); serverIdentity.SerializeServer(true, ownerWriter, observersWriter, out _);
// set up a "client" object // set up a "client" object
CreateNetworked(out _, out NetworkIdentity clientIdentity, out SyncVarAbstractNetworkBehaviour clientBehaviour); CreateNetworked(out _, out NetworkIdentity clientIdentity, out SyncVarAbstractNetworkBehaviour clientBehaviour);