mirror of
https://github.com/MirrorNetworking/Mirror.git
synced 2024-11-18 02:50:32 +00:00
feature: SyncMethod [Reliable/Unreliable] via Hybrid method
This commit is contained in:
parent
93995c28ac
commit
bf1d415193
@ -94,6 +94,7 @@ public struct ObjectHideMessage : NetworkMessage
|
|||||||
public uint netId;
|
public uint netId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// state update for reliable sync
|
||||||
public struct EntityStateMessage : NetworkMessage
|
public struct EntityStateMessage : NetworkMessage
|
||||||
{
|
{
|
||||||
public uint netId;
|
public uint netId;
|
||||||
@ -102,6 +103,38 @@ public struct EntityStateMessage : NetworkMessage
|
|||||||
public ArraySegment<byte> payload;
|
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.
|
// whoever wants to measure rtt, sends this to the other end.
|
||||||
public struct NetworkPingMessage : NetworkMessage
|
public struct NetworkPingMessage : NetworkMessage
|
||||||
{
|
{
|
||||||
|
@ -6,6 +6,11 @@
|
|||||||
|
|
||||||
namespace Mirror
|
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
|
// SyncMode decides if a component is synced to all observers, or only owner
|
||||||
public enum SyncMode { Observers, Owner }
|
public enum SyncMode { Observers, Owner }
|
||||||
|
|
||||||
@ -24,6 +29,9 @@ public enum SyncDirection { ServerToClient, ClientToServer }
|
|||||||
[HelpURL("https://mirror-networking.gitbook.io/docs/guides/networkbehaviour")]
|
[HelpURL("https://mirror-networking.gitbook.io/docs/guides/networkbehaviour")]
|
||||||
public abstract class NetworkBehaviour : MonoBehaviour
|
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>
|
/// <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.")]
|
[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;
|
[HideInInspector] public SyncDirection syncDirection = SyncDirection.ServerToClient;
|
||||||
@ -228,12 +236,17 @@ public bool IsDirty() =>
|
|||||||
// only check time if bits were dirty. this is more expensive.
|
// only check time if bits were dirty. this is more expensive.
|
||||||
NetworkTime.localTime - lastSyncTime >= syncInterval;
|
NetworkTime.localTime - lastSyncTime >= syncInterval;
|
||||||
|
|
||||||
|
// 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>
|
/// <summary>Clears all the dirty bits that were set by SetSyncVarDirtyBit() (formally SetDirtyBits)</summary>
|
||||||
// automatically invoked when an update is sent for this object, but can
|
// automatically invoked when an update is sent for this object, but can
|
||||||
// be called manually as well.
|
// be called manually as well.
|
||||||
public void ClearAllDirtyBits()
|
public void ClearAllDirtyBits(bool clearSyncTime = true)
|
||||||
{
|
{
|
||||||
lastSyncTime = NetworkTime.localTime;
|
if (clearSyncTime) lastSyncTime = NetworkTime.localTime;
|
||||||
syncVarDirtyBits = 0L;
|
syncVarDirtyBits = 0L;
|
||||||
syncObjectDirtyBits = 0L;
|
syncObjectDirtyBits = 0L;
|
||||||
|
|
||||||
|
@ -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
|
public static float sendInterval => sendRate < int.MaxValue ? 1f / sendRate : 0; // for 30 Hz, that's 33ms
|
||||||
static double lastSendTime;
|
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
|
// For security, it is recommended to disconnect a player if a networked
|
||||||
// action triggers an exception\nThis could prevent components being
|
// action triggers an exception\nThis could prevent components being
|
||||||
// accessed in an undefined state, which may be an attack vector for
|
// accessed in an undefined state, which may be an attack vector for
|
||||||
@ -505,6 +516,8 @@ internal static void RegisterMessageHandlers(bool hostMode)
|
|||||||
RegisterHandler<ObjectSpawnFinishedMessage>(_ => { });
|
RegisterHandler<ObjectSpawnFinishedMessage>(_ => { });
|
||||||
// host mode doesn't need state updates
|
// host mode doesn't need state updates
|
||||||
RegisterHandler<EntityStateMessage>(_ => { });
|
RegisterHandler<EntityStateMessage>(_ => { });
|
||||||
|
RegisterHandler<EntityStateMessageUnreliableBaseline>(_ => { });
|
||||||
|
RegisterHandler<EntityStateMessageUnreliableDelta>(_ => { });
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -516,6 +529,8 @@ internal static void RegisterMessageHandlers(bool hostMode)
|
|||||||
RegisterHandler<ObjectSpawnStartedMessage>(OnObjectSpawnStarted);
|
RegisterHandler<ObjectSpawnStartedMessage>(OnObjectSpawnStarted);
|
||||||
RegisterHandler<ObjectSpawnFinishedMessage>(OnObjectSpawnFinished);
|
RegisterHandler<ObjectSpawnFinishedMessage>(OnObjectSpawnFinished);
|
||||||
RegisterHandler<EntityStateMessage>(OnEntityStateMessage);
|
RegisterHandler<EntityStateMessage>(OnEntityStateMessage);
|
||||||
|
RegisterHandler<EntityStateMessageUnreliableBaseline>(OnEntityStateMessageUnreliableBaseline);
|
||||||
|
RegisterHandler<EntityStateMessageUnreliableDelta>(OnEntityStateMessageUnreliableDelta);
|
||||||
}
|
}
|
||||||
|
|
||||||
// These handlers are the same for host and remote clients
|
// 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.");
|
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)
|
static void OnRPCMessage(RpcMessage message)
|
||||||
{
|
{
|
||||||
// Debug.Log($"NetworkClient.OnRPCMessage hash:{message.functionHash} netId:{message.netId}");
|
// 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
|
// Unity 2019 doesn't have Time.timeAsDouble yet
|
||||||
bool sendIntervalElapsed = AccurateInterval.Elapsed(NetworkTime.localTime, sendInterval, ref lastSendTime);
|
bool sendIntervalElapsed = AccurateInterval.Elapsed(NetworkTime.localTime, sendInterval, ref lastSendTime);
|
||||||
|
bool unreliableBaselineElapsed = AccurateInterval.Elapsed(NetworkTime.localTime, unreliableBaselineInterval, ref lastUnreliableBaselineTime);
|
||||||
if (!Application.isPlaying || sendIntervalElapsed)
|
if (!Application.isPlaying || sendIntervalElapsed)
|
||||||
{
|
{
|
||||||
Broadcast();
|
Broadcast(unreliableBaselineElapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateConnectionQuality();
|
UpdateConnectionQuality();
|
||||||
@ -1596,7 +1696,9 @@ void UpdateConnectionQuality()
|
|||||||
// broadcast ///////////////////////////////////////////////////////////
|
// broadcast ///////////////////////////////////////////////////////////
|
||||||
// make sure Broadcast() is only called every sendInterval.
|
// make sure Broadcast() is only called every sendInterval.
|
||||||
// calling it every update() would require too much bandwidth.
|
// 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?
|
// joined the world yet?
|
||||||
if (!connection.isReady) return;
|
if (!connection.isReady) return;
|
||||||
@ -1608,12 +1710,14 @@ static void Broadcast()
|
|||||||
Send(new TimeSnapshotMessage(), Channels.Unreliable);
|
Send(new TimeSnapshotMessage(), Channels.Unreliable);
|
||||||
|
|
||||||
// broadcast client state to server
|
// broadcast client state to server
|
||||||
BroadcastToServer();
|
BroadcastToServer(unreliableBaselineElapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// NetworkServer has BroadcastToConnection.
|
// NetworkServer has BroadcastToConnection.
|
||||||
// NetworkClient has BroadcastToServer.
|
// 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
|
// for each entity that the client owns
|
||||||
foreach (NetworkIdentity identity in connection.owned)
|
foreach (NetworkIdentity identity in connection.owned)
|
||||||
@ -1624,21 +1728,64 @@ static void BroadcastToServer()
|
|||||||
// NetworkServer.Destroy)
|
// NetworkServer.Destroy)
|
||||||
if (identity != null)
|
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
|
// serialize reliable and unreliable components in only one iteration.
|
||||||
// (if anything was serialized this time)
|
// serializing reliable and unreliable separately in two iterations would be too costly.
|
||||||
identity.SerializeClient(writer);
|
identity.SerializeClient(writerReliable, writerUnreliableBaseline, writerUnreliableDelta, unreliableBaselineElapsed);
|
||||||
if (writer.Position > 0)
|
|
||||||
|
// any reliable components serialization?
|
||||||
|
if (writerReliable.Position > 0)
|
||||||
{
|
{
|
||||||
// send state update message
|
// send state update message
|
||||||
EntityStateMessage message = new EntityStateMessage
|
EntityStateMessage message = new EntityStateMessage
|
||||||
{
|
{
|
||||||
netId = identity.netId,
|
netId = identity.netId,
|
||||||
payload = writer.ToArraySegment()
|
payload = writerReliable.ToArraySegment()
|
||||||
};
|
};
|
||||||
Send(message);
|
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
|
// spawned list should have no null entries because we
|
||||||
|
@ -27,13 +27,26 @@ public struct NetworkIdentitySerialization
|
|||||||
{
|
{
|
||||||
// IMPORTANT: int tick avoids floating point inaccuracy over days/weeks
|
// IMPORTANT: int tick avoids floating point inaccuracy over days/weeks
|
||||||
public int tick;
|
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()
|
public void ResetWriters()
|
||||||
{
|
{
|
||||||
ownerWriter.Position = 0;
|
ownerWriterReliable.Position = 0;
|
||||||
observersWriter.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
|
// => way easier to store them per object
|
||||||
NetworkIdentitySerialization lastSerialization = new NetworkIdentitySerialization
|
NetworkIdentitySerialization lastSerialization = new NetworkIdentitySerialization
|
||||||
{
|
{
|
||||||
ownerWriter = new NetworkWriter(),
|
ownerWriterReliable = new NetworkWriter(),
|
||||||
observersWriter = 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
|
// Keep track of all sceneIds to detect scene duplicates
|
||||||
static readonly Dictionary<ulong, NetworkIdentity> sceneIds =
|
static readonly Dictionary<ulong, NetworkIdentity> sceneIds =
|
||||||
new Dictionary<ulong, NetworkIdentity>();
|
new Dictionary<ulong, NetworkIdentity>();
|
||||||
@ -837,7 +863,7 @@ internal void OnStopLocalPlayer()
|
|||||||
|
|
||||||
// build dirty mask for server owner & observers (= all dirty components).
|
// build dirty mask for server owner & observers (= all dirty components).
|
||||||
// faster to do it in one iteration instead of iterating separately.
|
// faster to do it in one iteration instead of iterating separately.
|
||||||
(ulong, ulong) ServerDirtyMasks(bool initialState)
|
(ulong, ulong) ServerDirtyMasks_Spawn()
|
||||||
{
|
{
|
||||||
ulong ownerMask = 0;
|
ulong ownerMask = 0;
|
||||||
ulong observerMask = 0;
|
ulong observerMask = 0;
|
||||||
@ -848,38 +874,140 @@ internal void OnStopLocalPlayer()
|
|||||||
NetworkBehaviour component = components[i];
|
NetworkBehaviour component = components[i];
|
||||||
ulong nthBit = (1u << i);
|
ulong nthBit = (1u << i);
|
||||||
|
|
||||||
bool dirty = component.IsDirty();
|
|
||||||
|
|
||||||
// owner needs to be considered for both SyncModes, because
|
// owner needs to be considered for both SyncModes, because
|
||||||
// Observers mode always includes the Owner.
|
// Observers mode always includes the Owner.
|
||||||
//
|
//
|
||||||
// for initial, it should always sync owner.
|
// for spawn message, it should always sync owner.
|
||||||
// for delta, only for ServerToClient and only if dirty.
|
ownerMask |= nthBit;
|
||||||
// ClientToServer comes from the owner client.
|
|
||||||
if (initialState || (component.syncDirection == SyncDirection.ServerToClient && dirty))
|
|
||||||
ownerMask |= nthBit;
|
|
||||||
|
|
||||||
// observers need to be considered only in Observers mode,
|
// observers need to be considered only in Observers mode,
|
||||||
// otherwise they receive no sync data of this component ever.
|
// otherwise they receive no sync data of this component ever.
|
||||||
if (component.syncMode == SyncMode.Observers)
|
if (component.syncMode == SyncMode.Observers)
|
||||||
{
|
{
|
||||||
// for initial, it should always sync to observers.
|
// for spawn message, it should always sync to observers.
|
||||||
// for delta, only if dirty.
|
|
||||||
// SyncDirection is irrelevant, as both are broadcast to
|
// SyncDirection is irrelevant, as both are broadcast to
|
||||||
// observers which aren't the owner.
|
// observers which aren't the owner.
|
||||||
if (initialState || dirty)
|
observerMask |= nthBit;
|
||||||
observerMask |= nthBit;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (ownerMask, observerMask);
|
return (ownerMask, observerMask);
|
||||||
}
|
}
|
||||||
|
|
||||||
// build dirty mask for client.
|
// build dirty mask for server owner & observers (= all dirty components).
|
||||||
// server always knows initialState, so we don't need it here.
|
// faster to do it in one iteration instead of iterating separately.
|
||||||
ulong ClientDirtyMask()
|
// -> 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;
|
NetworkBehaviour[] components = NetworkBehaviours;
|
||||||
for (int i = 0; i < components.Length; ++i)
|
for (int i = 0; i < components.Length; ++i)
|
||||||
@ -897,13 +1025,29 @@ ulong ClientDirtyMask()
|
|||||||
|
|
||||||
if (isOwned && component.syncDirection == SyncDirection.ClientToServer)
|
if (isOwned && component.syncDirection == SyncDirection.ClientToServer)
|
||||||
{
|
{
|
||||||
// set the n-th bit if dirty
|
// RELIABLE COMPONENTS /////////////////////////////////////////
|
||||||
// shifting from small to large numbers is varint-efficient.
|
if (component.syncMethod == SyncMethod.Reliable)
|
||||||
if (component.IsDirty()) mask |= nthBit;
|
{
|
||||||
|
// 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.
|
// check if n-th component is dirty.
|
||||||
@ -915,9 +1059,9 @@ internal static bool IsDirty(ulong mask, int index)
|
|||||||
return (mask & nthBit) != 0;
|
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
|
// 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
|
// ensure NetworkBehaviours are valid before usage
|
||||||
ValidateComponents();
|
ValidateComponents();
|
||||||
@ -930,7 +1074,7 @@ internal void SerializeServer(bool initialState, NetworkWriter ownerWriter, Netw
|
|||||||
// instead of writing a 1 byte index per component,
|
// instead of writing a 1 byte index per component,
|
||||||
// we limit components to 64 bits and write one ulong instead.
|
// we limit components to 64 bits and write one ulong instead.
|
||||||
// the ulong is also varint compressed for minimum bandwidth.
|
// the ulong is also varint compressed for minimum bandwidth.
|
||||||
(ulong ownerMask, ulong observerMask) = ServerDirtyMasks(initialState);
|
(ulong ownerMask, ulong observerMask) = ServerDirtyMasks_Spawn();
|
||||||
|
|
||||||
// if nothing dirty, then don't even write the mask.
|
// if nothing dirty, then don't even write the mask.
|
||||||
// otherwise, every unchanged object would send a 1 byte dirty mask!
|
// otherwise, every unchanged object would send a 1 byte dirty mask!
|
||||||
@ -964,7 +1108,7 @@ internal void SerializeServer(bool initialState, NetworkWriter ownerWriter, Netw
|
|||||||
// serialize into helper writer
|
// serialize into helper writer
|
||||||
using (NetworkWriterPooled temp = NetworkWriterPool.Get())
|
using (NetworkWriterPooled temp = NetworkWriterPool.Get())
|
||||||
{
|
{
|
||||||
comp.Serialize(temp, initialState);
|
comp.Serialize(temp, true);
|
||||||
ArraySegment<byte> segment = temp.ToArraySegment();
|
ArraySegment<byte> segment = temp.ToArraySegment();
|
||||||
|
|
||||||
// copy to owner / observers as needed
|
// copy to owner / observers as needed
|
||||||
@ -972,25 +1116,146 @@ internal void SerializeServer(bool initialState, NetworkWriter ownerWriter, Netw
|
|||||||
if (observersDirty) observersWriter.WriteBytes(segment.Array, segment.Offset, segment.Count);
|
if (observersDirty) observersWriter.WriteBytes(segment.Array, segment.Offset, segment.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear dirty bits for the components that we serialized.
|
// dirty bits indicate 'changed since last delta sync'.
|
||||||
// do not clear for _all_ components, only the ones that
|
// don't clear then on full sync here, since full sync
|
||||||
// were dirty and had their syncInterval elapsed.
|
// is called whenever a new player spawns and needs the
|
||||||
//
|
// full state!
|
||||||
// we don't want to clear bits before the syncInterval
|
//comp.ClearAllDirtyBits();
|
||||||
// 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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// serialize components into writer on the client.
|
// serialize server components, with delta state for broadcast messages.
|
||||||
internal void SerializeClient(NetworkWriter writer)
|
// 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
|
// ensure NetworkBehaviours are valid before usage
|
||||||
ValidateComponents();
|
ValidateComponents();
|
||||||
@ -1003,7 +1268,7 @@ internal void SerializeClient(NetworkWriter writer)
|
|||||||
// instead of writing a 1 byte index per component,
|
// instead of writing a 1 byte index per component,
|
||||||
// we limit components to 64 bits and write one ulong instead.
|
// we limit components to 64 bits and write one ulong instead.
|
||||||
// the ulong is also varint compressed for minimum bandwidth.
|
// the ulong is also varint compressed for minimum bandwidth.
|
||||||
ulong dirtyMask = ClientDirtyMask();
|
ClientDirtyMasks(out ulong dirtyMaskReliable, out ulong dirtyMaskUnreliableBaseline, out ulong dirtyMaskUnreliableDelta);
|
||||||
|
|
||||||
// varint compresses the mask to 1 byte in most cases.
|
// varint compresses the mask to 1 byte in most cases.
|
||||||
// instead of writing an 8 byte ulong.
|
// instead of writing an 8 byte ulong.
|
||||||
@ -1014,25 +1279,28 @@ internal void SerializeClient(NetworkWriter writer)
|
|||||||
|
|
||||||
// if nothing dirty, then don't even write the mask.
|
// if nothing dirty, then don't even write the mask.
|
||||||
// otherwise, every unchanged object would send a 1 byte dirty mask!
|
// otherwise, every unchanged object would send a 1 byte dirty mask!
|
||||||
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
|
// serialize all components
|
||||||
// perf: only iterate if dirty mask has dirty bits.
|
// perf: only iterate if dirty mask has dirty bits.
|
||||||
if (dirtyMask != 0)
|
if (dirtyMaskReliable != 0 || dirtyMaskUnreliableDelta != 0 || dirtyMaskUnreliableBaseline != 0)
|
||||||
{
|
{
|
||||||
// serialize all components
|
// serialize all components
|
||||||
for (int i = 0; i < components.Length; ++i)
|
for (int i = 0; i < components.Length; ++i)
|
||||||
{
|
{
|
||||||
NetworkBehaviour comp = components[i];
|
NetworkBehaviour comp = components[i];
|
||||||
|
|
||||||
|
// RELIABLE SERIALIZATION //////////////////////////////////
|
||||||
// is this component dirty?
|
// is this component dirty?
|
||||||
// reuse the mask instead of calling comp.IsDirty() again here.
|
// reuse the mask instead of calling comp.IsDirty() again here.
|
||||||
if (IsDirty(dirtyMask, i))
|
if (IsDirty(dirtyMaskReliable, i))
|
||||||
// if (isOwned && component.syncDirection == SyncDirection.ClientToServer)
|
// if (isOwned && component.syncDirection == SyncDirection.ClientToServer)
|
||||||
{
|
{
|
||||||
// serialize into writer.
|
// serialize into writer.
|
||||||
// server always knows initialState, we never need to send it
|
// 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.
|
// clear dirty bits for the components that we serialized.
|
||||||
// do not clear for _all_ components, only the ones that
|
// do not clear for _all_ components, only the ones that
|
||||||
@ -1042,13 +1310,39 @@ internal void SerializeClient(NetworkWriter writer)
|
|||||||
// was elapsed, as then they wouldn't be synced.
|
// was elapsed, as then they wouldn't be synced.
|
||||||
comp.ClearAllDirtyBits();
|
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.
|
// deserialize components from the client on the server.
|
||||||
// there's no 'initialState'. server always knows the initial state.
|
// for reliable state sync, server always knows the initial state.
|
||||||
internal bool DeserializeServer(NetworkReader reader)
|
// 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
|
// ensure NetworkBehaviours are valid before usage
|
||||||
ValidateComponents();
|
ValidateComponents();
|
||||||
@ -1072,7 +1366,7 @@ internal bool DeserializeServer(NetworkReader reader)
|
|||||||
// deserialize this component
|
// deserialize this component
|
||||||
// server always knows the initial state (initial=false)
|
// server always knows the initial state (initial=false)
|
||||||
// disconnect if failed, to prevent exploits etc.
|
// 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.
|
// server received state from the owner client.
|
||||||
// set dirty so it's broadcast to other clients too.
|
// set dirty so it's broadcast to other clients too.
|
||||||
@ -1116,7 +1410,10 @@ internal void DeserializeClient(NetworkReader reader, bool initialState)
|
|||||||
// get cached serialization for this tick (or serialize if none yet).
|
// get cached serialization for this tick (or serialize if none yet).
|
||||||
// IMPORTANT: int tick avoids floating point inaccuracy over days/weeks.
|
// IMPORTANT: int tick avoids floating point inaccuracy over days/weeks.
|
||||||
// calls SerializeServer, so this function is to be called on server.
|
// 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.
|
// only rebuild serialization once per tick. reuse otherwise.
|
||||||
// except for tests, where Time.frameCount never increases.
|
// except for tests, where Time.frameCount never increases.
|
||||||
@ -1133,10 +1430,17 @@ internal NetworkIdentitySerialization GetServerSerializationAtTick(int tick)
|
|||||||
// reset
|
// reset
|
||||||
lastSerialization.ResetWriters();
|
lastSerialization.ResetWriters();
|
||||||
|
|
||||||
// serialize
|
// serialize both Reliable and Unreliable components in one iteration.
|
||||||
SerializeServer(false,
|
// doing each in their own iteration would be too costly.
|
||||||
lastSerialization.ownerWriter,
|
SerializeServer_Broadcast(
|
||||||
lastSerialization.observersWriter);
|
lastSerialization.ownerWriterReliable,
|
||||||
|
lastSerialization.observersWriterReliable,
|
||||||
|
lastSerialization.ownerWriterUnreliableBaseline,
|
||||||
|
lastSerialization.observersWriterUnreliableBaseline,
|
||||||
|
lastSerialization.ownerWriterUnreliableDelta,
|
||||||
|
lastSerialization.observersWriterUnreliableDelta,
|
||||||
|
unreliableBaselineElapsed
|
||||||
|
);
|
||||||
|
|
||||||
// set tick
|
// set tick
|
||||||
lastSerialization.tick = tick;
|
lastSerialization.tick = tick;
|
||||||
|
@ -42,6 +42,16 @@ public class NetworkManager : MonoBehaviour
|
|||||||
[FormerlySerializedAs("serverTickRate")]
|
[FormerlySerializedAs("serverTickRate")]
|
||||||
public int sendRate = 60;
|
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
|
// 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>
|
/// <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.")]
|
// [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
|
// virtual so that inheriting classes' OnValidate() can call base.OnValidate() too
|
||||||
public virtual void OnValidate()
|
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
|
// always >= 0
|
||||||
maxConnections = Mathf.Max(maxConnections, 0);
|
maxConnections = Mathf.Max(maxConnections, 0);
|
||||||
|
|
||||||
@ -274,6 +289,8 @@ bool IsServerOnlineSceneChangeNeeded() =>
|
|||||||
void ApplyConfiguration()
|
void ApplyConfiguration()
|
||||||
{
|
{
|
||||||
NetworkServer.tickRate = sendRate;
|
NetworkServer.tickRate = sendRate;
|
||||||
|
NetworkServer.unreliableBaselineRate = unreliableBaselineRate;
|
||||||
|
NetworkServer.unreliableRedundancy = unreliableRedundancy;
|
||||||
NetworkClient.snapshotSettings = snapshotSettings;
|
NetworkClient.snapshotSettings = snapshotSettings;
|
||||||
NetworkClient.connectionQualityInterval = evaluationInterval;
|
NetworkClient.connectionQualityInterval = evaluationInterval;
|
||||||
NetworkClient.connectionQualityMethod = evaluationMethod;
|
NetworkClient.connectionQualityMethod = evaluationMethod;
|
||||||
|
@ -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
|
public static float sendInterval => sendRate < int.MaxValue ? 1f / sendRate : 0; // for 30 Hz, that's 33ms
|
||||||
static double lastSendTime;
|
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>
|
/// <summary>Connection to host mode client (if any)</summary>
|
||||||
public static LocalConnectionToClient localConnection { get; private set; }
|
public static LocalConnectionToClient localConnection { get; private set; }
|
||||||
|
|
||||||
@ -312,6 +323,8 @@ internal static void RegisterMessageHandlers()
|
|||||||
RegisterHandler<NetworkPingMessage>(NetworkTime.OnServerPing, false);
|
RegisterHandler<NetworkPingMessage>(NetworkTime.OnServerPing, false);
|
||||||
RegisterHandler<NetworkPongMessage>(NetworkTime.OnServerPong, false);
|
RegisterHandler<NetworkPongMessage>(NetworkTime.OnServerPong, false);
|
||||||
RegisterHandler<EntityStateMessage>(OnEntityStateMessage, true);
|
RegisterHandler<EntityStateMessage>(OnEntityStateMessage, true);
|
||||||
|
RegisterHandler<EntityStateMessageUnreliableBaseline>(OnEntityStateMessageUnreliableBaseline, true);
|
||||||
|
RegisterHandler<EntityStateMessageUnreliableDelta>(OnEntityStateMessageUnreliableDelta, true);
|
||||||
RegisterHandler<TimeSnapshotMessage>(OnTimeSnapshotMessage, false); // unreliable may arrive before reliable authority went through
|
RegisterHandler<TimeSnapshotMessage>(OnTimeSnapshotMessage, false); // unreliable may arrive before reliable authority went through
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -400,7 +413,10 @@ static void OnEntityStateMessage(NetworkConnectionToClient connection, EntitySta
|
|||||||
{
|
{
|
||||||
// DeserializeServer checks permissions internally.
|
// DeserializeServer checks permissions internally.
|
||||||
// failure to deserialize disconnects to prevent exploits.
|
// 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)
|
if (exceptionsDisconnect)
|
||||||
{
|
{
|
||||||
@ -423,6 +439,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.");
|
// 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.
|
// client sends TimeSnapshotMessage every sendInterval.
|
||||||
// batching already includes the remoteTimestamp.
|
// batching already includes the remoteTimestamp.
|
||||||
// we simply insert it on-message here.
|
// we simply insert it on-message here.
|
||||||
@ -1405,7 +1552,7 @@ static ArraySegment<byte> CreateSpawnMessagePayload(bool isOwner, NetworkIdentit
|
|||||||
|
|
||||||
// serialize all components with initialState = true
|
// serialize all components with initialState = true
|
||||||
// (can be null if has none)
|
// (can be null if has none)
|
||||||
identity.SerializeServer(true, ownerWriter, observersWriter);
|
identity.SerializeServer_Spawn(ownerWriter, observersWriter);
|
||||||
|
|
||||||
// convert to ArraySegment to avoid reader allocations
|
// convert to ArraySegment to avoid reader allocations
|
||||||
// if nothing was written, .ToArraySegment returns an empty segment.
|
// if nothing was written, .ToArraySegment returns an empty segment.
|
||||||
@ -1870,11 +2017,18 @@ public static void RebuildObservers(NetworkIdentity identity, bool initialize)
|
|||||||
|
|
||||||
// broadcasting ////////////////////////////////////////////////////////
|
// broadcasting ////////////////////////////////////////////////////////
|
||||||
// helper function to get the right serialization for a connection
|
// helper function to get the right serialization for a connection
|
||||||
static NetworkWriter SerializeForConnection(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)
|
// get serialization for this entity (cached)
|
||||||
// IMPORTANT: int tick avoids floating point inaccuracy over days/weeks
|
// 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?
|
// is this entity owned by this connection?
|
||||||
bool owned = identity.connectionToClient == connection;
|
bool owned = identity.connectionToClient == connection;
|
||||||
@ -1884,23 +2038,65 @@ static NetworkWriter SerializeForConnection(NetworkIdentity identity, NetworkCon
|
|||||||
if (owned)
|
if (owned)
|
||||||
{
|
{
|
||||||
// was it dirty / did we actually serialize anything?
|
// was it dirty / did we actually serialize anything?
|
||||||
if (serialization.ownerWriter.Position > 0)
|
if (serialization.ownerWriterReliable.Position > 0)
|
||||||
return serialization.ownerWriter;
|
return serialization.ownerWriterReliable;
|
||||||
}
|
}
|
||||||
// observers writer if not owned
|
// observers writer if not owned
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// was it dirty / did we actually serialize anything?
|
// was it dirty / did we actually serialize anything?
|
||||||
if (serialization.observersWriter.Position > 0)
|
if (serialization.observersWriterReliable.Position > 0)
|
||||||
return serialization.observersWriter;
|
return serialization.observersWriterReliable;
|
||||||
}
|
}
|
||||||
|
|
||||||
// nothing was serialized
|
// nothing was serialized
|
||||||
return null;
|
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
|
// 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
|
// for each entity that this connection is seeing
|
||||||
bool hasNull = false;
|
bool hasNull = false;
|
||||||
@ -1912,9 +2108,24 @@ static void BroadcastToConnection(NetworkConnectionToClient connection)
|
|||||||
// NetworkServer.Destroy)
|
// NetworkServer.Destroy)
|
||||||
if (identity != null)
|
if (identity != null)
|
||||||
{
|
{
|
||||||
|
// 'Reliable' sync: send Reliable components over reliable with initial/delta
|
||||||
// get serialization for this entity viewed by this connection
|
// get serialization for this entity viewed by this connection
|
||||||
// (if anything was serialized this time)
|
// (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)
|
if (serialization != null)
|
||||||
{
|
{
|
||||||
EntityStateMessage message = new EntityStateMessage
|
EntityStateMessage message = new EntityStateMessage
|
||||||
@ -1924,6 +2135,51 @@ static void BroadcastToConnection(NetworkConnectionToClient connection)
|
|||||||
};
|
};
|
||||||
connection.Send(message);
|
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
|
// spawned list should have no null entries because we
|
||||||
// always call Remove in OnObjectDestroy everywhere.
|
// always call Remove in OnObjectDestroy everywhere.
|
||||||
@ -1962,7 +2218,8 @@ static bool DisconnectIfInactive(NetworkConnectionToClient connection)
|
|||||||
internal static readonly List<NetworkConnectionToClient> connectionsCopy =
|
internal static readonly List<NetworkConnectionToClient> connectionsCopy =
|
||||||
new List<NetworkConnectionToClient>();
|
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
|
// copy all connections into a helper collection so that
|
||||||
// OnTransportDisconnected can be called while iterating.
|
// OnTransportDisconnected can be called while iterating.
|
||||||
@ -1999,7 +2256,7 @@ static void Broadcast()
|
|||||||
connection.Send(new TimeSnapshotMessage(), Channels.Unreliable);
|
connection.Send(new TimeSnapshotMessage(), Channels.Unreliable);
|
||||||
|
|
||||||
// broadcast world state to this connection
|
// broadcast world state to this connection
|
||||||
BroadcastToConnection(connection);
|
BroadcastToConnection(connection, unreliableBaselineElapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// update connection to flush out batched messages
|
// update connection to flush out batched messages
|
||||||
@ -2053,8 +2310,9 @@ internal static void NetworkLateUpdate()
|
|||||||
// snapshots _but_ not every single tick.
|
// snapshots _but_ not every single tick.
|
||||||
// Unity 2019 doesn't have Time.timeAsDouble yet
|
// Unity 2019 doesn't have Time.timeAsDouble yet
|
||||||
bool sendIntervalElapsed = AccurateInterval.Elapsed(NetworkTime.localTime, sendInterval, ref lastSendTime);
|
bool sendIntervalElapsed = AccurateInterval.Elapsed(NetworkTime.localTime, sendInterval, ref lastSendTime);
|
||||||
|
bool unreliableBaselineElapsed = AccurateInterval.Elapsed(NetworkTime.localTime, unreliableBaselineInterval, ref lastUnreliableBaselineTime);
|
||||||
if (!Application.isPlaying || sendIntervalElapsed)
|
if (!Application.isPlaying || sendIntervalElapsed)
|
||||||
Broadcast();
|
Broadcast(unreliableBaselineElapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// process all outgoing messages after updating the world
|
// process all outgoing messages after updating the world
|
||||||
|
@ -94,6 +94,16 @@ protected void DrawDefaultSyncSettings()
|
|||||||
if (syncDirection.enumValueIndex == (int)SyncDirection.ServerToClient)
|
if (syncDirection.enumValueIndex == (int)SyncDirection.ServerToClient)
|
||||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("syncMode"));
|
EditorGUILayout.PropertyField(serializedObject.FindProperty("syncMode"));
|
||||||
|
|
||||||
|
// sync 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
|
// sync interval
|
||||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("syncInterval"));
|
EditorGUILayout.PropertyField(serializedObject.FindProperty("syncInterval"));
|
||||||
|
|
||||||
|
@ -50,7 +50,7 @@ public void SerializeAndDeserializeAll()
|
|||||||
serverObserversComp.value = 42;
|
serverObserversComp.value = 42;
|
||||||
|
|
||||||
// serialize server object
|
// serialize server object
|
||||||
serverIdentity.SerializeServer(true, ownerWriter, observersWriter);
|
serverIdentity.SerializeServer_Spawn(ownerWriter, observersWriter);
|
||||||
|
|
||||||
// deserialize client object with OWNER payload
|
// deserialize client object with OWNER payload
|
||||||
NetworkReader reader = new NetworkReader(ownerWriter.ToArray());
|
NetworkReader reader = new NetworkReader(ownerWriter.ToArray());
|
||||||
@ -96,7 +96,7 @@ public void SerializationException()
|
|||||||
// serialize server object
|
// serialize server object
|
||||||
// should work even if compExc throws an exception.
|
// should work even if compExc throws an exception.
|
||||||
// error log because of the exception is expected.
|
// error log because of the exception is expected.
|
||||||
serverIdentity.SerializeServer(true, ownerWriter, observersWriter);
|
serverIdentity.SerializeServer_Spawn(ownerWriter, observersWriter);
|
||||||
|
|
||||||
// deserialize client object with OWNER payload
|
// deserialize client object with OWNER payload
|
||||||
// should work even if compExc throws an exception
|
// should work even if compExc throws an exception
|
||||||
@ -187,7 +187,7 @@ public void SerializationMismatch()
|
|||||||
serverComp.value = "42";
|
serverComp.value = "42";
|
||||||
|
|
||||||
// serialize server object
|
// serialize server object
|
||||||
serverIdentity.SerializeServer(true, ownerWriter, observersWriter);
|
serverIdentity.SerializeServer_Spawn(ownerWriter, observersWriter);
|
||||||
|
|
||||||
// deserialize on client
|
// deserialize on client
|
||||||
// ignore warning log because of serialization mismatch
|
// ignore warning log because of serialization mismatch
|
||||||
@ -219,7 +219,7 @@ public void SerializeServer_NotInitial_NotDirty_WritesNothing()
|
|||||||
// serialize server object.
|
// serialize server object.
|
||||||
// 'initial' would write everything.
|
// 'initial' would write everything.
|
||||||
// instead, try 'not initial' with 0 dirty bits
|
// instead, try 'not initial' with 0 dirty bits
|
||||||
serverIdentity.SerializeServer(false, ownerWriter, observersWriter);
|
serverIdentity.SerializeServer_Broadcast(ownerWriter, observersWriter, new NetworkWriter(), new NetworkWriter(), new NetworkWriter(), new NetworkWriter(), false);
|
||||||
Assert.That(ownerWriter.Position, Is.EqualTo(0));
|
Assert.That(ownerWriter.Position, Is.EqualTo(0));
|
||||||
Assert.That(observersWriter.Position, Is.EqualTo(0));
|
Assert.That(observersWriter.Position, Is.EqualTo(0));
|
||||||
}
|
}
|
||||||
@ -243,7 +243,7 @@ public void SerializeClient_NotInitial_NotDirty_WritesNothing()
|
|||||||
// clientComp.value = "42";
|
// clientComp.value = "42";
|
||||||
|
|
||||||
// serialize client object
|
// serialize client object
|
||||||
clientIdentity.SerializeClient(ownerWriter);
|
clientIdentity.SerializeClient(ownerWriter, new NetworkWriter(), new NetworkWriter(), false);
|
||||||
Assert.That(ownerWriter.Position, Is.EqualTo(0));
|
Assert.That(ownerWriter.Position, Is.EqualTo(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -267,7 +267,7 @@ public void SerializeAndDeserialize_ClientToServer_NOT_OWNED()
|
|||||||
comp2.value = "67890";
|
comp2.value = "67890";
|
||||||
|
|
||||||
// serialize all
|
// serialize all
|
||||||
identity.SerializeClient(ownerWriter);
|
identity.SerializeClient(ownerWriter, new NetworkWriter(), new NetworkWriter(), false);
|
||||||
|
|
||||||
// shouldn't sync anything. because even though it's ClientToServer,
|
// shouldn't sync anything. because even though it's ClientToServer,
|
||||||
// we don't own this one so we shouldn't serialize & sync it.
|
// we don't own this one so we shouldn't serialize & sync it.
|
||||||
@ -292,7 +292,7 @@ public void SerializeServer_OwnerMode_ClientToServer()
|
|||||||
comp.SetValue(11); // modify with helper function to avoid #3525
|
comp.SetValue(11); // modify with helper function to avoid #3525
|
||||||
|
|
||||||
// initial: should still write for owner
|
// initial: should still write for owner
|
||||||
identity.SerializeServer(true, ownerWriter, observersWriter);
|
identity.SerializeServer_Spawn(ownerWriter, observersWriter);
|
||||||
Debug.Log("initial ownerWriter: " + ownerWriter);
|
Debug.Log("initial ownerWriter: " + ownerWriter);
|
||||||
Debug.Log("initial observerWriter: " + observersWriter);
|
Debug.Log("initial observerWriter: " + observersWriter);
|
||||||
Assert.That(ownerWriter.Position, Is.GreaterThan(0));
|
Assert.That(ownerWriter.Position, Is.GreaterThan(0));
|
||||||
@ -302,7 +302,7 @@ public void SerializeServer_OwnerMode_ClientToServer()
|
|||||||
comp.SetValue(22); // modify with helper function to avoid #3525
|
comp.SetValue(22); // modify with helper function to avoid #3525
|
||||||
ownerWriter.Position = 0;
|
ownerWriter.Position = 0;
|
||||||
observersWriter.Position = 0;
|
observersWriter.Position = 0;
|
||||||
identity.SerializeServer(false, ownerWriter, observersWriter);
|
identity.SerializeServer_Broadcast(ownerWriter, observersWriter, new NetworkWriter(), new NetworkWriter(), new NetworkWriter(), new NetworkWriter(), false);
|
||||||
Debug.Log("delta ownerWriter: " + ownerWriter);
|
Debug.Log("delta ownerWriter: " + ownerWriter);
|
||||||
Debug.Log("delta observersWriter: " + observersWriter);
|
Debug.Log("delta observersWriter: " + observersWriter);
|
||||||
Assert.That(ownerWriter.Position, Is.EqualTo(0));
|
Assert.That(ownerWriter.Position, Is.EqualTo(0));
|
||||||
@ -330,7 +330,7 @@ public void SerializeServer_ObserversMode_ClientToServer()
|
|||||||
comp.SetValue(11); // modify with helper function to avoid #3525
|
comp.SetValue(11); // modify with helper function to avoid #3525
|
||||||
|
|
||||||
// initial: should write something for owner and observers
|
// initial: should write something for owner and observers
|
||||||
identity.SerializeServer(true, ownerWriter, observersWriter);
|
identity.SerializeServer_Spawn(ownerWriter, observersWriter);
|
||||||
Debug.Log("initial ownerWriter: " + ownerWriter);
|
Debug.Log("initial ownerWriter: " + ownerWriter);
|
||||||
Debug.Log("initial observerWriter: " + observersWriter);
|
Debug.Log("initial observerWriter: " + observersWriter);
|
||||||
Assert.That(ownerWriter.Position, Is.GreaterThan(0));
|
Assert.That(ownerWriter.Position, Is.GreaterThan(0));
|
||||||
@ -340,7 +340,7 @@ public void SerializeServer_ObserversMode_ClientToServer()
|
|||||||
comp.SetValue(22); // modify with helper function to avoid #3525
|
comp.SetValue(22); // modify with helper function to avoid #3525
|
||||||
ownerWriter.Position = 0;
|
ownerWriter.Position = 0;
|
||||||
observersWriter.Position = 0;
|
observersWriter.Position = 0;
|
||||||
identity.SerializeServer(false, ownerWriter, observersWriter);
|
identity.SerializeServer_Broadcast(ownerWriter, observersWriter, new NetworkWriter(), new NetworkWriter(), new NetworkWriter(), new NetworkWriter(), false);
|
||||||
Debug.Log("delta ownerWriter: " + ownerWriter);
|
Debug.Log("delta ownerWriter: " + ownerWriter);
|
||||||
Debug.Log("delta observersWriter: " + observersWriter);
|
Debug.Log("delta observersWriter: " + observersWriter);
|
||||||
Assert.That(ownerWriter.Position, Is.EqualTo(0));
|
Assert.That(ownerWriter.Position, Is.EqualTo(0));
|
||||||
|
@ -404,7 +404,7 @@ public void TestSyncingAbstractNetworkBehaviour()
|
|||||||
NetworkWriter ownerWriter = new NetworkWriter();
|
NetworkWriter ownerWriter = new NetworkWriter();
|
||||||
// not really used in this Test
|
// not really used in this Test
|
||||||
NetworkWriter observersWriter = new NetworkWriter();
|
NetworkWriter observersWriter = new NetworkWriter();
|
||||||
serverIdentity.SerializeServer(true, ownerWriter, observersWriter);
|
serverIdentity.SerializeServer_Spawn(ownerWriter, observersWriter);
|
||||||
|
|
||||||
// set up a "client" object
|
// set up a "client" object
|
||||||
CreateNetworked(out _, out NetworkIdentity clientIdentity, out SyncVarAbstractNetworkBehaviour clientBehaviour);
|
CreateNetworked(out _, out NetworkIdentity clientIdentity, out SyncVarAbstractNetworkBehaviour clientBehaviour);
|
||||||
|
@ -66,10 +66,10 @@ public IEnumerator TestSerializationWithLargeTimestamps()
|
|||||||
// 14 * 24 hours per day * 60 minutes per hour * 60 seconds per minute = 14 days
|
// 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
|
// NOTE: change this to 'float' to see the tests fail
|
||||||
int tick = 14 * 24 * 60 * 60;
|
int tick = 14 * 24 * 60 * 60;
|
||||||
NetworkIdentitySerialization serialization = identity.GetServerSerializationAtTick(tick);
|
NetworkIdentitySerialization serialization = identity.GetServerSerializationAtTick(tick, false);
|
||||||
// advance tick
|
// advance tick
|
||||||
++tick;
|
++tick;
|
||||||
NetworkIdentitySerialization serializationNew = identity.GetServerSerializationAtTick(tick);
|
NetworkIdentitySerialization serializationNew = identity.GetServerSerializationAtTick(tick, false);
|
||||||
|
|
||||||
// if the serialization has been changed the tickTimeStamp should have moved
|
// if the serialization has been changed the tickTimeStamp should have moved
|
||||||
Assert.That(serialization.tick == serializationNew.tick, Is.False);
|
Assert.That(serialization.tick == serializationNew.tick, Is.False);
|
||||||
|
Loading…
Reference in New Issue
Block a user