feature: SyncDirection to easily support client auth components without extra Rpcs/Cmds. previously OnSerialize was only server-to-client direction. (#3232)

* feature: SyncDirection

* broadcast without global sendInterval

* comments

* TODO

* disconnect moved up
This commit is contained in:
mischa 2022-10-18 11:10:22 +02:00 committed by GitHub
parent 25a45a9ce8
commit f64fcb8142
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 412 additions and 42 deletions

View File

@ -26,7 +26,7 @@ namespace Mirror
{ {
public abstract class NetworkTransformBase : NetworkBehaviour public abstract class NetworkTransformBase : NetworkBehaviour
{ {
// TODO SyncDirection { CLIENT_TO_SERVER, SERVER_TO_CLIENT } is easier? // TODO SyncDirection { ClientToServer, ServerToClient } is easier?
[Header("Authority")] [Header("Authority")]
[Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")] [Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")]
public bool clientAuthority; public bool clientAuthority;

View File

@ -9,12 +9,25 @@ namespace Mirror
// 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 }
// SyncDirection decides if a component is synced from:
// * server to all clients
// * owner client, to server, to all other clients
//
// naming: 'ClientToServer' etc. instead of 'ClientAuthority', because
// that wouldn't be accurate. server's OnDeserialize can still validate
// client data before applying. it's really about direction, not authority.
public enum SyncDirection { ServerToClient, ClientToServer }
/// <summary>Base class for networked components.</summary> /// <summary>Base class for networked components.</summary>
[AddComponentMenu("")] [AddComponentMenu("")]
[RequireComponent(typeof(NetworkIdentity))] [RequireComponent(typeof(NetworkIdentity))]
[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
{ {
/// <summary>Sync direction for OnSerialize. ServerToClient by default. ClientToServer for client authority.</summary>
[Tooltip("Server Authority calls OnSerialize on the server and syncs it to clients.\n\nClient Authority calls OnSerialize on the owning client, syncs it to server, which then broadcasts it to all other clients.\n\nUse server authority for cheat safety.")]
[HideInInspector] public SyncDirection syncDirection = SyncDirection.ServerToClient;
/// <summary>sync mode for OnSerialize</summary> /// <summary>sync mode for OnSerialize</summary>
// hidden because NetworkBehaviourInspector shows it only if has OnSerialize. // hidden because NetworkBehaviourInspector shows it only if has OnSerialize.
[Tooltip("By default synced data is sent from the server to all Observers of the object.\nChange this to Owner to only have the server update the client that has ownership authority for this object")] [Tooltip("By default synced data is sent from the server to all Observers of the object.\nChange this to Owner to only have the server update the client that has ownership authority for this object")]
@ -53,6 +66,24 @@ public abstract class NetworkBehaviour : MonoBehaviour
[Obsolete(".hasAuthority was renamed to .isOwned. This is easier to understand and prepares for SyncDirection, where there is a difference betwen isOwned and authority.")] // 2022-10-13 [Obsolete(".hasAuthority was renamed to .isOwned. This is easier to understand and prepares for SyncDirection, where there is a difference betwen isOwned and authority.")] // 2022-10-13
public bool hasAuthority => isOwned; public bool hasAuthority => isOwned;
/// <summary>authority is true if we are allowed to modify this component's state. On server, it's true if SyncDirection is ServerToClient. On client, it's true if SyncDirection is ClientToServer and(!) if this object is owned by the client.</summary>
// on the client: if owned and if clientAuthority sync direction
// on the server: if serverAuthority sync direction
//
// for example, NetworkTransform:
// client may modify position if ClientAuthority mode and owned
// server may modify position only if server authority
//
// note that in original Mirror, hasAuthority only meant 'isOwned'.
// there was no syncDirection to check.
//
// also note that this is a per-NetworkBehaviour flag.
// another component may not be client authoritative, etc.
public bool authority =>
isClient
? syncDirection == SyncDirection.ClientToServer && isOwned
: syncDirection == SyncDirection.ServerToClient;
/// <summary>The unique network Id of this object (unique at runtime).</summary> /// <summary>The unique network Id of this object (unique at runtime).</summary>
public uint netId => netIdentity.netId; public uint netId => netIdentity.netId;
@ -1118,8 +1149,13 @@ internal static int ErrorCorrection(int size, byte safety)
return (int)(cleared | safety); return (int)(cleared | safety);
} }
internal void Deserialize(NetworkReader reader, bool initialState) // returns false in case of errors.
// server needs to know in order to disconnect on error.
internal bool Deserialize(NetworkReader reader, bool initialState)
{ {
// detect errors, but attempt to correct before returning
bool result = true;
// read 1 byte length hash safety & capture beginning for size check // read 1 byte length hash safety & capture beginning for size check
byte safety = reader.ReadByte(); byte safety = reader.ReadByte();
int chunkStart = reader.Position; int chunkStart = reader.Position;
@ -1140,6 +1176,7 @@ internal void Deserialize(NetworkReader reader, bool initialState)
$" * Are the server and client the exact same project?\n" + $" * Are the server and client the exact same project?\n" +
$" * Maybe this OnDeserialize call was meant for another GameObject? The sceneIds can easily get out of sync if the Hierarchy was modified only in the client OR the server. Try rebuilding both.\n\n" + $" * Maybe this OnDeserialize call was meant for another GameObject? The sceneIds can easily get out of sync if the Hierarchy was modified only in the client OR the server. Try rebuilding both.\n\n" +
$"Exception {e}"); $"Exception {e}");
result = false;
} }
// compare bytes read with length hash // compare bytes read with length hash
@ -1157,7 +1194,10 @@ internal void Deserialize(NetworkReader reader, bool initialState)
// see test: SerializationSizeMismatch. // see test: SerializationSizeMismatch.
int correctedSize = ErrorCorrection(size, safety); int correctedSize = ErrorCorrection(size, safety);
reader.Position = chunkStart + correctedSize; reader.Position = chunkStart + correctedSize;
result = false;
} }
return result;
} }
internal void ResetSyncObjects() internal void ResetSyncObjects()

View File

@ -1031,7 +1031,7 @@ internal static void ApplySpawnPayload(NetworkIdentity identity, SpawnMessage me
{ {
using (NetworkReaderPooled payloadReader = NetworkReaderPool.Get(message.payload)) using (NetworkReaderPooled payloadReader = NetworkReaderPool.Get(message.payload))
{ {
identity.Deserialize(payloadReader, true); identity.DeserializeClient(payloadReader, true);
} }
} }
@ -1284,7 +1284,7 @@ static void OnEntityStateMessage(EntityStateMessage message)
if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null) if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null)
{ {
using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload)) using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload))
identity.Deserialize(reader, false); identity.DeserializeClient(reader, false);
} }
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.");
} }
@ -1408,6 +1408,54 @@ static void DestroyObject(uint netId)
//else Debug.LogWarning($"Did not find target for destroy message for {netId}"); //else Debug.LogWarning($"Did not find target for destroy message for {netId}");
} }
// broadcast ///////////////////////////////////////////////////////////
// make sure Broadcast() is only called every sendInterval.
// calling it every update() would require too much bandwidth.
static void Broadcast()
{
// joined the world yet?
if (!connection.isReady) return;
// nothing to do in host mode. server already knows the state.
if (NetworkServer.active) return;
// for each entity that the client owns
foreach (NetworkIdentity identity in connection.owned)
{
// make sure it's not null or destroyed.
// (which can happen if someone uses
// GameObject.Destroy instead of
// NetworkServer.Destroy)
if (identity != null)
{
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
{
// get serialization for this entity viewed by this connection
// (if anything was serialized this time)
identity.SerializeClient(writer);
if (writer.Position > 0)
{
// send state update message
EntityStateMessage message = new EntityStateMessage
{
netId = identity.netId,
payload = writer.ToArraySegment()
};
Send(message);
// reset dirty bits so it's not resent next time.
identity.ClearDirtyComponentsDirtyBits();
}
}
}
// spawned list should have no null entries because we
// always call Remove in OnObjectDestroy everywhere.
// if it does have null then someone used
// GameObject.Destroy instead of NetworkServer.Destroy.
else Debug.LogWarning($"Found 'null' entry in observing list for connectionId={connection.connectionId}. Please call NetworkServer.Destroy to destroy networked objects. Don't use GameObject.Destroy.");
}
}
// update ////////////////////////////////////////////////////////////// // update //////////////////////////////////////////////////////////////
// NetworkEarlyUpdate called before any Update/FixedUpdate // NetworkEarlyUpdate called before any Update/FixedUpdate
// (we add this to the UnityEngine in NetworkLoop) // (we add this to the UnityEngine in NetworkLoop)
@ -1425,6 +1473,19 @@ internal static void NetworkEarlyUpdate()
// (we add this to the UnityEngine in NetworkLoop) // (we add this to the UnityEngine in NetworkLoop)
internal static void NetworkLateUpdate() internal static void NetworkLateUpdate()
{ {
// broadcast ClientToServer components while active
// note that Broadcast() runs every update.
// on clients with 120 Hz, this will run 120 times per second.
// however, Broadcast only checks .owned, which usually aren't many.
//
// we could use a .sendInterval, but it would also put a minimum
// limit to every component's sendInterval automatically.
if (active)
{
Broadcast();
}
// update connections to flush out messages _after_ broadcast
// local connection? // local connection?
if (connection is LocalConnectionToServer localConnection) if (connection is LocalConnectionToServer localConnection)
{ {

View File

@ -884,9 +884,9 @@ internal void OnStopAuthority()
} }
} }
// build dirty mask for owner & observer (= 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) DirtyMasks(bool initialState) (ulong, ulong) ServerDirtyMasks(bool initialState)
{ {
ulong ownerMask = 0; ulong ownerMask = 0;
ulong observerMask = 0; ulong observerMask = 0;
@ -896,21 +896,50 @@ internal void OnStopAuthority()
{ {
NetworkBehaviour component = components[i]; NetworkBehaviour component = components[i];
// check if dirty. // initially consider all. afterwards only ServerToClient comps.
// for owner, it's always included if dirty. // see explanation in SerializeServer().
// for observers, it's only included if dirty AND syncmode to observers. // see test: Serialize_ClientToServer_ServerOnlySendsInitial().
bool ownerDirty = initialState || component.IsDirty(); if (initialState || component.syncDirection == SyncDirection.ServerToClient)
bool observerDirty = ownerDirty && component.syncMode == SyncMode.Observers; {
// check if dirty.
// for owner, it's always included if dirty.
// for observers, it's only included if dirty AND syncmode to observers.
bool ownerDirty = (initialState || component.IsDirty());
bool observerDirty = ownerDirty && component.syncMode == SyncMode.Observers;
// set the n-th bit. // set the n-th bit if dirty.
// shifting from small to large numbers is varint-efficient. // shifting from small to large numbers is varint-efficient.
ownerMask |= (ulong)(ownerDirty ? 1 : 0) << i; if (ownerDirty) ownerMask |= (1u << i);
observerMask |= (ulong)(observerDirty ? 1 : 0) << i; if (observerDirty) observerMask |= (1u << i);
}
} }
return (ownerMask, observerMask); return (ownerMask, observerMask);
} }
// build dirty mask for client.
// server always knows initialState, so we don't need it here.
ulong ClientDirtyMask()
{
ulong mask = 0;
NetworkBehaviour[] components = NetworkBehaviours;
for (int i = 0; i < components.Length; ++i)
{
NetworkBehaviour component = components[i];
// on client, only consider owned components with SyncDirection to server
if (isOwned && component.syncDirection == SyncDirection.ClientToServer)
{
// set the n-th bit if dirty
// shifting from small to large numbers is varint-efficient.
if (component.IsDirty()) mask |= (1u << i);
}
}
return mask;
}
// check if n-th component is dirty. // check if n-th component is dirty.
// in other words, if it has the n-th bit set in the dirty mask. // in other words, if it has the n-th bit set in the dirty mask.
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -920,11 +949,11 @@ internal static bool IsDirty(ulong mask, int index)
return (mask & nthBit) != 0; return (mask & nthBit) != 0;
} }
// serialize all components using dirtyComponentsMask // serialize components into writer on the server.
// check ownerWritten/observersWritten to know if anything was written // check ownerWritten/observersWritten to know if anything was written
// We pass dirtyComponentsMask into this function so that we can check // We pass dirtyComponentsMask into this function so that we can check
// if any Components are dirty before creating writers // if any Components are dirty before creating writers
internal void Serialize(bool initialState, NetworkWriter ownerWriter, NetworkWriter observersWriter) internal void SerializeServer(bool initialState, NetworkWriter ownerWriter, NetworkWriter observersWriter)
{ {
// ensure NetworkBehaviours are valid before usage // ensure NetworkBehaviours are valid before usage
ValidateComponents(); ValidateComponents();
@ -933,7 +962,7 @@ internal void Serialize(bool initialState, NetworkWriter ownerWriter, NetworkWri
// 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) = DirtyMasks(initialState); (ulong ownerMask, ulong observerMask) = ServerDirtyMasks(initialState);
// 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.
@ -953,6 +982,24 @@ internal void Serialize(bool initialState, NetworkWriter ownerWriter, NetworkWri
{ {
for (int i = 0; i < components.Length; ++i) for (int i = 0; i < components.Length; ++i)
{ {
// on the server, we need to consider different sync scenarios:
//
// ServerToClient SyncDirection:
// always serialize for owner.
// serialize for observers only if SyncMode == Observers.
//
// ClientToServer SyncDirection:
// only serialize 'initial' for spawn data.
// skip if not initial, as it comes from the client.
// for example, the server sets the initial spawn position.
// if we wouldn't sync to the owning client too, then it
// would not have any spawn data and always spawn at (0,0,0).
// serialize for observers only if SyncMode == Observers.
//
// since we always send to authority owners too, we end up with
// the same behaviour for both authority modes.
// this makes the code a lot easier & better for branch prediction.
NetworkBehaviour comp = components[i]; NetworkBehaviour comp = components[i];
// is this component dirty? // is this component dirty?
@ -990,7 +1037,95 @@ internal void Serialize(bool initialState, NetworkWriter ownerWriter, NetworkWri
} }
} }
internal void Deserialize(NetworkReader reader, bool initialState) // serialize components into writer on the client.
internal void SerializeClient(NetworkWriter writer)
{
// ensure NetworkBehaviours are valid before usage
ValidateComponents();
NetworkBehaviour[] components = NetworkBehaviours;
// instead of writing a 1 byte index per component,
// we limit components to 64 bits and write one ulong instead.
// the ulong is also varint compressed for minimum bandwidth.
ulong dirtyMask = ClientDirtyMask();
// varint compresses the mask to 1 byte in most cases.
// instead of writing an 8 byte ulong.
// 7 components fit into 1 byte. (previously 7 bytes)
// 11 components fit into 2 bytes. (previously 11 bytes)
// 16 components fit into 3 bytes. (previously 16 bytes)
// TODO imer: server knows amount of comps, write N bytes instead
// if nothing dirty, then don't even write the mask.
// otherwise, every unchanged object would send a 1 byte dirty mask!
if (dirtyMask != 0) Compression.CompressVarUInt(writer, dirtyMask);
// serialize all components
// perf: only iterate if dirty mask has dirty bits.
if (dirtyMask != 0)
{
// serialize all components
for (int i = 0; i < components.Length; ++i)
{
// on the client, we need to consider different sync scenarios:
//
// ServerToClient SyncDirection:
// do nothing.
// ClientToServer SyncDirection:
// serialize only if owned.
NetworkBehaviour comp = components[i];
// is this component dirty?
// reuse the mask instead of calling comp.IsDirty() again here.
if (IsDirty(dirtyMask, i))
// if (isOwned && component.syncDirection == SyncDirection.ClientToServer)
{
// serialize into writer.
// server always knows initialState, we never need to send it
comp.Serialize(writer, false);
}
}
}
}
// deserialize components from the client on the server.
// there's no 'initialState'. server always knows the initial state.
internal bool DeserializeServer(NetworkReader reader)
{
// ensure NetworkBehaviours are valid before usage
ValidateComponents();
NetworkBehaviour[] components = NetworkBehaviours;
// first we deserialize the varinted dirty mask
ulong mask = Compression.DecompressVarUInt(reader);
// now deserialize every dirty component
for (int i = 0; i < components.Length; ++i)
{
// was this one dirty?
if (IsDirty(mask, i))
{
NetworkBehaviour comp = components[i];
// safety check to ensure clients can only modify their own
// ClientToServer components, nothing else.
if (comp.syncDirection == SyncDirection.ClientToServer)
{
// deserialize this component
// server always knows the initial state (initial=false)
// disconnect if failed, to prevent exploits etc.
if (!comp.Deserialize(reader, false)) return false;
}
}
}
// successfully deserialized everything
return true;
}
// deserialize components from server on the client.
internal void DeserializeClient(NetworkReader reader, bool initialState)
{ {
// ensure NetworkBehaviours are valid before usage // ensure NetworkBehaviours are valid before usage
ValidateComponents(); ValidateComponents();
@ -1011,9 +1146,10 @@ internal void Deserialize(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.
internal NetworkIdentitySerialization GetSerializationAtTick(int tick) // calls SerializeServer, so this function is to be called on server.
internal NetworkIdentitySerialization GetServerSerializationAtTick(int tick)
{ {
// 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.
@ -1032,9 +1168,9 @@ internal NetworkIdentitySerialization GetSerializationAtTick(int tick)
lastSerialization.observersWriter.Position = 0; lastSerialization.observersWriter.Position = 0;
// serialize // serialize
Serialize(false, SerializeServer(false,
lastSerialization.ownerWriter, lastSerialization.ownerWriter,
lastSerialization.observersWriter); lastSerialization.observersWriter);
// clear dirty bits for the components that we serialized. // clear dirty bits for the components that we serialized.
// previously we did this in NetworkServer.BroadcastToConnection // previously we did this in NetworkServer.BroadcastToConnection

View File

@ -134,6 +134,7 @@ internal static void RegisterMessageHandlers()
RegisterHandler<ReadyMessage>(OnClientReadyMessage); RegisterHandler<ReadyMessage>(OnClientReadyMessage);
RegisterHandler<CommandMessage>(OnCommandMessage); RegisterHandler<CommandMessage>(OnCommandMessage);
RegisterHandler<NetworkPingMessage>(NetworkTime.OnServerPing, false); RegisterHandler<NetworkPingMessage>(NetworkTime.OnServerPing, false);
RegisterHandler<EntityStateMessage>(OnEntityStateMessage, true);
} }
/// <summary>Starts server and listens to incoming connections with max connections limit.</summary> /// <summary>Starts server and listens to incoming connections with max connections limit.</summary>
@ -985,6 +986,41 @@ static void OnCommandMessage(NetworkConnectionToClient conn, CommandMessage msg,
identity.HandleRemoteCall(msg.componentIndex, msg.functionHash, RemoteCallType.Command, networkReader, conn); identity.HandleRemoteCall(msg.componentIndex, msg.functionHash, RemoteCallType.Command, networkReader, conn);
} }
// client to server broadcast //////////////////////////////////////////
// for client's owned ClientToServer components.
static void OnEntityStateMessage(NetworkConnectionToClient connection, EntityStateMessage message)
{
// 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)
{
using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload))
{
// DeserializeServer checks permissions internally.
// failure to deserialize disconnects to prevent exploits.
if (!identity.DeserializeServer(reader))
{
Debug.LogWarning($"Server failed to deserialize client state for {identity.name} with netId={identity.netId}, Disconnecting.");
connection.Disconnect();
}
}
}
// an attacker may attempt to modify another connection's entity
else
{
Debug.LogWarning($"Connection {connection.connectionId} attempted to modify {identity} which is not owned by the connection. Disconnecting the connection.");
connection.Disconnect();
}
}
// no warning. don't spam server logs.
// 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.");
}
// spawning //////////////////////////////////////////////////////////// // spawning ////////////////////////////////////////////////////////////
static ArraySegment<byte> CreateSpawnMessagePayload(bool isOwner, NetworkIdentity identity, NetworkWriterPooled ownerWriter, NetworkWriterPooled observersWriter) static ArraySegment<byte> CreateSpawnMessagePayload(bool isOwner, NetworkIdentity identity, NetworkWriterPooled ownerWriter, NetworkWriterPooled observersWriter)
{ {
@ -996,7 +1032,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.Serialize(true, ownerWriter, observersWriter); identity.SerializeServer(true, 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.
@ -1592,7 +1628,7 @@ static NetworkWriter GetEntitySerializationForConnection(NetworkIdentity identit
{ {
// 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.GetSerializationAtTick(Time.frameCount); NetworkIdentitySerialization serialization = identity.GetServerSerializationAtTick(Time.frameCount);
// is this entity owned by this connection? // is this entity owned by this connection?
bool owned = identity.connectionToClient == connection; bool owned = identity.connectionToClient == connection;

View File

@ -85,7 +85,15 @@ protected void DrawDefaultSyncSettings()
EditorGUILayout.Space(); EditorGUILayout.Space();
EditorGUILayout.LabelField("Sync Settings", EditorStyles.boldLabel); EditorGUILayout.LabelField("Sync Settings", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(serializedObject.FindProperty("syncMode")); // sync direction
SerializedProperty syncDirection = serializedObject.FindProperty("syncDirection");
EditorGUILayout.PropertyField(syncDirection);
// sync mdoe: only show for ServerToClient components
if (syncDirection.enumValueIndex == (int)SyncDirection.ServerToClient)
EditorGUILayout.PropertyField(serializedObject.FindProperty("syncMode"));
// sync interval
EditorGUILayout.PropertyField(serializedObject.FindProperty("syncInterval")); EditorGUILayout.PropertyField(serializedObject.FindProperty("syncInterval"));
// apply // apply

View File

@ -49,11 +49,11 @@ public void SerializeAndDeserializeAll()
serverComp2.value = "42"; serverComp2.value = "42";
// serialize server object // serialize server object
serverIdentity.Serialize(true, ownerWriter, observersWriter); serverIdentity.SerializeServer(true, 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());
clientIdentity.Deserialize(reader, true); clientIdentity.DeserializeClient(reader, true);
Assert.That(clientComp1.value, Is.EqualTo(42)); Assert.That(clientComp1.value, Is.EqualTo(42));
Assert.That(clientComp2.value, Is.EqualTo("42")); Assert.That(clientComp2.value, Is.EqualTo("42"));
@ -63,7 +63,7 @@ public void SerializeAndDeserializeAll()
// deserialize client object with OBSERVERS payload // deserialize client object with OBSERVERS payload
reader = new NetworkReader(observersWriter.ToArray()); reader = new NetworkReader(observersWriter.ToArray());
clientIdentity.Deserialize(reader, true); clientIdentity.DeserializeClient(reader, true);
Assert.That(clientComp1.value, Is.EqualTo(42)); // observers mode should be in data Assert.That(clientComp1.value, Is.EqualTo(42)); // observers mode should be in data
Assert.That(clientComp2.value, Is.EqualTo(null)); // owner mode shouldn't be in data Assert.That(clientComp2.value, Is.EqualTo(null)); // owner mode shouldn't be in data
} }
@ -95,13 +95,13 @@ 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.Serialize(true, ownerWriter, observersWriter); serverIdentity.SerializeServer(true, 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
// error log because of the exception is expected // error log because of the exception is expected
NetworkReader reader = new NetworkReader(ownerWriter.ToArray()); NetworkReader reader = new NetworkReader(ownerWriter.ToArray());
clientIdentity.Deserialize(reader, true); clientIdentity.DeserializeClient(reader, true);
Assert.That(clientComp2.value, Is.EqualTo("42")); Assert.That(clientComp2.value, Is.EqualTo("42"));
// reset component values // reset component values
@ -111,7 +111,7 @@ public void SerializationException()
// 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
reader = new NetworkReader(observersWriter.ToArray()); reader = new NetworkReader(observersWriter.ToArray());
clientIdentity.Deserialize(reader, true); clientIdentity.DeserializeClient(reader, true);
Assert.That(clientComp2.value, Is.EqualTo(null)); // owner mode should be in data Assert.That(clientComp2.value, Is.EqualTo(null)); // owner mode should be in data
// restore error checks // restore error checks
@ -186,13 +186,13 @@ public void SerializationMismatch()
serverComp.value = "42"; serverComp.value = "42";
// serialize server object // serialize server object
serverIdentity.Serialize(true, ownerWriter, observersWriter); serverIdentity.SerializeServer(true, ownerWriter, observersWriter);
// deserialize on client // deserialize on client
// ignore warning log because of serialization mismatch // ignore warning log because of serialization mismatch
LogAssert.ignoreFailingMessages = true; LogAssert.ignoreFailingMessages = true;
NetworkReader reader = new NetworkReader(ownerWriter.ToArray()); NetworkReader reader = new NetworkReader(ownerWriter.ToArray());
clientIdentity.Deserialize(reader, true); clientIdentity.DeserializeClient(reader, true);
LogAssert.ignoreFailingMessages = false; LogAssert.ignoreFailingMessages = false;
// the mismatch component will fail, but the one before and after // the mismatch component will fail, but the one before and after
@ -205,7 +205,7 @@ public void SerializationMismatch()
// 0-dirty-mask. instead, we need to ensure it writes nothing. // 0-dirty-mask. instead, we need to ensure it writes nothing.
// too easy to miss, with too significant bandwidth implications. // too easy to miss, with too significant bandwidth implications.
[Test] [Test]
public void Serialize_NotInitial_NotDirty_WritesNothing() public void SerializeServer_NotInitial_NotDirty_WritesNothing()
{ {
// create spawned so that isServer/isClient is set properly // create spawned so that isServer/isClient is set properly
CreateNetworkedAndSpawn( CreateNetworkedAndSpawn(
@ -218,9 +218,88 @@ public void Serialize_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.Serialize(false, ownerWriter, observersWriter); serverIdentity.SerializeServer(false, ownerWriter, observersWriter);
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));
} }
[Test]
public void SerializeClient_NotInitial_NotDirty_WritesNothing()
{
// create spawned so that isServer/isClient is set properly
CreateNetworkedAndSpawn(
out _, out NetworkIdentity serverIdentity, out SerializeTest1NetworkBehaviour serverComp1, out SerializeTest2NetworkBehaviour serverComp2,
out _, out NetworkIdentity clientIdentity, out SerializeTest1NetworkBehaviour clientComp1, out SerializeTest2NetworkBehaviour clientComp2);
// change nothing
// clientComp.value = "42";
// serialize client object
serverIdentity.SerializeClient(ownerWriter);
Assert.That(ownerWriter.Position, Is.EqualTo(0));
}
// serialize -> deserialize. multiple components to be sure.
// one for Owner, one for Observer
// one ServerToClient, one ClientToServer
[Test]
public void SerializeAndDeserialize_ClientToServer_NOT_OWNED()
{
CreateNetworked(out GameObject _, out NetworkIdentity identity,
out SerializeTest1NetworkBehaviour comp1,
out SerializeTest2NetworkBehaviour comp2);
// set to CLIENT with some unique values
// and set connection to server to pretend we are the owner.
identity.isOwned = false;
identity.connectionToServer = null; // NOT OWNED
comp1.syncDirection = SyncDirection.ServerToClient;
comp1.value = 12345;
comp2.syncDirection = SyncDirection.ClientToServer;
comp2.value = "67890";
// serialize all
identity.SerializeClient(ownerWriter);
// shouldn't sync anything. because even though it's ClientToServer,
// we don't own this one so we shouldn't serialize & sync it.
Assert.That(ownerWriter.Position, Is.EqualTo(0));
}
// even for ClientToServer components, server still sends initial state.
// however, after initial it should never send anything anymore.
// otherwise for components like NetworkTransform, it would waste bandwidth.
[Test]
public void Serialize_ClientToServer_ServerOnlySendsInitial()
{
CreateNetworked(out GameObject _, out NetworkIdentity identity,
out SyncVarTest1NetworkBehaviour comp);
// pretend to be owned
identity.isOwned = true;
comp.syncInterval = 0;
// set to CLIENT with some unique values
// and set connection to server to pretend we are the owner.
comp.syncDirection = SyncDirection.ClientToServer;
comp.value = 12345;
// serialize server with 'initial'. should write something
identity.SerializeServer(true, ownerWriter, observersWriter);
Assert.That(ownerWriter.Position, Is.GreaterThan(0));
Assert.That(observersWriter.Position, Is.GreaterThan(0));
Debug.Log("ownerWriter: " + ownerWriter.ToArray());
Debug.Log("observerWriter: " + ownerWriter.ToArray());
// serialize after 'initial' shouldn't write anything.
++comp.value; // change something
ownerWriter.Position = 0;
observersWriter.Position = 0;
identity.SerializeServer(false, ownerWriter, observersWriter);
Assert.That(ownerWriter.Position, Is.EqualTo(0));
Assert.That(observersWriter.Position, Is.EqualTo(0));
Debug.Log("ownerWriter: " + ownerWriter.ToArray());
Debug.Log("observerWriter: " + ownerWriter.ToArray());
}
} }
} }

View File

@ -153,6 +153,16 @@ public override void OnDeserialize(NetworkReader reader, bool initialState)
} }
} }
class SyncVarTest1NetworkBehaviour : NetworkBehaviour
{
[SyncVar] public int value;
}
class SyncVarTest2NetworkBehaviour : NetworkBehaviour
{
[SyncVar] public string value;
}
class SerializeExceptionNetworkBehaviour : NetworkBehaviour class SerializeExceptionNetworkBehaviour : NetworkBehaviour
{ {
public override void OnSerialize(NetworkWriter writer, bool initialState) public override void OnSerialize(NetworkWriter writer, bool initialState)

View File

@ -407,14 +407,14 @@ 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.Serialize(true, ownerWriter, observersWriter); serverIdentity.SerializeServer(true, 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);
// apply all the data from the server object // apply all the data from the server object
NetworkReader reader = new NetworkReader(ownerWriter.ToArray()); NetworkReader reader = new NetworkReader(ownerWriter.ToArray());
clientIdentity.Deserialize(reader, true); clientIdentity.DeserializeClient(reader, true);
// check that the syncvars got updated // check that the syncvars got updated
Assert.That(clientBehaviour.monster1, Is.EqualTo(serverBehaviour.monster1), "Data should be synchronized"); Assert.That(clientBehaviour.monster1, Is.EqualTo(serverBehaviour.monster1), "Data should be synchronized");

View File

@ -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.GetSerializationAtTick(tick); NetworkIdentitySerialization serialization = identity.GetServerSerializationAtTick(tick);
// advance tick // advance tick
++tick; ++tick;
NetworkIdentitySerialization serializationNew = identity.GetSerializationAtTick(tick); NetworkIdentitySerialization serializationNew = identity.GetServerSerializationAtTick(tick);
// 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);