This commit is contained in:
mischa 2024-11-13 09:39:32 +01:00 committed by GitHub
commit 80043b28cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 1155 additions and 147 deletions

View File

@ -94,6 +94,7 @@ public struct ObjectHideMessage : NetworkMessage
public uint netId;
}
// state update for reliable sync
public struct EntityStateMessage : NetworkMessage
{
public uint netId;
@ -102,6 +103,38 @@ public struct EntityStateMessage : NetworkMessage
public ArraySegment<byte> payload;
}
// state update for unreliable sync.
// baseline is always sent over Reliable channel.
public struct EntityStateMessageUnreliableBaseline : NetworkMessage
{
// baseline messages send their tick number as byte.
// delta messages are checked against that tick to avoid applying a
// delta on top of the wrong baseline.
// (byte is enough, we just need something small to compare against)
public byte baselineTick;
public uint netId;
// the serialized component data
// -> ArraySegment to avoid unnecessary allocations
public ArraySegment<byte> payload;
}
// state update for unreliable sync
// delta is always sent over Unreliable channel.
public struct EntityStateMessageUnreliableDelta : NetworkMessage
{
// baseline messages send their tick number as byte.
// delta messages are checked against that tick to avoid applying a
// delta on top of the wrong baseline.
// (byte is enough, we just need something small to compare against)
public byte baselineTick;
public uint netId;
// the serialized component data
// -> ArraySegment to avoid unnecessary allocations
public ArraySegment<byte> payload;
}
// whoever wants to measure rtt, sends this to the other end.
public struct NetworkPingMessage : NetworkMessage
{

View File

@ -6,6 +6,11 @@
namespace Mirror
{
// SyncMethod to choose between:
// * Reliable: oldschool reliable sync every syncInterval. If nothing changes, nothing is sent.
// * Unreliable: quake style unreliable state sync & delta compression, for fast paced games.
public enum SyncMethod { Reliable, Unreliable }
// SyncMode decides if a component is synced to all observers, or only owner
public enum SyncMode { Observers, Owner }
@ -24,6 +29,9 @@ public enum SyncDirection { ServerToClient, ClientToServer }
[HelpURL("https://mirror-networking.gitbook.io/docs/guides/networkbehaviour")]
public abstract class NetworkBehaviour : MonoBehaviour
{
[Tooltip("Choose between:\n- Reliable: only sends when changed. Recommended for most games!\n- Unreliable: immediately sends at the expense of bandwidth. Only for hardcore competitive games.\nClick the Help icon for full details.")]
[HideInInspector] public SyncMethod syncMethod = SyncMethod.Reliable;
/// <summary>Sync direction for OnSerialize. ServerToClient by default. ClientToServer for client authority.</summary>
[Tooltip("Server Authority calls OnSerialize on the server and syncs it to clients.\n\nClient Authority calls OnSerialize on the owning client, syncs it to server, which then broadcasts it to all other clients.\n\nUse server authority for cheat safety.")]
[HideInInspector] public SyncDirection syncDirection = SyncDirection.ServerToClient;
@ -228,12 +236,17 @@ public bool IsDirty() =>
// only check time if bits were dirty. this is more expensive.
NetworkTime.localTime - lastSyncTime >= syncInterval;
// true if any SyncVar or SyncObject is dirty
// OR both bitmasks. != 0 if either was dirty.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsDirty_BitsOnly() => (syncVarDirtyBits | syncObjectDirtyBits) != 0UL;
/// <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
// be called manually as well.
public void ClearAllDirtyBits()
public void ClearAllDirtyBits(bool clearSyncTime = true)
{
lastSyncTime = NetworkTime.localTime;
if (clearSyncTime) lastSyncTime = NetworkTime.localTime;
syncVarDirtyBits = 0L;
syncObjectDirtyBits = 0L;
@ -1077,7 +1090,7 @@ protected T GetSyncVarNetworkBehaviour<T>(NetworkBehaviourSyncVar syncNetBehavio
{
return null;
}
// ensure componentIndex is in range.
// show explicit errors if something went wrong, instead of IndexOutOfRangeException.
// removing components at runtime isn't allowed, yet this happened in a project so we need to check for it.

View File

@ -33,6 +33,17 @@ public static partial class NetworkClient
public static float sendInterval => sendRate < int.MaxValue ? 1f / sendRate : 0; // for 30 Hz, that's 33ms
static double lastSendTime;
// ocassionally send a full reliable state for unreliable components to delta compress against.
// this only applies to Components with SyncMethod=Unreliable.
public static int unreliableBaselineRate => NetworkServer.unreliableBaselineRate;
public static float unreliableBaselineInterval => NetworkServer.unreliableBaselineInterval;
static double lastUnreliableBaselineTime;
// quake sends unreliable messages twice to make up for message drops.
// this double bandwidth, but allows for smaller buffer time / faster sync.
// best to turn this off unless the game is extremely fast paced.
public static bool unreliableRedundancy => NetworkServer.unreliableRedundancy;
// For security, it is recommended to disconnect a player if a networked
// action triggers an exception\nThis could prevent components being
// accessed in an undefined state, which may be an attack vector for
@ -505,6 +516,8 @@ internal static void RegisterMessageHandlers(bool hostMode)
RegisterHandler<ObjectSpawnFinishedMessage>(_ => { });
// host mode doesn't need state updates
RegisterHandler<EntityStateMessage>(_ => { });
RegisterHandler<EntityStateMessageUnreliableBaseline>(_ => { });
RegisterHandler<EntityStateMessageUnreliableDelta>(_ => { });
}
else
{
@ -516,6 +529,8 @@ internal static void RegisterMessageHandlers(bool hostMode)
RegisterHandler<ObjectSpawnStartedMessage>(OnObjectSpawnStarted);
RegisterHandler<ObjectSpawnFinishedMessage>(OnObjectSpawnFinished);
RegisterHandler<EntityStateMessage>(OnEntityStateMessage);
RegisterHandler<EntityStateMessageUnreliableBaseline>(OnEntityStateMessageUnreliableBaseline);
RegisterHandler<EntityStateMessageUnreliableDelta>(OnEntityStateMessageUnreliableDelta);
}
// These handlers are the same for host and remote clients
@ -1417,6 +1432,90 @@ static void OnEntityStateMessage(EntityStateMessage message)
else Debug.LogWarning($"Did not find target for sync message for {message.netId}. Were all prefabs added to the NetworkManager's spawnable list?\nNote: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message.");
}
static void OnEntityStateMessageUnreliableBaseline(EntityStateMessageUnreliableBaseline message, int channelId)
{
// safety check: baseline should always arrive over Reliable channel.
if (channelId != Channels.Reliable)
{
Debug.LogError($"Client OnEntityStateMessageUnreliableBaseline arrived on channel {channelId} instead of Reliable. This should never happen!");
return;
}
// Debug.Log($"NetworkClient.OnUpdateVarsMessage {msg.netId}");
if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null)
{
// set the last received reliable baseline tick number.
identity.lastUnreliableBaselineReceived = message.baselineTick;
// iniital is always 'true' because unreliable state sync alwasy serializes full
using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload))
{
// full state updates (initial=true) arrive over reliable.
identity.DeserializeClient(reader, true);
}
}
// no warning. unreliable messages often arrive before/after the reliable spawn/despawn messages.
// else Debug.LogWarning($"Did not find target for sync message for {message.netId}. Were all prefabs added to the NetworkManager's spawnable list?\nNote: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message.");
}
static void OnEntityStateMessageUnreliableDelta(EntityStateMessageUnreliableDelta message, int channelId)
{
// safety check: baseline should always arrive over Reliable channel.
if (channelId != Channels.Unreliable)
{
Debug.LogError($"Client OnEntityStateMessageUnreliableDelta arrived on channel {channelId} instead of Unreliable. This should never happen!");
return;
}
// Debug.Log($"NetworkClient.OnUpdateVarsMessage {msg.netId}");
if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null)
{
// unreliable state sync messages may arrive out of order.
// only ever apply state that's newer than the last received state.
// note that we send one EntityStateMessage per Entity,
// so there will be multiple with the same == timestamp.
//
// note that a reliable baseline may arrive before/after a delta.
// that is fine.
if (connection.remoteTimeStamp < identity.lastUnreliableStateTime)
{
// debug log to show that it's working.
// can be tested via LatencySimulation scramble easily.
Debug.Log($"Client caught out of order Unreliable state message for {identity.name}. This is fine.\nIdentity timestamp={identity.lastUnreliableStateTime:F3} batch remoteTimestamp={connection.remoteTimeStamp:F3}");
return;
}
// UDP messages may accidentally arrive twice.
// or even intentionally, if unreliableRedundancy is turned on.
else if (connection.remoteTimeStamp == identity.lastUnreliableStateTime)
{
// only log this if unreliableRedundancy is disabled.
// otherwise it's expected and will happen a lot.
if (!unreliableRedundancy) Debug.Log($"Client caught duplicate Unreliable state message for {identity.name}. This is fine.\nIdentity timestamp={identity.lastUnreliableStateTime:F3} batch remoteTimestamp={connection.remoteTimeStamp:F3}");
return;
}
// make sure this delta is for the correct baseline.
// we don't want to apply an old delta on top of a new baseline.
if (message.baselineTick != identity.lastUnreliableBaselineReceived)
{
Debug.Log($"Client caught Unreliable state message for old baseline for {identity} with baselineTick={identity.lastUnreliableBaselineReceived} messageBaseline={message.baselineTick}. This is fine.");
return;
}
// set the new last received time for unreliable
identity.lastUnreliableStateTime = connection.remoteTimeStamp;
// iniital is always 'true' because unreliable state sync alwasy serializes full
using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload))
{
// delta state updates (initial=false) arrive over unreliable.
identity.DeserializeClient(reader, false);
}
}
// no warning. unreliable messages often arrive before/after the reliable spawn/despawn messages.
// else Debug.LogWarning($"Did not find target for sync message for {message.netId}. Were all prefabs added to the NetworkManager's spawnable list?\nNote: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message.");
}
static void OnRPCMessage(RpcMessage message)
{
// Debug.Log($"NetworkClient.OnRPCMessage hash:{message.functionHash} netId:{message.netId}");
@ -1530,9 +1629,10 @@ internal static void NetworkLateUpdate()
//
// Unity 2019 doesn't have Time.timeAsDouble yet
bool sendIntervalElapsed = AccurateInterval.Elapsed(NetworkTime.localTime, sendInterval, ref lastSendTime);
bool unreliableBaselineElapsed = AccurateInterval.Elapsed(NetworkTime.localTime, unreliableBaselineInterval, ref lastUnreliableBaselineTime);
if (!Application.isPlaying || sendIntervalElapsed)
{
Broadcast();
Broadcast(unreliableBaselineElapsed);
}
UpdateConnectionQuality();
@ -1596,7 +1696,9 @@ void UpdateConnectionQuality()
// broadcast ///////////////////////////////////////////////////////////
// make sure Broadcast() is only called every sendInterval.
// calling it every update() would require too much bandwidth.
static void Broadcast()
//
// unreliableFullSendIntervalElapsed: indicates that unreliable sync components need a reliable baseline sync this time.
static void Broadcast(bool unreliableBaselineElapsed)
{
// joined the world yet?
if (!connection.isReady) return;
@ -1608,12 +1710,14 @@ static void Broadcast()
Send(new TimeSnapshotMessage(), Channels.Unreliable);
// broadcast client state to server
BroadcastToServer();
BroadcastToServer(unreliableBaselineElapsed);
}
// NetworkServer has BroadcastToConnection.
// NetworkClient has BroadcastToServer.
static void BroadcastToServer()
//
// unreliableFullSendIntervalElapsed: indicates that unreliable sync components need a reliable baseline sync this time.
static void BroadcastToServer(bool unreliableBaselineElapsed)
{
// for each entity that the client owns
foreach (NetworkIdentity identity in connection.owned)
@ -1624,21 +1728,64 @@ static void BroadcastToServer()
// NetworkServer.Destroy)
if (identity != null)
{
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
// 'Reliable' sync: send Reliable components over reliable.
using (NetworkWriterPooled writerReliable = NetworkWriterPool.Get(),
writerUnreliableDelta = NetworkWriterPool.Get(),
writerUnreliableBaseline = NetworkWriterPool.Get())
{
// get serialization for this entity viewed by this connection
// (if anything was serialized this time)
identity.SerializeClient(writer);
if (writer.Position > 0)
// serialize reliable and unreliable components in only one iteration.
// serializing reliable and unreliable separately in two iterations would be too costly.
identity.SerializeClient(writerReliable, writerUnreliableBaseline, writerUnreliableDelta, unreliableBaselineElapsed);
// any reliable components serialization?
if (writerReliable.Position > 0)
{
// send state update message
EntityStateMessage message = new EntityStateMessage
{
netId = identity.netId,
payload = writer.ToArraySegment()
payload = writerReliable.ToArraySegment()
};
Send(message);
}
// any unreliable components serialization?
// we always send unreliable deltas to ensure interpolation always has a data point that arrives immediately.
if (writerUnreliableDelta.Position > 0)
{
EntityStateMessageUnreliableDelta message = new EntityStateMessageUnreliableDelta
{
// baselineTick: the last unreliable baseline to compare against
baselineTick = identity.lastUnreliableBaselineSent,
netId = identity.netId,
payload = writerUnreliableDelta.ToArraySegment()
};
Send(message, Channels.Unreliable);
}
// time for unreliable baseline sync?
// we always send this after the unreliable delta,
// so there's a higher chance that it arrives after the delta.
// in other words: so that the delta can still be used against previous baseline.
if (unreliableBaselineElapsed)
{
if (writerUnreliableBaseline.Position > 0)
{
// remember last sent baseline tick for this entity.
// (byte) to minimize bandwidth. we don't need the full tick,
// just something small to compare against.
identity.lastUnreliableBaselineSent = (byte)Time.frameCount;
// send state update message
EntityStateMessageUnreliableBaseline message = new EntityStateMessageUnreliableBaseline
{
baselineTick = identity.lastUnreliableBaselineSent,
netId = identity.netId,
payload = writerUnreliableBaseline.ToArraySegment()
};
Send(message, Channels.Reliable);
}
}
}
}
// spawned list should have no null entries because we

View File

@ -27,13 +27,26 @@ public struct NetworkIdentitySerialization
{
// IMPORTANT: int tick avoids floating point inaccuracy over days/weeks
public int tick;
public NetworkWriter ownerWriter;
public NetworkWriter observersWriter;
// reliable sync
public NetworkWriter ownerWriterReliable;
public NetworkWriter observersWriterReliable;
// unreliable sync
public NetworkWriter ownerWriterUnreliableBaseline;
public NetworkWriter observersWriterUnreliableBaseline;
public NetworkWriter ownerWriterUnreliableDelta;
public NetworkWriter observersWriterUnreliableDelta;
public void ResetWriters()
{
ownerWriter.Position = 0;
observersWriter.Position = 0;
ownerWriterReliable.Position = 0;
observersWriterReliable.Position = 0;
ownerWriterUnreliableBaseline.Position = 0;
observersWriterUnreliableBaseline.Position = 0;
ownerWriterUnreliableDelta.Position = 0;
observersWriterUnreliableDelta.Position = 0;
}
}
@ -216,10 +229,23 @@ internal set
// => way easier to store them per object
NetworkIdentitySerialization lastSerialization = new NetworkIdentitySerialization
{
ownerWriter = new NetworkWriter(),
observersWriter = new NetworkWriter()
ownerWriterReliable = new NetworkWriter(),
observersWriterReliable = new NetworkWriter(),
ownerWriterUnreliableBaseline = new NetworkWriter(),
observersWriterUnreliableBaseline = new NetworkWriter(),
ownerWriterUnreliableDelta = new NetworkWriter(),
observersWriterUnreliableDelta = new NetworkWriter(),
};
// unreliable state sync messages may arrive out of order, or duplicated.
// keep latest received timestamp so we don't apply older messages.
internal double lastUnreliableStateTime;
// the last baseline we received for this object.
// deltas are based on the baseline, need to make sure we don't apply on an old one.
internal byte lastUnreliableBaselineSent;
internal byte lastUnreliableBaselineReceived;
// Keep track of all sceneIds to detect scene duplicates
static readonly Dictionary<ulong, NetworkIdentity> sceneIds =
new Dictionary<ulong, NetworkIdentity>();
@ -854,7 +880,7 @@ internal void OnStopLocalPlayer()
// build dirty mask for server owner & observers (= all dirty components).
// faster to do it in one iteration instead of iterating separately.
(ulong, ulong) ServerDirtyMasks(bool initialState)
(ulong, ulong) ServerDirtyMasks_Spawn()
{
ulong ownerMask = 0;
ulong observerMask = 0;
@ -865,38 +891,140 @@ internal void OnStopLocalPlayer()
NetworkBehaviour component = components[i];
ulong nthBit = 1ul << i;
bool dirty = component.IsDirty();
// owner needs to be considered for both SyncModes, because
// Observers mode always includes the Owner.
//
// for initial, it should always sync owner.
// for delta, only for ServerToClient and only if dirty.
// ClientToServer comes from the owner client.
if (initialState || (component.syncDirection == SyncDirection.ServerToClient && dirty))
ownerMask |= nthBit;
// for spawn message, it should always sync owner.
ownerMask |= nthBit;
// observers need to be considered only in Observers mode,
// otherwise they receive no sync data of this component ever.
if (component.syncMode == SyncMode.Observers)
{
// for initial, it should always sync to observers.
// for delta, only if dirty.
// for spawn message, it should always sync to observers.
// SyncDirection is irrelevant, as both are broadcast to
// observers which aren't the owner.
if (initialState || dirty)
observerMask |= nthBit;
observerMask |= nthBit;
}
}
return (ownerMask, observerMask);
}
// build dirty mask for client.
// server always knows initialState, so we don't need it here.
ulong ClientDirtyMask()
// build dirty mask for server owner & observers (= all dirty components).
// faster to do it in one iteration instead of iterating separately.
// -> build Reliable and Unreliable masks in one iteration.
// running two loops would be too costly.
void ServerDirtyMasks_Broadcast(
out ulong ownerMaskReliable, out ulong observerMaskReliable,
out ulong ownerMaskUnreliableBaseline, out ulong observerMaskUnreliableBaseline,
out ulong ownerMaskUnreliableDelta, out ulong observerMaskUnreliableDelta)
{
ulong mask = 0;
// clear
ownerMaskReliable = 0;
observerMaskReliable = 0;
ownerMaskUnreliableBaseline = 0;
observerMaskUnreliableBaseline = 0;
ownerMaskUnreliableDelta = 0;
observerMaskUnreliableDelta = 0;
NetworkBehaviour[] components = NetworkBehaviours;
for (int i = 0; i < components.Length; ++i)
{
NetworkBehaviour component = components[i];
ulong nthBit = (1u << i);
// RELIABLE COMPONENTS /////////////////////////////////////////
if (component.syncMethod == SyncMethod.Reliable)
{
// check if this component is dirty
bool dirty = component.IsDirty();
// owner needs to be considered for both SyncModes, because
// Observers mode always includes the Owner.
//
// for broadcast, only for ServerToClient and only if dirty.
// ClientToServer comes from the owner client.
if (component.syncDirection == SyncDirection.ServerToClient && dirty)
ownerMaskReliable |= nthBit;
// observers need to be considered only in Observers mode,
// otherwise they receive no sync data of this component ever.
if (component.syncMode == SyncMode.Observers)
{
// for broadcast, only sync to observers if dirty.
// SyncDirection is irrelevant, as both are broadcast to
// observers which aren't the owner.
if (dirty) observerMaskReliable |= nthBit;
}
}
// UNRELIABLE COMPONENTS ///////////////////////////////////////
else if (component.syncMethod == SyncMethod.Unreliable)
{
// UNRELIABLE DELTAS ///////////////////////////////////////
{
// check if this component is dirty.
// delta sync runs @ syncInterval.
// this allows for significant bandwidth savings.
bool dirty = component.IsDirty();
// owner needs to be considered for both SyncModes, because
// Observers mode always includes the Owner.
//
// for broadcast, only for ServerToClient and only if dirty.
// ClientToServer comes from the owner client.
if (component.syncDirection == SyncDirection.ServerToClient && dirty)
ownerMaskUnreliableDelta |= nthBit;
// observers need to be considered only in Observers mode,
// otherwise they receive no sync data of this component ever.
if (component.syncMode == SyncMode.Observers)
{
// for broadcast, only sync to observers if dirty.
// SyncDirection is irrelevant, as both are broadcast to
// observers which aren't the owner.
if (dirty) observerMaskUnreliableDelta |= nthBit;
}
}
// UNRELIABLE BASELINE /////////////////////////////////////
{
// check if this component is dirty.
// baseline sync runs @ 1 Hz (netmanager configurable).
// only consider dirty bits, ignore syncinterval.
bool dirty = component.IsDirty_BitsOnly();
// owner needs to be considered for both SyncModes, because
// Observers mode always includes the Owner.
//
// for broadcast, only for ServerToClient and only if dirty.
// ClientToServer comes from the owner client.
if (component.syncDirection == SyncDirection.ServerToClient && dirty)
ownerMaskUnreliableBaseline |= nthBit;
// observers need to be considered only in Observers mode,
// otherwise they receive no sync data of this component ever.
if (component.syncMode == SyncMode.Observers)
{
// for broadcast, only sync to observers if dirty.
// SyncDirection is irrelevant, as both are broadcast to
// observers which aren't the owner.
if (dirty) observerMaskUnreliableBaseline |= nthBit;
}
}
////////////////////////////////////////////////////////////
}
}
}
// build dirty mask for client components.
// server always knows initialState, so we don't need it here.
// -> build Reliable and Unreliable masks in one iteration.
// running two loops would be too costly.
void ClientDirtyMasks(out ulong dirtyMaskReliable, out ulong dirtyMaskUnreliableBaseline, out ulong dirtyMaskUnreliableDelta)
{
dirtyMaskReliable = 0;
dirtyMaskUnreliableBaseline = 0;
dirtyMaskUnreliableDelta = 0;
NetworkBehaviour[] components = NetworkBehaviours;
for (int i = 0; i < components.Length; ++i)
@ -914,13 +1042,29 @@ ulong ClientDirtyMask()
if (isOwned && component.syncDirection == SyncDirection.ClientToServer)
{
// set the n-th bit if dirty
// shifting from small to large numbers is varint-efficient.
if (component.IsDirty()) mask |= nthBit;
// RELIABLE COMPONENTS /////////////////////////////////////////
if (component.syncMethod == SyncMethod.Reliable)
{
// set the n-th bit if dirty
// shifting from small to large numbers is varint-efficient.
if (component.IsDirty()) dirtyMaskReliable |= nthBit;
}
// UNRELIABLE COMPONENTS ///////////////////////////////////////
else if (component.syncMethod == SyncMethod.Unreliable)
{
// set the n-th bit if dirty
// shifting from small to large numbers is varint-efficient.
// baseline sync runs @ 1 Hz (netmanager configurable).
// only consider dirty bits, ignore syncinterval.
if (component.IsDirty_BitsOnly()) dirtyMaskUnreliableBaseline |= nthBit;
// delta sync runs @ syncInterval.
// this allows for significant bandwidth savings.
if (component.IsDirty()) dirtyMaskUnreliableDelta |= nthBit;
}
}
}
return mask;
}
// check if n-th component is dirty.
@ -932,9 +1076,9 @@ internal static bool IsDirty(ulong mask, int index)
return (mask & nthBit) != 0;
}
// serialize components into writer on the server.
// serialize server components, with full state for spawn message.
// check ownerWritten/observersWritten to know if anything was written
internal void SerializeServer(bool initialState, NetworkWriter ownerWriter, NetworkWriter observersWriter)
internal void SerializeServer_Spawn(NetworkWriter ownerWriter, NetworkWriter observersWriter)
{
// ensure NetworkBehaviours are valid before usage
ValidateComponents();
@ -947,7 +1091,7 @@ internal void SerializeServer(bool initialState, NetworkWriter ownerWriter, Netw
// instead of writing a 1 byte index per component,
// we limit components to 64 bits and write one ulong instead.
// the ulong is also varint compressed for minimum bandwidth.
(ulong ownerMask, ulong observerMask) = ServerDirtyMasks(initialState);
(ulong ownerMask, ulong observerMask) = ServerDirtyMasks_Spawn();
// if nothing dirty, then don't even write the mask.
// otherwise, every unchanged object would send a 1 byte dirty mask!
@ -981,7 +1125,7 @@ internal void SerializeServer(bool initialState, NetworkWriter ownerWriter, Netw
// serialize into helper writer
using (NetworkWriterPooled temp = NetworkWriterPool.Get())
{
comp.Serialize(temp, initialState);
comp.Serialize(temp, true);
ArraySegment<byte> segment = temp.ToArraySegment();
// copy to owner / observers as needed
@ -989,25 +1133,146 @@ internal void SerializeServer(bool initialState, NetworkWriter ownerWriter, Netw
if (observersDirty) observersWriter.WriteBytes(segment.Array, segment.Offset, segment.Count);
}
// clear dirty bits for the components that we serialized.
// do not clear for _all_ components, only the ones that
// were dirty and had their syncInterval elapsed.
//
// we don't want to clear bits before the syncInterval
// was elapsed, as then they wouldn't be synced.
//
// only clear for delta, not for full (spawn messages).
// otherwise if a player joins, we serialize monster,
// and shouldn't clear dirty bits not yet synced to
// other players.
if (!initialState) comp.ClearAllDirtyBits();
// dirty bits indicate 'changed since last delta sync'.
// don't clear then on full sync here, since full sync
// is called whenever a new player spawns and needs the
// full state!
//comp.ClearAllDirtyBits();
}
}
}
}
// serialize components into writer on the client.
internal void SerializeClient(NetworkWriter writer)
// serialize server components, with delta state for broadcast messages.
// check ownerWritten/observersWritten to know if anything was written
//
// serialize Reliable and Unreliable components in one iteration.
// having two separate functions doing two iterations would be too costly.
internal void SerializeServer_Broadcast(
NetworkWriter ownerWriterReliable, NetworkWriter observersWriterReliable,
NetworkWriter ownerWriterUnreliableBaseline, NetworkWriter observersWriterUnreliableBaseline,
NetworkWriter ownerWriterUnreliableDelta, NetworkWriter observersWriterUnreliableDelta,
bool unreliableBaseline)
{
// ensure NetworkBehaviours are valid before usage
ValidateComponents();
NetworkBehaviour[] components = NetworkBehaviours;
// check which components are dirty for owner / observers.
// this is quite complicated with SyncMode + SyncDirection.
// see the function for explanation.
//
// instead of writing a 1 byte index per component,
// we limit components to 64 bits and write one ulong instead.
// the ulong is also varint compressed for minimum bandwidth.
ServerDirtyMasks_Broadcast(
out ulong ownerMaskReliable, out ulong observerMaskReliable,
out ulong ownerMaskUnreliableBaseline, out ulong observerMaskUnreliableBaseline,
out ulong ownerMaskUnreliableDelta, out ulong observerMaskUnreliableDelta
);
// if nothing dirty, then don't even write the mask.
// otherwise, every unchanged object would send a 1 byte dirty mask!
if (ownerMaskReliable != 0) Compression.CompressVarUInt(ownerWriterReliable, ownerMaskReliable);
if (observerMaskReliable != 0) Compression.CompressVarUInt(observersWriterReliable, observerMaskReliable);
if (ownerMaskUnreliableDelta != 0) Compression.CompressVarUInt(ownerWriterUnreliableDelta, ownerMaskUnreliableDelta);
if (observerMaskUnreliableDelta != 0) Compression.CompressVarUInt(observersWriterUnreliableDelta, observerMaskUnreliableDelta);
if (ownerMaskUnreliableBaseline != 0) Compression.CompressVarUInt(ownerWriterUnreliableBaseline, ownerMaskUnreliableBaseline);
if (observerMaskUnreliableBaseline != 0) Compression.CompressVarUInt(observersWriterUnreliableBaseline, observerMaskUnreliableBaseline);
// serialize all components
// perf: only iterate if either dirty mask has dirty bits.
if ((ownerMaskReliable | observerMaskReliable |
ownerMaskUnreliableBaseline | observerMaskUnreliableBaseline |
ownerMaskUnreliableDelta | observerMaskUnreliableDelta)
!= 0)
{
for (int i = 0; i < components.Length; ++i)
{
NetworkBehaviour comp = components[i];
// is the component dirty for anyone (owner or observers)?
// may be serialized to owner, observer, both, or neither.
//
// OnSerialize should only be called once.
// this is faster, and it cleaner because it may set
// internal state, counters, logs, etc.
//
// previously we always serialized to owner and then copied
// the serialization to observers. however, since
// SyncDirection it's not guaranteed to be in owner anymore.
// so we need to serialize to temporary writer first.
// and then copy as needed.
bool ownerDirtyReliable = IsDirty(ownerMaskReliable, i);
bool observersDirtyReliable = IsDirty(observerMaskReliable, i);
bool ownerDirtyUnreliableBaseline = IsDirty(ownerMaskUnreliableBaseline, i);
bool observersDirtyUnreliableBaseline = IsDirty(observerMaskUnreliableBaseline, i);
bool ownerDirtyUnreliableDelta = IsDirty(ownerMaskUnreliableDelta, i);
bool observersDirtyUnreliableDelta = IsDirty(observerMaskUnreliableDelta, i);
// RELIABLE COMPONENTS /////////////////////////////////////
if (ownerDirtyReliable || observersDirtyReliable)
{
// serialize into helper writer
using (NetworkWriterPooled temp = NetworkWriterPool.Get())
{
comp.Serialize(temp, false);
ArraySegment<byte> segment = temp.ToArraySegment();
// copy to owner / observers as needed
if (ownerDirtyReliable) ownerWriterReliable.WriteBytes(segment.Array, segment.Offset, segment.Count);
if (observersDirtyReliable) observersWriterReliable.WriteBytes(segment.Array, segment.Offset, segment.Count);
}
// dirty bits indicate 'changed since last delta sync'.
// clear them after a delta sync here.
comp.ClearAllDirtyBits();
}
// UNRELIABLE DELTA ////////////////////////////////////////
// we always send the unreliable delta no matter what
if (ownerDirtyUnreliableDelta || observersDirtyUnreliableDelta)
{
using (NetworkWriterPooled temp = NetworkWriterPool.Get())
{
comp.Serialize(temp, false);
ArraySegment<byte> segment = temp.ToArraySegment();
// copy to owner / observers as needed
if (ownerDirtyUnreliableDelta) ownerWriterUnreliableDelta.WriteBytes(segment.Array, segment.Offset, segment.Count);
if (observersDirtyUnreliableDelta) observersWriterUnreliableDelta.WriteBytes(segment.Array, segment.Offset, segment.Count);
// clear sync time to only send delta again after syncInterval.
comp.lastSyncTime = NetworkTime.localTime;
}
}
// UNRELIABLE BASELINE /////////////////////////////////////
// sometimes we need the unreliable baseline
// (we always sync deltas, so no 'else if' here)
if (unreliableBaseline && (ownerDirtyUnreliableBaseline || observersDirtyUnreliableBaseline))
{
using (NetworkWriterPooled temp = NetworkWriterPool.Get())
{
comp.Serialize(temp, true);
ArraySegment<byte> segment = temp.ToArraySegment();
// copy to owner / observers as needed
if (ownerDirtyUnreliableBaseline) ownerWriterUnreliableBaseline.WriteBytes(segment.Array, segment.Offset, segment.Count);
if (observersDirtyUnreliableBaseline) observersWriterUnreliableBaseline.WriteBytes(segment.Array, segment.Offset, segment.Count);
}
// for unreliable components, only clear dirty bits after the reliable baseline.
// -> don't clear sync time: that's for delta syncs.
comp.ClearAllDirtyBits(false);
}
}
}
}
// serialize Reliable and Unreliable components in one iteration.
// having two separate functions doing two iterations would be too costly.
internal void SerializeClient(NetworkWriter writerReliable, NetworkWriter writerUnreliableBaseline, NetworkWriter writerUnreliableDelta, bool unreliableBaseline)
{
// ensure NetworkBehaviours are valid before usage
ValidateComponents();
@ -1020,7 +1285,7 @@ internal void SerializeClient(NetworkWriter writer)
// instead of writing a 1 byte index per component,
// we limit components to 64 bits and write one ulong instead.
// the ulong is also varint compressed for minimum bandwidth.
ulong dirtyMask = ClientDirtyMask();
ClientDirtyMasks(out ulong dirtyMaskReliable, out ulong dirtyMaskUnreliableBaseline, out ulong dirtyMaskUnreliableDelta);
// varint compresses the mask to 1 byte in most cases.
// instead of writing an 8 byte ulong.
@ -1031,25 +1296,28 @@ internal void SerializeClient(NetworkWriter writer)
// if nothing dirty, then don't even write the mask.
// otherwise, every unchanged object would send a 1 byte dirty mask!
if (dirtyMask != 0) Compression.CompressVarUInt(writer, dirtyMask);
if (dirtyMaskReliable != 0) Compression.CompressVarUInt(writerReliable, dirtyMaskReliable);
if (dirtyMaskUnreliableDelta != 0) Compression.CompressVarUInt(writerUnreliableDelta, dirtyMaskUnreliableDelta);
if (dirtyMaskUnreliableBaseline != 0) Compression.CompressVarUInt(writerUnreliableBaseline, dirtyMaskUnreliableBaseline);
// serialize all components
// perf: only iterate if dirty mask has dirty bits.
if (dirtyMask != 0)
if (dirtyMaskReliable != 0 || dirtyMaskUnreliableDelta != 0 || dirtyMaskUnreliableBaseline != 0)
{
// serialize all components
for (int i = 0; i < components.Length; ++i)
{
NetworkBehaviour comp = components[i];
// RELIABLE SERIALIZATION //////////////////////////////////
// is this component dirty?
// reuse the mask instead of calling comp.IsDirty() again here.
if (IsDirty(dirtyMask, i))
if (IsDirty(dirtyMaskReliable, i))
// if (isOwned && component.syncDirection == SyncDirection.ClientToServer)
{
// serialize into writer.
// server always knows initialState, we never need to send it
comp.Serialize(writer, false);
comp.Serialize(writerReliable, false);
// clear dirty bits for the components that we serialized.
// do not clear for _all_ components, only the ones that
@ -1059,13 +1327,39 @@ internal void SerializeClient(NetworkWriter writer)
// was elapsed, as then they wouldn't be synced.
comp.ClearAllDirtyBits();
}
// UNRELIABLE DELTA ////////////////////////////////////////
// we always send the unreliable delta no matter what
if (IsDirty(dirtyMaskUnreliableDelta, i))
// if (isOwned && component.syncDirection == SyncDirection.ClientToServer)
{
comp.Serialize(writerUnreliableDelta, false);
// clear sync time to only send delta again after syncInterval.
comp.lastSyncTime = NetworkTime.localTime;
}
// UNRELIABLE BASELINE /////////////////////////////////////
// sometimes we need the unreliable baseline
// (we always sync deltas, so no 'else if' here)
if (unreliableBaseline && IsDirty(dirtyMaskUnreliableBaseline, i))
// if (isOwned && component.syncDirection == SyncDirection.ClientToServer)
{
comp.Serialize(writerUnreliableBaseline, true);
// for unreliable components, only clear dirty bits after the reliable baseline.
// unreliable deltas aren't guaranteed to be delivered, no point in clearing bits.
// -> don't clear sync time: that's for delta syncs.
comp.ClearAllDirtyBits(false);
}
////////////////////////////////////////////////////////////
}
}
}
// deserialize components from the client on the server.
// there's no 'initialState'. server always knows the initial state.
internal bool DeserializeServer(NetworkReader reader)
// for reliable state sync, server always knows the initial state.
// for unreliable, we always sync full state so we still need the parameter.
internal bool DeserializeServer(NetworkReader reader, bool initialState)
{
// ensure NetworkBehaviours are valid before usage
ValidateComponents();
@ -1089,7 +1383,7 @@ internal bool DeserializeServer(NetworkReader reader)
// deserialize this component
// server always knows the initial state (initial=false)
// disconnect if failed, to prevent exploits etc.
if (!comp.Deserialize(reader, false)) return false;
if (!comp.Deserialize(reader, initialState)) return false;
// server received state from the owner client.
// set dirty so it's broadcast to other clients too.
@ -1133,7 +1427,10 @@ 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)
//
// unreliableBaselineElapsed: indicates that unreliable sync components need a reliable baseline sync this time.
// for reliable components, it just means sync as usual.
internal NetworkIdentitySerialization GetServerSerializationAtTick(int tick, bool unreliableBaselineElapsed)
{
// only rebuild serialization once per tick. reuse otherwise.
// except for tests, where Time.frameCount never increases.
@ -1150,10 +1447,17 @@ internal NetworkIdentitySerialization GetServerSerializationAtTick(int tick)
// reset
lastSerialization.ResetWriters();
// serialize
SerializeServer(false,
lastSerialization.ownerWriter,
lastSerialization.observersWriter);
// serialize both Reliable and Unreliable components in one iteration.
// doing each in their own iteration would be too costly.
SerializeServer_Broadcast(
lastSerialization.ownerWriterReliable,
lastSerialization.observersWriterReliable,
lastSerialization.ownerWriterUnreliableBaseline,
lastSerialization.observersWriterUnreliableBaseline,
lastSerialization.ownerWriterUnreliableDelta,
lastSerialization.observersWriterUnreliableDelta,
unreliableBaselineElapsed
);
// set tick
lastSerialization.tick = tick;

View File

@ -42,6 +42,16 @@ public class NetworkManager : MonoBehaviour
[FormerlySerializedAs("serverTickRate")]
public int sendRate = 60;
/// <summary> </summary>
[Tooltip("Ocassionally send a full reliable state for unreliable components to delta compress against. This only applies to Components with SyncMethod=Unreliable.")]
public int unreliableBaselineRate = 1;
// quake sends unreliable messages twice to make up for message drops.
// this double bandwidth, but allows for smaller buffer time / faster sync.
// best to turn this off unless the game is extremely fast paced.
[Tooltip("Send unreliable messages twice to make up for message drops. This doubles bandwidth, but allows for smaller buffer time / faster sync.\nBest to turn this off unless your game is extremely fast paced.")]
public bool unreliableRedundancy = false;
// client send rate follows server send rate to avoid errors for now
/// <summary>Client Update frequency, per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE.</summary>
// [Tooltip("Client broadcasts 'sendRate' times per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE.")]
@ -165,6 +175,11 @@ public class NetworkManager : MonoBehaviour
// virtual so that inheriting classes' OnValidate() can call base.OnValidate() too
public virtual void OnValidate()
{
// unreliable full send rate needs to be >= 0.
// we need to have something to delta compress against.
// it should also be <= sendRate otherwise there's no point.
unreliableBaselineRate = Mathf.Clamp(unreliableBaselineRate, 1, sendRate);
// always >= 0
maxConnections = Mathf.Max(maxConnections, 0);
@ -274,6 +289,8 @@ bool IsServerOnlineSceneChangeNeeded() =>
void ApplyConfiguration()
{
NetworkServer.tickRate = sendRate;
NetworkServer.unreliableBaselineRate = unreliableBaselineRate;
NetworkServer.unreliableRedundancy = unreliableRedundancy;
NetworkClient.snapshotSettings = snapshotSettings;
NetworkClient.connectionQualityInterval = evaluationInterval;
NetworkClient.connectionQualityMethod = evaluationMethod;

View File

@ -54,6 +54,17 @@ public static partial class NetworkServer
public static float sendInterval => sendRate < int.MaxValue ? 1f / sendRate : 0; // for 30 Hz, that's 33ms
static double lastSendTime;
// ocassionally send a full reliable state for unreliable components to delta compress against.
// this only applies to Components with SyncMethod=Unreliable.
public static int unreliableBaselineRate = 1;
public static float unreliableBaselineInterval => unreliableBaselineRate < int.MaxValue ? 1f / unreliableBaselineRate : 0; // for 1 Hz, that's 1000ms
static double lastUnreliableBaselineTime;
// quake sends unreliable messages twice to make up for message drops.
// this double bandwidth, but allows for smaller buffer time / faster sync.
// best to turn this off unless the game is extremely fast paced.
public static bool unreliableRedundancy = false;
/// <summary>Connection to host mode client (if any)</summary>
public static LocalConnectionToClient localConnection { get; private set; }
@ -319,6 +330,8 @@ internal static void RegisterMessageHandlers()
RegisterHandler<NetworkPingMessage>(NetworkTime.OnServerPing, false);
RegisterHandler<NetworkPongMessage>(NetworkTime.OnServerPong, false);
RegisterHandler<EntityStateMessage>(OnEntityStateMessage, true);
RegisterHandler<EntityStateMessageUnreliableBaseline>(OnEntityStateMessageUnreliableBaseline, true);
RegisterHandler<EntityStateMessageUnreliableDelta>(OnEntityStateMessageUnreliableDelta, true);
RegisterHandler<TimeSnapshotMessage>(OnTimeSnapshotMessage, false); // unreliable may arrive before reliable authority went through
}
@ -407,7 +420,10 @@ static void OnEntityStateMessage(NetworkConnectionToClient connection, EntitySta
{
// DeserializeServer checks permissions internally.
// failure to deserialize disconnects to prevent exploits.
if (!identity.DeserializeServer(reader))
// -> initialState=false because for Reliable messages,
// initial always comes from server and broadcast
// updates are always deltas.
if (!identity.DeserializeServer(reader, false))
{
if (exceptionsDisconnect)
{
@ -430,6 +446,137 @@ static void OnEntityStateMessage(NetworkConnectionToClient connection, EntitySta
// else Debug.LogWarning($"Did not find target for sync message for {message.netId} . Note: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message.");
}
// for client's owned ClientToServer components.
static void OnEntityStateMessageUnreliableBaseline(NetworkConnectionToClient connection, EntityStateMessageUnreliableBaseline message, int channelId)
{
// safety check: baseline should always arrive over Reliable channel.
if (channelId != Channels.Reliable)
{
Debug.LogError($"Server OnEntityStateMessageUnreliableBaseline arrived on channel {channelId} instead of Reliable. This should never happen!");
return;
}
// need to validate permissions carefully.
// an attacker may attempt to modify a not-owned or not-ClientToServer component.
// valid netId?
if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null)
{
// owned by the connection?
if (identity.connectionToClient == connection)
{
// set the last received reliable baseline tick number.
identity.lastUnreliableBaselineReceived = message.baselineTick;
using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload))
{
// DeserializeServer checks permissions internally.
// failure to deserialize disconnects to prevent exploits.
//
// full state updates (initial=true) arrive over reliable.
if (!identity.DeserializeServer(reader, true))
{
if (exceptionsDisconnect)
{
Debug.LogError($"Server failed to deserialize client unreliable state for {identity.name} with netId={identity.netId}, Disconnecting.");
connection.Disconnect();
}
else
Debug.LogWarning($"Server failed to deserialize client unreliable state for {identity.name} with netId={identity.netId}.");
}
}
}
// An attacker may attempt to modify another connection's entity
// This could also be a race condition of message in flight when
// RemoveClientAuthority is called, so not malicious.
// Don't disconnect, just log the warning.
else
Debug.LogWarning($"EntityStateMessage from {connection} for {identity.name} without authority.");
}
// no warning. unreliable messages often arrive before/after the reliable spawn/despawn messages.
// else Debug.LogWarning($"Did not find target for sync message for {message.netId} . Note: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message.");
}
// for client's owned ClientToServer components.
static void OnEntityStateMessageUnreliableDelta(NetworkConnectionToClient connection, EntityStateMessageUnreliableDelta message, int channelId)
{
// safety check: baseline should always arrive over Reliable channel.
if (channelId != Channels.Unreliable)
{
Debug.LogError($"Server OnEntityStateMessageUnreliableDelta arrived on channel {channelId} instead of Unreliable. This should never happen!");
return;
}
// need to validate permissions carefully.
// an attacker may attempt to modify a not-owned or not-ClientToServer component.
// valid netId?
if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null)
{
// owned by the connection?
if (identity.connectionToClient == connection)
{
// unreliable state sync messages may arrive out of order.
// only ever apply state that's newer than the last received state.
// note that we send one EntityStateMessage per Entity,
// so there will be multiple with the same == timestamp.
if (connection.remoteTimeStamp < identity.lastUnreliableStateTime)
{
// debug log to show that it's working.
// can be tested via LatencySimulation scramble easily.
Debug.Log($"Server caught out of order Unreliable state message for {identity.name}. This is fine.\nIdentity timestamp={identity.lastUnreliableStateTime:F3} batch remoteTimestamp={connection.remoteTimeStamp:F3}");
return;
}
// UDP messages may accidentally arrive twice.
// or even intentionally, if unreliableRedundancy is turned on.
else if (connection.remoteTimeStamp == identity.lastUnreliableStateTime)
{
// only log this if unreliableRedundancy is disabled.
// otherwise it's expected and will happen a lot.
if (!unreliableRedundancy) Debug.Log($"Server caught duplicate Unreliable state message for {identity.name}. This is fine.\nIdentity timestamp={identity.lastUnreliableStateTime:F3} batch remoteTimestamp={connection.remoteTimeStamp:F3}");
return;
}
// make sure this delta is for the correct baseline.
// we don't want to apply an old delta on top of a new baseline.
if (message.baselineTick != identity.lastUnreliableBaselineReceived)
{
Debug.Log($"Server caught Unreliable state message for old baseline for {identity} with baselineTick={identity.lastUnreliableBaselineReceived} messageBaseline={message.baselineTick}. This is fine.");
return;
}
// set the new last received time for unreliable
identity.lastUnreliableStateTime = connection.remoteTimeStamp;
using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload))
{
// DeserializeServer checks permissions internally.
// failure to deserialize disconnects to prevent exploits.
//
// delta state updates (initial=false) arrive over unreliable.
if (!identity.DeserializeServer(reader, false))
{
if (exceptionsDisconnect)
{
Debug.LogError($"Server failed to deserialize client unreliable state for {identity.name} with netId={identity.netId}, Disconnecting.");
connection.Disconnect();
}
else
Debug.LogWarning($"Server failed to deserialize client unreliable state for {identity.name} with netId={identity.netId}.");
}
}
}
// An attacker may attempt to modify another connection's entity
// This could also be a race condition of message in flight when
// RemoveClientAuthority is called, so not malicious.
// Don't disconnect, just log the warning.
else
Debug.LogWarning($"EntityStateMessage from {connection} for {identity.name} without authority.");
}
// no warning. unreliable messages often arrive before/after the reliable spawn/despawn messages.
// else Debug.LogWarning($"Did not find target for sync message for {message.netId} . Note: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message.");
}
// client sends TimeSnapshotMessage every sendInterval.
// batching already includes the remoteTimestamp.
// we simply insert it on-message here.
@ -1419,7 +1566,7 @@ static ArraySegment<byte> CreateSpawnMessagePayload(bool isOwner, NetworkIdentit
// serialize all components with initialState = true
// (can be null if has none)
identity.SerializeServer(true, ownerWriter, observersWriter);
identity.SerializeServer_Spawn(ownerWriter, observersWriter);
// convert to ArraySegment to avoid reader allocations
// if nothing was written, .ToArraySegment returns an empty segment.
@ -1884,11 +2031,18 @@ 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)
// -> unreliableBaselineElapsed: even though we only care about RELIABLE
// components here, GetServerSerializationAtTick still caches all
// the serializations for this frame. and when caching we already
// need to know if the unreliable baseline will be needed or not.
static NetworkWriter SerializeForConnection_ReliableComponents(
NetworkIdentity identity,
NetworkConnectionToClient connection,
bool unreliableBaselineElapsed)
{
// get serialization for this entity (cached)
// IMPORTANT: int tick avoids floating point inaccuracy over days/weeks
NetworkIdentitySerialization serialization = identity.GetServerSerializationAtTick(Time.frameCount);
NetworkIdentitySerialization serialization = identity.GetServerSerializationAtTick(Time.frameCount, unreliableBaselineElapsed);
// is this entity owned by this connection?
bool owned = identity.connectionToClient == connection;
@ -1898,23 +2052,65 @@ static NetworkWriter SerializeForConnection(NetworkIdentity identity, NetworkCon
if (owned)
{
// was it dirty / did we actually serialize anything?
if (serialization.ownerWriter.Position > 0)
return serialization.ownerWriter;
if (serialization.ownerWriterReliable.Position > 0)
return serialization.ownerWriterReliable;
}
// observers writer if not owned
else
{
// was it dirty / did we actually serialize anything?
if (serialization.observersWriter.Position > 0)
return serialization.observersWriter;
if (serialization.observersWriterReliable.Position > 0)
return serialization.observersWriterReliable;
}
// nothing was serialized
return null;
}
// helper function to get the right serialization for a connection
static (NetworkWriter, NetworkWriter) SerializeForConnection_UnreliableComponents(
NetworkIdentity identity,
NetworkConnectionToClient connection,
bool unreliableBaselineElapsed)
{
// get serialization for this entity (cached)
// IMPORTANT: int tick avoids floating point inaccuracy over days/weeks
NetworkIdentitySerialization serialization = identity.GetServerSerializationAtTick(Time.frameCount, unreliableBaselineElapsed);
// is this entity owned by this connection?
bool owned = identity.connectionToClient == connection;
NetworkWriter baselineWriter = null;
NetworkWriter deltaWriter = null;
// send serialized data
// owner writer if owned
if (owned)
{
// was it dirty / did we actually serialize anything?
if (serialization.ownerWriterUnreliableBaseline.Position > 0)
baselineWriter = serialization.ownerWriterUnreliableBaseline;
if (serialization.ownerWriterUnreliableDelta.Position > 0)
deltaWriter = serialization.ownerWriterUnreliableDelta;
}
// observers writer if not owned
else
{
// was it dirty / did we actually serialize anything?
if (serialization.observersWriterUnreliableBaseline.Position > 0)
baselineWriter = serialization.observersWriterUnreliableBaseline;
if (serialization.observersWriterUnreliableDelta.Position > 0)
deltaWriter = serialization.observersWriterUnreliableDelta;
}
// nothing was serialized
return (baselineWriter, deltaWriter);
}
// helper function to broadcast the world to a connection
static void BroadcastToConnection(NetworkConnectionToClient connection)
static void BroadcastToConnection(NetworkConnectionToClient connection, bool unreliableBaselineElapsed)
{
// for each entity that this connection is seeing
bool hasNull = false;
@ -1926,9 +2122,24 @@ static void BroadcastToConnection(NetworkConnectionToClient connection)
// NetworkServer.Destroy)
if (identity != null)
{
// 'Reliable' sync: send Reliable components over reliable with initial/delta
// get serialization for this entity viewed by this connection
// (if anything was serialized this time)
NetworkWriter serialization = SerializeForConnection(identity, connection);
NetworkWriter serialization = SerializeForConnection_ReliableComponents(identity, connection,
// IMPORTANT: even for Reliable components we must pass unreliableBaselineElapsed!
//
// consider this (in one frame):
// Serialize Reliable (unreliableBaseline=false)
// GetServerSerializationAtTick (unreliableBaseline=false)
// serializes new, clears dirty bits
// Serialize Unreliable (unreliableBaseline=true)
// GetServerSerializationAtTick (unreliableBaseline=true)
// last.baseline != baseline
// serializes new, which does nothing since dirty bits were already cleared above!
//
// TODO make this less magic in the future. too easy to miss.
unreliableBaselineElapsed);
if (serialization != null)
{
EntityStateMessage message = new EntityStateMessage
@ -1938,6 +2149,51 @@ static void BroadcastToConnection(NetworkConnectionToClient connection)
};
connection.Send(message);
}
// 'Unreliable' sync: send Unreliable components over unreliable
// 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.
(NetworkWriter baselineSerialization, NetworkWriter deltaSerialization) = SerializeForConnection_UnreliableComponents(identity, connection, unreliableBaselineElapsed);
// send unreliable delta first. ideally we want this to arrive before the new baseline.
// reliable baseline also clears dirty bits, so unreliable must be sent first.
if (deltaSerialization != null)
{
EntityStateMessageUnreliableDelta message = new EntityStateMessageUnreliableDelta
{
baselineTick = identity.lastUnreliableBaselineSent,
netId = identity.netId,
payload = deltaSerialization.ToArraySegment()
};
connection.Send(message, Channels.Unreliable);
// quake sends unreliable messages twice to make up for message drops.
// this double bandwidth, but allows for smaller buffer time / faster sync.
// best to turn this off unless the game is extremely fast paced.
if (unreliableRedundancy) connection.Send(message, Channels.Unreliable);
}
// if it's for a baseline sync, then send a reliable baseline message too.
// this will likely arrive slightly after the unreliable delta above.
if (unreliableBaselineElapsed)
{
if (baselineSerialization != null)
{
// remember last sent baseline tick for this entity.
// (byte) to minimize bandwidth. we don't need the full tick,
// just something small to compare against.
identity.lastUnreliableBaselineSent = (byte)Time.frameCount;
EntityStateMessageUnreliableBaseline message = new EntityStateMessageUnreliableBaseline
{
baselineTick = identity.lastUnreliableBaselineSent,
netId = identity.netId,
payload = baselineSerialization.ToArraySegment()
};
connection.Send(message, Channels.Reliable);
}
}
}
// spawned list should have no null entries because we
// always call Remove in OnObjectDestroy everywhere.
@ -1976,7 +2232,8 @@ static bool DisconnectIfInactive(NetworkConnectionToClient connection)
internal static readonly List<NetworkConnectionToClient> connectionsCopy =
new List<NetworkConnectionToClient>();
static void Broadcast()
// unreliableFullSendIntervalElapsed: indicates that unreliable sync components need a reliable baseline sync this time.
static void Broadcast(bool unreliableBaselineElapsed)
{
// copy all connections into a helper collection so that
// OnTransportDisconnected can be called while iterating.
@ -2013,7 +2270,7 @@ static void Broadcast()
connection.Send(new TimeSnapshotMessage(), Channels.Unreliable);
// broadcast world state to this connection
BroadcastToConnection(connection);
BroadcastToConnection(connection, unreliableBaselineElapsed);
}
// update connection to flush out batched messages
@ -2067,8 +2324,9 @@ internal static void NetworkLateUpdate()
// snapshots _but_ not every single tick.
// Unity 2019 doesn't have Time.timeAsDouble yet
bool sendIntervalElapsed = AccurateInterval.Elapsed(NetworkTime.localTime, sendInterval, ref lastSendTime);
bool unreliableBaselineElapsed = AccurateInterval.Elapsed(NetworkTime.localTime, unreliableBaselineInterval, ref lastUnreliableBaselineTime);
if (!Application.isPlaying || sendIntervalElapsed)
Broadcast();
Broadcast(unreliableBaselineElapsed);
}
// process all outgoing messages after updating the world

View File

@ -94,6 +94,16 @@ protected void DrawDefaultSyncSettings()
if (syncDirection.enumValueIndex == (int)SyncDirection.ServerToClient)
EditorGUILayout.PropertyField(serializedObject.FindProperty("syncMode"));
// sync method
SerializedProperty syncMethod = serializedObject.FindProperty("syncMethod");
EditorGUILayout.PropertyField(syncMethod);
// Unreliable sync method: show a warning!
if (syncMethod.enumValueIndex == (int)SyncMethod.Unreliable)
{
EditorGUILayout.HelpBox("Beware!\nUnreliable is experimental, do not use this yet!", MessageType.Warning);
}
// sync interval
EditorGUILayout.PropertyField(serializedObject.FindProperty("syncInterval"));

View File

@ -11,15 +11,26 @@ namespace Mirror.Tests.NetworkIdentities
public class NetworkIdentitySerializationTests : MirrorEditModeTest
{
// writers are always needed. create in setup for convenience.
NetworkWriter ownerWriter;
NetworkWriter observersWriter;
NetworkWriter ownerWriterReliable;
NetworkWriter observersWriterReliable;
NetworkWriter ownerWriterUnreliableBaseline;
NetworkWriter observersWriterUnreliableBaseline;
NetworkWriter ownerWriterUnreliableDelta;
NetworkWriter observersWriterUnreliableDelta;
[SetUp]
public override void SetUp()
{
base.SetUp();
ownerWriter = new NetworkWriter();
observersWriter = new NetworkWriter();
ownerWriterReliable = new NetworkWriter();
observersWriterReliable = new NetworkWriter();
ownerWriterUnreliableBaseline = new NetworkWriter();
observersWriterUnreliableBaseline = new NetworkWriter();
ownerWriterUnreliableDelta = new NetworkWriter();
observersWriterUnreliableDelta = new NetworkWriter();
NetworkServer.Listen(1);
ConnectClientBlockingAuthenticatedAndReady(out _);
@ -34,7 +45,7 @@ public override void TearDown()
// serialize -> deserialize. multiple components to be sure.
// one for Owner, one for Observer
[Test]
public void SerializeAndDeserializeAll()
public void SerializeServer_Spawn_OwnerAndObserver()
{
// need two of both versions so we can serialize -> deserialize
CreateNetworkedAndSpawn(
@ -51,10 +62,10 @@ public void SerializeAndDeserializeAll()
serverObserversComp.value = 42;
// serialize server object
serverIdentity.SerializeServer(true, ownerWriter, observersWriter);
serverIdentity.SerializeServer_Spawn(ownerWriterReliable, observersWriterReliable);
// deserialize client object with OWNER payload
NetworkReader reader = new NetworkReader(ownerWriter.ToArray());
NetworkReader reader = new NetworkReader(ownerWriterReliable.ToArray());
clientIdentity.DeserializeClient(reader, true);
Assert.That(clientOwnerComp.value, Is.EqualTo("42"));
Assert.That(clientObserversComp.value, Is.EqualTo(42));
@ -64,7 +75,7 @@ public void SerializeAndDeserializeAll()
clientObserversComp.value = 0;
// deserialize client object with OBSERVERS payload
reader = new NetworkReader(observersWriter.ToArray());
reader = new NetworkReader(observersWriterReliable.ToArray());
clientIdentity.DeserializeClient(reader, true);
Assert.That(clientOwnerComp.value, Is.EqualTo(null)); // owner mode shouldn't be in data
Assert.That(clientObserversComp.value, Is.EqualTo(42)); // observers mode should be in data
@ -214,12 +225,12 @@ public void SerializationException()
// serialize server object
// should work even if compExc throws an exception.
// error log because of the exception is expected.
serverIdentity.SerializeServer(true, ownerWriter, observersWriter);
serverIdentity.SerializeServer_Spawn(ownerWriterReliable, observersWriterReliable);
// deserialize client object with OWNER payload
// should work even if compExc throws an exception
// error log because of the exception is expected
NetworkReader reader = new NetworkReader(ownerWriter.ToArray());
NetworkReader reader = new NetworkReader(ownerWriterReliable.ToArray());
clientIdentity.DeserializeClient(reader, true);
Assert.That(clientComp2.value, Is.EqualTo("42"));
@ -229,7 +240,7 @@ public void SerializationException()
// deserialize client object with OBSERVER payload
// should work even if compExc throws an exception
// error log because of the exception is expected
reader = new NetworkReader(observersWriter.ToArray());
reader = new NetworkReader(observersWriterReliable.ToArray());
clientIdentity.DeserializeClient(reader, true);
Assert.That(clientComp2.value, Is.EqualTo(null)); // owner mode should be in data
@ -305,12 +316,12 @@ public void SerializationMismatch()
serverComp.value = "42";
// serialize server object
serverIdentity.SerializeServer(true, ownerWriter, observersWriter);
serverIdentity.SerializeServer_Spawn(ownerWriterReliable, observersWriterReliable);
// deserialize on client
// ignore warning log because of serialization mismatch
LogAssert.ignoreFailingMessages = true;
NetworkReader reader = new NetworkReader(ownerWriter.ToArray());
NetworkReader reader = new NetworkReader(ownerWriterReliable.ToArray());
clientIdentity.DeserializeClient(reader, true);
LogAssert.ignoreFailingMessages = false;
@ -324,22 +335,37 @@ public void SerializationMismatch()
// 0-dirty-mask. instead, we need to ensure it writes nothing.
// too easy to miss, with too significant bandwidth implications.
[Test]
public void SerializeServer_NotInitial_NotDirty_WritesNothing()
public void SerializeServer_Broadcast_NotDirty_WritesNothing()
{
// create spawned so that isServer/isClient is set properly
CreateNetworkedAndSpawn(
out _, out NetworkIdentity serverIdentity, out SerializeTest1NetworkBehaviour serverComp1, out SerializeTest2NetworkBehaviour serverComp2,
out _, out NetworkIdentity clientIdentity, out SerializeTest1NetworkBehaviour clientComp1, out SerializeTest2NetworkBehaviour clientComp2);
// some reliable, some unreliable components
serverComp1.syncMethod = clientComp1.syncMethod = SyncMethod.Reliable;
serverComp2.syncMethod = clientComp2.syncMethod = SyncMethod.Unreliable;
// change nothing
// serverComp.value = "42";
// serialize server object.
// 'initial' would write everything.
// instead, try 'not initial' with 0 dirty bits
serverIdentity.SerializeServer(false, ownerWriter, observersWriter);
Assert.That(ownerWriter.Position, Is.EqualTo(0));
Assert.That(observersWriter.Position, Is.EqualTo(0));
serverIdentity.SerializeServer_Broadcast(
ownerWriterReliable, observersWriterReliable,
ownerWriterUnreliableBaseline, observersWriterUnreliableBaseline,
ownerWriterUnreliableDelta, observersWriterUnreliableDelta,
false);
Assert.That(ownerWriterReliable.Position, Is.EqualTo(0));
Assert.That(observersWriterReliable.Position, Is.EqualTo(0));
Assert.That(ownerWriterUnreliableBaseline.Position, Is.EqualTo(0));
Assert.That(observersWriterUnreliableBaseline.Position, Is.EqualTo(0));
Assert.That(ownerWriterUnreliableDelta.Position, Is.EqualTo(0));
Assert.That(observersWriterUnreliableDelta.Position, Is.EqualTo(0));
}
[Test]
@ -350,6 +376,10 @@ public void SerializeClient_NotInitial_NotDirty_WritesNothing()
out _, out NetworkIdentity serverIdentity, out SerializeTest1NetworkBehaviour serverComp1, out SerializeTest2NetworkBehaviour serverComp2,
out _, out NetworkIdentity clientIdentity, out SerializeTest1NetworkBehaviour clientComp1, out SerializeTest2NetworkBehaviour clientComp2);
// some reliable, some unreliable components
serverComp1.syncMethod = clientComp1.syncMethod = SyncMethod.Reliable;
serverComp2.syncMethod = clientComp2.syncMethod = SyncMethod.Unreliable;
// client only serializes owned ClientToServer components
clientIdentity.isOwned = true;
serverComp1.syncDirection = SyncDirection.ClientToServer;
@ -361,8 +391,10 @@ public void SerializeClient_NotInitial_NotDirty_WritesNothing()
// clientComp.value = "42";
// serialize client object
clientIdentity.SerializeClient(ownerWriter);
Assert.That(ownerWriter.Position, Is.EqualTo(0));
clientIdentity.SerializeClient(ownerWriterReliable, ownerWriterUnreliableBaseline, ownerWriterUnreliableDelta, false);
Assert.That(ownerWriterReliable.Position, Is.EqualTo(0));
Assert.That(ownerWriterUnreliableBaseline.Position, Is.EqualTo(0));
Assert.That(ownerWriterUnreliableDelta.Position, Is.EqualTo(0));
}
// serialize -> deserialize. multiple components to be sure.
@ -385,11 +417,11 @@ public void SerializeAndDeserialize_ClientToServer_NOT_OWNED()
comp2.value = "67890";
// serialize all
identity.SerializeClient(ownerWriter);
identity.SerializeClient(ownerWriterReliable, new NetworkWriter(), new NetworkWriter(), false);
// shouldn't sync anything. because even though it's ClientToServer,
// we don't own this one so we shouldn't serialize & sync it.
Assert.That(ownerWriter.Position, Is.EqualTo(0));
Assert.That(ownerWriterReliable.Position, Is.EqualTo(0));
}
// server should still send initial even if Owner + ClientToServer
@ -397,72 +429,262 @@ public void SerializeAndDeserialize_ClientToServer_NOT_OWNED()
public void SerializeServer_OwnerMode_ClientToServer()
{
CreateNetworked(out GameObject _, out NetworkIdentity identity,
out SyncVarTest1NetworkBehaviour comp);
out SyncVarTest1NetworkBehaviour comp1,
out SyncVarTest2NetworkBehaviour comp2);
// one Reliable, one Unreliable component
comp1.syncMethod = SyncMethod.Reliable;
comp2.syncMethod = SyncMethod.Unreliable;
// pretend to be owned
identity.isOwned = true;
comp.syncMode = SyncMode.Owner;
comp.syncInterval = 0;
comp1.syncMode = comp2.syncMode = SyncMode.Owner;
comp1.syncInterval = comp2.syncInterval = 0;
// set to CLIENT with some unique values
// and set connection to server to pretend we are the owner.
comp.syncDirection = SyncDirection.ClientToServer;
comp.SetValue(11); // modify with helper function to avoid #3525
comp1.syncDirection = comp2.syncDirection = SyncDirection.ClientToServer;
comp1.SetValue(11); // modify with helper function to avoid #3525
comp2.SetValue("22"); // modify with helper function to avoid #3525
// initial: should still write for owner
identity.SerializeServer(true, ownerWriter, observersWriter);
Debug.Log("initial ownerWriter: " + ownerWriter);
Debug.Log("initial observerWriter: " + observersWriter);
Assert.That(ownerWriter.Position, Is.GreaterThan(0));
Assert.That(observersWriter.Position, Is.EqualTo(0));
identity.SerializeServer_Spawn(ownerWriterReliable, observersWriterReliable);
Debug.Log("initial ownerWriter: " + ownerWriterReliable);
Debug.Log("initial observerWriter: " + observersWriterReliable);
Assert.That(ownerWriterReliable.Position, Is.GreaterThan(0));
Assert.That(observersWriterReliable.Position, Is.EqualTo(0));
// delta: ClientToServer comes from the client
comp.SetValue(22); // modify with helper function to avoid #3525
ownerWriter.Position = 0;
observersWriter.Position = 0;
identity.SerializeServer(false, ownerWriter, observersWriter);
Debug.Log("delta ownerWriter: " + ownerWriter);
Debug.Log("delta observersWriter: " + observersWriter);
Assert.That(ownerWriter.Position, Is.EqualTo(0));
Assert.That(observersWriter.Position, Is.EqualTo(0));
comp1.SetValue(33); // modify with helper function to avoid #3525
comp2.SetValue("44"); // modify with helper function to avoid #3525
ownerWriterReliable.Position = 0;
observersWriterReliable.Position = 0;
identity.SerializeServer_Broadcast(
ownerWriterReliable, observersWriterReliable,
ownerWriterUnreliableBaseline, observersWriterUnreliableBaseline,
ownerWriterUnreliableDelta, observersWriterUnreliableDelta,
false);
Debug.Log("delta ownerWriter: " + ownerWriterReliable);
Debug.Log("delta observersWriter: " + observersWriterReliable);
Assert.That(ownerWriterReliable.Position, Is.EqualTo(0));
Assert.That(observersWriterReliable.Position, Is.EqualTo(0));
Assert.That(ownerWriterUnreliableBaseline.Position, Is.EqualTo(0));
Assert.That(observersWriterUnreliableBaseline.Position, Is.EqualTo(0));
Assert.That(ownerWriterUnreliableDelta.Position, Is.EqualTo(0));
Assert.That(observersWriterUnreliableDelta.Position, Is.EqualTo(0));
}
// TODO this started failing after we moved SyncVarTest1NetworkBehaviour
// into it's own asmdef.
// server should still broadcast ClientToServer components to everyone
// except the owner.
[Test]
public void SerializeServer_ObserversMode_ClientToServer()
{
CreateNetworked(out GameObject _, out NetworkIdentity identity,
out SyncVarTest1NetworkBehaviour comp);
out SyncVarTest1NetworkBehaviour comp1,
out SyncVarTest2NetworkBehaviour comp2);
// one Reliable, one Unreliable component
comp1.syncMethod = SyncMethod.Reliable;
comp2.syncMethod = SyncMethod.Unreliable;
// pretend to be owned
identity.isOwned = true;
comp.syncMode = SyncMode.Observers;
comp.syncInterval = 0;
comp1.syncMode = comp2.syncMode = SyncMode.Observers;
comp1.syncInterval = comp2.syncInterval = 0;
// set to CLIENT with some unique values
// and set connection to server to pretend we are the owner.
comp.syncDirection = SyncDirection.ClientToServer;
comp.SetValue(11); // modify with helper function to avoid #3525
comp1.syncDirection = comp2.syncDirection = SyncDirection.ClientToServer;
comp1.SetValue(11); // modify with helper function to avoid #3525
comp2.SetValue("22"); // modify with helper function to avoid #3525
// initial: should write something for owner and observers
identity.SerializeServer(true, ownerWriter, observersWriter);
Debug.Log("initial ownerWriter: " + ownerWriter);
Debug.Log("initial observerWriter: " + observersWriter);
Assert.That(ownerWriter.Position, Is.GreaterThan(0));
Assert.That(observersWriter.Position, Is.GreaterThan(0));
// initial: should still write for owner AND observers
identity.SerializeServer_Spawn(ownerWriterReliable, observersWriterReliable);
Debug.Log("initial ownerWriter: " + ownerWriterReliable);
Debug.Log("initial observerWriter: " + observersWriterReliable);
Assert.That(ownerWriterReliable.Position, Is.GreaterThan(0));
Assert.That(observersWriterReliable.Position, Is.GreaterThan(0));
// delta: should only write for observers
comp.SetValue(22); // modify with helper function to avoid #3525
ownerWriter.Position = 0;
observersWriter.Position = 0;
identity.SerializeServer(false, ownerWriter, observersWriter);
Debug.Log("delta ownerWriter: " + ownerWriter);
Debug.Log("delta observersWriter: " + observersWriter);
Assert.That(ownerWriter.Position, Is.EqualTo(0));
Assert.That(observersWriter.Position, Is.GreaterThan(0));
comp1.SetValue(33); // modify with helper function to avoid #3525
comp2.SetValue("44"); // modify with helper function to avoid #3525
ownerWriterReliable.Position = 0;
observersWriterReliable.Position = 0;
identity.SerializeServer_Broadcast(
ownerWriterReliable, observersWriterReliable,
ownerWriterUnreliableBaseline, observersWriterUnreliableBaseline,
ownerWriterUnreliableDelta, observersWriterUnreliableDelta,
false);
Debug.Log("delta ownerWriter: " + ownerWriterReliable);
Debug.Log("delta observersWriter: " + observersWriterReliable);
Assert.That(ownerWriterReliable.Position, Is.EqualTo(0));
Assert.That(observersWriterReliable.Position, Is.GreaterThan(0));
Assert.That(ownerWriterUnreliableBaseline.Position, Is.EqualTo(0));
Assert.That(observersWriterUnreliableBaseline.Position, Is.GreaterThan(0));
Assert.That(ownerWriterUnreliableDelta.Position, Is.EqualTo(0));
Assert.That(observersWriterUnreliableDelta.Position, Is.GreaterThan(0));
}
[Test]
public void SerializeServer_ObserversMode_ServerToClient_ReliableAndUnreliable()
{
CreateNetworked(out GameObject _, out NetworkIdentity identity,
out SyncVarTest1NetworkBehaviour comp1,
out SyncVarTest2NetworkBehaviour comp2);
// one Reliable, one Unreliable component
comp1.syncMethod = SyncMethod.Reliable;
comp2.syncMethod = SyncMethod.Unreliable;
// pretend to be owned
identity.isOwned = true;
comp1.syncMode = comp2.syncMode = SyncMode.Observers;
comp1.syncInterval = comp2.syncInterval = 0;
// set to CLIENT with some unique values
// and set connection to server to pretend we are the owner.
comp1.syncDirection = comp2.syncDirection = SyncDirection.ServerToClient;
comp1.SetValue(11); // modify with helper function to avoid #3525
comp2.SetValue("22"); // modify with helper function to avoid #3525
// initial: should still write for owner AND observers
identity.SerializeServer_Spawn(ownerWriterReliable, observersWriterReliable);
Debug.Log("initial ownerWriter: " + ownerWriterReliable);
Debug.Log("initial observerWriter: " + observersWriterReliable);
Assert.That(ownerWriterReliable.Position, Is.GreaterThan(0));
Assert.That(observersWriterReliable.Position, Is.GreaterThan(0));
// delta: should write something for all
comp1.SetValue(33); // modify with helper function to avoid #3525
comp2.SetValue("44"); // modify with helper function to avoid #3525
ownerWriterReliable.Position = 0;
observersWriterReliable.Position = 0;
identity.SerializeServer_Broadcast(
ownerWriterReliable, observersWriterReliable,
ownerWriterUnreliableBaseline, observersWriterUnreliableBaseline,
ownerWriterUnreliableDelta, observersWriterUnreliableDelta,
false);
Debug.Log("delta ownerWriter: " + ownerWriterReliable);
Debug.Log("delta observersWriter: " + observersWriterReliable);
Assert.That(ownerWriterReliable.Position, Is.GreaterThan(0));
Assert.That(observersWriterReliable.Position, Is.GreaterThan(0));
Assert.That(ownerWriterUnreliableBaseline.Position, Is.GreaterThan(0));
Assert.That(observersWriterUnreliableBaseline.Position, Is.GreaterThan(0));
Assert.That(ownerWriterUnreliableDelta.Position, Is.GreaterThan(0));
Assert.That(observersWriterUnreliableDelta.Position, Is.GreaterThan(0));
}
[Test]
public void SerializeServer_ObserversMode_ServerToClient_ReliableOnly()
{
CreateNetworked(out GameObject _, out NetworkIdentity identity,
out SyncVarTest1NetworkBehaviour comp1,
out SyncVarTest2NetworkBehaviour comp2);
// one Reliable, one Unreliable component
comp1.syncMethod = SyncMethod.Reliable;
comp2.syncMethod = SyncMethod.Unreliable;
// pretend to be owned
identity.isOwned = true;
comp1.syncMode = comp2.syncMode = SyncMode.Observers;
comp1.syncInterval = comp2.syncInterval = 0;
// set to CLIENT with some unique values
// and set connection to server to pretend we are the owner.
comp1.syncDirection = comp2.syncDirection = SyncDirection.ServerToClient;
comp1.SetValue(11); // modify with helper function to avoid #3525
// comp2.SetValue("22"); // Unreliable component doesn't change this time
// initial: should still write for owner AND observers
identity.SerializeServer_Spawn(ownerWriterReliable, observersWriterReliable);
Debug.Log("initial ownerWriter: " + ownerWriterReliable);
Debug.Log("initial observerWriter: " + observersWriterReliable);
Assert.That(ownerWriterReliable.Position, Is.GreaterThan(0));
Assert.That(observersWriterReliable.Position, Is.GreaterThan(0));
// delta: should write something for all
comp1.SetValue(33); // modify with helper function to avoid #3525
// comp2.SetValue("44"); // Unreliable component doesn't change this time
ownerWriterReliable.Position = 0;
observersWriterReliable.Position = 0;
identity.SerializeServer_Broadcast(
ownerWriterReliable, observersWriterReliable,
ownerWriterUnreliableBaseline, observersWriterUnreliableBaseline,
ownerWriterUnreliableDelta, observersWriterUnreliableDelta,
false);
Debug.Log("delta ownerWriter: " + ownerWriterReliable);
Debug.Log("delta observersWriter: " + observersWriterReliable);
Assert.That(ownerWriterReliable.Position, Is.GreaterThan(0));
Assert.That(observersWriterReliable.Position, Is.GreaterThan(0));
Assert.That(ownerWriterUnreliableBaseline.Position, Is.EqualTo(0));
Assert.That(observersWriterUnreliableBaseline.Position, Is.EqualTo(0));
Assert.That(ownerWriterUnreliableDelta.Position, Is.EqualTo(0));
Assert.That(observersWriterUnreliableDelta.Position, Is.EqualTo(0));
}
[Test]
public void SerializeServer_ObserversMode_ServerToClient_UnreliableOnly()
{
CreateNetworked(out GameObject _, out NetworkIdentity identity,
out SyncVarTest1NetworkBehaviour comp1,
out SyncVarTest2NetworkBehaviour comp2);
// one Reliable, one Unreliable component
comp1.syncMethod = SyncMethod.Reliable;
comp2.syncMethod = SyncMethod.Unreliable;
// pretend to be owned
identity.isOwned = true;
comp1.syncMode = comp2.syncMode = SyncMode.Observers;
comp1.syncInterval = comp2.syncInterval = 0;
// set to CLIENT with some unique values
// and set connection to server to pretend we are the owner.
comp1.syncDirection = comp2.syncDirection = SyncDirection.ServerToClient;
// comp1.SetValue(11); // Reliable component doesn't change this time
comp2.SetValue("22"); // modify with helper function to avoid #3525
// initial: should still write for owner AND observers
identity.SerializeServer_Spawn(ownerWriterReliable, observersWriterReliable);
Debug.Log("initial ownerWriter: " + ownerWriterReliable);
Debug.Log("initial observerWriter: " + observersWriterReliable);
Assert.That(ownerWriterReliable.Position, Is.GreaterThan(0));
Assert.That(observersWriterReliable.Position, Is.GreaterThan(0));
// delta: should write something for all
// comp1.SetValue(33); // Reliable component doesn't change this time
comp2.SetValue("44"); // modify with helper function to avoid #3525
ownerWriterReliable.Position = 0;
observersWriterReliable.Position = 0;
identity.SerializeServer_Broadcast(
ownerWriterReliable, observersWriterReliable,
ownerWriterUnreliableBaseline, observersWriterUnreliableBaseline,
ownerWriterUnreliableDelta, observersWriterUnreliableDelta,
false);
Debug.Log("delta ownerWriter: " + ownerWriterReliable);
Debug.Log("delta observersWriter: " + observersWriterReliable);
Assert.That(ownerWriterReliable.Position, Is.EqualTo(0));
Assert.That(observersWriterReliable.Position, Is.EqualTo(0));
Assert.That(ownerWriterUnreliableBaseline.Position, Is.GreaterThan(0));
Assert.That(observersWriterUnreliableBaseline.Position, Is.GreaterThan(0));
Assert.That(ownerWriterUnreliableDelta.Position, Is.GreaterThan(0));
Assert.That(observersWriterUnreliableDelta.Position, Is.GreaterThan(0));
}
}
}

View File

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

View File

@ -133,6 +133,10 @@ public class SyncVarTest1NetworkBehaviour : NetworkBehaviour
public class SyncVarTest2NetworkBehaviour : NetworkBehaviour
{
[SyncVar] public string value;
// function to modify the [SyncVar] from other assemblies.
// workaround for https://github.com/MirrorNetworking/Mirror/issues/3525
public void SetValue(string s) => value = s;
}
[AddComponentMenu("")]

View File

@ -66,10 +66,10 @@ 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);
NetworkIdentitySerialization serialization = identity.GetServerSerializationAtTick(tick, false);
// advance tick
++tick;
NetworkIdentitySerialization serializationNew = identity.GetServerSerializationAtTick(tick);
NetworkIdentitySerialization serializationNew = identity.GetServerSerializationAtTick(tick, false);
// if the serialization has been changed the tickTimeStamp should have moved
Assert.That(serialization.tick == serializationNew.tick, Is.False);