mirror of
https://github.com/MirrorNetworking/Mirror.git
synced 2024-11-18 02:50:32 +00:00
perf: NetworkServer.dirtySpawned via NetworkIdentity.OnBecameDirty callback
This commit is contained in:
parent
d24386b6e9
commit
98b6ba0a25
@ -852,6 +852,8 @@ internal void OnBecameDirty(NetworkBehaviour component)
|
|||||||
// OnSerialize will decide how to use them.
|
// OnSerialize will decide how to use them.
|
||||||
if (isServer)
|
if (isServer)
|
||||||
{
|
{
|
||||||
|
bool clean = (serverOwnerDirtyMask | serverObserversDirtyMask) == 0;
|
||||||
|
|
||||||
// owner needs to be considered for both SyncModes, because
|
// owner needs to be considered for both SyncModes, because
|
||||||
// Observers mode always includes the Owner.
|
// Observers mode always includes the Owner.
|
||||||
//
|
//
|
||||||
@ -869,6 +871,14 @@ internal void OnBecameDirty(NetworkBehaviour component)
|
|||||||
// observers which aren't the owner.
|
// observers which aren't the owner.
|
||||||
if (component.syncMode == SyncMode.Observers)
|
if (component.syncMode == SyncMode.Observers)
|
||||||
serverObserversDirtyMask |= nthBit;
|
serverObserversDirtyMask |= nthBit;
|
||||||
|
|
||||||
|
// add to dirty spawned, but only when we get dirty the first time.
|
||||||
|
// don't do a dictionary lookup for every [SyncVar] change...
|
||||||
|
|
||||||
|
// only add if observed.
|
||||||
|
// otherwise no point in adding + iterating from broadcast.
|
||||||
|
if (clean && observers.Count > 0)
|
||||||
|
NetworkServer.dirtySpawned[netId] = this;
|
||||||
}
|
}
|
||||||
if (isClient)
|
if (isClient)
|
||||||
{
|
{
|
||||||
@ -885,6 +895,9 @@ internal void OnBecameDirty(NetworkBehaviour component)
|
|||||||
// set the n-th bit if dirty
|
// set the n-th bit if dirty
|
||||||
// shifting from small to large numbers is varint-efficient.
|
// shifting from small to large numbers is varint-efficient.
|
||||||
clientDirtyMask |= (1u << component.ComponentIndex);
|
clientDirtyMask |= (1u << component.ComponentIndex);
|
||||||
|
|
||||||
|
// note: we don't have NetworkClient.dirtySpawned.
|
||||||
|
// checking the few owned objects is enough.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,6 +51,10 @@ 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).
|
||||||
|
internal static readonly Dictionary<uint, NetworkIdentity> dirtySpawned =
|
||||||
|
new Dictionary<uint, 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;
|
||||||
@ -91,6 +95,11 @@ public static partial class NetworkServer
|
|||||||
// capture full Unity update time from before Early- to after LateUpdate
|
// capture full Unity update time from before Early- to after LateUpdate
|
||||||
public static TimeSample fullUpdateDuration;
|
public static TimeSample fullUpdateDuration;
|
||||||
|
|
||||||
|
// cache broadcast writers.
|
||||||
|
// don't need to get/return from/to pool for every identity.
|
||||||
|
static NetworkWriter ownerWriter = new NetworkWriter();
|
||||||
|
static NetworkWriter observersWriter = new NetworkWriter();
|
||||||
|
|
||||||
// initialization / shutdown ///////////////////////////////////////////
|
// initialization / shutdown ///////////////////////////////////////////
|
||||||
static void Initialize()
|
static void Initialize()
|
||||||
{
|
{
|
||||||
@ -1709,41 +1718,14 @@ static NetworkWriter SerializeForConnection(NetworkConnectionToClient connection
|
|||||||
internal static readonly List<NetworkConnectionToClient> connectionsCopy =
|
internal static readonly List<NetworkConnectionToClient> connectionsCopy =
|
||||||
new List<NetworkConnectionToClient>();
|
new List<NetworkConnectionToClient>();
|
||||||
|
|
||||||
static void Broadcast()
|
// if we have large worlds with 10k+ Npcs, Items, etc.
|
||||||
|
// then we can easily ignore all of those which didn't change.
|
||||||
|
// otherwise this loop would be very costly.
|
||||||
|
static void BroadcastDirty()
|
||||||
{
|
{
|
||||||
// copy all connections into a helper collection so that
|
// if (dirtySpawned.Count > 0) Debug.Log($"NetworkServer: Broadcasting {dirtySpawned.Count} dirty out of {spawned.Count} spawned.");
|
||||||
// OnTransportDisconnected can be called while iterating.
|
|
||||||
// -> OnTransportDisconnected removes from the collection
|
|
||||||
// -> which would throw 'can't modify while iterating' errors
|
|
||||||
// => see also: https://github.com/vis2k/Mirror/issues/2739
|
|
||||||
// (copy nonalloc)
|
|
||||||
// TODO remove this when we move to 'lite' transports with only
|
|
||||||
// socket send/recv later.
|
|
||||||
connectionsCopy.Clear();
|
|
||||||
connections.Values.CopyTo(connectionsCopy);
|
|
||||||
|
|
||||||
// PULL-Broadcasting vs. PUSH-Broadcasting:
|
foreach (NetworkIdentity identity in dirtySpawned.Values)
|
||||||
//
|
|
||||||
// - Pull: foreach connection: foreach observing: send
|
|
||||||
// + easier to build LocalWorldState
|
|
||||||
// + avoids iterating _all_ spawned. only iterates those w/ observers
|
|
||||||
// - doesn't work with dirtyIdentities.
|
|
||||||
// each connection would require .dirtyObserving
|
|
||||||
// - requires .observing
|
|
||||||
//
|
|
||||||
// - Push: foreach spawned: foreach observer: send
|
|
||||||
// + works with dirtyIdentities
|
|
||||||
// + doesn't require .observing
|
|
||||||
// - iterates all spawned. unless we use dirtyIdentities.
|
|
||||||
// - LocalWorldState building is more complex, but still possible
|
|
||||||
// - need to cache identity.Serialize() so it's only called once.
|
|
||||||
// instead of once per observing.
|
|
||||||
//
|
|
||||||
using (NetworkWriterPooled ownerWriter = NetworkWriterPool.Get(),
|
|
||||||
observersWriter = NetworkWriterPool.Get())
|
|
||||||
{
|
|
||||||
// let's use push broadcasting to prepare for dirtyObjects.
|
|
||||||
foreach (NetworkIdentity identity in spawned.Values)
|
|
||||||
{
|
{
|
||||||
// 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
|
||||||
@ -1751,9 +1733,21 @@ static void Broadcast()
|
|||||||
// NetworkServer.Destroy)
|
// NetworkServer.Destroy)
|
||||||
if (identity != null)
|
if (identity != null)
|
||||||
{
|
{
|
||||||
// only serialize if it has any observers
|
// only serialize if it has any observers.
|
||||||
// TODO only set dirty if has observers? would be easiest.
|
// this is checked in NetworkIdentity.OnBecameDirty too.
|
||||||
|
// however, a connection may have disconnected since then.
|
||||||
if (identity.observers.Count > 0)
|
if (identity.observers.Count > 0)
|
||||||
|
BroadcastIdentity(identity);
|
||||||
|
}
|
||||||
|
// spawned list should have no null entries because we
|
||||||
|
// always call Remove in OnObjectDestroy everywhere.
|
||||||
|
// if it does have null then someone used
|
||||||
|
// 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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void BroadcastIdentity(NetworkIdentity identity)
|
||||||
{
|
{
|
||||||
// serialize for owner & observers
|
// serialize for owner & observers
|
||||||
ownerWriter.Position = 0;
|
ownerWriter.Position = 0;
|
||||||
@ -1784,16 +1778,9 @@ static void Broadcast()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// spawned list should have no null entries because we
|
|
||||||
// always call Remove in OnObjectDestroy everywhere.
|
|
||||||
// if it does have null then someone used
|
|
||||||
// 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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// flush all connection's batched messages
|
static void FlushConnections()
|
||||||
|
{
|
||||||
foreach (NetworkConnectionToClient connection in connectionsCopy)
|
foreach (NetworkConnectionToClient connection in connectionsCopy)
|
||||||
{
|
{
|
||||||
// has this connection joined the world yet?
|
// has this connection joined the world yet?
|
||||||
@ -1816,6 +1803,47 @@ static void Broadcast()
|
|||||||
// update connection to flush out batched messages
|
// update connection to flush out batched messages
|
||||||
connection.Update();
|
connection.Update();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Broadcast()
|
||||||
|
{
|
||||||
|
// copy all connections into a helper collection so that
|
||||||
|
// OnTransportDisconnected can be called while iterating.
|
||||||
|
// -> OnTransportDisconnected removes from the collection
|
||||||
|
// -> which would throw 'can't modify while iterating' errors
|
||||||
|
// => see also: https://github.com/vis2k/Mirror/issues/2739
|
||||||
|
// (copy nonalloc)
|
||||||
|
// TODO remove this when we move to 'lite' transports with only
|
||||||
|
// socket send/recv later.
|
||||||
|
connectionsCopy.Clear();
|
||||||
|
connections.Values.CopyTo(connectionsCopy);
|
||||||
|
|
||||||
|
// PULL-Broadcasting vs. PUSH-Broadcasting:
|
||||||
|
//
|
||||||
|
// - Pull: foreach connection: foreach observing: send
|
||||||
|
// + easier to build LocalWorldState
|
||||||
|
// + avoids iterating _all_ spawned. only iterates those w/ observers
|
||||||
|
// - doesn't work with dirtyIdentities.
|
||||||
|
// each connection would require .dirtyObserving
|
||||||
|
// - requires .observing
|
||||||
|
//
|
||||||
|
// - Push: foreach spawned: foreach observer: send
|
||||||
|
// + works with dirtyIdentities
|
||||||
|
// + doesn't require .observing
|
||||||
|
// - iterates all spawned. unless we use dirtyIdentities.
|
||||||
|
// - LocalWorldState building is more complex, but still possible
|
||||||
|
// - need to cache identity.Serialize() so it's only called once.
|
||||||
|
// instead of once per observing.
|
||||||
|
//
|
||||||
|
|
||||||
|
// only broadcast dirty entities.
|
||||||
|
BroadcastDirty();
|
||||||
|
|
||||||
|
// clear dirty spawned, now that all were broadcasted
|
||||||
|
dirtySpawned.Clear();
|
||||||
|
|
||||||
|
// flush all connection's batched messages
|
||||||
|
FlushConnections();
|
||||||
|
|
||||||
// TODO this is way too slow because we iterate ALL spawned :/
|
// TODO this is way too slow because we iterate ALL spawned :/
|
||||||
// TODO this is way too complicated :/
|
// TODO this is way too complicated :/
|
||||||
|
@ -1251,17 +1251,18 @@ public void HasExternalConnections_WithHostAndConnection()
|
|||||||
// should log a warning. someone probably used GameObject.Destroy
|
// should log a warning. someone probably used GameObject.Destroy
|
||||||
// instead of NetworkServer.Destroy.
|
// instead of NetworkServer.Destroy.
|
||||||
[Test]
|
[Test]
|
||||||
public void UpdateDetectsNullEntryInObserving()
|
public void UpdateDetectsNullEntryInDirtySpawned()
|
||||||
{
|
{
|
||||||
// start
|
// start
|
||||||
NetworkServer.Listen(1);
|
NetworkServer.Listen(1);
|
||||||
ConnectHostClientBlockingAuthenticatedAndReady();
|
ConnectHostClientBlockingAuthenticatedAndReady();
|
||||||
|
|
||||||
CreateNetworkedAndSpawn(out GameObject go, out NetworkIdentity ni);
|
CreateNetworkedAndSpawn(out GameObject go, out NetworkIdentity ni);
|
||||||
|
NetworkServer.dirtySpawned[ni.netId] = ni; // add manually
|
||||||
Assert.That(NetworkServer.spawned.ContainsKey(ni.netId));
|
Assert.That(NetworkServer.spawned.ContainsKey(ni.netId));
|
||||||
|
|
||||||
// set null
|
// set null
|
||||||
NetworkServer.spawned[ni.netId] = null;
|
NetworkServer.dirtySpawned[ni.netId] = null;
|
||||||
|
|
||||||
// update
|
// update
|
||||||
LogAssert.Expect(LogType.Warning, new Regex("Found 'null' entry in spawned.*"));
|
LogAssert.Expect(LogType.Warning, new Regex("Found 'null' entry in spawned.*"));
|
||||||
@ -1274,14 +1275,15 @@ public void UpdateDetectsNullEntryInObserving()
|
|||||||
//
|
//
|
||||||
// => need extra test because of Unity's custom null check
|
// => need extra test because of Unity's custom null check
|
||||||
[Test]
|
[Test]
|
||||||
public void UpdateDetectsDestroyedSpawned()
|
public void UpdateDetectsDestroyedDirtySpawned()
|
||||||
{
|
{
|
||||||
// start
|
// start
|
||||||
NetworkServer.Listen(1);
|
NetworkServer.Listen(1);
|
||||||
ConnectHostClientBlockingAuthenticatedAndReady();
|
ConnectHostClientBlockingAuthenticatedAndReady();
|
||||||
|
|
||||||
CreateNetworkedAndSpawn(out GameObject go, out NetworkIdentity ni);
|
CreateNetworkedAndSpawn(out GameObject go, out NetworkIdentity ni);
|
||||||
Assert.That(NetworkServer.spawned.ContainsKey(ni.netId));
|
NetworkServer.dirtySpawned[ni.netId] = ni; // add manually
|
||||||
|
Assert.That(NetworkServer.dirtySpawned.ContainsKey(ni.netId));
|
||||||
|
|
||||||
// destroy
|
// destroy
|
||||||
GameObject.DestroyImmediate(go);
|
GameObject.DestroyImmediate(go);
|
||||||
|
Loading…
Reference in New Issue
Block a user