feat: component based SyncToOwner, fixes #39 (#1023)

* adjust comments

* NetworkBehaviour.syncMode

* show in Editor

* feat: component based SyncToOwner, fixes #39

* rename to Observers

* rename writers and comments too

* SendToReady old signature version

* shorter syntax

* fix segment write length

* fix NullReferenceException for local player

* update comment

* support runtime syncMode changes

* add a test

* remove comments

* UL suffix to avoid cast

* UL suffix

* remove empty line

* OnSerializeAllSafely returns how many components were written. MirrorUpdate only sends to owner/observer if anything written.

* fix tests
This commit is contained in:
vis2k 2019-08-26 15:21:59 +02:00 committed by GitHub
parent c877b5939a
commit c6d86b301b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 205 additions and 39 deletions

View File

@ -170,18 +170,25 @@ public override void OnInspectorGUI()
} }
} }
// only show SyncInterval if we have an OnSerialize function. // does it sync anything? then show extra properties
// No need to show it if the class only has Cmds/Rpcs and no sync. // (no need to show it if the class only has Cmds/Rpcs and no sync)
if (syncsAnything) if (syncsAnything)
{ {
NetworkBehaviour networkBehaviour = target as NetworkBehaviour; NetworkBehaviour networkBehaviour = target as NetworkBehaviour;
if (networkBehaviour != null) if (networkBehaviour != null)
{ {
// syncMode
serializedObject.FindProperty("syncMode").enumValueIndex = (int)(SyncMode)
EditorGUILayout.EnumPopup("Network Sync Mode", networkBehaviour.syncMode);
// syncInterval
// [0,2] should be enough. anything >2s is too laggy anyway. // [0,2] should be enough. anything >2s is too laggy anyway.
serializedObject.FindProperty("syncInterval").floatValue = EditorGUILayout.Slider( serializedObject.FindProperty("syncInterval").floatValue = EditorGUILayout.Slider(
new GUIContent("Network Sync Interval", new GUIContent("Network Sync Interval",
"Time in seconds until next change is synchronized to the client. '0' means send immediately if changed. '0.5' means only send changes every 500ms.\n(This is for state synchronization like SyncVars, SyncLists, OnSerialize. Not for Cmds, Rpcs, etc.)"), "Time in seconds until next change is synchronized to the client. '0' means send immediately if changed. '0.5' means only send changes every 500ms.\n(This is for state synchronization like SyncVars, SyncLists, OnSerialize. Not for Cmds, Rpcs, etc.)"),
networkBehaviour.syncInterval, 0, 2); networkBehaviour.syncInterval, 0, 2);
// apply
serializedObject.ApplyModifiedProperties(); serializedObject.ApplyModifiedProperties();
} }
} }

View File

@ -5,6 +5,11 @@
namespace Mirror namespace Mirror
{ {
/// <summary>
/// Sync to everyone, or only to owner.
/// </summary>
public enum SyncMode { Observers, Owner }
/// <summary> /// <summary>
/// Base class which should be inherited by scripts which contain networking functionality. /// Base class which should be inherited by scripts which contain networking functionality.
/// </summary> /// </summary>
@ -19,6 +24,12 @@ public class NetworkBehaviour : MonoBehaviour
{ {
float lastSyncTime; float lastSyncTime;
// hidden because NetworkBehaviourInspector shows it only if has OnSerialize.
/// <summary>
/// sync mode for OnSerialize
/// </summary>
[HideInInspector] public SyncMode syncMode = SyncMode.Observers;
// hidden because NetworkBehaviourInspector shows it only if has OnSerialize. // hidden because NetworkBehaviourInspector shows it only if has OnSerialize.
/// <summary> /// <summary>
/// sync interval for OnSerialize (in seconds) /// sync interval for OnSerialize (in seconds)

View File

@ -676,20 +676,38 @@ bool OnSerializeSafely(NetworkBehaviour comp, NetworkWriter writer, bool initial
} }
// serialize all components (or only dirty ones if not initial state) // serialize all components (or only dirty ones if not initial state)
// -> returns true if something was written // -> check ownerWritten/observersWritten to know if anything was written
internal bool OnSerializeAllSafely(bool initialState, NetworkWriter writer) internal void OnSerializeAllSafely(bool initialState, NetworkWriter ownerWriter, out int ownerWritten, NetworkWriter observersWriter, out int observersWritten)
{ {
// clear 'written' variables
ownerWritten = observersWritten = 0;
if (NetworkBehaviours.Length > 64) if (NetworkBehaviours.Length > 64)
{ {
Debug.LogError("Only 64 NetworkBehaviour components are allowed for NetworkIdentity: " + name + " because of the dirtyComponentMask"); Debug.LogError("Only 64 NetworkBehaviour components are allowed for NetworkIdentity: " + name + " because of the dirtyComponentMask");
return false; return;
} }
ulong dirtyComponentsMask = GetDirtyMask(initialState); ulong dirtyComponentsMask = GetDirtyMask(initialState);
if (dirtyComponentsMask == 0L) if (dirtyComponentsMask == 0L)
return false; return;
writer.WritePackedUInt64(dirtyComponentsMask); // WritePacked64 so we don't write full 8 bytes if we don't have to // calculate syncMode mask at runtime. this allows users to change
// component.syncMode while the game is running, which can be a huge
// advantage over syncvar-based sync modes. e.g. if a player decides
// to share or not share his inventory, or to go invisible, etc.
//
// (this also lets the TestSynchronizingObjects test pass because
// otherwise if we were to cache it in Awake, then we would call
// GetComponents<NetworkBehaviour> before all the test behaviours
// were added)
ulong syncModeObserversMask = GetSyncModeObserversMask();
// write regular dirty mask for owner,
// writer 'dirty mask & syncMode==Everyone' for everyone else
// (WritePacked64 so we don't write full 8 bytes if we don't have to)
ownerWriter.WritePackedUInt64(dirtyComponentsMask);
observersWriter.WritePackedUInt64(dirtyComponentsMask & syncModeObserversMask);
foreach (NetworkBehaviour comp in NetworkBehaviours) foreach (NetworkBehaviour comp in NetworkBehaviours)
{ {
@ -698,13 +716,32 @@ internal bool OnSerializeAllSafely(bool initialState, NetworkWriter writer)
// -> note: IsDirty() is false if the component isn't dirty or sendInterval isn't elapsed yet // -> note: IsDirty() is false if the component isn't dirty or sendInterval isn't elapsed yet
if (initialState || comp.IsDirty()) if (initialState || comp.IsDirty())
{ {
// serialize the data
if (LogFilter.Debug) Debug.Log("OnSerializeAllSafely: " + name + " -> " + comp.GetType() + " initial=" + initialState); if (LogFilter.Debug) Debug.Log("OnSerializeAllSafely: " + name + " -> " + comp.GetType() + " initial=" + initialState);
OnSerializeSafely(comp, writer, initialState);
}
}
return true; // serialize into ownerWriter first
// (owner always gets everything!)
int startPosition = ownerWriter.Position;
OnSerializeSafely(comp, ownerWriter, initialState);
++ownerWritten;
// copy into observersWriter too if SyncMode.Observers
// -> we copy instead of calling OnSerialize again because
// we don't know what magic the user does in OnSerialize.
// -> it's not guaranteed that calling it twice gets the
// same result
// -> it's not guaranteed that calling it twice doesn't mess
// with the user's OnSerialize timing code etc.
// => so we just copy the result without touching
// OnSerialize again
if (comp.syncMode == SyncMode.Observers)
{
ArraySegment<byte> segment = ownerWriter.ToArraySegment();
int length = ownerWriter.Position - startPosition;
observersWriter.WriteBytes(segment.Array, startPosition, length);
++observersWritten;
}
}
}
} }
internal ulong GetDirtyMask(bool initialState) internal ulong GetDirtyMask(bool initialState)
@ -724,6 +761,24 @@ internal ulong GetDirtyMask(bool initialState)
return dirtyComponentsMask; return dirtyComponentsMask;
} }
// a mask that contains all the components with SyncMode.Observers
internal ulong GetSyncModeObserversMask()
{
// loop through all components
ulong mask = 0UL;
NetworkBehaviour[] components = NetworkBehaviours;
for (int i = 0; i < NetworkBehaviours.Length; ++i)
{
NetworkBehaviour comp = components[i];
if (comp.syncMode == SyncMode.Observers)
{
mask |= 1UL << i;
}
}
return mask;
}
void OnDeserializeSafely(NetworkBehaviour comp, NetworkReader reader, bool initialState) void OnDeserializeSafely(NetworkBehaviour comp, NetworkReader reader, bool initialState)
{ {
// read header as 4 bytes and calculate this chunk's start+end // read header as 4 bytes and calculate this chunk's start+end
@ -1141,21 +1196,42 @@ internal void MirrorUpdate()
{ {
if (observers != null && observers.Count > 0) if (observers != null && observers.Count > 0)
{ {
NetworkWriter writer = NetworkWriterPool.GetWriter(); // one writer for owner, one for observers
NetworkWriter ownerWriter = NetworkWriterPool.GetWriter();
NetworkWriter observersWriter = NetworkWriterPool.GetWriter();
// serialize all the dirty components and send (if any were dirty) // serialize all the dirty components and send (if any were dirty)
if (OnSerializeAllSafely(false, writer)) OnSerializeAllSafely(false, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten);
if (ownerWritten > 0 || observersWritten > 0)
{ {
// populate cached UpdateVarsMessage and send // populate cached UpdateVarsMessage and send
varsMessage.netId = netId; varsMessage.netId = netId;
// segment to avoid reader allocations.
// (never null because of our above check) // send ownerWriter to owner
varsMessage.payload = writer.ToArraySegment(); // (only if we serialized anything for owner)
NetworkServer.SendToReady(this, varsMessage); // (only if there is a connection (e.g. if not a monster),
// and if connection is ready because we use SendToReady
// below too)
if (ownerWritten > 0)
{
varsMessage.payload = ownerWriter.ToArraySegment();
if (connectionToClient != null && connectionToClient.isReady)
NetworkServer.SendToClientOfPlayer(this, varsMessage);
}
// send observersWriter to everyone but owner
// (only if we serialized anything for observers)
if (observersWritten > 0)
{
varsMessage.payload = observersWriter.ToArraySegment();
NetworkServer.SendToReady(this, varsMessage, false);
}
// only clear bits if we sent something // only clear bits if we sent something
ClearDirtyBits(); ClearDirtyBits();
} }
NetworkWriterPool.Recycle(writer); NetworkWriterPool.Recycle(ownerWriter);
NetworkWriterPool.Recycle(observersWriter);
} }
else else
{ {

View File

@ -333,9 +333,10 @@ public static bool SendToReady(NetworkIdentity identity, short msgType, MessageB
/// <typeparam name="T">Message type.</typeparam> /// <typeparam name="T">Message type.</typeparam>
/// <param name="identity"></param> /// <param name="identity"></param>
/// <param name="msg">Message structure.</param> /// <param name="msg">Message structure.</param>
/// <param name="includeSelf">Send to observers including self..</param>
/// <param name="channelId">Transport channel to use</param> /// <param name="channelId">Transport channel to use</param>
/// <returns></returns> /// <returns></returns>
public static bool SendToReady<T>(NetworkIdentity identity,T msg, int channelId = Channels.DefaultReliable) where T : IMessageBase public static bool SendToReady<T>(NetworkIdentity identity, T msg, bool includeSelf = true, int channelId = Channels.DefaultReliable) where T : IMessageBase
{ {
if (LogFilter.Debug) Debug.Log("Server.SendToReady msgType:" + typeof(T)); if (LogFilter.Debug) Debug.Log("Server.SendToReady msgType:" + typeof(T));
@ -347,7 +348,9 @@ public static bool SendToReady<T>(NetworkIdentity identity,T msg, int channelId
bool result = true; bool result = true;
foreach (KeyValuePair<int, NetworkConnection> kvp in identity.observers) foreach (KeyValuePair<int, NetworkConnection> kvp in identity.observers)
{ {
if (kvp.Value.isReady) bool isSelf = kvp.Value == identity.connectionToClient;
if ((!isSelf || includeSelf) &&
kvp.Value.isReady)
{ {
result &= kvp.Value.SendBytes(bytes, channelId); result &= kvp.Value.SendBytes(bytes, channelId);
} }
@ -357,6 +360,20 @@ public static bool SendToReady<T>(NetworkIdentity identity,T msg, int channelId
return false; return false;
} }
/// <summary>
/// Send a message structure with the given type number to only clients which are ready.
/// <para>See Networking.NetworkClient.Ready.</para>
/// </summary>
/// <typeparam name="T">Message type.</typeparam>
/// <param name="identity"></param>
/// <param name="msg">Message structure.</param>
/// <param name="channelId">Transport channel to use</param>
/// <returns></returns>
public static bool SendToReady<T>(NetworkIdentity identity, T msg, int channelId = Channels.DefaultReliable) where T : IMessageBase
{
return SendToReady(identity, msg, true, channelId);
}
/// <summary> /// <summary>
/// Disconnect all currently connected clients, including the local connection. /// Disconnect all currently connected clients, including the local connection.
/// <para>This can only be called on the server. Clients will receive the Disconnect message.</para> /// <para>This can only be called on the server. Clients will receive the Disconnect message.</para>
@ -1009,18 +1026,19 @@ internal static void SendSpawnMessage(NetworkIdentity identity, NetworkConnectio
if (LogFilter.Debug) Debug.Log("Server SendSpawnMessage: name=" + identity.name + " sceneId=" + identity.sceneId.ToString("X") + " netid=" + identity.netId); // for easier debugging if (LogFilter.Debug) Debug.Log("Server SendSpawnMessage: name=" + identity.name + " sceneId=" + identity.sceneId.ToString("X") + " netid=" + identity.netId); // for easier debugging
NetworkWriter writer = NetworkWriterPool.GetWriter(); // one writer for owner, one for observers
NetworkWriter ownerWriter = NetworkWriterPool.GetWriter();
NetworkWriter observersWriter = NetworkWriterPool.GetWriter();
// convert to ArraySegment to avoid reader allocations
// (need to handle null case too)
ArraySegment<byte> segment = default;
// serialize all components with initialState = true // serialize all components with initialState = true
// (can be null if has none) // (can be null if has none)
if (identity.OnSerializeAllSafely(true, writer)) identity.OnSerializeAllSafely(true, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten);
{
segment = writer.ToArraySegment(); // convert to ArraySegment to avoid reader allocations
} // (need to handle null case too)
ArraySegment<byte> ownerSegment = ownerWritten > 0 ? ownerWriter.ToArraySegment() : default;
ArraySegment<byte> observersSegment = observersWritten > 0 ? observersWriter.ToArraySegment() : default;
// 'identity' is a prefab that should be spawned // 'identity' is a prefab that should be spawned
if (identity.sceneId == 0) if (identity.sceneId == 0)
@ -1033,19 +1051,35 @@ internal static void SendSpawnMessage(NetworkIdentity identity, NetworkConnectio
// use local values for VR support // use local values for VR support
position = identity.transform.localPosition, position = identity.transform.localPosition,
rotation = identity.transform.localRotation, rotation = identity.transform.localRotation,
scale = identity.transform.localScale, scale = identity.transform.localScale
payload = segment
}; };
// conn is != null when spawning it for a client // conn is != null when spawning it for a client
if (conn != null) if (conn != null)
{ {
// use owner segment if 'conn' owns this identity, otherwise
// use observers segment
bool isOwner = identity.connectionToClient == conn;
msg.payload = isOwner ? ownerSegment : observersSegment;
conn.Send(msg); conn.Send(msg);
} }
// conn is == null when spawning it for the local player // conn is == null when spawning it for the local player
else else
{ {
SendToReady(identity, msg); // send ownerWriter to owner
// (spawn no matter what, even if no components were
// serialized because the spawn message contains more data.
// components might still be updated later on.)
msg.payload = ownerSegment;
SendToClientOfPlayer(identity, msg);
// send observersWriter to everyone but owner
// (spawn no matter what, even if no components were
// serialized because the spawn message contains more data.
// components might still be updated later on.)
msg.payload = observersSegment;
SendToReady(identity, msg, false);
} }
} }
// 'identity' is a scene object that should be spawned again // 'identity' is a scene object that should be spawned again
@ -1059,23 +1093,40 @@ internal static void SendSpawnMessage(NetworkIdentity identity, NetworkConnectio
// use local values for VR support // use local values for VR support
position = identity.transform.localPosition, position = identity.transform.localPosition,
rotation = identity.transform.localRotation, rotation = identity.transform.localRotation,
scale = identity.transform.localScale, scale = identity.transform.localScale
payload = segment
}; };
// conn is != null when spawning it for a client // conn is != null when spawning it for a client
if (conn != null) if (conn != null)
{ {
// use owner segment if 'conn' owns this identity, otherwise
// use observers segment
bool isOwner = identity.connectionToClient == conn;
msg.payload = isOwner ? ownerSegment : observersSegment;
conn.Send(msg); conn.Send(msg);
} }
// conn is == null when spawning it for the local player // conn is == null when spawning it for the local player
else else
{ {
SendToReady(identity, msg); // send ownerWriter to owner
// (spawn no matter what, even if no components were
// serialized because the spawn message contains more data.
// components might still be updated later on.)
msg.payload = ownerSegment;
SendToClientOfPlayer(identity, msg);
// send observersWriter to everyone but owner
// (spawn no matter what, even if no components were
// serialized because the spawn message contains more data.
// components might still be updated later on.)
msg.payload = observersSegment;
SendToReady(identity, msg, false);
} }
} }
NetworkWriterPool.Recycle(writer); NetworkWriterPool.Recycle(ownerWriter);
NetworkWriterPool.Recycle(observersWriter);
} }
/// <summary> /// <summary>

View File

@ -65,8 +65,9 @@ public void TestSynchronizingObjects()
player1.guild = myGuild; player1.guild = myGuild;
// serialize all the data as we would for the network // serialize all the data as we would for the network
NetworkWriter writer = new NetworkWriter(); NetworkWriter ownerWriter = new NetworkWriter();
identity1.OnSerializeAllSafely(true, writer); NetworkWriter observersWriter = new NetworkWriter(); // not really used in this Test
identity1.OnSerializeAllSafely(true, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten);
// set up a "client" object // set up a "client" object
GameObject gameObject2 = new GameObject(); GameObject gameObject2 = new GameObject();
@ -74,11 +75,31 @@ public void TestSynchronizingObjects()
MockPlayer player2 = gameObject2.AddComponent<MockPlayer>(); MockPlayer player2 = gameObject2.AddComponent<MockPlayer>();
// apply all the data from the server object // apply all the data from the server object
NetworkReader reader = new NetworkReader(writer.ToArray()); NetworkReader reader = new NetworkReader(ownerWriter.ToArray());
identity2.OnDeserializeAllSafely(reader, true); identity2.OnDeserializeAllSafely(reader, true);
// check that the syncvars got updated // check that the syncvars got updated
Assert.That(player2.guild.name, Is.EqualTo("Back street boys"), "Data should be synchronized"); Assert.That(player2.guild.name, Is.EqualTo("Back street boys"), "Data should be synchronized");
} }
[Test]
public void TestSyncModeObserversMask()
{
GameObject gameObject1 = new GameObject();
NetworkIdentity identity = gameObject1.AddComponent<NetworkIdentity>();
MockPlayer player1 = gameObject1.AddComponent<MockPlayer>();
player1.syncInterval = 0;
MockPlayer player2 = gameObject1.AddComponent<MockPlayer>();
player2.syncInterval = 0;
MockPlayer player3 = gameObject1.AddComponent<MockPlayer>();
player3.syncInterval = 0;
// sync mode
player1.syncMode = SyncMode.Observers;
player2.syncMode = SyncMode.Owner;
player3.syncMode = SyncMode.Observers;
Assert.That(identity.GetSyncModeObserversMask(), Is.EqualTo(0b101));
}
} }
} }