diff --git a/Assets/Mirror/Editor/NetworkBehaviourInspector.cs b/Assets/Mirror/Editor/NetworkBehaviourInspector.cs index a1c8bff07..44d55f5bf 100644 --- a/Assets/Mirror/Editor/NetworkBehaviourInspector.cs +++ b/Assets/Mirror/Editor/NetworkBehaviourInspector.cs @@ -170,18 +170,25 @@ public override void OnInspectorGUI() } } - // only show SyncInterval if we have an OnSerialize function. - // No need to show it if the class only has Cmds/Rpcs and no sync. + // does it sync anything? then show extra properties + // (no need to show it if the class only has Cmds/Rpcs and no sync) if (syncsAnything) { NetworkBehaviour networkBehaviour = target as NetworkBehaviour; 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. serializedObject.FindProperty("syncInterval").floatValue = EditorGUILayout.Slider( 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.)"), networkBehaviour.syncInterval, 0, 2); + + // apply serializedObject.ApplyModifiedProperties(); } } diff --git a/Assets/Mirror/Runtime/NetworkBehaviour.cs b/Assets/Mirror/Runtime/NetworkBehaviour.cs index 39eb8b177..7a21ac45f 100644 --- a/Assets/Mirror/Runtime/NetworkBehaviour.cs +++ b/Assets/Mirror/Runtime/NetworkBehaviour.cs @@ -5,6 +5,11 @@ namespace Mirror { + /// + /// Sync to everyone, or only to owner. + /// + public enum SyncMode { Observers, Owner } + /// /// Base class which should be inherited by scripts which contain networking functionality. /// @@ -19,6 +24,12 @@ public class NetworkBehaviour : MonoBehaviour { float lastSyncTime; + // hidden because NetworkBehaviourInspector shows it only if has OnSerialize. + /// + /// sync mode for OnSerialize + /// + [HideInInspector] public SyncMode syncMode = SyncMode.Observers; + // hidden because NetworkBehaviourInspector shows it only if has OnSerialize. /// /// sync interval for OnSerialize (in seconds) @@ -321,7 +332,7 @@ protected void SendEventInternal(Type invokeClass, string eventName, NetworkWrit payload = writer.ToArraySegment() // segment to avoid reader allocations }; - NetworkServer.SendToReady(netIdentity,message, channelId); + NetworkServer.SendToReady(netIdentity, message, channelId); } /// diff --git a/Assets/Mirror/Runtime/NetworkIdentity.cs b/Assets/Mirror/Runtime/NetworkIdentity.cs index 8ec3ab7bb..e5b350a30 100644 --- a/Assets/Mirror/Runtime/NetworkIdentity.cs +++ b/Assets/Mirror/Runtime/NetworkIdentity.cs @@ -676,20 +676,38 @@ bool OnSerializeSafely(NetworkBehaviour comp, NetworkWriter writer, bool initial } // serialize all components (or only dirty ones if not initial state) - // -> returns true if something was written - internal bool OnSerializeAllSafely(bool initialState, NetworkWriter writer) + // -> check ownerWritten/observersWritten to know if anything was written + 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) { Debug.LogError("Only 64 NetworkBehaviour components are allowed for NetworkIdentity: " + name + " because of the dirtyComponentMask"); - return false; + return; } ulong dirtyComponentsMask = GetDirtyMask(initialState); 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 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) { @@ -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 if (initialState || comp.IsDirty()) { - // serialize the data if (LogFilter.Debug) Debug.Log("OnSerializeAllSafely: " + name + " -> " + comp.GetType() + " initial=" + initialState); - OnSerializeSafely(comp, writer, initialState); + + // 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 segment = ownerWriter.ToArraySegment(); + int length = ownerWriter.Position - startPosition; + observersWriter.WriteBytes(segment.Array, startPosition, length); + ++observersWritten; + } } } - - return true; } internal ulong GetDirtyMask(bool initialState) @@ -724,6 +761,24 @@ internal ulong GetDirtyMask(bool initialState) 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) { // 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) { - 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) - if (OnSerializeAllSafely(false, writer)) + OnSerializeAllSafely(false, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten); + if (ownerWritten > 0 || observersWritten > 0) { // populate cached UpdateVarsMessage and send varsMessage.netId = netId; - // segment to avoid reader allocations. - // (never null because of our above check) - varsMessage.payload = writer.ToArraySegment(); - NetworkServer.SendToReady(this, varsMessage); + + // send ownerWriter to owner + // (only if we serialized anything for owner) + // (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 ClearDirtyBits(); } - NetworkWriterPool.Recycle(writer); + NetworkWriterPool.Recycle(ownerWriter); + NetworkWriterPool.Recycle(observersWriter); } else { diff --git a/Assets/Mirror/Runtime/NetworkServer.cs b/Assets/Mirror/Runtime/NetworkServer.cs index 50e3d8eb1..d323361f7 100644 --- a/Assets/Mirror/Runtime/NetworkServer.cs +++ b/Assets/Mirror/Runtime/NetworkServer.cs @@ -333,9 +333,10 @@ public static bool SendToReady(NetworkIdentity identity, short msgType, MessageB /// Message type. /// /// Message structure. + /// Send to observers including self.. /// Transport channel to use /// - public static bool SendToReady(NetworkIdentity identity,T msg, int channelId = Channels.DefaultReliable) where T : IMessageBase + public static bool SendToReady(NetworkIdentity identity, T msg, bool includeSelf = true, int channelId = Channels.DefaultReliable) where T : IMessageBase { if (LogFilter.Debug) Debug.Log("Server.SendToReady msgType:" + typeof(T)); @@ -347,7 +348,9 @@ public static bool SendToReady(NetworkIdentity identity,T msg, int channelId bool result = true; foreach (KeyValuePair 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); } @@ -357,6 +360,20 @@ public static bool SendToReady(NetworkIdentity identity,T msg, int channelId return false; } + /// + /// Send a message structure with the given type number to only clients which are ready. + /// See Networking.NetworkClient.Ready. + /// + /// Message type. + /// + /// Message structure. + /// Transport channel to use + /// + public static bool SendToReady(NetworkIdentity identity, T msg, int channelId = Channels.DefaultReliable) where T : IMessageBase + { + return SendToReady(identity, msg, true, channelId); + } + /// /// Disconnect all currently connected clients, including the local connection. /// This can only be called on the server. Clients will receive the Disconnect message. @@ -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 - 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 segment = default; // serialize all components with initialState = true // (can be null if has none) - if (identity.OnSerializeAllSafely(true, writer)) - { - segment = writer.ToArraySegment(); - } + identity.OnSerializeAllSafely(true, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten); + + // convert to ArraySegment to avoid reader allocations + // (need to handle null case too) + ArraySegment ownerSegment = ownerWritten > 0 ? ownerWriter.ToArraySegment() : default; + ArraySegment observersSegment = observersWritten > 0 ? observersWriter.ToArraySegment() : default; // 'identity' is a prefab that should be spawned if (identity.sceneId == 0) @@ -1033,19 +1051,35 @@ internal static void SendSpawnMessage(NetworkIdentity identity, NetworkConnectio // use local values for VR support position = identity.transform.localPosition, rotation = identity.transform.localRotation, - scale = identity.transform.localScale, - payload = segment + scale = identity.transform.localScale }; // conn is != null when spawning it for a client 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 is == null when spawning it for the local player 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 @@ -1059,23 +1093,40 @@ internal static void SendSpawnMessage(NetworkIdentity identity, NetworkConnectio // use local values for VR support position = identity.transform.localPosition, rotation = identity.transform.localRotation, - scale = identity.transform.localScale, - payload = segment + scale = identity.transform.localScale }; // conn is != null when spawning it for a client 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 is == null when spawning it for the local player 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); } /// diff --git a/Assets/Mirror/Tests/SyncVarTest.cs b/Assets/Mirror/Tests/SyncVarTest.cs index 31d54227b..2533d9e33 100644 --- a/Assets/Mirror/Tests/SyncVarTest.cs +++ b/Assets/Mirror/Tests/SyncVarTest.cs @@ -65,8 +65,9 @@ public void TestSynchronizingObjects() player1.guild = myGuild; // serialize all the data as we would for the network - NetworkWriter writer = new NetworkWriter(); - identity1.OnSerializeAllSafely(true, writer); + NetworkWriter ownerWriter = new NetworkWriter(); + 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 GameObject gameObject2 = new GameObject(); @@ -74,11 +75,31 @@ public void TestSynchronizingObjects() MockPlayer player2 = gameObject2.AddComponent(); // apply all the data from the server object - NetworkReader reader = new NetworkReader(writer.ToArray()); + NetworkReader reader = new NetworkReader(ownerWriter.ToArray()); identity2.OnDeserializeAllSafely(reader, true); // check that the syncvars got updated 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(); + MockPlayer player1 = gameObject1.AddComponent(); + player1.syncInterval = 0; + MockPlayer player2 = gameObject1.AddComponent(); + player2.syncInterval = 0; + MockPlayer player3 = gameObject1.AddComponent(); + player3.syncInterval = 0; + + // sync mode + player1.syncMode = SyncMode.Observers; + player2.syncMode = SyncMode.Owner; + player3.syncMode = SyncMode.Observers; + + Assert.That(identity.GetSyncModeObserversMask(), Is.EqualTo(0b101)); + } } } diff --git a/README.md b/README.md index 511d8dcdc..ccbe78377 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,9 @@ What previously required **10.000** lines of code, now takes **1.000** lines of _Note: Mirror is based on Unity's abandoned UNET Networking system. We fixed it up and pushed it to MMO Scale._ ## Documentation -Check out our [Documentation](https://vis2k.github.io/Mirror/). +Check out our [Documentation](https://mirror-networking.com/xmldocs/). -If you are migrating from UNET, then please check out our [Migration Guide](https://vis2k.github.io/Mirror/General/Migration). Don't panic, it's very easy and won't take more than 5 minutes. +If you are migrating from UNET, then please check out our [Migration Guide](https://mirror-networking.com/xmldocs/articles/General/Migration.html). Don't panic, it's very easy and won't take more than 5 minutes. ## Installation We **recommend** to download the most **stable Mirror version** from the [Asset Store](https://www.assetstore.unity3d.com/#!/content/129321)!