mirror of
https://github.com/MirrorNetworking/Mirror.git
synced 2024-11-18 02:50:32 +00:00
push->pull broadcasting
This commit is contained in:
parent
9d0e90f484
commit
f9ea52257d
@ -23,14 +23,6 @@ namespace Mirror
|
||||
// to everyone etc.
|
||||
public enum Visibility { Default, ForceHidden, ForceShown }
|
||||
|
||||
public struct NetworkIdentitySerialization
|
||||
{
|
||||
// IMPORTANT: int tick avoids floating point inaccuracy over days/weeks
|
||||
public int tick;
|
||||
public NetworkWriter ownerWriter;
|
||||
public NetworkWriter observersWriter;
|
||||
}
|
||||
|
||||
/// <summary>NetworkIdentity identifies objects across the network.</summary>
|
||||
[DisallowMultipleComponent]
|
||||
// NetworkIdentity.Awake initializes all NetworkComponents.
|
||||
@ -203,19 +195,6 @@ internal set
|
||||
[Tooltip("Visibility can overwrite interest management. ForceHidden can be useful to hide monsters while they respawn. ForceShown can be useful for score NetworkIdentities that should always broadcast to everyone in the world.")]
|
||||
public Visibility visible = Visibility.Default;
|
||||
|
||||
// broadcasting serializes all entities around a player for each player.
|
||||
// we don't want to serialize one entity twice in the same tick.
|
||||
// so we cache the last serialization and remember the timestamp so we
|
||||
// know which Update it was serialized.
|
||||
// (timestamp is the same while inside Update)
|
||||
// => this way we don't need to pool thousands of writers either.
|
||||
// => way easier to store them per object
|
||||
NetworkIdentitySerialization lastSerialization = new NetworkIdentitySerialization
|
||||
{
|
||||
ownerWriter = new NetworkWriter(),
|
||||
observersWriter = new NetworkWriter()
|
||||
};
|
||||
|
||||
// Keep track of all sceneIds to detect scene duplicates
|
||||
static readonly Dictionary<ulong, NetworkIdentity> sceneIds =
|
||||
new Dictionary<ulong, NetworkIdentity>();
|
||||
@ -1102,41 +1081,6 @@ internal void DeserializeClient(NetworkReader reader, bool initialState)
|
||||
}
|
||||
}
|
||||
|
||||
// get cached serialization for this tick (or serialize if none yet).
|
||||
// IMPORTANT: int tick avoids floating point inaccuracy over days/weeks.
|
||||
// calls SerializeServer, so this function is to be called on server.
|
||||
internal NetworkIdentitySerialization GetServerSerializationAtTick(int tick)
|
||||
{
|
||||
// only rebuild serialization once per tick. reuse otherwise.
|
||||
// except for tests, where Time.frameCount never increases.
|
||||
// so during tests, we always rebuild.
|
||||
// (otherwise [SyncVar] changes would never be serialized in tests)
|
||||
//
|
||||
// NOTE: != instead of < because int.max+1 overflows at some point.
|
||||
if (lastSerialization.tick != tick
|
||||
#if UNITY_EDITOR
|
||||
|| !Application.isPlaying
|
||||
#endif
|
||||
)
|
||||
{
|
||||
// reset
|
||||
lastSerialization.ownerWriter.Position = 0;
|
||||
lastSerialization.observersWriter.Position = 0;
|
||||
|
||||
// serialize
|
||||
SerializeServer(false,
|
||||
lastSerialization.ownerWriter,
|
||||
lastSerialization.observersWriter);
|
||||
|
||||
// set tick
|
||||
lastSerialization.tick = tick;
|
||||
//Debug.Log($"{name} (netId={netId}) serialized for tick={tickTimeStamp}");
|
||||
}
|
||||
|
||||
// return it
|
||||
return lastSerialization;
|
||||
}
|
||||
|
||||
internal void AddObserver(NetworkConnectionToClient conn)
|
||||
{
|
||||
if (observers.ContainsKey(conn.connectionId))
|
||||
|
@ -1626,65 +1626,111 @@ public static void RebuildObservers(NetworkIdentity identity, bool initialize)
|
||||
|
||||
// broadcasting ////////////////////////////////////////////////////////
|
||||
// helper function to get the right serialization for a connection
|
||||
static NetworkWriter SerializeForConnection(NetworkIdentity identity, NetworkConnectionToClient connection)
|
||||
static NetworkWriter SerializeForConnection(
|
||||
NetworkConnectionToClient connection,
|
||||
bool owned,
|
||||
NetworkWriter ownerWriter,
|
||||
NetworkWriter observersWriter)
|
||||
{
|
||||
// get serialization for this entity (cached)
|
||||
// IMPORTANT: int tick avoids floating point inaccuracy over days/weeks
|
||||
NetworkIdentitySerialization serialization = identity.GetServerSerializationAtTick(Time.frameCount);
|
||||
|
||||
// is this entity owned by this connection?
|
||||
bool owned = identity.connectionToClient == connection;
|
||||
|
||||
// send serialized data
|
||||
// owner writer if owned
|
||||
if (owned)
|
||||
{
|
||||
// was it dirty / did we actually serialize anything?
|
||||
if (serialization.ownerWriter.Position > 0)
|
||||
return serialization.ownerWriter;
|
||||
if (ownerWriter.Position > 0)
|
||||
return ownerWriter;
|
||||
}
|
||||
// observers writer if not owned
|
||||
else
|
||||
{
|
||||
// was it dirty / did we actually serialize anything?
|
||||
if (serialization.observersWriter.Position > 0)
|
||||
return serialization.observersWriter;
|
||||
if (observersWriter.Position > 0)
|
||||
return observersWriter;
|
||||
}
|
||||
|
||||
// nothing was serialized
|
||||
return null;
|
||||
}
|
||||
|
||||
// helper function to broadcast the world to a connection
|
||||
static void BroadcastToConnection(NetworkConnectionToClient connection)
|
||||
static void BroadcastIdentity(
|
||||
NetworkIdentity identity,
|
||||
NetworkWriterPooled ownerWriter,
|
||||
NetworkWriterPooled observersWriter)
|
||||
{
|
||||
// for each entity that this connection is seeing
|
||||
foreach (NetworkIdentity identity in connection.observing)
|
||||
// only serialize if it has any observers
|
||||
// TODO only set dirty if has observers? would be easiest.
|
||||
if (identity.observers.Count > 0)
|
||||
{
|
||||
// make sure it's not null or destroyed.
|
||||
// (which can happen if someone uses
|
||||
// GameObject.Destroy instead of
|
||||
// NetworkServer.Destroy)
|
||||
if (identity != null)
|
||||
// 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)
|
||||
{
|
||||
// get serialization for this entity viewed by this connection
|
||||
// (if anything was serialized this time)
|
||||
NetworkWriter serialization = SerializeForConnection(identity, connection);
|
||||
if (serialization != null)
|
||||
// has this connection joined the world yet?
|
||||
if (connection.isReady)
|
||||
{
|
||||
EntityStateMessage message = new EntityStateMessage
|
||||
// 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)
|
||||
{
|
||||
netId = identity.netId,
|
||||
payload = serialization.ToArraySegment()
|
||||
};
|
||||
connection.Send(message);
|
||||
EntityStateMessage message = new EntityStateMessage
|
||||
{
|
||||
netId = identity.netId,
|
||||
payload = serialization.ToArraySegment()
|
||||
};
|
||||
connection.Send(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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 observing list for connectionId={connection.connectionId}. Please call NetworkServer.Destroy to destroy networked objects. Don't use GameObject.Destroy.");
|
||||
}
|
||||
}
|
||||
|
||||
static void BroadcastSpawned()
|
||||
{
|
||||
// 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.
|
||||
//
|
||||
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.
|
||||
// (which can happen if someone uses
|
||||
// GameObject.Destroy instead of
|
||||
// NetworkServer.Destroy)
|
||||
if (identity != null)
|
||||
{
|
||||
BroadcastIdentity(identity, ownerWriter, observersWriter);
|
||||
}
|
||||
// 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1703,26 +1749,9 @@ static bool DisconnectIfInactive(NetworkConnectionToClient connection)
|
||||
return false;
|
||||
}
|
||||
|
||||
// NetworkLateUpdate called after any Update/FixedUpdate/LateUpdate
|
||||
// (we add this to the UnityEngine in NetworkLoop)
|
||||
// internal for tests
|
||||
internal static readonly List<NetworkConnectionToClient> connectionsCopy =
|
||||
new List<NetworkConnectionToClient>();
|
||||
|
||||
static void Broadcast()
|
||||
// flush all connection's batched messages
|
||||
static void FlushConnections()
|
||||
{
|
||||
// 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);
|
||||
|
||||
// go through all connections
|
||||
foreach (NetworkConnectionToClient connection in connectionsCopy)
|
||||
{
|
||||
// check for inactivity. disconnects if necessary.
|
||||
@ -1744,14 +1773,37 @@ static void Broadcast()
|
||||
// even if targetFrameRate isn't set in host mode (!)
|
||||
// (done via AccurateInterval)
|
||||
connection.Send(new TimeSnapshotMessage(), Channels.Unreliable);
|
||||
|
||||
// broadcast world state to this connection
|
||||
BroadcastToConnection(connection);
|
||||
}
|
||||
|
||||
// update connection to flush out batched messages
|
||||
connection.Update();
|
||||
}
|
||||
}
|
||||
|
||||
// NetworkLateUpdate called after any Update/FixedUpdate/LateUpdate
|
||||
// (we add this to the UnityEngine in NetworkLoop)
|
||||
// internal for tests
|
||||
internal static readonly List<NetworkConnectionToClient> connectionsCopy =
|
||||
new List<NetworkConnectionToClient>();
|
||||
|
||||
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);
|
||||
|
||||
// broadcast spawned entities
|
||||
BroadcastSpawned();
|
||||
|
||||
// flush all connection's batched messages
|
||||
FlushConnections();
|
||||
|
||||
// TODO this is way too slow because we iterate ALL spawned :/
|
||||
// TODO this is way too complicated :/
|
||||
|
@ -1221,35 +1221,39 @@ public void UpdateDetectsNullEntryInObserving()
|
||||
{
|
||||
// start
|
||||
NetworkServer.Listen(1);
|
||||
ConnectHostClientBlockingAuthenticatedAndReady();
|
||||
|
||||
// add a connection that is observed by a null entity
|
||||
NetworkServer.connections[42] = new FakeNetworkConnectionToClient{isReady=true};
|
||||
NetworkServer.connections[42].observing.Add(null);
|
||||
CreateNetworkedAndSpawn(out GameObject go, out NetworkIdentity ni);
|
||||
Assert.That(NetworkServer.spawned.ContainsKey(ni.netId));
|
||||
|
||||
// set null
|
||||
NetworkServer.spawned[ni.netId] = null;
|
||||
|
||||
// update
|
||||
LogAssert.Expect(LogType.Warning, new Regex("Found 'null' entry in observing list.*"));
|
||||
LogAssert.Expect(LogType.Warning, new Regex("Found 'null' entry in spawned.*"));
|
||||
NetworkServer.NetworkLateUpdate();
|
||||
}
|
||||
|
||||
// updating NetworkServer with a null entry in connection.observing
|
||||
// should log a warning. someone probably used GameObject.Destroy
|
||||
// instead of NetworkServer.Destroy.
|
||||
// updating NetworkServer with a null entry in .spawned should log a
|
||||
// warning. someone probably used GameObject.Destroy instead of
|
||||
// NetworkServer.Destroy.
|
||||
//
|
||||
// => need extra test because of Unity's custom null check
|
||||
[Test]
|
||||
public void UpdateDetectsDestroyedEntryInObserving()
|
||||
public void UpdateDetectsDestroyedSpawned()
|
||||
{
|
||||
// start
|
||||
NetworkServer.Listen(1);
|
||||
ConnectHostClientBlockingAuthenticatedAndReady();
|
||||
|
||||
// add a connection that is observed by a destroyed entity
|
||||
CreateNetworked(out GameObject go, out NetworkIdentity ni);
|
||||
NetworkServer.connections[42] = new FakeNetworkConnectionToClient{isReady=true};
|
||||
NetworkServer.connections[42].observing.Add(ni);
|
||||
CreateNetworkedAndSpawn(out GameObject go, out NetworkIdentity ni);
|
||||
Assert.That(NetworkServer.spawned.ContainsKey(ni.netId));
|
||||
|
||||
// destroy
|
||||
GameObject.DestroyImmediate(go);
|
||||
|
||||
// update
|
||||
LogAssert.Expect(LogType.Warning, new Regex("Found 'null' entry in observing list.*"));
|
||||
// LogAssert.Expect(LogType.Warning, new Regex("Found 'null' entry in spawned.*"));
|
||||
NetworkServer.NetworkLateUpdate();
|
||||
}
|
||||
|
||||
|
@ -56,24 +56,5 @@ public IEnumerator OnDestroyIsServerTrueWhenNetworkServerDestroyIsCalled()
|
||||
// Confirm it has been destroyed
|
||||
Assert.That(identity == null, Is.True);
|
||||
}
|
||||
|
||||
// imer: There's currently an issue with dropped/skipped serializations
|
||||
// once a server has been running for around a week, this test should
|
||||
// highlight the potential cause
|
||||
[UnityTest]
|
||||
public IEnumerator TestSerializationWithLargeTimestamps()
|
||||
{
|
||||
// 14 * 24 hours per day * 60 minutes per hour * 60 seconds per minute = 14 days
|
||||
// NOTE: change this to 'float' to see the tests fail
|
||||
int tick = 14 * 24 * 60 * 60;
|
||||
NetworkIdentitySerialization serialization = identity.GetServerSerializationAtTick(tick);
|
||||
// advance tick
|
||||
++tick;
|
||||
NetworkIdentitySerialization serializationNew = identity.GetServerSerializationAtTick(tick);
|
||||
|
||||
// if the serialization has been changed the tickTimeStamp should have moved
|
||||
Assert.That(serialization.tick == serializationNew.tick, Is.False);
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user