From 07f206ff9e795b7710ecfc1dbea4c333e0189aaf Mon Sep 17 00:00:00 2001 From: Unity Technologies <@unity> Date: Tue, 12 Dec 2023 00:00:00 +0000 Subject: [PATCH] com.unity.netcode.gameobjects@1.8.0 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Additional documentation and release notes are available at [Multiplayer Documentation](https://docs-multiplayer.unity3d.com). ## [1.8.0] - 2023-12-12 ### Added - Added a new RPC attribute, which is simply `Rpc`. (#2762) - This is a generic attribute that can perform the functions of both Server and Client RPCs, as well as enabling client-to-client RPCs. Includes several default targets: `Server`, `NotServer`, `Owner`, `NotOwner`, `Me`, `NotMe`, `ClientsAndHost`, and `Everyone`. Runtime overrides are available for any of these targets, as well as for sending to a specific ID or groups of IDs. - This attribute also includes the ability to defer RPCs that are sent to the local process to the start of the next frame instead of executing them immediately, treating them as if they had gone across the network. The default behavior is to execute immediately. - This attribute effectively replaces `ServerRpc` and `ClientRpc`. `ServerRpc` and `ClientRpc` remain in their existing forms for backward compatibility, but `Rpc` will be the recommended and most supported option. - Added `NetworkManager.OnConnectionEvent` as a unified connection event callback to notify clients and servers of all client connections and disconnections within the session (#2762) - Added `NetworkManager.ServerIsHost` and `NetworkBehaviour.ServerIsHost` to allow a client to tell if it is connected to a host or to a dedicated server (#2762) - Added `SceneEventProgress.SceneManagementNotEnabled` return status to be returned when a `NetworkSceneManager` method is invoked and scene management is not enabled. (#2735) - Added `SceneEventProgress.ServerOnlyAction` return status to be returned when a `NetworkSceneManager` method is invoked by a client. (#2735) - Added `NetworkObject.InstantiateAndSpawn` and `NetworkSpawnManager.InstantiateAndSpawn` methods to simplify prefab spawning by assuring that the prefab is valid and applies any override prior to instantiating the `GameObject` and spawning the `NetworkObject` instance. (#2710) ### Fixed - Fixed issue where a client disconnected by a server-host would not receive a local notification. (#2789) - Fixed issue where a server-host could shutdown during a relay connection but periodically the transport disconnect message sent to any connected clients could be dropped. (#2789) - Fixed issue where a host could disconnect its local client but remain running as a server. (#2789) - Fixed issue where `OnClientDisconnectedCallback` was not being invoked under certain conditions. (#2789) - Fixed issue where `OnClientDisconnectedCallback` was always returning 0 as the client identifier. (#2789) - Fixed issue where if a host or server shutdown while a client owned NetworkObjects (other than the player) it would throw an exception. (#2789) - Fixed issue where setting values on a `NetworkVariable` or `NetworkList` within `OnNetworkDespawn` during a shutdown sequence would throw an exception. (#2789) - Fixed issue where a teleport state could potentially be overridden by a previous unreliable delta state. (#2777) - Fixed issue where `NetworkTransform` was using the `NetworkManager.ServerTime.Tick` as opposed to `NetworkManager.NetworkTickSystem.ServerTime.Tick` during the authoritative side's tick update where it performed a delta state check. (#2777) - Fixed issue where a parented in-scene placed NetworkObject would be destroyed upon a client or server exiting a network session but not unloading the original scene in which the NetworkObject was placed. (#2737) - Fixed issue where during client synchronization and scene loading, when client synchronization or the scene loading mode are set to `LoadSceneMode.Single`, a `CreateObjectMessage` could be received, processed, and the resultant spawned `NetworkObject` could be instantiated in the client's currently active scene that could, towards the end of the client synchronization or loading process, be unloaded and cause the newly created `NetworkObject` to be destroyed (and throw and exception). (#2735) - Fixed issue where a `NetworkTransform` instance with interpolation enabled would result in wide visual motion gaps (stuttering) under above normal latency conditions and a 1-5% or higher packet are drop rate. (#2713) - Fixed issue where you could not have multiple source network prefab overrides targeting the same network prefab as their override. (#2710) ### Changed - Changed the server or host shutdown so it will now perform a "soft shutdown" when `NetworkManager.Shutdown` is invoked. This will send a disconnect notification to all connected clients and the server-host will wait for all connected clients to disconnect or timeout after a 5 second period before completing the shutdown process. (#2789) - Changed `OnClientDisconnectedCallback` will now return the assigned client identifier on the local client side if the client was approved and assigned one prior to being disconnected. (#2789) - Changed `NetworkTransform.SetState` (and related methods) now are cumulative during a fractional tick period and sent on the next pending tick. (#2777) - `NetworkManager.ConnectedClientsIds` is now accessible on the client side and will contain the list of all clients in the session, including the host client if the server is operating in host mode (#2762) - Changed `NetworkSceneManager` to return a `SceneEventProgress` status and not throw exceptions for methods invoked when scene management is disabled and when a client attempts to access a `NetworkSceneManager` method by a client. (#2735) - Changed `NetworkTransform` authoritative instance tick registration so a single `NetworkTransform` specific tick event update will update all authoritative instances to improve perofmance. (#2713) - Changed `NetworkPrefabs.OverrideToNetworkPrefab` dictionary is no longer used/populated due to it ending up being related to a regression bug and not allowing more than one override to be assigned to a network prefab asset. (#2710) - Changed in-scene placed `NetworkObject`s now store their source network prefab asset's `GlobalObjectIdHash` internally that is used, when scene management is disabled, by clients to spawn the correct prefab even if the `NetworkPrefab` entry has an override. This does not impact dynamically spawning the same prefab which will yield the override on both host and client. (#2710) - Changed in-scene placed `NetworkObject`s no longer require a `NetworkPrefab` entry with `GlobalObjectIdHash` override in order for clients to properly synchronize. (#2710) - Changed in-scene placed `NetworkObject`s now set their `IsSceneObject` value when generating their `GlobalObjectIdHash` value. (#2710) - Changed the default `NetworkConfig.SpawnTimeout` value from 1.0s to 10.0s. (#2710) --- CHANGELOG.md | 47 +- Components/NetworkAnimator.cs | 6 +- Components/NetworkDeltaPosition.cs | 12 + Components/NetworkTransform.cs | 721 +++++-- Documentation~/index.md | 2 +- Editor/CodeGen/CodeGenHelpers.cs | 2 + Editor/CodeGen/NetworkBehaviourILPP.cs | 362 +++- Editor/CodeGen/RuntimeAccessModifiersILPP.cs | 12 + Editor/NetworkManagerEditor.cs | 558 +++-- Editor/NetworkManagerRelayIntegration.cs | 120 ++ Editor/NetworkManagerRelayIntegration.cs.meta | 3 + Editor/NetworkTransformEditor.cs | 6 +- Editor/com.unity.netcode.editor.asmdef | 27 +- Runtime/Configuration/NetworkConfig.cs | 2 +- Runtime/Configuration/NetworkPrefabs.cs | 31 +- .../Connection/NetworkConnectionManager.cs | 219 +- Runtime/Core/NetworkBehaviour.cs | 137 +- Runtime/Core/NetworkManager.cs | 137 +- Runtime/Core/NetworkObject.cs | 205 +- Runtime/Messaging/DeferredMessageManager.cs | 2 +- .../IDeferredNetworkMessageManager.cs | 1 + .../Messages/ClientConnectedMessage.cs | 35 + .../Messages/ClientConnectedMessage.cs.meta | 3 + .../Messages/ClientDisconnectedMessage.cs | 35 + .../ClientDisconnectedMessage.cs.meta | 3 + .../Messages/ConnectionApprovedMessage.cs | 29 +- .../Messaging/Messages/CreateObjectMessage.cs | 25 +- Runtime/Messaging/Messages/NamedMessage.cs | 4 +- Runtime/Messaging/Messages/ProxyMessage.cs | 70 + .../Messaging/Messages/ProxyMessage.cs.meta | 3 + Runtime/Messaging/Messages/RpcMessages.cs | 38 + Runtime/Messaging/NetworkManagerHooks.cs | 37 +- Runtime/Messaging/NetworkMessageManager.cs | 20 +- Runtime/Messaging/RpcAttributes.cs | 44 +- Runtime/Messaging/RpcParams.cs | 55 + Runtime/Messaging/RpcTargets.meta | 3 + Runtime/Messaging/RpcTargets/BaseRpcTarget.cs | 57 + .../RpcTargets/BaseRpcTarget.cs.meta | 11 + .../RpcTargets/ClientsAndHostRpcTarget.cs | 37 + .../ClientsAndHostRpcTarget.cs.meta | 3 + .../RpcTargets/DirectSendRpcTarget.cs | 34 + .../RpcTargets/DirectSendRpcTarget.cs.meta | 3 + .../Messaging/RpcTargets/EveryoneRpcTarget.cs | 26 + .../RpcTargets/EveryoneRpcTarget.cs.meta | 3 + .../Messaging/RpcTargets/IGroupRpcTarget.cs | 9 + .../RpcTargets/IGroupRpcTarget.cs.meta | 3 + .../RpcTargets/IIndividualRpcTarget.cs | 8 + .../RpcTargets/IIndividualRpcTarget.cs.meta | 3 + .../RpcTargets/LocalSendRpcTarget.cs | 67 + .../RpcTargets/LocalSendRpcTarget.cs.meta | 3 + .../Messaging/RpcTargets/NotMeRpcTarget.cs | 67 + .../RpcTargets/NotMeRpcTarget.cs.meta | 3 + .../Messaging/RpcTargets/NotOwnerRpcTarget.cs | 83 + .../RpcTargets/NotOwnerRpcTarget.cs.meta | 3 + .../RpcTargets/NotServerRpcTarget.cs | 72 + .../RpcTargets/NotServerRpcTarget.cs.meta | 3 + .../Messaging/RpcTargets/OwnerRpcTarget.cs | 54 + .../RpcTargets/OwnerRpcTarget.cs.meta | 3 + .../Messaging/RpcTargets/ProxyRpcTarget.cs | 16 + .../RpcTargets/ProxyRpcTarget.cs.meta | 3 + .../RpcTargets/ProxyRpcTargetGroup.cs | 100 + .../RpcTargets/ProxyRpcTargetGroup.cs.meta | 3 + Runtime/Messaging/RpcTargets/RpcTarget.cs | 564 +++++ .../Messaging/RpcTargets/RpcTarget.cs.meta | 3 + .../Messaging/RpcTargets/RpcTargetGroup.cs | 80 + .../RpcTargets/RpcTargetGroup.cs.meta | 3 + .../Messaging/RpcTargets/ServerRpcTarget.cs | 36 + .../RpcTargets/ServerRpcTarget.cs.meta | 3 + .../NetworkVariable/NetworkVariableBase.cs | 10 +- .../SceneManagement/NetworkSceneManager.cs | 147 +- Runtime/SceneManagement/SceneEventProgress.cs | 8 + Runtime/Spawning/NetworkSpawnManager.cs | 258 ++- Runtime/Transports/UTP/BatchedSendQueue.cs | 68 +- Runtime/Transports/UTP/UnityTransport.cs | 20 +- TestHelpers/Runtime/NetcodeIntegrationTest.cs | 57 +- .../Runtime/NetcodeIntegrationTestHelpers.cs | 3 +- .../Transports/BatchedSendQueueTests.cs | 29 + Tests/Runtime/DeferredMessagingTests.cs | 4 + Tests/Runtime/InvalidConnectionEventsTest.cs | 18 +- .../NetworkObjectDestroyTests.cs | 10 +- .../NetworkObjectOnNetworkDespawnTests.cs | 137 +- .../NetworkObjectSpawnManyObjectsTests.cs | 6 +- .../NetworkTransform/NetworkTransformBase.cs | 1004 +++++++++ .../NetworkTransformBase.cs.meta | 11 + .../NetworkTransformGeneral.cs | 314 +++ .../NetworkTransformGeneral.cs.meta | 11 + .../NetworkTransformPacketLossTests.cs | 485 +++++ .../NetworkTransformPacketLossTests.cs.meta | 3 + .../NetworkTransformStateTests.cs | 7 + .../NetworkTransform/NetworkTransformTests.cs | 1148 ++-------- Tests/Runtime/NetworkVariableTests.cs | 67 + Tests/Runtime/PeerDisconnectCallbackTests.cs | 185 ++ .../PeerDisconnectCallbackTests.cs.meta | 3 + Tests/Runtime/RpcTests.cs | 1 - Tests/Runtime/StopStartRuntimeTests.cs | 25 +- .../Runtime/Transports/UnityTransportTests.cs | 24 +- Tests/Runtime/UniversalRpcTests.cs | 1917 +++++++++++++++++ Tests/Runtime/UniversalRpcTests.cs.meta | 3 + package.json | 12 +- 99 files changed, 8667 insertions(+), 1710 deletions(-) create mode 100644 Editor/NetworkManagerRelayIntegration.cs create mode 100644 Editor/NetworkManagerRelayIntegration.cs.meta create mode 100644 Runtime/Messaging/Messages/ClientConnectedMessage.cs create mode 100644 Runtime/Messaging/Messages/ClientConnectedMessage.cs.meta create mode 100644 Runtime/Messaging/Messages/ClientDisconnectedMessage.cs create mode 100644 Runtime/Messaging/Messages/ClientDisconnectedMessage.cs.meta create mode 100644 Runtime/Messaging/Messages/ProxyMessage.cs create mode 100644 Runtime/Messaging/Messages/ProxyMessage.cs.meta create mode 100644 Runtime/Messaging/RpcTargets.meta create mode 100644 Runtime/Messaging/RpcTargets/BaseRpcTarget.cs create mode 100644 Runtime/Messaging/RpcTargets/BaseRpcTarget.cs.meta create mode 100644 Runtime/Messaging/RpcTargets/ClientsAndHostRpcTarget.cs create mode 100644 Runtime/Messaging/RpcTargets/ClientsAndHostRpcTarget.cs.meta create mode 100644 Runtime/Messaging/RpcTargets/DirectSendRpcTarget.cs create mode 100644 Runtime/Messaging/RpcTargets/DirectSendRpcTarget.cs.meta create mode 100644 Runtime/Messaging/RpcTargets/EveryoneRpcTarget.cs create mode 100644 Runtime/Messaging/RpcTargets/EveryoneRpcTarget.cs.meta create mode 100644 Runtime/Messaging/RpcTargets/IGroupRpcTarget.cs create mode 100644 Runtime/Messaging/RpcTargets/IGroupRpcTarget.cs.meta create mode 100644 Runtime/Messaging/RpcTargets/IIndividualRpcTarget.cs create mode 100644 Runtime/Messaging/RpcTargets/IIndividualRpcTarget.cs.meta create mode 100644 Runtime/Messaging/RpcTargets/LocalSendRpcTarget.cs create mode 100644 Runtime/Messaging/RpcTargets/LocalSendRpcTarget.cs.meta create mode 100644 Runtime/Messaging/RpcTargets/NotMeRpcTarget.cs create mode 100644 Runtime/Messaging/RpcTargets/NotMeRpcTarget.cs.meta create mode 100644 Runtime/Messaging/RpcTargets/NotOwnerRpcTarget.cs create mode 100644 Runtime/Messaging/RpcTargets/NotOwnerRpcTarget.cs.meta create mode 100644 Runtime/Messaging/RpcTargets/NotServerRpcTarget.cs create mode 100644 Runtime/Messaging/RpcTargets/NotServerRpcTarget.cs.meta create mode 100644 Runtime/Messaging/RpcTargets/OwnerRpcTarget.cs create mode 100644 Runtime/Messaging/RpcTargets/OwnerRpcTarget.cs.meta create mode 100644 Runtime/Messaging/RpcTargets/ProxyRpcTarget.cs create mode 100644 Runtime/Messaging/RpcTargets/ProxyRpcTarget.cs.meta create mode 100644 Runtime/Messaging/RpcTargets/ProxyRpcTargetGroup.cs create mode 100644 Runtime/Messaging/RpcTargets/ProxyRpcTargetGroup.cs.meta create mode 100644 Runtime/Messaging/RpcTargets/RpcTarget.cs create mode 100644 Runtime/Messaging/RpcTargets/RpcTarget.cs.meta create mode 100644 Runtime/Messaging/RpcTargets/RpcTargetGroup.cs create mode 100644 Runtime/Messaging/RpcTargets/RpcTargetGroup.cs.meta create mode 100644 Runtime/Messaging/RpcTargets/ServerRpcTarget.cs create mode 100644 Runtime/Messaging/RpcTargets/ServerRpcTarget.cs.meta create mode 100644 Tests/Runtime/NetworkTransform/NetworkTransformBase.cs create mode 100644 Tests/Runtime/NetworkTransform/NetworkTransformBase.cs.meta create mode 100644 Tests/Runtime/NetworkTransform/NetworkTransformGeneral.cs create mode 100644 Tests/Runtime/NetworkTransform/NetworkTransformGeneral.cs.meta create mode 100644 Tests/Runtime/NetworkTransform/NetworkTransformPacketLossTests.cs create mode 100644 Tests/Runtime/NetworkTransform/NetworkTransformPacketLossTests.cs.meta create mode 100644 Tests/Runtime/PeerDisconnectCallbackTests.cs create mode 100644 Tests/Runtime/PeerDisconnectCallbackTests.cs.meta create mode 100644 Tests/Runtime/UniversalRpcTests.cs create mode 100644 Tests/Runtime/UniversalRpcTests.cs.meta diff --git a/CHANGELOG.md b/CHANGELOG.md index d53a191..775a39a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,49 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) Additional documentation and release notes are available at [Multiplayer Documentation](https://docs-multiplayer.unity3d.com). +## [1.8.0] - 2023-12-12 + +### Added + +- Added a new RPC attribute, which is simply `Rpc`. (#2762) + - This is a generic attribute that can perform the functions of both Server and Client RPCs, as well as enabling client-to-client RPCs. Includes several default targets: `Server`, `NotServer`, `Owner`, `NotOwner`, `Me`, `NotMe`, `ClientsAndHost`, and `Everyone`. Runtime overrides are available for any of these targets, as well as for sending to a specific ID or groups of IDs. + - This attribute also includes the ability to defer RPCs that are sent to the local process to the start of the next frame instead of executing them immediately, treating them as if they had gone across the network. The default behavior is to execute immediately. + - This attribute effectively replaces `ServerRpc` and `ClientRpc`. `ServerRpc` and `ClientRpc` remain in their existing forms for backward compatibility, but `Rpc` will be the recommended and most supported option. +- Added `NetworkManager.OnConnectionEvent` as a unified connection event callback to notify clients and servers of all client connections and disconnections within the session (#2762) +- Added `NetworkManager.ServerIsHost` and `NetworkBehaviour.ServerIsHost` to allow a client to tell if it is connected to a host or to a dedicated server (#2762) +- Added `SceneEventProgress.SceneManagementNotEnabled` return status to be returned when a `NetworkSceneManager` method is invoked and scene management is not enabled. (#2735) +- Added `SceneEventProgress.ServerOnlyAction` return status to be returned when a `NetworkSceneManager` method is invoked by a client. (#2735) +- Added `NetworkObject.InstantiateAndSpawn` and `NetworkSpawnManager.InstantiateAndSpawn` methods to simplify prefab spawning by assuring that the prefab is valid and applies any override prior to instantiating the `GameObject` and spawning the `NetworkObject` instance. (#2710) + +### Fixed + +- Fixed issue where a client disconnected by a server-host would not receive a local notification. (#2789) +- Fixed issue where a server-host could shutdown during a relay connection but periodically the transport disconnect message sent to any connected clients could be dropped. (#2789) +- Fixed issue where a host could disconnect its local client but remain running as a server. (#2789) +- Fixed issue where `OnClientDisconnectedCallback` was not being invoked under certain conditions. (#2789) +- Fixed issue where `OnClientDisconnectedCallback` was always returning 0 as the client identifier. (#2789) +- Fixed issue where if a host or server shutdown while a client owned NetworkObjects (other than the player) it would throw an exception. (#2789) +- Fixed issue where setting values on a `NetworkVariable` or `NetworkList` within `OnNetworkDespawn` during a shutdown sequence would throw an exception. (#2789) +- Fixed issue where a teleport state could potentially be overridden by a previous unreliable delta state. (#2777) +- Fixed issue where `NetworkTransform` was using the `NetworkManager.ServerTime.Tick` as opposed to `NetworkManager.NetworkTickSystem.ServerTime.Tick` during the authoritative side's tick update where it performed a delta state check. (#2777) +- Fixed issue where a parented in-scene placed NetworkObject would be destroyed upon a client or server exiting a network session but not unloading the original scene in which the NetworkObject was placed. (#2737) +- Fixed issue where during client synchronization and scene loading, when client synchronization or the scene loading mode are set to `LoadSceneMode.Single`, a `CreateObjectMessage` could be received, processed, and the resultant spawned `NetworkObject` could be instantiated in the client's currently active scene that could, towards the end of the client synchronization or loading process, be unloaded and cause the newly created `NetworkObject` to be destroyed (and throw and exception). (#2735) +- Fixed issue where a `NetworkTransform` instance with interpolation enabled would result in wide visual motion gaps (stuttering) under above normal latency conditions and a 1-5% or higher packet are drop rate. (#2713) +- Fixed issue where you could not have multiple source network prefab overrides targeting the same network prefab as their override. (#2710) + +### Changed +- Changed the server or host shutdown so it will now perform a "soft shutdown" when `NetworkManager.Shutdown` is invoked. This will send a disconnect notification to all connected clients and the server-host will wait for all connected clients to disconnect or timeout after a 5 second period before completing the shutdown process. (#2789) +- Changed `OnClientDisconnectedCallback` will now return the assigned client identifier on the local client side if the client was approved and assigned one prior to being disconnected. (#2789) +- Changed `NetworkTransform.SetState` (and related methods) now are cumulative during a fractional tick period and sent on the next pending tick. (#2777) +- `NetworkManager.ConnectedClientsIds` is now accessible on the client side and will contain the list of all clients in the session, including the host client if the server is operating in host mode (#2762) +- Changed `NetworkSceneManager` to return a `SceneEventProgress` status and not throw exceptions for methods invoked when scene management is disabled and when a client attempts to access a `NetworkSceneManager` method by a client. (#2735) +- Changed `NetworkTransform` authoritative instance tick registration so a single `NetworkTransform` specific tick event update will update all authoritative instances to improve perofmance. (#2713) +- Changed `NetworkPrefabs.OverrideToNetworkPrefab` dictionary is no longer used/populated due to it ending up being related to a regression bug and not allowing more than one override to be assigned to a network prefab asset. (#2710) +- Changed in-scene placed `NetworkObject`s now store their source network prefab asset's `GlobalObjectIdHash` internally that is used, when scene management is disabled, by clients to spawn the correct prefab even if the `NetworkPrefab` entry has an override. This does not impact dynamically spawning the same prefab which will yield the override on both host and client. (#2710) +- Changed in-scene placed `NetworkObject`s no longer require a `NetworkPrefab` entry with `GlobalObjectIdHash` override in order for clients to properly synchronize. (#2710) +- Changed in-scene placed `NetworkObject`s now set their `IsSceneObject` value when generating their `GlobalObjectIdHash` value. (#2710) +- Changed the default `NetworkConfig.SpawnTimeout` value from 1.0s to 10.0s. (#2710) + ## [1.7.1] - 2023-11-15 ### Added @@ -15,8 +58,6 @@ Additional documentation and release notes are available at [Multiplayer Documen - Fixed a bug where having a class with Rpcs that inherits from a class without Rpcs that inherits from NetworkVariable would cause a compile error. (#2751) - Fixed issue where `NetworkBehaviour.Synchronize` was not truncating the write buffer if nothing was serialized during `NetworkBehaviour.OnSynchronize` causing an additional 6 bytes to be written per `NetworkBehaviour` component instance. (#2749) -### Changed - ## [1.7.0] - 2023-10-11 ### Added @@ -74,7 +115,7 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Added ### Fixed - +- Bumped minimum Unity version supported to 2021.3 LTS - Fixed issue where `NetworkClient.OwnedObjects` was not returning any owned objects due to the `NetworkClient.IsConnected` not being properly set. (#2631) - Fixed a crash when calling TrySetParent with a null Transform (#2625) - Fixed issue where a `NetworkTransform` using full precision state updates was losing transform state updates when interpolation was enabled. (#2624) diff --git a/Components/NetworkAnimator.cs b/Components/NetworkAnimator.cs index 538b4b9..79030b7 100644 --- a/Components/NetworkAnimator.cs +++ b/Components/NetworkAnimator.cs @@ -833,8 +833,7 @@ namespace Unity.Netcode.Components stateChangeDetected = true; //Debug.Log($"[Cross-Fade] To-Hash: {nt.fullPathHash} | TI-Duration: ({tt.duration}) | TI-Norm: ({tt.normalizedTime}) | From-Hash: ({m_AnimationHash[layer]}) | SI-FPHash: ({st.fullPathHash}) | SI-Norm: ({st.normalizedTime})"); } - else - if (!tt.anyState && tt.fullPathHash != m_TransitionHash[layer]) + else if (!tt.anyState && tt.fullPathHash != m_TransitionHash[layer]) { // first time in this transition for this layer m_TransitionHash[layer] = tt.fullPathHash; @@ -1053,8 +1052,7 @@ namespace Unity.Netcode.Components BytePacker.WriteValuePacked(writer, valueBool); } } - else - if (cacheValue.Type == AnimationParamEnumWrapper.AnimatorControllerParameterFloat) + else if (cacheValue.Type == AnimationParamEnumWrapper.AnimatorControllerParameterFloat) { var valueFloat = m_Animator.GetFloat(hash); fixed (void* value = cacheValue.Value) diff --git a/Components/NetworkDeltaPosition.cs b/Components/NetworkDeltaPosition.cs index a7ed563..ec5c176 100644 --- a/Components/NetworkDeltaPosition.cs +++ b/Components/NetworkDeltaPosition.cs @@ -23,12 +23,20 @@ namespace Unity.Netcode.Components internal Vector3 DeltaPosition; internal int NetworkTick; + internal bool SynchronizeBase; + + internal bool CollapsedDeltaIntoBase; + /// /// The serialization implementation of /// public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter { HalfVector3.NetworkSerialize(serializer); + if (SynchronizeBase) + { + serializer.SerializeValue(ref CurrentBasePosition); + } } /// @@ -122,6 +130,7 @@ namespace Unity.Netcode.Components [MethodImpl(MethodImplOptions.AggressiveInlining)] public void UpdateFrom(ref Vector3 vector3, int networkTick) { + CollapsedDeltaIntoBase = false; NetworkTick = networkTick; DeltaPosition = (vector3 + PrecisionLossDelta) - CurrentBasePosition; for (int i = 0; i < HalfVector3.Length; i++) @@ -136,6 +145,7 @@ namespace Unity.Netcode.Components CurrentBasePosition[i] += HalfDeltaConvertedBack[i]; HalfDeltaConvertedBack[i] = 0.0f; DeltaPosition[i] = 0.0f; + CollapsedDeltaIntoBase = true; } } } @@ -164,6 +174,8 @@ namespace Unity.Netcode.Components DeltaPosition = Vector3.zero; HalfDeltaConvertedBack = Vector3.zero; HalfVector3 = new HalfVector3(vector3, axisToSynchronize); + SynchronizeBase = false; + CollapsedDeltaIntoBase = false; UpdateFrom(ref vector3, networkTick); } diff --git a/Components/NetworkTransform.cs b/Components/NetworkTransform.cs index 2e0b8d8..c463d39 100644 --- a/Components/NetworkTransform.cs +++ b/Components/NetworkTransform.cs @@ -55,6 +55,29 @@ namespace Unity.Netcode.Components /// internal static bool TrackByStateId; + /// + /// Enabled by default. + /// When set (enabled by default), NetworkTransform will send common state updates using unreliable network delivery + /// to provide a higher tolerance to poor network conditions (especially packet loss). When disabled, all state updates + /// are sent using a reliable fragmented sequenced network delivery. + /// + /// + /// The following more critical state updates are still sent as reliable fragmented sequenced: + /// - The initial synchronization state update + /// - The teleporting state update. + /// - When using half float precision and the `NetworkDeltaPosition` delta exceeds the maximum delta forcing the axis in + /// question to be collapsed into the core base position, this state update will be sent as reliable fragmented sequenced. + /// + /// In order to preserve a continual consistency of axial values when unreliable delta messaging is enabled (due to the + /// possibility of dropping packets), NetworkTransform instances will send 1 axial frame synchronization update per + /// second (only for the axis marked to synchronize are sent as reliable fragmented sequenced) as long as a delta state + /// update had been previously sent. When a NetworkObject is at rest, axial frame synchronization updates are not sent. + /// + [Tooltip("When set, NetworkTransform will send common state updates using unreliable network delivery " + + "to provide a higher tolerance to poor network conditions (especially packet loss). When disabled, all state updates are " + + "sent using reliable fragmented sequenced network delivery.")] + public bool UseUnreliableDeltas = false; + /// /// Data structure used to synchronize the /// @@ -78,6 +101,10 @@ namespace Unity.Netcode.Components private const int k_Synchronization = 0x00008000; private const int k_PositionSlerp = 0x00010000; // Persists between state updates (authority dictates if this is set) private const int k_IsParented = 0x00020000; // When parented and synchronizing, we need to have both lossy and local scale due to varying spawn order + private const int k_SynchBaseHalfFloat = 0x00040000; + private const int k_ReliableSequenced = 0x00080000; + private const int k_UseUnreliableDeltas = 0x00100000; + private const int k_UnreliableFrameSync = 0x00200000; private const int k_TrackStateId = 0x10000000; // (Internal Debugging) When set each state update will contain a state identifier // Stores persistent and state relative flags @@ -134,6 +161,9 @@ namespace Unity.Netcode.Components // Used when tracking by state ID is enabled internal int StateId; + // Set when a state has been explicitly set (i.e. SetState) + internal bool ExplicitSet; + // Used during serialization private FastBufferReader m_Reader; private FastBufferWriter m_Writer; @@ -429,6 +459,28 @@ namespace Unity.Netcode.Components } } + /// + /// Returns whether this state update was a frame synchronization when + /// UseUnreliableDeltas is enabled. When set, the entire transform will + /// be or has been synchronized. + /// + public bool IsUnreliableFrameSync() + { + return UnreliableFrameSync; + } + + /// + /// Returns true if this state was sent with reliable delivery. + /// If false, then it was sent with unreliable delivery. + /// + /// + /// Unreliable delivery will only be used if is set. + /// + public bool IsReliableStateUpdate() + { + return ReliableSequenced; + } + internal bool IsParented { get => GetFlag(k_IsParented); @@ -438,6 +490,42 @@ namespace Unity.Netcode.Components } } + internal bool SynchronizeBaseHalfFloat + { + get => GetFlag(k_SynchBaseHalfFloat); + set + { + SetFlag(value, k_SynchBaseHalfFloat); + } + } + + internal bool ReliableSequenced + { + get => GetFlag(k_ReliableSequenced); + set + { + SetFlag(value, k_ReliableSequenced); + } + } + + internal bool UseUnreliableDeltas + { + get => GetFlag(k_UseUnreliableDeltas); + set + { + SetFlag(value, k_UseUnreliableDeltas); + } + } + + internal bool UnreliableFrameSync + { + get => GetFlag(k_UnreliableFrameSync); + set + { + SetFlag(value, k_UnreliableFrameSync); + } + } + internal bool TrackByStateId { get => GetFlag(k_TrackStateId); @@ -463,7 +551,7 @@ namespace Unity.Netcode.Components internal void ClearBitSetForNextTick() { // Clear everything but flags that should persist between state updates until changed by authority - m_Bitset &= k_InLocalSpaceBit | k_Interpolate | k_UseHalfFloats | k_QuaternionSync | k_QuaternionCompress | k_PositionSlerp; + m_Bitset &= k_InLocalSpaceBit | k_Interpolate | k_UseHalfFloats | k_QuaternionSync | k_QuaternionCompress | k_PositionSlerp | k_UseUnreliableDeltas; IsDirty = false; } @@ -594,6 +682,24 @@ namespace Unity.Netcode.Components { if (isWriting) { + if (UseUnreliableDeltas) + { + // If teleporting, synchronizing, doing an axial frame sync, or using half float precision and we collapsed a delta into the base position + if (IsTeleportingNextFrame || IsSynchronizing || UnreliableFrameSync || (UseHalfFloatPrecision && NetworkDeltaPosition.CollapsedDeltaIntoBase)) + { + // Send the message reliably + ReliableSequenced = true; + } + else + { + ReliableSequenced = false; + } + } + else // If not using UseUnreliableDeltas, then always use reliable fragmented sequenced + { + ReliableSequenced = true; + } + BytePacker.WriteValueBitPacked(m_Writer, m_Bitset); // We use network ticks as opposed to absolute time as the authoritative // side updates on every new tick. @@ -620,6 +726,8 @@ namespace Unity.Netcode.Components { if (UseHalfFloatPrecision) { + NetworkDeltaPosition.SynchronizeBase = SynchronizeBaseHalfFloat; + // Apply which axis should be updated for both write/read (teleporting, synchronizing, or just updating) NetworkDeltaPosition.HalfVector3.AxisToSynchronize[0] = HasPositionX; NetworkDeltaPosition.HalfVector3.AxisToSynchronize[1] = HasPositionY; @@ -1083,7 +1191,7 @@ namespace Unity.Netcode.Components /// Internally used by to keep track of the instance assigned to this /// this derived class instance. /// - protected NetworkManager m_CachedNetworkManager; // Note: we no longer use this and are only keeping it until we decide to deprecate it + protected NetworkManager m_CachedNetworkManager; /// /// Helper method that returns the space relative position of the transform. @@ -1194,7 +1302,17 @@ namespace Unity.Netcode.Components // This represents the most recent local authoritative state. private NetworkTransformState m_LocalAuthoritativeNetworkState; - internal NetworkTransformState LocalAuthoritativeNetworkState => m_LocalAuthoritativeNetworkState; + internal NetworkTransformState LocalAuthoritativeNetworkState + { + get + { + return m_LocalAuthoritativeNetworkState; + } + set + { + m_LocalAuthoritativeNetworkState = value; + } + } private ClientRpcParams m_ClientRpcParams = new ClientRpcParams() { Send = new ClientRpcSendParams() }; private List m_ClientIds = new List() { 0 }; @@ -1294,6 +1412,9 @@ namespace Unity.Netcode.Components return true; } + // For test logging purposes + internal NetworkTransformState SynchronizeState; + /// /// This is invoked when a new client joins (server and client sides) /// Server Side: Serializes as if we were teleporting (everything is sent via NetworkTransformState) @@ -1324,6 +1445,7 @@ namespace Unity.Netcode.Components // for the non-authority side to be able to properly synchronize delta position updates. ApplyTransformToNetworkStateWithInfo(ref synchronizationState, ref transformToCommit, true, targetClientId); synchronizationState.NetworkSerialize(serializer); + SynchronizeState = synchronizationState; } else { @@ -1339,7 +1461,7 @@ namespace Unity.Netcode.Components // Teleport/Fully Initialize based on the state ApplyTeleportingState(synchronizationState); - + SynchronizeState = synchronizationState; m_LocalAuthoritativeNetworkState = synchronizationState; m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = false; m_LocalAuthoritativeNetworkState.IsSynchronizing = false; @@ -1406,14 +1528,16 @@ namespace Unity.Netcode.Components { } - // Tracks the last tick a state update was sent (see further below) - private int m_LastTick; + // Only set if a delta has been sent, this is reset after an axial synch has been sent + // to assure the instance doesn't continue to send axial synchs when an object is at rest. + private bool m_DeltaSynch; + /// /// Authoritative side only /// If there are any transform delta states, this method will synchronize the /// state with all non-authority instances. /// - private void TryCommitTransform(ref Transform transformToCommit, bool synchronize = false) + private void TryCommitTransform(ref Transform transformToCommit, bool synchronize = false, bool settingState = false) { // Only the server or the owner is allowed to commit a transform if (!IsServer && !IsOwner) @@ -1422,32 +1546,50 @@ namespace Unity.Netcode.Components return; } - // If the transform has deltas (returns dirty) then... - if (ApplyTransformToNetworkStateWithInfo(ref m_LocalAuthoritativeNetworkState, ref transformToCommit, synchronize)) + // If the transform has deltas (returns dirty) or if an explicitly set state is pending + if (m_LocalAuthoritativeNetworkState.ExplicitSet || ApplyTransformToNetworkStateWithInfo(ref m_LocalAuthoritativeNetworkState, ref transformToCommit, synchronize)) { m_LocalAuthoritativeNetworkState.LastSerializedSize = m_OldState.LastSerializedSize; - // Make sure our network tick is incremented - if (m_LastTick == m_LocalAuthoritativeNetworkState.NetworkTick && !m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame) + // If the state was explicitly set, then update the network tick to match the locally calculate tick + if (m_LocalAuthoritativeNetworkState.ExplicitSet) { - // When running in authority and a remote client is the owner, the client can hit a perfect window of time where - // it is still on the previous network tick (as a count) but still have had the tick event triggered. - // (This is cheaper than calculating the exact tick each time and only can occur on clients) - if (!IsServer) - { - m_LocalAuthoritativeNetworkState.NetworkTick = m_LocalAuthoritativeNetworkState.NetworkTick + 1; - } - else - { - NetworkLog.LogError($"[NT TICK DUPLICATE] Server already sent an update on tick {m_LastTick} and is attempting to send again on the same network tick!"); - } + m_LocalAuthoritativeNetworkState.NetworkTick = m_CachedNetworkManager.NetworkTickSystem.ServerTime.Tick; } - m_LastTick = m_LocalAuthoritativeNetworkState.NetworkTick; - // Update the state + + // Send the state update UpdateTransformState(); - OnAuthorityPushTransformState(ref m_LocalAuthoritativeNetworkState); + // Mark the last tick and the old state (for next ticks) + m_OldState = m_LocalAuthoritativeNetworkState; + + // Reset the teleport and explicit state flags after we have sent the state update. + // These could be set again in the below OnAuthorityPushTransformState virtual method m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = false; + m_LocalAuthoritativeNetworkState.ExplicitSet = false; + + try + { + // Notify of the pushed state update + OnAuthorityPushTransformState(ref m_LocalAuthoritativeNetworkState); + } + catch (Exception ex) + { + Debug.LogException(ex); + } + + // The below is part of assuring we only send a frame synch, when sending unreliable deltas, if + // we have already sent at least one unreliable delta state update. At this point in the callstack, + // a delta state update has just been sent in the above UpdateTransformState() call and as long as + // we didn't send a frame synch and we are not synchronizing then we know at least one unreliable + // delta has been sent. Under this scenario, we should start checking for this instance's alloted + // frame synch "tick slot". Once we send a frame synch, if no other deltas occur after that + // (i.e. the object is at rest) then we will stop sending frame synch's until the object begins + // moving, rotating, or scaling again. + if (UseUnreliableDeltas && !m_LocalAuthoritativeNetworkState.UnreliableFrameSync && !synchronize) + { + m_DeltaSynch = true; + } } } @@ -1490,11 +1632,13 @@ namespace Unity.Netcode.Components /// internal bool ApplyTransformToNetworkState(ref NetworkTransformState networkState, double dirtyTime, Transform transformToUse) { + m_CachedNetworkManager = NetworkManager; // Apply the interpolate and PostionDeltaCompression flags, otherwise we get false positives whether something changed or not. networkState.UseInterpolation = Interpolate; networkState.QuaternionSync = UseQuaternionSynchronization; networkState.UseHalfFloatPrecision = UseHalfFloatPrecision; networkState.QuaternionCompression = UseQuaternionCompression; + networkState.UseUnreliableDeltas = UseUnreliableDeltas; m_HalfPositionState = new NetworkDeltaPosition(Vector3.zero, 0, math.bool3(SyncPositionX, SyncPositionY, SyncPositionZ)); return ApplyTransformToNetworkStateWithInfo(ref networkState, ref transformToUse); @@ -1506,6 +1650,30 @@ namespace Unity.Netcode.Components [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState networkState, ref Transform transformToUse, bool isSynchronization = false, ulong targetClientId = 0) { + // As long as we are not doing our first synchronization and we are sending unreliable deltas, each + // NetworkTransform will stagger its full transfom synchronization over a 1 second period based on the + // assigned tick slot (m_TickSync). + // More about m_DeltaSynch: + // If we have not sent any deltas since our last frame synch, then this will prevent us from sending + // frame synch's when the object is at rest. If this is false and a state update is detected and sent, + // then it will be set to true and each subsequent tick will do this check to determine if it should + // send a full frame synch. + var isAxisSync = false; + // We compare against the NetworkTickSystem version since ServerTime is set when updating ticks + if (UseUnreliableDeltas && !isSynchronization && m_DeltaSynch && m_NextTickSync <= m_CachedNetworkManager.NetworkTickSystem.ServerTime.Tick) + { + // Increment to the next frame synch tick position for this instance + m_NextTickSync += (int)m_CachedNetworkManager.NetworkConfig.TickRate; + // If we are teleporting, we do not need to send a frame synch for this tick slot + // as a "frame synch" really is effectively just a teleport. + isAxisSync = !networkState.IsTeleportingNextFrame; + // Reset our delta synch trigger so we don't send another frame synch until we + // send at least 1 unreliable state update after this fame synch or teleport + m_DeltaSynch = false; + } + // This is used to determine if we need to send the state update reliably (if we are doing an axial sync) + networkState.UnreliableFrameSync = isAxisSync; + var isTeleportingAndNotSynchronizing = networkState.IsTeleportingNextFrame && !isSynchronization; var isDirty = false; var isPositionDirty = isTeleportingAndNotSynchronizing ? networkState.HasPositionChange : false; @@ -1515,9 +1683,57 @@ namespace Unity.Netcode.Components var position = InLocalSpace ? transformToUse.localPosition : transformToUse.position; var rotAngles = InLocalSpace ? transformToUse.localEulerAngles : transformToUse.eulerAngles; var scale = transformToUse.localScale; - networkState.IsSynchronizing = isSynchronization; + // Check for parenting when synchronizing and/or teleporting + if (isSynchronization || networkState.IsTeleportingNextFrame) + { + // This all has to do with complex nested hierarchies and how it impacts scale + // when set for the first time or teleporting and depends upon whether the + // NetworkObject is parented (or "de-parented") at the same time any scale + // values are applied. + var hasParentNetworkObject = false; + + // If the NetworkObject belonging to this NetworkTransform instance has a parent + // (i.e. this handles nested NetworkTransforms under a parent at some layer above) + if (NetworkObject.transform.parent != null) + { + var parentNetworkObject = NetworkObject.transform.parent.GetComponent(); + + // In-scene placed NetworkObjects parented under a GameObject with no + // NetworkObject preserve their lossyScale when synchronizing. + if (parentNetworkObject == null && NetworkObject.IsSceneObject != false) + { + hasParentNetworkObject = true; + } + else + { + // Or if the relative NetworkObject has a parent NetworkObject + hasParentNetworkObject = parentNetworkObject != null; + } + } + + networkState.IsParented = hasParentNetworkObject; + + // When synchronizing with a parent, world position stays impacts position whether + // the NetworkTransform is using world or local space synchronization. + // WorldPositionStays: (always use world space) + // !WorldPositionStays: (always use local space) + if (isSynchronization) + { + if (NetworkObject.WorldPositionStays()) + { + position = transformToUse.position; + } + else + { + position = transformToUse.localPosition; + } + } + } + + // All of the checks below, up to the delta position checking portion, are to determine if the + // authority changed a property during runtime that requires a full synchronizing. if (InLocalSpace != networkState.InLocalSpace) { networkState.InLocalSpace = InLocalSpace; @@ -1561,23 +1777,31 @@ namespace Unity.Netcode.Components networkState.IsTeleportingNextFrame = true; } + if (UseUnreliableDeltas != networkState.UseUnreliableDeltas) + { + networkState.UseUnreliableDeltas = UseUnreliableDeltas; + isDirty = true; + networkState.IsTeleportingNextFrame = true; + } + + // Begin delta checks against last sent state update if (!UseHalfFloatPrecision) { - if (SyncPositionX && (Mathf.Abs(networkState.PositionX - position.x) >= PositionThreshold || networkState.IsTeleportingNextFrame)) + if (SyncPositionX && (Mathf.Abs(networkState.PositionX - position.x) >= PositionThreshold || networkState.IsTeleportingNextFrame || isAxisSync)) { networkState.PositionX = position.x; networkState.HasPositionX = true; isPositionDirty = true; } - if (SyncPositionY && (Mathf.Abs(networkState.PositionY - position.y) >= PositionThreshold || networkState.IsTeleportingNextFrame)) + if (SyncPositionY && (Mathf.Abs(networkState.PositionY - position.y) >= PositionThreshold || networkState.IsTeleportingNextFrame || isAxisSync)) { networkState.PositionY = position.y; networkState.HasPositionY = true; isPositionDirty = true; } - if (SyncPositionZ && (Mathf.Abs(networkState.PositionZ - position.z) >= PositionThreshold || networkState.IsTeleportingNextFrame)) + if (SyncPositionZ && (Mathf.Abs(networkState.PositionZ - position.z) >= PositionThreshold || networkState.IsTeleportingNextFrame || isAxisSync)) { networkState.PositionZ = position.z; networkState.HasPositionZ = true; @@ -1587,7 +1811,11 @@ namespace Unity.Netcode.Components else if (SynchronizePosition) { // If we are teleporting then we can skip the delta threshold check - isPositionDirty = networkState.IsTeleportingNextFrame; + isPositionDirty = networkState.IsTeleportingNextFrame || isAxisSync; + if (m_HalfFloatTargetTickOwnership > m_CachedNetworkManager.ServerTime.Tick) + { + isPositionDirty = true; + } // For NetworkDeltaPosition, if any axial value is dirty then we always send a full update if (!isPositionDirty) @@ -1628,6 +1856,16 @@ namespace Unity.Netcode.Components } networkState.NetworkDeltaPosition = m_HalfPositionState; + + // If ownership offset is greater or we are doing an axial synchronization then synchronize the base position + if ((m_HalfFloatTargetTickOwnership > m_CachedNetworkManager.ServerTime.Tick || isAxisSync) && !networkState.IsTeleportingNextFrame) + { + networkState.SynchronizeBaseHalfFloat = true; + } + else + { + networkState.SynchronizeBaseHalfFloat = UseUnreliableDeltas ? m_HalfPositionState.CollapsedDeltaIntoBase : false; + } } else // If synchronizing is set, then use the current full position value on the server side { @@ -1679,21 +1917,21 @@ namespace Unity.Netcode.Components if (!UseQuaternionSynchronization) { - if (SyncRotAngleX && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleX, rotAngles.x)) >= RotAngleThreshold || networkState.IsTeleportingNextFrame)) + if (SyncRotAngleX && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleX, rotAngles.x)) >= RotAngleThreshold || networkState.IsTeleportingNextFrame || isAxisSync)) { networkState.RotAngleX = rotAngles.x; networkState.HasRotAngleX = true; isRotationDirty = true; } - if (SyncRotAngleY && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleY, rotAngles.y)) >= RotAngleThreshold || networkState.IsTeleportingNextFrame)) + if (SyncRotAngleY && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleY, rotAngles.y)) >= RotAngleThreshold || networkState.IsTeleportingNextFrame || isAxisSync)) { networkState.RotAngleY = rotAngles.y; networkState.HasRotAngleY = true; isRotationDirty = true; } - if (SyncRotAngleZ && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleZ, rotAngles.z)) >= RotAngleThreshold || networkState.IsTeleportingNextFrame)) + if (SyncRotAngleZ && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleZ, rotAngles.z)) >= RotAngleThreshold || networkState.IsTeleportingNextFrame || isAxisSync)) { networkState.RotAngleZ = rotAngles.z; networkState.HasRotAngleZ = true; @@ -1703,7 +1941,7 @@ namespace Unity.Netcode.Components else if (SynchronizeRotation) { // If we are teleporting then we can skip the delta threshold check - isRotationDirty = networkState.IsTeleportingNextFrame; + isRotationDirty = networkState.IsTeleportingNextFrame || isAxisSync; // For quaternion synchronization, if one angle is dirty we send a full update if (!isRotationDirty) { @@ -1729,34 +1967,9 @@ namespace Unity.Netcode.Components // For scale, we need to check for parenting when synchronizing and/or teleporting if (isSynchronization || networkState.IsTeleportingNextFrame) { - // This all has to do with complex nested hierarchies and how it impacts scale - // when set for the first time and depending upon whether the NetworkObject is parented - // (or not parented) at the time the scale values are applied. - var hasParentNetworkObject = false; - - // If the NetworkObject belonging to this NetworkTransform instance has a parent - // (i.e. this handles nested NetworkTransforms under a parent at some layer above) - if (NetworkObject.transform.parent != null) - { - var parentNetworkObject = NetworkObject.transform.parent.GetComponent(); - - // In-scene placed NetworkObjects parented under a GameObject with no - // NetworkObject preserve their lossyScale when synchronizing. - if (parentNetworkObject == null && NetworkObject.IsSceneObject != false) - { - hasParentNetworkObject = true; - } - else - { - // Or if the relative NetworkObject has a parent NetworkObject - hasParentNetworkObject = parentNetworkObject != null; - } - } - - networkState.IsParented = hasParentNetworkObject; // If we are synchronizing and the associated NetworkObject has a parent then we want to send the - // LossyScale if the NetworkObject has a parent since NetworkObject spawn order is not guaranteed - if (hasParentNetworkObject) + // LossyScale if the NetworkObject has a parent since NetworkObject spawn order is not guaranteed + if (networkState.IsParented) { networkState.LossyScale = transform.lossyScale; } @@ -1767,21 +1980,21 @@ namespace Unity.Netcode.Components { if (!UseHalfFloatPrecision) { - if (SyncScaleX && (Mathf.Abs(networkState.ScaleX - scale.x) >= ScaleThreshold || networkState.IsTeleportingNextFrame)) + if (SyncScaleX && (Mathf.Abs(networkState.ScaleX - scale.x) >= ScaleThreshold || networkState.IsTeleportingNextFrame || isAxisSync)) { networkState.ScaleX = scale.x; networkState.HasScaleX = true; isScaleDirty = true; } - if (SyncScaleY && (Mathf.Abs(networkState.ScaleY - scale.y) >= ScaleThreshold || networkState.IsTeleportingNextFrame)) + if (SyncScaleY && (Mathf.Abs(networkState.ScaleY - scale.y) >= ScaleThreshold || networkState.IsTeleportingNextFrame || isAxisSync)) { networkState.ScaleY = scale.y; networkState.HasScaleY = true; isScaleDirty = true; } - if (SyncScaleZ && (Mathf.Abs(networkState.ScaleZ - scale.z) >= ScaleThreshold || networkState.IsTeleportingNextFrame)) + if (SyncScaleZ && (Mathf.Abs(networkState.ScaleZ - scale.z) >= ScaleThreshold || networkState.IsTeleportingNextFrame || isAxisSync)) { networkState.ScaleZ = scale.z; networkState.HasScaleZ = true; @@ -1793,7 +2006,7 @@ namespace Unity.Netcode.Components var previousScale = networkState.Scale; for (int i = 0; i < 3; i++) { - if (Mathf.Abs(scale[i] - previousScale[i]) >= ScaleThreshold || networkState.IsTeleportingNextFrame) + if (Mathf.Abs(scale[i] - previousScale[i]) >= ScaleThreshold || networkState.IsTeleportingNextFrame || isAxisSync) { isScaleDirty = true; networkState.Scale[i] = scale[i]; @@ -1828,7 +2041,8 @@ namespace Unity.Netcode.Components // NetworkManager if (enabled) { - networkState.NetworkTick = NetworkManager.ServerTime.Tick; + // We use the NetworkTickSystem version since ServerTime is set when updating ticks + networkState.NetworkTick = m_CachedNetworkManager.NetworkTickSystem.ServerTime.Tick; } } @@ -1857,6 +2071,8 @@ namespace Unity.Netcode.Components UseHalfFloatPrecision = networkState.UseHalfFloatPrecision; UseQuaternionSynchronization = networkState.QuaternionSync; UseQuaternionCompression = networkState.QuaternionCompression; + UseUnreliableDeltas = networkState.UseUnreliableDeltas; + if (SlerpPosition != networkState.UsePositionSlerp) { SlerpPosition = networkState.UsePositionSlerp; @@ -1922,7 +2138,7 @@ namespace Unity.Netcode.Components { if (networkState.HasPositionChange && SynchronizePosition) { - adjustedPosition = networkState.CurrentPosition; + adjustedPosition = m_TargetPosition; } if (networkState.HasScaleChange && SynchronizeScale) @@ -2056,7 +2272,6 @@ namespace Unity.Netcode.Components { currentPosition.z = newState.PositionZ; } - UpdatePositionInterpolator(currentPosition, sentTime, true); } else { @@ -2067,6 +2282,7 @@ namespace Unity.Netcode.Components // offset or not. This is specific to owner authoritative mode on the owner side only if (isSynchronization) { + // Need to use NetworkManager vs m_CachedNetworkManager here since we are yet to be spawned if (ShouldSynchronizeHalfFloat(NetworkManager.LocalClientId)) { m_HalfPositionState.HalfVector3.Axis = newState.NetworkDeltaPosition.HalfVector3.Axis; @@ -2086,12 +2302,6 @@ namespace Unity.Netcode.Components // set the current position to the state's current position currentPosition = newState.CurrentPosition; } - - if (Interpolate) - { - UpdatePositionInterpolator(currentPosition, sentTime, true); - } - } m_CurrentPosition = currentPosition; @@ -2106,6 +2316,11 @@ namespace Unity.Netcode.Components { transform.position = currentPosition; } + + if (Interpolate) + { + UpdatePositionInterpolator(currentPosition, sentTime, true); + } } if (newState.HasScaleChange) @@ -2123,7 +2338,6 @@ namespace Unity.Netcode.Components } } - if (UseHalfFloatPrecision) { currentScale = shouldUseLossy ? newState.LossyScale : newState.Scale; @@ -2149,10 +2363,14 @@ namespace Unity.Netcode.Components m_CurrentScale = currentScale; m_TargetScale = currentScale; - m_ScaleInterpolator.ResetTo(currentScale, sentTime); // Apply the adjusted scale transform.localScale = currentScale; + + if (Interpolate) + { + m_ScaleInterpolator.ResetTo(currentScale, sentTime); + } } if (newState.HasRotAngleChange) @@ -2183,7 +2401,6 @@ namespace Unity.Netcode.Components m_CurrentRotation = currentRotation; m_TargetRotation = currentRotation.eulerAngles; - m_RotationInterpolator.ResetTo(currentRotation, sentTime); if (InLocalSpace) { @@ -2193,6 +2410,11 @@ namespace Unity.Netcode.Components { transform.rotation = currentRotation; } + + if (Interpolate) + { + m_RotationInterpolator.ResetTo(currentRotation, sentTime); + } } // Add log after to applying the update if AddLogEntry is defined @@ -2216,6 +2438,8 @@ namespace Unity.Netcode.Components UseQuaternionSynchronization = newState.QuaternionSync; UseQuaternionCompression = newState.QuaternionCompression; UseHalfFloatPrecision = newState.UseHalfFloatPrecision; + UseUnreliableDeltas = newState.UseUnreliableDeltas; + if (SlerpPosition != newState.UsePositionSlerp) { SlerpPosition = newState.UsePositionSlerp; @@ -2236,11 +2460,21 @@ namespace Unity.Netcode.Components // Only if using half float precision and our position had changed last update then if (UseHalfFloatPrecision && m_LocalAuthoritativeNetworkState.HasPositionChange) { - // assure our local NetworkDeltaPosition state is updated - m_HalfPositionState.HalfVector3.Axis = m_LocalAuthoritativeNetworkState.NetworkDeltaPosition.HalfVector3.Axis; - // and update our target position + if (m_LocalAuthoritativeNetworkState.SynchronizeBaseHalfFloat) + { + m_HalfPositionState = m_LocalAuthoritativeNetworkState.NetworkDeltaPosition; + } + else + { + // assure our local NetworkDeltaPosition state is updated + m_HalfPositionState.HalfVector3.Axis = m_LocalAuthoritativeNetworkState.NetworkDeltaPosition.HalfVector3.Axis; + m_LocalAuthoritativeNetworkState.NetworkDeltaPosition.CurrentBasePosition = m_HalfPositionState.CurrentBasePosition; + + // This is to assure when you get the position of the state it is the correct position + m_LocalAuthoritativeNetworkState.NetworkDeltaPosition.ToVector3(0); + } + // Update our target position m_TargetPosition = m_HalfPositionState.ToVector3(newState.NetworkTick); - m_LocalAuthoritativeNetworkState.NetworkDeltaPosition.CurrentBasePosition = m_HalfPositionState.CurrentBasePosition; m_LocalAuthoritativeNetworkState.CurrentPosition = m_TargetPosition; } @@ -2368,14 +2602,24 @@ namespace Unity.Netcode.Components return; } + // If we are using UseUnreliableDeltas and our old state tick is greater than the new state tick, + // then just ignore the newstate. This avoids any scenario where the new state is out of order + // from the old state (with unreliable traffic and/or mixed unreliable and reliable) + if (UseUnreliableDeltas && oldState.NetworkTick > newState.NetworkTick && !newState.IsTeleportingNextFrame && !newState.UnreliableFrameSync) + { + return; + } + // Get the time when this new state was sent - newState.SentTime = new NetworkTime(NetworkManager.NetworkConfig.TickRate, newState.NetworkTick).Time; + newState.SentTime = new NetworkTime(m_CachedNetworkManager.NetworkConfig.TickRate, newState.NetworkTick).Time; // Apply the new state ApplyUpdatedState(newState); // Provide notifications when the state has been updated - OnNetworkTransformStateUpdated(ref oldState, ref newState); + // We use the m_LocalAuthoritativeNetworkState because newState has been applied and adjustments could have + // been made (i.e. half float precision position values will have been updated) + OnNetworkTransformStateUpdated(ref oldState, ref m_LocalAuthoritativeNetworkState); } /// @@ -2455,7 +2699,7 @@ namespace Unity.Netcode.Components internal void OnUpdateAuthoritativeState(ref Transform transformSource) { // If our replicated state is not dirty and our local authority state is dirty, clear it. - if (m_LocalAuthoritativeNetworkState.IsDirty && !m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame) + if (!m_LocalAuthoritativeNetworkState.ExplicitSet && m_LocalAuthoritativeNetworkState.IsDirty && !m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame) { // Now clear our bitset and prepare for next network tick state update m_LocalAuthoritativeNetworkState.ClearBitSetForNextTick(); @@ -2491,13 +2735,10 @@ namespace Unity.Netcode.Components m_CurrentPosition = GetSpaceRelativePosition(); m_TargetPosition = GetSpaceRelativePosition(); } - else + else // If we are no longer authority, unsubscribe to the tick event + if (NetworkManager != null && NetworkManager.NetworkTickSystem != null) { - // If we are no longer authority, unsubscribe to the tick event - if (NetworkManager != null && NetworkManager.NetworkTickSystem != null) - { - NetworkManager.NetworkTickSystem.Tick -= NetworkTickSystem_Tick; - } + NetworkManager.NetworkTickSystem.Tick -= NetworkTickSystem_Tick; } } @@ -2507,22 +2748,28 @@ namespace Unity.Netcode.Components /////////////////////////////////////////////////////////////// // NOTE: Legacy and no longer used (candidates for deprecation) m_CachedIsServer = IsServer; - m_CachedNetworkManager = NetworkManager; /////////////////////////////////////////////////////////////// + // Started using this again to avoid the getter processing cost of NetworkBehaviour.NetworkManager + m_CachedNetworkManager = NetworkManager; + // Register a custom named message specifically for this instance m_MessageName = $"NTU_{NetworkObjectId}_{NetworkBehaviourId}"; - NetworkManager.CustomMessagingManager.RegisterNamedMessageHandler(m_MessageName, TransformStateUpdate); + m_CachedNetworkManager.CustomMessagingManager.RegisterNamedMessageHandler(m_MessageName, TransformStateUpdate); Initialize(); } /// public override void OnNetworkDespawn() { + // During destroy, use NetworkBehaviour.NetworkManager as opposed to m_CachedNetworkManager if (!NetworkManager.ShutdownInProgress && NetworkManager.CustomMessagingManager != null) { NetworkManager.CustomMessagingManager.UnregisterNamedMessageHandler(m_MessageName); } + + DeregisterForTickUpdate(this); + CanCommitToTransform = false; if (NetworkManager != null && NetworkManager.NetworkTickSystem != null) { @@ -2533,6 +2780,7 @@ namespace Unity.Netcode.Components /// public override void OnDestroy() { + // During destroy, use NetworkBehaviour.NetworkManager as opposed to m_CachedNetworkManager if (NetworkManager != null && NetworkManager.NetworkTickSystem != null) { NetworkManager.NetworkTickSystem.Tick -= NetworkTickSystem_Tick; @@ -2556,9 +2804,9 @@ namespace Unity.Netcode.Components protected override void OnOwnershipChanged(ulong previous, ulong current) { // If we were the previous owner or the newly assigned owner then reinitialize - if (current == NetworkManager.LocalClientId || previous == NetworkManager.LocalClientId) + if (current == m_CachedNetworkManager.LocalClientId || previous == m_CachedNetworkManager.LocalClientId) { - Initialize(); + InternalInitialization(true); } base.OnOwnershipChanged(previous, current); } @@ -2585,11 +2833,13 @@ namespace Unity.Netcode.Components } + private int m_HalfFloatTargetTickOwnership; /// - /// Initializes NetworkTransform when spawned and ownership changes. + /// The internal initialzation method to allow for internal API adjustments /// - protected void Initialize() + /// + private void InternalInitialization(bool isOwnershipChange = false) { if (!IsSpawned) { @@ -2604,25 +2854,26 @@ namespace Unity.Netcode.Components { if (UseHalfFloatPrecision) { - m_HalfPositionState = new NetworkDeltaPosition(currentPosition, NetworkManager.NetworkTickSystem.ServerTime.Tick, math.bool3(SyncPositionX, SyncPositionY, SyncPositionZ)); + m_HalfPositionState = new NetworkDeltaPosition(currentPosition, m_CachedNetworkManager.ServerTime.Tick, math.bool3(SyncPositionX, SyncPositionY, SyncPositionZ)); } m_CurrentPosition = currentPosition; m_TargetPosition = currentPosition; - // Authority only updates once per network tick - NetworkManager.NetworkTickSystem.Tick -= NetworkTickSystem_Tick; - NetworkManager.NetworkTickSystem.Tick += NetworkTickSystem_Tick; - // Teleport to current position - SetStateInternal(currentPosition, currentRotation, transform.localScale, true); + RegisterForTickUpdate(this); + + m_LocalAuthoritativeNetworkState.SynchronizeBaseHalfFloat = false; + if (UseHalfFloatPrecision && isOwnershipChange && !IsServerAuthoritative() && Interpolate) + { + m_HalfFloatTargetTickOwnership = m_CachedNetworkManager.ServerTime.Tick; + } } else { - - // Assure we no longer subscribe to the tick event - NetworkManager.NetworkTickSystem.Tick -= NetworkTickSystem_Tick; + // Remove this instance from the tick update + DeregisterForTickUpdate(this); ResetInterpolatedStateToCurrentAuthoritativeState(); - + m_LocalAuthoritativeNetworkState.SynchronizeBaseHalfFloat = false; m_CurrentPosition = currentPosition; m_TargetPosition = currentPosition; m_CurrentScale = transform.localScale; @@ -2640,6 +2891,14 @@ namespace Unity.Netcode.Components } } + /// + /// Initializes NetworkTransform when spawned and ownership changes. + /// + protected void Initialize() + { + InternalInitialization(); + } + /// /// /// When a parent changes, non-authoritative instances should: @@ -2653,16 +2912,23 @@ namespace Unity.Netcode.Components // Only if we are not authority if (!CanCommitToTransform) { - m_CurrentPosition = GetSpaceRelativePosition(); + m_TargetPosition = m_CurrentPosition = GetSpaceRelativePosition(); m_CurrentRotation = GetSpaceRelativeRotation(); - m_CurrentScale = GetScale(); - m_ScaleInterpolator.Clear(); - m_PositionInterpolator.Clear(); - m_RotationInterpolator.Clear(); - var tempTime = new NetworkTime(NetworkManager.NetworkConfig.TickRate, NetworkManager.ServerTime.Tick).Time; - UpdatePositionInterpolator(m_CurrentPosition, tempTime, true); - m_ScaleInterpolator.ResetTo(m_CurrentScale, tempTime); - m_RotationInterpolator.ResetTo(m_CurrentRotation, tempTime); + m_TargetRotation = m_CurrentRotation.eulerAngles; + m_TargetScale = m_CurrentScale = GetScale(); + + if (Interpolate) + { + m_ScaleInterpolator.Clear(); + m_PositionInterpolator.Clear(); + m_RotationInterpolator.Clear(); + + // Always use NetworkManager here as this can be invoked prior to spawning + var tempTime = new NetworkTime(NetworkManager.NetworkConfig.TickRate, NetworkManager.ServerTime.Tick).Time; + UpdatePositionInterpolator(m_CurrentPosition, tempTime, true); + m_ScaleInterpolator.ResetTo(m_CurrentScale, tempTime); + m_RotationInterpolator.ResetTo(m_CurrentRotation, tempTime); + } } base.OnNetworkObjectParentChanged(parentNetworkObject); } @@ -2739,8 +3005,27 @@ namespace Unity.Netcode.Components } transform.localScale = scale; m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = shouldTeleport; + var transformToCommit = transform; - TryCommitTransform(ref transformToCommit); + + // Explicit set states are cumulative during a fractional tick period of time (i.e. each SetState invocation will + // update the axial deltas to whatever changes are applied). As such, we need to preserve the dirty and explicit + // state flags. + var stateWasDirty = m_LocalAuthoritativeNetworkState.IsDirty; + var explicitState = m_LocalAuthoritativeNetworkState.ExplicitSet; + + // Apply any delta states to the m_LocalAuthoritativeNetworkState + var isDirty = ApplyTransformToNetworkStateWithInfo(ref m_LocalAuthoritativeNetworkState, ref transformToCommit); + + // If we were dirty and the explicit state was set (prior to checking for deltas) or the current explicit state is dirty, + // then we set the explicit state flag. + m_LocalAuthoritativeNetworkState.ExplicitSet = (stateWasDirty && explicitState) || isDirty; + + // If the current explicit set flag is set, then we are dirty. This assures if more than one explicit set state is invoked + // in between a fractional tick period and the current explicit set state did not find any deltas that we preserve any + // previous dirty state. + m_LocalAuthoritativeNetworkState.IsDirty = m_LocalAuthoritativeNetworkState.ExplicitSet; + } /// @@ -2773,24 +3058,14 @@ namespace Unity.Netcode.Components SetStateInternal(pos, rot, scale, shouldTeleport); } - /// - /// - /// If you override this method, be sure that: - /// - Non-authority always invokes this base class method. - /// - protected virtual void Update() - { - // If not spawned or this instance has authority, exit early - if (!IsSpawned || CanCommitToTransform) - { - return; - } + private void UpdateInterpolation() + { // Non-Authority if (Interpolate) { - var serverTime = NetworkManager.ServerTime; - var cachedDeltaTime = NetworkManager.RealTimeProvider.DeltaTime; + var serverTime = m_CachedNetworkManager.ServerTime; + var cachedDeltaTime = m_CachedNetworkManager.RealTimeProvider.DeltaTime; var cachedServerTime = serverTime.Time; // With owner authoritative mode, non-authority clients can lag behind @@ -2819,6 +3094,23 @@ namespace Unity.Netcode.Components m_ScaleInterpolator.Update(cachedDeltaTime, cachedRenderTime, cachedServerTime); } } + } + + /// + /// + /// If you override this method, be sure that: + /// - Non-authority always invokes this base class method. + /// + protected virtual void Update() + { + // If not spawned or this instance has authority, exit early + if (!IsSpawned || CanCommitToTransform) + { + return; + } + + // Non-Authority + UpdateInterpolation(); // Apply the current authoritative state ApplyAuthoritativeState(); @@ -2870,24 +3162,39 @@ namespace Unity.Netcode.Components /// serialzied private void TransformStateUpdate(ulong senderId, FastBufferReader messagePayload) { - if (!OnIsServerAuthoritative() && IsServer && OwnerClientId == NetworkManager.ServerClientId) + var ownerAuthoritativeServerSide = !OnIsServerAuthoritative() && IsServer; + if (ownerAuthoritativeServerSide && OwnerClientId == NetworkManager.ServerClientId) { // Ownership must have changed, ignore any additional pending messages that might have // come from a previous owner client. return; } - // Forward owner authoritative messages before doing anything else - if (IsServer && !OnIsServerAuthoritative()) - { - ForwardStateUpdateMessage(messagePayload); - } // Store the previous/old state m_OldState = m_LocalAuthoritativeNetworkState; - // Deserialize the message + // Save the current payload stream position + var currentPosition = messagePayload.Position; + + // Deserialize the message (and determine network delivery) messagePayload.ReadNetworkSerializableInPlace(ref m_LocalAuthoritativeNetworkState); + // Rewind back prior to serialization + messagePayload.Seek(currentPosition); + + // Get the network delivery method used to send this state update + var networkDelivery = m_LocalAuthoritativeNetworkState.ReliableSequenced ? NetworkDelivery.ReliableSequenced : NetworkDelivery.UnreliableSequenced; + + // Forward owner authoritative messages before doing anything else + if (ownerAuthoritativeServerSide) + { + // Forward the state update if there are any remote clients to foward it to + if (m_CachedNetworkManager.ConnectionManager.ConnectedClientsList.Count > (IsHost ? 2 : 1)) + { + ForwardStateUpdateMessage(messagePayload, networkDelivery); + } + } + // Apply the message OnNetworkStateChanged(m_OldState, m_LocalAuthoritativeNetworkState); } @@ -2896,7 +3203,7 @@ namespace Unity.Netcode.Components /// Forwards owner authoritative state updates when received by the server /// /// the owner state message payload - private unsafe void ForwardStateUpdateMessage(FastBufferReader messagePayload) + private unsafe void ForwardStateUpdateMessage(FastBufferReader messagePayload, NetworkDelivery networkDelivery) { var serverAuthoritative = OnIsServerAuthoritative(); var currentPosition = messagePayload.Position; @@ -2906,15 +3213,15 @@ namespace Unity.Netcode.Components { writer.WriteBytesSafe(messagePayload.GetUnsafePtr(), messageSize, currentPosition); - var clientCount = NetworkManager.ConnectionManager.ConnectedClientsList.Count; + var clientCount = m_CachedNetworkManager.ConnectionManager.ConnectedClientsList.Count; for (int i = 0; i < clientCount; i++) { - var clientId = NetworkManager.ConnectionManager.ConnectedClientsList[i].ClientId; + var clientId = m_CachedNetworkManager.ConnectionManager.ConnectedClientsList[i].ClientId; if (NetworkManager.ServerClientId == clientId || (!serverAuthoritative && clientId == OwnerClientId)) { continue; } - NetworkManager.CustomMessagingManager.SendNamedMessage(m_MessageName, clientId, writer); + m_CachedNetworkManager.CustomMessagingManager.SendNamedMessage(m_MessageName, clientId, writer, networkDelivery); } } messagePayload.Seek(currentPosition); @@ -2925,7 +3232,7 @@ namespace Unity.Netcode.Components /// private void UpdateTransformState() { - if (NetworkManager.ShutdownInProgress) + if (m_CachedNetworkManager.ShutdownInProgress) { return; } @@ -2939,31 +3246,151 @@ namespace Unity.Netcode.Components { Debug.LogError($"Owner authoritative {nameof(NetworkTransform)} can only be updated by the owner!"); } - var customMessageManager = NetworkManager.CustomMessagingManager; + var customMessageManager = m_CachedNetworkManager.CustomMessagingManager; var writer = new FastBufferWriter(128, Allocator.Temp); + // Determine what network delivery method to use: + // When to send reliable packets: + // - If UsUnrealiable is not enabled + // - If teleporting or synchronizing + // - If sending an UnrealiableFrameSync or synchronizing the base position of the NetworkDeltaPosition + var networkDelivery = !UseUnreliableDeltas | m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame | m_LocalAuthoritativeNetworkState.IsSynchronizing + | m_LocalAuthoritativeNetworkState.UnreliableFrameSync | m_LocalAuthoritativeNetworkState.SynchronizeBaseHalfFloat + ? NetworkDelivery.ReliableSequenced : NetworkDelivery.UnreliableSequenced; + using (writer) { writer.WriteNetworkSerializable(m_LocalAuthoritativeNetworkState); // Server-host always sends updates to all clients (but itself) if (IsServer) { - var clientCount = NetworkManager.ConnectionManager.ConnectedClientsList.Count; + var clientCount = m_CachedNetworkManager.ConnectionManager.ConnectedClientsList.Count; for (int i = 0; i < clientCount; i++) { - var clientId = NetworkManager.ConnectionManager.ConnectedClientsList[i].ClientId; + var clientId = m_CachedNetworkManager.ConnectionManager.ConnectedClientsList[i].ClientId; if (NetworkManager.ServerClientId == clientId) { continue; } - customMessageManager.SendNamedMessage(m_MessageName, clientId, writer); + customMessageManager.SendNamedMessage(m_MessageName, clientId, writer, networkDelivery); } } else { // Clients (owner authoritative) send messages to the server-host - customMessageManager.SendNamedMessage(m_MessageName, NetworkManager.ServerClientId, writer); + customMessageManager.SendNamedMessage(m_MessageName, NetworkManager.ServerClientId, writer, networkDelivery); + } + } + } + + + private static Dictionary s_NetworkTickRegistration = new Dictionary(); + + private static void RemoveTickUpdate(NetworkManager networkManager) + { + s_NetworkTickRegistration.Remove(networkManager); + } + + /// + /// Having the tick update once and cycling through registered instances to update is evidently less processor + /// intensive than having each instance subscribe and update individually. + /// + private class NetworkTransformTickRegistration + { + private Action m_NetworkTickUpdate; + private NetworkManager m_NetworkManager; + public HashSet NetworkTransforms = new HashSet(); + + private int m_LastTick; + private void OnNetworkManagerStopped(bool value) + { + Remove(); + } + + public void Remove() + { + m_NetworkManager.NetworkTickSystem.Tick -= m_NetworkTickUpdate; + m_NetworkTickUpdate = null; + NetworkTransforms.Clear(); + RemoveTickUpdate(m_NetworkManager); + } + + /// + /// Invoked once per network tick, this will update any registered + /// authority instances. + /// + private void TickUpdate() + { + // TODO FIX: The local NetworkTickSystem can invoke with the same network tick as before + if (m_NetworkManager.ServerTime.Tick <= m_LastTick) + { + return; + } + foreach (var networkTransform in NetworkTransforms) + { + if (networkTransform.IsSpawned) + { + networkTransform.NetworkTickSystem_Tick(); + } + } + m_LastTick = m_NetworkManager.ServerTime.Tick; + } + + public NetworkTransformTickRegistration(NetworkManager networkManager) + { + m_NetworkManager = networkManager; + m_NetworkTickUpdate = new Action(TickUpdate); + networkManager.NetworkTickSystem.Tick += m_NetworkTickUpdate; + if (networkManager.IsServer) + { + networkManager.OnServerStopped += OnNetworkManagerStopped; + } + else + { + networkManager.OnClientStopped += OnNetworkManagerStopped; + } + } + } + private static int s_TickSynchPosition; + private int m_NextTickSync; + + internal void RegisterForTickSynchronization() + { + s_TickSynchPosition++; + m_NextTickSync = NetworkManager.ServerTime.Tick + (s_TickSynchPosition % (int)NetworkManager.NetworkConfig.TickRate); + } + + /// + /// Will register the NetworkTransform instance for the single tick update entry point. + /// If a NetworkTransformTickRegistration has not yet been registered for the NetworkManager + /// instance, then create an entry. + /// + /// + private static void RegisterForTickUpdate(NetworkTransform networkTransform) + { + if (!s_NetworkTickRegistration.ContainsKey(networkTransform.NetworkManager)) + { + s_NetworkTickRegistration.Add(networkTransform.NetworkManager, new NetworkTransformTickRegistration(networkTransform.NetworkManager)); + } + networkTransform.RegisterForTickSynchronization(); + s_NetworkTickRegistration[networkTransform.NetworkManager].NetworkTransforms.Add(networkTransform); + } + + /// + /// If a NetworkTransformTickRegistration exists for the NetworkManager instance, then this will + /// remove the NetworkTransform instance from the single tick update entry point. + /// + /// + private static void DeregisterForTickUpdate(NetworkTransform networkTransform) + { + if (s_NetworkTickRegistration.ContainsKey(networkTransform.NetworkManager)) + { + s_NetworkTickRegistration[networkTransform.NetworkManager].NetworkTransforms.Remove(networkTransform); + if (s_NetworkTickRegistration[networkTransform.NetworkManager].NetworkTransforms.Count == 0) + { + var registrationEntry = s_NetworkTickRegistration[networkTransform.NetworkManager]; + registrationEntry.Remove(); } } } diff --git a/Documentation~/index.md b/Documentation~/index.md index 31e766f..bd5e331 100644 --- a/Documentation~/index.md +++ b/Documentation~/index.md @@ -16,7 +16,7 @@ See guides below to install Unity Netcode for GameObjects, set up your project, ## Requirements Netcode for GameObjects targets the following Unity versions: -- Unity 2020.3, 2021.1, 2021.2 and 2021.3 +- Unity 2021.3 (LTS), 2022.3 (LTS) and 2023.2 On the following runtime platforms: - Windows, MacOS, and Linux diff --git a/Editor/CodeGen/CodeGenHelpers.cs b/Editor/CodeGen/CodeGenHelpers.cs index 1bb3ab1..6368e5e 100644 --- a/Editor/CodeGen/CodeGenHelpers.cs +++ b/Editor/CodeGen/CodeGenHelpers.cs @@ -26,8 +26,10 @@ namespace Unity.Netcode.Editor.CodeGen public static readonly string INetworkMessage_FullName = typeof(INetworkMessage).FullName; public static readonly string ServerRpcAttribute_FullName = typeof(ServerRpcAttribute).FullName; public static readonly string ClientRpcAttribute_FullName = typeof(ClientRpcAttribute).FullName; + public static readonly string RpcAttribute_FullName = typeof(RpcAttribute).FullName; public static readonly string ServerRpcParams_FullName = typeof(ServerRpcParams).FullName; public static readonly string ClientRpcParams_FullName = typeof(ClientRpcParams).FullName; + public static readonly string RpcParams_FullName = typeof(RpcParams).FullName; public static readonly string ClientRpcSendParams_FullName = typeof(ClientRpcSendParams).FullName; public static readonly string ClientRpcReceiveParams_FullName = typeof(ClientRpcReceiveParams).FullName; public static readonly string ServerRpcSendParams_FullName = typeof(ServerRpcSendParams).FullName; diff --git a/Editor/CodeGen/NetworkBehaviourILPP.cs b/Editor/CodeGen/NetworkBehaviourILPP.cs index 0f33294..cd2da52 100644 --- a/Editor/CodeGen/NetworkBehaviourILPP.cs +++ b/Editor/CodeGen/NetworkBehaviourILPP.cs @@ -363,11 +363,14 @@ namespace Unity.Netcode.Editor.CodeGen private FieldReference m_NetworkManager_LogLevel_FieldRef; private MethodReference m_NetworkBehaviour___registerRpc_MethodRef; private TypeReference m_NetworkBehaviour_TypeRef; + private TypeReference m_AttributeParamsType_TypeRef; private TypeReference m_NetworkVariableBase_TypeRef; private MethodReference m_NetworkVariableBase_Initialize_MethodRef; private MethodReference m_NetworkBehaviour___nameNetworkVariable_MethodRef; private MethodReference m_NetworkBehaviour_beginSendServerRpc_MethodRef; private MethodReference m_NetworkBehaviour_endSendServerRpc_MethodRef; + private MethodReference m_NetworkBehaviour_beginSendRpc_MethodRef; + private MethodReference m_NetworkBehaviour_endSendRpc_MethodRef; private MethodReference m_NetworkBehaviour_beginSendClientRpc_MethodRef; private MethodReference m_NetworkBehaviour_endSendClientRpc_MethodRef; private FieldReference m_NetworkBehaviour_rpc_exec_stage_FieldRef; @@ -378,9 +381,13 @@ namespace Unity.Netcode.Editor.CodeGen private TypeReference m_RpcParams_TypeRef; private FieldReference m_RpcParams_Server_FieldRef; private FieldReference m_RpcParams_Client_FieldRef; + private FieldReference m_RpcParams_Ext_FieldRef; private TypeReference m_ServerRpcParams_TypeRef; private FieldReference m_ServerRpcParams_Receive_FieldRef; private FieldReference m_ServerRpcParams_Receive_SenderClientId_FieldRef; + private FieldReference m_UniversalRpcParams_Receive_FieldRef; + private FieldReference m_UniversalRpcParams_Receive_SenderClientId_FieldRef; + private TypeReference m_UniversalRpcParams_TypeRef; private TypeReference m_ClientRpcParams_TypeRef; private MethodReference m_NetworkVariableSerializationTypes_InitializeSerializer_UnmanagedByMemcpy_MethodRef; private MethodReference m_NetworkVariableSerializationTypes_InitializeSerializer_UnmanagedByMemcpyArray_MethodRef; @@ -495,6 +502,8 @@ namespace Unity.Netcode.Editor.CodeGen private const string k_NetworkBehaviour_NetworkVariableFields = nameof(NetworkBehaviour.NetworkVariableFields); private const string k_NetworkBehaviour_beginSendServerRpc = nameof(NetworkBehaviour.__beginSendServerRpc); private const string k_NetworkBehaviour_endSendServerRpc = nameof(NetworkBehaviour.__endSendServerRpc); + private const string k_NetworkBehaviour_beginSendRpc = nameof(NetworkBehaviour.__beginSendRpc); + private const string k_NetworkBehaviour_endSendRpc = nameof(NetworkBehaviour.__endSendRpc); private const string k_NetworkBehaviour_beginSendClientRpc = nameof(NetworkBehaviour.__beginSendClientRpc); private const string k_NetworkBehaviour_endSendClientRpc = nameof(NetworkBehaviour.__endSendClientRpc); private const string k_NetworkBehaviour___initializeVariables = nameof(NetworkBehaviour.__initializeVariables); @@ -511,8 +520,11 @@ namespace Unity.Netcode.Editor.CodeGen private const string k_ServerRpcAttribute_RequireOwnership = nameof(ServerRpcAttribute.RequireOwnership); private const string k_RpcParams_Server = nameof(__RpcParams.Server); private const string k_RpcParams_Client = nameof(__RpcParams.Client); + private const string k_RpcParams_Ext = nameof(__RpcParams.Ext); private const string k_ServerRpcParams_Receive = nameof(ServerRpcParams.Receive); + private const string k_RpcParams_Receive = nameof(RpcParams.Receive); private const string k_ServerRpcReceiveParams_SenderClientId = nameof(ServerRpcReceiveParams.SenderClientId); + private const string k_RpcReceiveParams_SenderClientId = nameof(RpcReceiveParams.SenderClientId); // CodeGen cannot reference the collections assembly to do a typeof() on it due to a bug that causes that to crash. private const string k_INativeListBool_FullName = "Unity.Collections.INativeList`1"; @@ -545,13 +557,20 @@ namespace Unity.Netcode.Editor.CodeGen TypeDefinition rpcParamsTypeDef = null; TypeDefinition serverRpcParamsTypeDef = null; TypeDefinition clientRpcParamsTypeDef = null; + TypeDefinition universalRpcParamsTypeDef = null; TypeDefinition fastBufferWriterTypeDef = null; TypeDefinition fastBufferReaderTypeDef = null; TypeDefinition networkVariableSerializationTypesTypeDef = null; TypeDefinition bytePackerTypeDef = null; TypeDefinition byteUnpackerTypeDef = null; + TypeDefinition attributeParamsType = null; foreach (var netcodeTypeDef in m_NetcodeModule.GetAllTypes()) { + if (attributeParamsType == null && netcodeTypeDef.Name == nameof(RpcAttribute.RpcAttributeParams)) + { + attributeParamsType = netcodeTypeDef; + continue; + } if (networkManagerTypeDef == null && netcodeTypeDef.Name == nameof(NetworkManager)) { networkManagerTypeDef = netcodeTypeDef; @@ -588,6 +607,12 @@ namespace Unity.Netcode.Editor.CodeGen continue; } + if (universalRpcParamsTypeDef == null && netcodeTypeDef.Name == nameof(RpcParams)) + { + universalRpcParamsTypeDef = netcodeTypeDef; + continue; + } + if (clientRpcParamsTypeDef == null && netcodeTypeDef.Name == nameof(ClientRpcParams)) { clientRpcParamsTypeDef = netcodeTypeDef; @@ -662,6 +687,8 @@ namespace Unity.Netcode.Editor.CodeGen } } + m_AttributeParamsType_TypeRef = moduleDefinition.ImportReference(attributeParamsType); + foreach (var fieldDef in networkManagerTypeDef.Fields) { switch (fieldDef.Name) @@ -696,6 +723,12 @@ namespace Unity.Netcode.Editor.CodeGen case k_NetworkBehaviour_endSendServerRpc: m_NetworkBehaviour_endSendServerRpc_MethodRef = moduleDefinition.ImportReference(methodDef); break; + case k_NetworkBehaviour_beginSendRpc: + m_NetworkBehaviour_beginSendRpc_MethodRef = moduleDefinition.ImportReference(methodDef); + break; + case k_NetworkBehaviour_endSendRpc: + m_NetworkBehaviour_endSendRpc_MethodRef = moduleDefinition.ImportReference(methodDef); + break; case k_NetworkBehaviour_beginSendClientRpc: m_NetworkBehaviour_beginSendClientRpc_MethodRef = moduleDefinition.ImportReference(methodDef); break; @@ -763,6 +796,9 @@ namespace Unity.Netcode.Editor.CodeGen case k_RpcParams_Client: m_RpcParams_Client_FieldRef = moduleDefinition.ImportReference(fieldDef); break; + case k_RpcParams_Ext: + m_RpcParams_Ext_FieldRef = moduleDefinition.ImportReference(fieldDef); + break; } } @@ -786,6 +822,26 @@ namespace Unity.Netcode.Editor.CodeGen break; } } + m_UniversalRpcParams_TypeRef = moduleDefinition.ImportReference(rpcParamsTypeDef); + foreach (var fieldDef in rpcParamsTypeDef.Fields) + { + switch (fieldDef.Name) + { + case k_RpcParams_Receive: + foreach (var recvFieldDef in fieldDef.FieldType.Resolve().Fields) + { + switch (recvFieldDef.Name) + { + case k_RpcReceiveParams_SenderClientId: + m_UniversalRpcParams_Receive_SenderClientId_FieldRef = moduleDefinition.ImportReference(recvFieldDef); + break; + } + } + + m_UniversalRpcParams_Receive_FieldRef = moduleDefinition.ImportReference(fieldDef); + break; + } + } m_ClientRpcParams_TypeRef = moduleDefinition.ImportReference(clientRpcParamsTypeDef); m_FastBufferWriter_TypeRef = moduleDefinition.ImportReference(fastBufferWriterTypeDef); @@ -1354,7 +1410,8 @@ namespace Unity.Netcode.Editor.CodeGen var customAttributeType_FullName = customAttribute.AttributeType.FullName; if (customAttributeType_FullName == CodeGenHelpers.ServerRpcAttribute_FullName || - customAttributeType_FullName == CodeGenHelpers.ClientRpcAttribute_FullName) + customAttributeType_FullName == CodeGenHelpers.ClientRpcAttribute_FullName || + customAttributeType_FullName == CodeGenHelpers.RpcAttribute_FullName) { bool isValid = true; @@ -1389,6 +1446,13 @@ namespace Unity.Netcode.Editor.CodeGen isValid = false; } + if (customAttributeType_FullName == CodeGenHelpers.RpcAttribute_FullName && + !methodDefinition.Name.EndsWith("Rpc", StringComparison.OrdinalIgnoreCase)) + { + m_Diagnostics.AddError(methodDefinition, "Rpc method must end with 'Rpc' suffix!"); + isValid = false; + } + if (customAttributeType_FullName == CodeGenHelpers.ClientRpcAttribute_FullName && !methodDefinition.Name.EndsWith("ClientRpc", StringComparison.OrdinalIgnoreCase)) { @@ -1411,11 +1475,15 @@ namespace Unity.Netcode.Editor.CodeGen { if (methodDefinition.Name.EndsWith("ServerRpc", StringComparison.OrdinalIgnoreCase)) { - m_Diagnostics.AddError(methodDefinition, "ServerRpc method must be marked with 'ServerRpc' attribute!"); + m_Diagnostics.AddError(methodDefinition, $"ServerRpc method {methodDefinition} must be marked with 'ServerRpc' attribute!"); } else if (methodDefinition.Name.EndsWith("ClientRpc", StringComparison.OrdinalIgnoreCase)) { - m_Diagnostics.AddError(methodDefinition, "ClientRpc method must be marked with 'ClientRpc' attribute!"); + m_Diagnostics.AddError(methodDefinition, $"ClientRpc method {methodDefinition} must be marked with 'ClientRpc' attribute!"); + } + else if (methodDefinition.Name.EndsWith("ExtRpc", StringComparison.OrdinalIgnoreCase)) + { + m_Diagnostics.AddError(methodDefinition, $"Ext Rpc method {methodDefinition} must be marked with 'ExtRpc' attribute!"); } return null; @@ -1887,8 +1955,17 @@ namespace Unity.Netcode.Editor.CodeGen var instructions = new List(); var processor = methodDefinition.Body.GetILProcessor(); var isServerRpc = rpcAttribute.AttributeType.FullName == CodeGenHelpers.ServerRpcAttribute_FullName; + var isClientRpc = rpcAttribute.AttributeType.FullName == CodeGenHelpers.ClientRpcAttribute_FullName; + var isGenericRpc = rpcAttribute.AttributeType.FullName == CodeGenHelpers.RpcAttribute_FullName; var requireOwnership = true; // default value MUST be == `ServerRpcAttribute.RequireOwnership` var rpcDelivery = RpcDelivery.Reliable; // default value MUST be == `RpcAttribute.Delivery` + var defaultTarget = SendTo.Everyone; + var allowTargetOverride = false; + + if (isGenericRpc) + { + defaultTarget = (SendTo)rpcAttribute.ConstructorArguments[0].Value; + } foreach (var attrField in rpcAttribute.Fields) { switch (attrField.Name) @@ -1899,6 +1976,9 @@ namespace Unity.Netcode.Editor.CodeGen case k_ServerRpcAttribute_RequireOwnership: requireOwnership = attrField.Argument.Type == typeSystem.Boolean && (bool)attrField.Argument.Value; break; + case nameof(RpcAttribute.AllowTargetOverride): + allowTargetOverride = attrField.Argument.Type == typeSystem.Boolean && (bool)attrField.Argument.Value; + break; } } @@ -1906,7 +1986,33 @@ namespace Unity.Netcode.Editor.CodeGen var hasRpcParams = paramCount > 0 && ((isServerRpc && methodDefinition.Parameters[paramCount - 1].ParameterType.FullName == CodeGenHelpers.ServerRpcParams_FullName) || - (!isServerRpc && methodDefinition.Parameters[paramCount - 1].ParameterType.FullName == CodeGenHelpers.ClientRpcParams_FullName)); + (isClientRpc && methodDefinition.Parameters[paramCount - 1].ParameterType.FullName == CodeGenHelpers.ClientRpcParams_FullName) || + (isGenericRpc && methodDefinition.Parameters[paramCount - 1].ParameterType.FullName == CodeGenHelpers.RpcParams_FullName)); + + if (isGenericRpc && defaultTarget == SendTo.SpecifiedInParams) + { + if (!hasRpcParams) + { + m_Diagnostics.AddError($"{methodDefinition}: {nameof(SendTo)}.{nameof(SendTo.SpecifiedInParams)} cannot be used without a final parameter of type {CodeGenHelpers.RpcParams_FullName}."); + } + + foreach (var attrField in rpcAttribute.Fields) + { + switch (attrField.Name) + { + case nameof(RpcAttribute.AllowTargetOverride): + m_Diagnostics.AddWarning($"{methodDefinition}: {nameof(RpcAttribute.AllowTargetOverride)} is ignored with {nameof(SendTo)}.{nameof(SendTo.SpecifiedInParams)}"); + break; + } + } + } + if (isGenericRpc && allowTargetOverride) + { + if (!hasRpcParams) + { + m_Diagnostics.AddError($"{methodDefinition}: {nameof(RpcAttribute.AllowTargetOverride)} cannot be used without a final parameter of type {CodeGenHelpers.RpcParams_FullName}."); + } + } methodDefinition.Body.InitLocals = true; // NetworkManager networkManager; @@ -1919,10 +2025,17 @@ namespace Unity.Netcode.Editor.CodeGen // XXXRpcParams if (!hasRpcParams) { - methodDefinition.Body.Variables.Add(new VariableDefinition(isServerRpc ? m_ServerRpcParams_TypeRef : m_ClientRpcParams_TypeRef)); + methodDefinition.Body.Variables.Add(new VariableDefinition(isServerRpc ? m_ServerRpcParams_TypeRef : (isClientRpc ? m_ClientRpcParams_TypeRef : m_UniversalRpcParams_TypeRef))); } int rpcParamsIdx = !hasRpcParams ? methodDefinition.Body.Variables.Count - 1 : -1; + if (isGenericRpc) + { + methodDefinition.Body.Variables.Add(new VariableDefinition(m_AttributeParamsType_TypeRef)); + } + + int rpcAttributeParamsIdx = isGenericRpc ? methodDefinition.Body.Variables.Count - 1 : -1; + { var returnInstr = processor.Create(OpCodes.Ret); var lastInstr = processor.Create(OpCodes.Nop); @@ -1952,20 +2065,23 @@ namespace Unity.Netcode.Editor.CodeGen // if (__rpc_exec_stage != __RpcExecStage.Client) -> ClientRpc instructions.Add(processor.Create(OpCodes.Ldarg_0)); instructions.Add(processor.Create(OpCodes.Ldfld, m_NetworkBehaviour_rpc_exec_stage_FieldRef)); - instructions.Add(processor.Create(OpCodes.Ldc_I4, (int)(isServerRpc ? NetworkBehaviour.__RpcExecStage.Server : NetworkBehaviour.__RpcExecStage.Client))); + instructions.Add(processor.Create(OpCodes.Ldc_I4, (int)NetworkBehaviour.__RpcExecStage.Execute)); instructions.Add(processor.Create(OpCodes.Ceq)); instructions.Add(processor.Create(OpCodes.Ldc_I4, 0)); instructions.Add(processor.Create(OpCodes.Ceq)); instructions.Add(processor.Create(OpCodes.Brfalse, lastInstr)); - // if (networkManager.IsClient || networkManager.IsHost) { ... } -> ServerRpc - // if (networkManager.IsServer || networkManager.IsHost) { ... } -> ClientRpc - instructions.Add(processor.Create(OpCodes.Ldloc, netManLocIdx)); - instructions.Add(processor.Create(OpCodes.Callvirt, isServerRpc ? m_NetworkManager_getIsClient_MethodRef : m_NetworkManager_getIsServer_MethodRef)); - instructions.Add(processor.Create(OpCodes.Brtrue, beginInstr)); - instructions.Add(processor.Create(OpCodes.Ldloc, netManLocIdx)); - instructions.Add(processor.Create(OpCodes.Callvirt, m_NetworkManager_getIsHost_MethodRef)); - instructions.Add(processor.Create(OpCodes.Brfalse, lastInstr)); + if (!isGenericRpc) + { + // if (networkManager.IsClient || networkManager.IsHost) { ... } -> ServerRpc + // if (networkManager.IsServer || networkManager.IsHost) { ... } -> ClientRpc + instructions.Add(processor.Create(OpCodes.Ldloc, netManLocIdx)); + instructions.Add(processor.Create(OpCodes.Callvirt, isServerRpc ? m_NetworkManager_getIsClient_MethodRef : m_NetworkManager_getIsServer_MethodRef)); + instructions.Add(processor.Create(OpCodes.Brtrue, beginInstr)); + instructions.Add(processor.Create(OpCodes.Ldloc, netManLocIdx)); + instructions.Add(processor.Create(OpCodes.Callvirt, m_NetworkManager_getIsHost_MethodRef)); + instructions.Add(processor.Create(OpCodes.Brfalse, lastInstr)); + } instructions.Add(beginInstr); @@ -2027,7 +2143,7 @@ namespace Unity.Netcode.Editor.CodeGen instructions.Add(processor.Create(OpCodes.Call, m_NetworkBehaviour_beginSendServerRpc_MethodRef)); instructions.Add(processor.Create(OpCodes.Stloc, bufWriterLocIdx)); } - else + else if (isClientRpc) { // ClientRpc @@ -2047,6 +2163,89 @@ namespace Unity.Netcode.Editor.CodeGen instructions.Add(processor.Create(OpCodes.Call, m_NetworkBehaviour_beginSendClientRpc_MethodRef)); instructions.Add(processor.Create(OpCodes.Stloc, bufWriterLocIdx)); } + else + { + // Generic RPC + + // var bufferWriter = __beginSendRpc(rpcMethodId, rpcParams, rpcAttributeParams, defaultTarget, rpcDelivery); + instructions.Add(processor.Create(OpCodes.Ldarg_0)); + + // rpcMethodId + instructions.Add(processor.Create(OpCodes.Ldc_I4, unchecked((int)rpcMethodId))); + + // rpcParams + instructions.Add(hasRpcParams ? processor.Create(OpCodes.Ldarg, paramCount) : processor.Create(OpCodes.Ldloc, rpcParamsIdx)); + + // rpcAttributeParams + instructions.Add(processor.Create(OpCodes.Ldloca, rpcAttributeParamsIdx)); + instructions.Add(processor.Create(OpCodes.Initobj, m_AttributeParamsType_TypeRef)); + + RpcAttribute.RpcAttributeParams dflt = default; + foreach (var field in rpcAttribute.Fields) + { + var found = false; + foreach (var attrField in m_AttributeParamsType_TypeRef.Resolve().Fields) + { + if (attrField.Name == field.Name) + { + found = true; + var value = field.Argument.Value; + var paramField = dflt.GetType().GetField(attrField.Name); + if (value != paramField.GetValue(dflt)) + { + instructions.Add(processor.Create(OpCodes.Ldloca, rpcAttributeParamsIdx)); + var type = value.GetType(); + if (type == typeof(bool)) + { + instructions.Add(processor.Create(OpCodes.Ldc_I4, (bool)value ? 1 : 0)); + } + else if (type == typeof(short) || type == typeof(int) || type == typeof(ushort) + || type == typeof(byte) || type == typeof(sbyte) || type == typeof(char)) + { + instructions.Add(processor.Create(OpCodes.Ldc_I4, (int)value)); + } + else if (type == typeof(long) || type == typeof(ulong)) + { + instructions.Add(processor.Create(OpCodes.Ldc_I8, (long)value)); + } + else if (type == typeof(float)) + { + instructions.Add(processor.Create(OpCodes.Ldc_R8, (float)value)); + + } + else if (type == typeof(double)) + { + instructions.Add(processor.Create(OpCodes.Ldc_R8, (double)value)); + } + else + { + m_Diagnostics.AddError("Unsupported attribute parameter type."); + } + } + + instructions.Add(processor.Create(OpCodes.Stfld, m_MainModule.ImportReference(attrField))); + + break; + } + } + + if (!found) + { + m_Diagnostics.AddError($"{nameof(RpcAttribute)} contains field {field} which is not present in {nameof(RpcAttribute.RpcAttributeParams)}."); + } + } + instructions.Add(processor.Create(OpCodes.Ldloc, rpcAttributeParamsIdx)); + + // defaultTarget + instructions.Add(processor.Create(OpCodes.Ldc_I4, (int)defaultTarget)); + + // rpcDelivery + instructions.Add(processor.Create(OpCodes.Ldc_I4, (int)rpcDelivery)); + + // __beginSendRpc + instructions.Add(processor.Create(OpCodes.Call, m_NetworkBehaviour_beginSendRpc_MethodRef)); + instructions.Add(processor.Create(OpCodes.Stloc, bufWriterLocIdx)); + } // write method parameters into stream for (int paramIndex = 0; paramIndex < paramCount; ++paramIndex) @@ -2075,7 +2274,7 @@ namespace Unity.Netcode.Editor.CodeGen } if (!isServerRpc) { - m_Diagnostics.AddError($"ClientRpcs may not accept {nameof(ServerRpcParams)} as a parameter."); + m_Diagnostics.AddError($"Only ServerRpcs may accept {nameof(ServerRpcParams)} as a parameter."); } continue; } @@ -2086,9 +2285,22 @@ namespace Unity.Netcode.Editor.CodeGen { m_Diagnostics.AddError(methodDefinition, $"{nameof(ClientRpcParams)} must be the last parameter in a ClientRpc."); } - if (isServerRpc) + if (!isClientRpc) { - m_Diagnostics.AddError($"ServerRpcs may not accept {nameof(ClientRpcParams)} as a parameter."); + m_Diagnostics.AddError($"Only clientRpcs may accept {nameof(ClientRpcParams)} as a parameter."); + } + continue; + } + // RpcParams + if (paramType.FullName == CodeGenHelpers.RpcParams_FullName) + { + if (paramIndex != paramCount - 1) + { + m_Diagnostics.AddError(methodDefinition, $"{nameof(RpcParams)} must be the last parameter in a ClientRpc."); + } + if (!isGenericRpc) + { + m_Diagnostics.AddError($"Only Rpcs may accept {nameof(RpcParams)} as a parameter."); } continue; } @@ -2251,7 +2463,7 @@ namespace Unity.Netcode.Editor.CodeGen // __endSendServerRpc instructions.Add(processor.Create(OpCodes.Call, m_NetworkBehaviour_endSendServerRpc_MethodRef)); } - else + else if (isClientRpc) { // ClientRpc @@ -2279,6 +2491,41 @@ namespace Unity.Netcode.Editor.CodeGen // __endSendClientRpc instructions.Add(processor.Create(OpCodes.Call, m_NetworkBehaviour_endSendClientRpc_MethodRef)); } + else + { + // Generic Rpc + + // __endSendRpc(ref bufferWriter, rpcMethodId, rpcParams, rpcAttributeParams, defaultTarget, rpcDelivery); + instructions.Add(processor.Create(OpCodes.Ldarg_0)); + + // bufferWriter + instructions.Add(processor.Create(OpCodes.Ldloca, bufWriterLocIdx)); + + // rpcMethodId + instructions.Add(processor.Create(OpCodes.Ldc_I4, unchecked((int)rpcMethodId))); + if (hasRpcParams) + { + // rpcParams + instructions.Add(processor.Create(OpCodes.Ldarg, paramCount)); + } + else + { + // default + instructions.Add(processor.Create(OpCodes.Ldloc, rpcParamsIdx)); + } + + // rpcAttributeParams + instructions.Add(processor.Create(OpCodes.Ldloc, rpcAttributeParamsIdx)); + + // defaultTarget + instructions.Add(processor.Create(OpCodes.Ldc_I4, (int)defaultTarget)); + + // rpcDelivery + instructions.Add(processor.Create(OpCodes.Ldc_I4, (int)rpcDelivery)); + + // __endSendClientRpc + instructions.Add(processor.Create(OpCodes.Call, m_NetworkBehaviour_endSendRpc_MethodRef)); + } instructions.Add(lastInstr); } @@ -2287,25 +2534,53 @@ namespace Unity.Netcode.Editor.CodeGen var returnInstr = processor.Create(OpCodes.Ret); var lastInstr = processor.Create(OpCodes.Nop); - // if (__rpc_exec_stage == __RpcExecStage.Server) -> ServerRpc - // if (__rpc_exec_stage == __RpcExecStage.Client) -> ClientRpc - instructions.Add(processor.Create(OpCodes.Ldarg_0)); - instructions.Add(processor.Create(OpCodes.Ldfld, m_NetworkBehaviour_rpc_exec_stage_FieldRef)); - instructions.Add(processor.Create(OpCodes.Ldc_I4, (int)(isServerRpc ? NetworkBehaviour.__RpcExecStage.Server : NetworkBehaviour.__RpcExecStage.Client))); - instructions.Add(processor.Create(OpCodes.Ceq)); - instructions.Add(processor.Create(OpCodes.Brfalse, returnInstr)); + if (!isGenericRpc) + { + // if (__rpc_exec_stage == __RpcExecStage.Execute) + instructions.Add(processor.Create(OpCodes.Ldarg_0)); + instructions.Add(processor.Create(OpCodes.Ldfld, m_NetworkBehaviour_rpc_exec_stage_FieldRef)); + instructions.Add(processor.Create(OpCodes.Ldc_I4, (int)NetworkBehaviour.__RpcExecStage.Execute)); + instructions.Add(processor.Create(OpCodes.Ceq)); + instructions.Add(processor.Create(OpCodes.Brfalse, returnInstr)); - // if (networkManager.IsServer || networkManager.IsHost) -> ServerRpc - // if (networkManager.IsClient || networkManager.IsHost) -> ClientRpc - instructions.Add(processor.Create(OpCodes.Ldloc, netManLocIdx)); - instructions.Add(processor.Create(OpCodes.Callvirt, isServerRpc ? m_NetworkManager_getIsServer_MethodRef : m_NetworkManager_getIsClient_MethodRef)); - instructions.Add(processor.Create(OpCodes.Brtrue, lastInstr)); - instructions.Add(processor.Create(OpCodes.Ldloc, netManLocIdx)); - instructions.Add(processor.Create(OpCodes.Callvirt, m_NetworkManager_getIsHost_MethodRef)); - instructions.Add(processor.Create(OpCodes.Brtrue, lastInstr)); + // if (networkManager.IsServer || networkManager.IsHost) -> ServerRpc + // if (networkManager.IsClient || networkManager.IsHost) -> ClientRpc + instructions.Add(processor.Create(OpCodes.Ldloc, netManLocIdx)); + instructions.Add(processor.Create(OpCodes.Callvirt, isServerRpc ? m_NetworkManager_getIsServer_MethodRef : m_NetworkManager_getIsClient_MethodRef)); + instructions.Add(processor.Create(OpCodes.Brtrue, lastInstr)); + instructions.Add(processor.Create(OpCodes.Ldloc, netManLocIdx)); + instructions.Add(processor.Create(OpCodes.Callvirt, m_NetworkManager_getIsHost_MethodRef)); + instructions.Add(processor.Create(OpCodes.Brtrue, lastInstr)); + instructions.Add(returnInstr); + instructions.Add(lastInstr); + + // This needs to be set back before executing the callback or else sending another RPC + // from within an RPC will not work. + // __rpc_exec_stage = __RpcExecStage.Send + instructions.Add(processor.Create(OpCodes.Ldarg_0)); + instructions.Add(processor.Create(OpCodes.Ldc_I4, (int)NetworkBehaviour.__RpcExecStage.Send)); + instructions.Add(processor.Create(OpCodes.Stfld, m_NetworkBehaviour_rpc_exec_stage_FieldRef)); + } + else + { + // if (__rpc_exec_stage == __RpcExecStage.Execute) + instructions.Add(processor.Create(OpCodes.Ldarg_0)); + instructions.Add(processor.Create(OpCodes.Ldfld, m_NetworkBehaviour_rpc_exec_stage_FieldRef)); + instructions.Add(processor.Create(OpCodes.Ldc_I4, (int)NetworkBehaviour.__RpcExecStage.Execute)); + instructions.Add(processor.Create(OpCodes.Ceq)); + instructions.Add(processor.Create(OpCodes.Brtrue, lastInstr)); + + instructions.Add(returnInstr); + instructions.Add(lastInstr); + + // This needs to be set back before executing the callback or else sending another RPC + // from within an RPC will not work. + // __rpc_exec_stage = __RpcExecStage.Send + instructions.Add(processor.Create(OpCodes.Ldarg_0)); + instructions.Add(processor.Create(OpCodes.Ldc_I4, (int)NetworkBehaviour.__RpcExecStage.Send)); + instructions.Add(processor.Create(OpCodes.Stfld, m_NetworkBehaviour_rpc_exec_stage_FieldRef)); + } - instructions.Add(returnInstr); - instructions.Add(lastInstr); } instructions.Reverse(); @@ -2458,6 +2733,8 @@ namespace Unity.Netcode.Editor.CodeGen var processor = rpcHandler.Body.GetILProcessor(); var isServerRpc = rpcAttribute.AttributeType.FullName == CodeGenHelpers.ServerRpcAttribute_FullName; + var isCientRpc = rpcAttribute.AttributeType.FullName == CodeGenHelpers.ClientRpcAttribute_FullName; + var isGenericRpc = rpcAttribute.AttributeType.FullName == CodeGenHelpers.RpcAttribute_FullName; var requireOwnership = true; // default value MUST be == `ServerRpcAttribute.RequireOwnership` foreach (var attrField in rpcAttribute.Fields) { @@ -2564,6 +2841,15 @@ namespace Unity.Netcode.Editor.CodeGen processor.Emit(OpCodes.Stloc, localIndex); continue; } + + // RpcParams + if (paramType.FullName == CodeGenHelpers.RpcParams_FullName) + { + processor.Emit(OpCodes.Ldarg_2); + processor.Emit(OpCodes.Ldfld, m_RpcParams_Ext_FieldRef); + processor.Emit(OpCodes.Stloc, localIndex); + continue; + } } Instruction jumpInstruction = null; @@ -2690,7 +2976,7 @@ namespace Unity.Netcode.Editor.CodeGen // NetworkBehaviour.__rpc_exec_stage = __RpcExecStage.Server; -> ServerRpc // NetworkBehaviour.__rpc_exec_stage = __RpcExecStage.Client; -> ClientRpc processor.Emit(OpCodes.Ldarg_0); - processor.Emit(OpCodes.Ldc_I4, (int)(isServerRpc ? NetworkBehaviour.__RpcExecStage.Server : NetworkBehaviour.__RpcExecStage.Client)); + processor.Emit(OpCodes.Ldc_I4, (int)(NetworkBehaviour.__RpcExecStage.Execute)); processor.Emit(OpCodes.Stfld, m_NetworkBehaviour_rpc_exec_stage_FieldRef); // NetworkBehaviour.XXXRpc(...); @@ -2713,7 +2999,7 @@ namespace Unity.Netcode.Editor.CodeGen // NetworkBehaviour.__rpc_exec_stage = __RpcExecStage.None; processor.Emit(OpCodes.Ldarg_0); - processor.Emit(OpCodes.Ldc_I4, (int)NetworkBehaviour.__RpcExecStage.None); + processor.Emit(OpCodes.Ldc_I4, (int)NetworkBehaviour.__RpcExecStage.Send); processor.Emit(OpCodes.Stfld, m_NetworkBehaviour_rpc_exec_stage_FieldRef); processor.Emit(OpCodes.Ret); diff --git a/Editor/CodeGen/RuntimeAccessModifiersILPP.cs b/Editor/CodeGen/RuntimeAccessModifiersILPP.cs index 0ef4f2c..bdcc74e 100644 --- a/Editor/CodeGen/RuntimeAccessModifiersILPP.cs +++ b/Editor/CodeGen/RuntimeAccessModifiersILPP.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using Mono.Cecil; using Mono.Cecil.Cil; +using Mono.Cecil.Rocks; using Unity.CompilationPipeline.Common.Diagnostics; using Unity.CompilationPipeline.Common.ILPostProcessing; using ILPPInterface = Unity.CompilationPipeline.Common.ILPostProcessing.ILPostProcessor; @@ -52,6 +53,15 @@ namespace Unity.Netcode.Editor.CodeGen case nameof(NetworkBehaviour): ProcessNetworkBehaviour(typeDefinition); break; + case nameof(RpcAttribute): + foreach (var methodDefinition in typeDefinition.GetConstructors()) + { + if (methodDefinition.Parameters.Count == 0) + { + methodDefinition.IsPublic = true; + } + } + break; case nameof(__RpcParams): case nameof(RpcFallbackSerialization): typeDefinition.IsPublic = true; @@ -154,6 +164,8 @@ namespace Unity.Netcode.Editor.CodeGen methodDefinition.Name == nameof(NetworkBehaviour.__endSendServerRpc) || methodDefinition.Name == nameof(NetworkBehaviour.__beginSendClientRpc) || methodDefinition.Name == nameof(NetworkBehaviour.__endSendClientRpc) || + methodDefinition.Name == nameof(NetworkBehaviour.__beginSendRpc) || + methodDefinition.Name == nameof(NetworkBehaviour.__endSendRpc) || methodDefinition.Name == nameof(NetworkBehaviour.__initializeVariables) || methodDefinition.Name == nameof(NetworkBehaviour.__initializeRpcs) || methodDefinition.Name == nameof(NetworkBehaviour.__registerRpc) || diff --git a/Editor/NetworkManagerEditor.cs b/Editor/NetworkManagerEditor.cs index e4acb69..1713ba2 100644 --- a/Editor/NetworkManagerEditor.cs +++ b/Editor/NetworkManagerEditor.cs @@ -1,3 +1,6 @@ +#if UNITY_2022_3_OR_NEWER && (RELAY_SDK_INSTALLED && !UNITY_WEBGL ) || (RELAY_SDK_INSTALLED && UNITY_WEBGL && UTP_TRANSPORT_2_0_ABOVE) +#define RELAY_INTEGRATION_AVAILABLE +#endif using System; using System.Collections.Generic; using System.IO; @@ -48,6 +51,27 @@ namespace Unity.Netcode.Editor private readonly List m_TransportTypes = new List(); private string[] m_TransportNames = { "Select transport..." }; + /// + public override void OnInspectorGUI() + { + Initialize(); + CheckNullProperties(); + +#if !MULTIPLAYER_TOOLS + DrawInstallMultiplayerToolsTip(); +#endif + + if (m_NetworkManager.IsServer || m_NetworkManager.IsClient) + { + DrawDisconnectButton(); + } + else + { + DrawAllPropertyFields(); + ShowStartConnectionButtons(); + } + } + private void ReloadTransports() { m_TransportTypes.Clear(); @@ -138,209 +162,311 @@ namespace Unity.Netcode.Editor .FindPropertyRelative(nameof(NetworkPrefabs.NetworkPrefabsLists)); } - /// - public override void OnInspectorGUI() + private void DrawAllPropertyFields() { - Initialize(); - CheckNullProperties(); + serializedObject.Update(); + EditorGUILayout.PropertyField(m_RunInBackgroundProperty); + EditorGUILayout.PropertyField(m_LogLevelProperty); + EditorGUILayout.Space(); -#if !MULTIPLAYER_TOOLS - DrawInstallMultiplayerToolsTip(); -#endif + EditorGUILayout.PropertyField(m_PlayerPrefabProperty); + EditorGUILayout.Space(); - if (!m_NetworkManager.IsServer && !m_NetworkManager.IsClient) + DrawPrefabListField(); + + EditorGUILayout.Space(); + + EditorGUILayout.LabelField("General", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(m_ProtocolVersionProperty); + + DrawTransportField(); + + EditorGUILayout.PropertyField(m_TickRateProperty); + + EditorGUILayout.LabelField("Performance", EditorStyles.boldLabel); + + EditorGUILayout.PropertyField(m_EnsureNetworkVariableLengthSafetyProperty); + + EditorGUILayout.LabelField("Connection", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(m_ConnectionApprovalProperty); + + using (new EditorGUI.DisabledScope(!m_NetworkManager.NetworkConfig.ConnectionApproval)) { - serializedObject.Update(); - EditorGUILayout.PropertyField(m_RunInBackgroundProperty); - EditorGUILayout.PropertyField(m_LogLevelProperty); - EditorGUILayout.Space(); - - EditorGUILayout.PropertyField(m_PlayerPrefabProperty); - EditorGUILayout.Space(); - - if (m_NetworkManager.NetworkConfig.HasOldPrefabList()) - { - EditorGUILayout.HelpBox("Network Prefabs serialized in old format. Migrate to new format to edit the list.", MessageType.Info); - if (GUILayout.Button(new GUIContent("Migrate Prefab List", "Converts the old format Network Prefab list to a new Scriptable Object"))) - { - // Default directory - var directory = "Assets/"; - var assetPath = AssetDatabase.GetAssetPath(m_NetworkManager); - if (assetPath == "") - { - assetPath = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(m_NetworkManager); - } - - if (assetPath != "") - { - directory = Path.GetDirectoryName(assetPath); - } - else - { -#if UNITY_2021_1_OR_NEWER - var prefabStage = UnityEditor.SceneManagement.PrefabStageUtility.GetPrefabStage(m_NetworkManager.gameObject); -#else - var prefabStage = UnityEditor.Experimental.SceneManagement.PrefabStageUtility.GetPrefabStage(m_NetworkManager.gameObject); -#endif - if (prefabStage != null) - { - var prefabPath = prefabStage.assetPath; - if (!string.IsNullOrEmpty(prefabPath)) - { - directory = Path.GetDirectoryName(prefabPath); - } - } - if (m_NetworkManager.gameObject.scene != null) - { - var scenePath = m_NetworkManager.gameObject.scene.path; - if (!string.IsNullOrEmpty(scenePath)) - { - directory = Path.GetDirectoryName(scenePath); - } - } - } - var networkPrefabs = m_NetworkManager.NetworkConfig.MigrateOldNetworkPrefabsToNetworkPrefabsList(); - string path = Path.Combine(directory, $"NetworkPrefabs-{m_NetworkManager.GetInstanceID()}.asset"); - Debug.Log("Saving migrated Network Prefabs List to " + path); - AssetDatabase.CreateAsset(networkPrefabs, path); - EditorUtility.SetDirty(m_NetworkManager); - } - } - else - { - if (m_NetworkManager.NetworkConfig.Prefabs.NetworkPrefabsLists.Count == 0) - { - EditorGUILayout.HelpBox("You have no prefab list selected. You will have to add your prefabs manually at runtime for netcode to work.", MessageType.Warning); - } - EditorGUILayout.PropertyField(m_PrefabsList); - } - EditorGUILayout.Space(); - - EditorGUILayout.LabelField("General", EditorStyles.boldLabel); - EditorGUILayout.PropertyField(m_ProtocolVersionProperty); - - EditorGUILayout.PropertyField(m_NetworkTransportProperty); - - if (m_NetworkTransportProperty.objectReferenceValue == null) - { - EditorGUILayout.HelpBox("You have no transport selected. A transport is required for netcode to work. Which one do you want?", MessageType.Warning); - - int selection = EditorGUILayout.Popup(0, m_TransportNames); - - if (selection > 0) - { - ReloadTransports(); - - var transportComponent = m_NetworkManager.gameObject.GetComponent(m_TransportTypes[selection - 1]) ?? m_NetworkManager.gameObject.AddComponent(m_TransportTypes[selection - 1]); - m_NetworkTransportProperty.objectReferenceValue = transportComponent; - - Repaint(); - } - } - - EditorGUILayout.PropertyField(m_TickRateProperty); - - EditorGUILayout.LabelField("Performance", EditorStyles.boldLabel); - - EditorGUILayout.PropertyField(m_EnsureNetworkVariableLengthSafetyProperty); - - EditorGUILayout.LabelField("Connection", EditorStyles.boldLabel); - EditorGUILayout.PropertyField(m_ConnectionApprovalProperty); - - using (new EditorGUI.DisabledScope(!m_NetworkManager.NetworkConfig.ConnectionApproval)) - { - EditorGUILayout.PropertyField(m_ClientConnectionBufferTimeoutProperty); - } - - EditorGUILayout.LabelField("Spawning", EditorStyles.boldLabel); - EditorGUILayout.PropertyField(m_ForceSamePrefabsProperty); - - - EditorGUILayout.PropertyField(m_RecycleNetworkIdsProperty); - - using (new EditorGUI.DisabledScope(!m_NetworkManager.NetworkConfig.RecycleNetworkIds)) - { - EditorGUILayout.PropertyField(m_NetworkIdRecycleDelayProperty); - } - - EditorGUILayout.LabelField("Bandwidth", EditorStyles.boldLabel); - EditorGUILayout.PropertyField(m_RpcHashSizeProperty); - - EditorGUILayout.LabelField("Scene Management", EditorStyles.boldLabel); - EditorGUILayout.PropertyField(m_EnableSceneManagementProperty); - - using (new EditorGUI.DisabledScope(!m_NetworkManager.NetworkConfig.EnableSceneManagement)) - { - EditorGUILayout.PropertyField(m_LoadSceneTimeOutProperty); - } - - serializedObject.ApplyModifiedProperties(); - - - // Start buttons below - { - string buttonDisabledReasonSuffix = ""; - - if (!EditorApplication.isPlaying) - { - buttonDisabledReasonSuffix = ". This can only be done in play mode"; - GUI.enabled = false; - } - - if (GUILayout.Button(new GUIContent("Start Host", "Starts a host instance" + buttonDisabledReasonSuffix))) - { - m_NetworkManager.StartHost(); - } - - if (GUILayout.Button(new GUIContent("Start Server", "Starts a server instance" + buttonDisabledReasonSuffix))) - { - m_NetworkManager.StartServer(); - } - - if (GUILayout.Button(new GUIContent("Start Client", "Starts a client instance" + buttonDisabledReasonSuffix))) - { - m_NetworkManager.StartClient(); - } - - if (!EditorApplication.isPlaying) - { - GUI.enabled = true; - } - } + EditorGUILayout.PropertyField(m_ClientConnectionBufferTimeoutProperty); } - else + + EditorGUILayout.LabelField("Spawning", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(m_ForceSamePrefabsProperty); + + EditorGUILayout.PropertyField(m_RecycleNetworkIdsProperty); + + using (new EditorGUI.DisabledScope(!m_NetworkManager.NetworkConfig.RecycleNetworkIds)) { - string instanceType = string.Empty; + EditorGUILayout.PropertyField(m_NetworkIdRecycleDelayProperty); + } - if (m_NetworkManager.IsHost) - { - instanceType = "Host"; - } - else if (m_NetworkManager.IsServer) - { - instanceType = "Server"; - } - else if (m_NetworkManager.IsClient) - { - instanceType = "Client"; - } + EditorGUILayout.LabelField("Bandwidth", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(m_RpcHashSizeProperty); - EditorGUILayout.HelpBox("You cannot edit the NetworkConfig when a " + instanceType + " is running.", MessageType.Info); + EditorGUILayout.LabelField("Scene Management", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(m_EnableSceneManagementProperty); - if (GUILayout.Button(new GUIContent("Stop " + instanceType, "Stops the " + instanceType + " instance."))) + using (new EditorGUI.DisabledScope(!m_NetworkManager.NetworkConfig.EnableSceneManagement)) + { + EditorGUILayout.PropertyField(m_LoadSceneTimeOutProperty); + } + + serializedObject.ApplyModifiedProperties(); + } + + private void DrawTransportField() + { +#if RELAY_INTEGRATION_AVAILABLE + var useRelay = EditorPrefs.GetBool(k_UseEasyRelayIntegrationKey, false); +#else + var useRelay = false; +#endif + + if (useRelay) + { + EditorGUILayout.HelpBox("Test connection with relay is enabled, so the default Unity Transport will be used", MessageType.Info); + GUI.enabled = false; + EditorGUILayout.PropertyField(m_NetworkTransportProperty); + GUI.enabled = true; + return; + } + + EditorGUILayout.PropertyField(m_NetworkTransportProperty); + + if (m_NetworkTransportProperty.objectReferenceValue == null) + { + EditorGUILayout.HelpBox("You have no transport selected. A transport is required for netcode to work. Which one do you want?", MessageType.Warning); + + int selection = EditorGUILayout.Popup(0, m_TransportNames); + + if (selection > 0) { - m_NetworkManager.Shutdown(); + ReloadTransports(); + + var transportComponent = m_NetworkManager.gameObject.GetComponent(m_TransportTypes[selection - 1]) ?? m_NetworkManager.gameObject.AddComponent(m_TransportTypes[selection - 1]); + m_NetworkTransportProperty.objectReferenceValue = transportComponent; + + Repaint(); } } } +#if RELAY_INTEGRATION_AVAILABLE + private readonly string k_UseEasyRelayIntegrationKey = "NetworkManagerUI_UseRelay_" + Application.dataPath.GetHashCode(); + private string m_JoinCode = ""; + private string m_StartConnectionError = null; + private string m_Region = ""; + + // wait for next frame so that ImGui finishes the current frame + private static void RunNextFrame(Action action) => EditorApplication.delayCall += () => action(); +#endif + + private void ShowStartConnectionButtons() + { + EditorGUILayout.LabelField("Start Connection", EditorStyles.boldLabel); + +#if RELAY_INTEGRATION_AVAILABLE + // use editor prefs to persist the setting when entering / leaving play mode / exiting Unity + var useRelay = EditorPrefs.GetBool(k_UseEasyRelayIntegrationKey, false); + GUILayout.BeginHorizontal(); + useRelay = GUILayout.Toggle(useRelay, "Try Relay in the Editor"); + + var icon = EditorGUIUtility.IconContent("_Help"); + icon.tooltip = "This will help you test relay in the Editor. Click here to know how to integrate Relay in your build"; + if (GUILayout.Button(icon, GUIStyle.none, GUILayout.Width(20))) + { + Application.OpenURL("https://docs-multiplayer.unity3d.com/netcode/current/relay/"); + } + GUILayout.EndHorizontal(); + + EditorPrefs.SetBool(k_UseEasyRelayIntegrationKey, useRelay); + if (useRelay && !Application.isPlaying && !CloudProjectSettings.projectBound) + { + EditorGUILayout.HelpBox("To use relay, you need to setup your project in the Project Settings in the Services section.", MessageType.Warning); + if (GUILayout.Button("Open Project settings")) + { + SettingsService.OpenProjectSettings("Project/Services"); + } + } +#else + var useRelay = false; +#endif + + string buttonDisabledReasonSuffix = ""; + + if (!EditorApplication.isPlaying) + { + buttonDisabledReasonSuffix = ". This can only be done in play mode"; + GUI.enabled = false; + } + + if (useRelay) + { + ShowStartConnectionButtons_Relay(buttonDisabledReasonSuffix); + } + else + { + ShowStartConnectionButtons_Standard(buttonDisabledReasonSuffix); + } + + if (!EditorApplication.isPlaying) + { + GUI.enabled = true; + } + } + + private void ShowStartConnectionButtons_Relay(string buttonDisabledReasonSuffix) + { +#if RELAY_INTEGRATION_AVAILABLE + + void AddStartServerOrHostButton(bool isServer) + { + var type = isServer ? "Server" : "Host"; + if (GUILayout.Button(new GUIContent($"Start {type}", $"Starts a {type} instance with Relay{buttonDisabledReasonSuffix}"))) + { + m_StartConnectionError = null; + RunNextFrame(async () => + { + try + { + var (joinCode, allocation) = isServer ? await m_NetworkManager.StartServerWithRelay() : await m_NetworkManager.StartHostWithRelay(); + m_JoinCode = joinCode; + m_Region = allocation.Region; + Repaint(); + } + catch (Exception e) + { + m_StartConnectionError = e.Message; + throw; + } + }); + } + } + + AddStartServerOrHostButton(isServer: true); + AddStartServerOrHostButton(isServer: false); + + GUILayout.Space(8f); + m_JoinCode = EditorGUILayout.TextField("Relay Join Code", m_JoinCode); + if (GUILayout.Button(new GUIContent("Start Client", "Starts a client instance with Relay" + buttonDisabledReasonSuffix))) + { + m_StartConnectionError = null; + RunNextFrame(async () => + { + if (string.IsNullOrEmpty(m_JoinCode)) + { + m_StartConnectionError = "Please provide a join code!"; + return; + } + + try + { + var allocation = await m_NetworkManager.StartClientWithRelay(m_JoinCode); + m_Region = allocation.Region; + Repaint(); + } + catch (Exception e) + { + m_StartConnectionError = e.Message; + throw; + } + }); + } + + if (Application.isPlaying && !string.IsNullOrEmpty(m_StartConnectionError)) + { + EditorGUILayout.HelpBox(m_StartConnectionError, MessageType.Error); + } +#endif + } + + private void ShowStartConnectionButtons_Standard(string buttonDisabledReasonSuffix) + { + if (GUILayout.Button(new GUIContent("Start Host", "Starts a host instance" + buttonDisabledReasonSuffix))) + { + m_NetworkManager.StartHost(); + } + + if (GUILayout.Button(new GUIContent("Start Server", "Starts a server instance" + buttonDisabledReasonSuffix))) + { + m_NetworkManager.StartServer(); + } + + if (GUILayout.Button(new GUIContent("Start Client", "Starts a client instance" + buttonDisabledReasonSuffix))) + { + m_NetworkManager.StartClient(); + } + } + + private void DrawDisconnectButton() + { + string instanceType = string.Empty; + + if (m_NetworkManager.IsHost) + { + instanceType = "Host"; + } + else if (m_NetworkManager.IsServer) + { + instanceType = "Server"; + } + else if (m_NetworkManager.IsClient) + { + instanceType = "Client"; + } + + EditorGUILayout.HelpBox($"You cannot edit the NetworkConfig when a {instanceType} is running.", MessageType.Info); + +#if RELAY_INTEGRATION_AVAILABLE + if (!string.IsNullOrEmpty(m_JoinCode) && !string.IsNullOrEmpty(m_Region)) + { + var style = new GUIStyle(EditorStyles.helpBox) + { + fontSize = 10, + alignment = TextAnchor.MiddleCenter, + }; + + GUILayout.BeginHorizontal(style, GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(false), GUILayout.MaxWidth(800)); + GUILayout.Label(new GUIContent(EditorGUIUtility.IconContent(k_InfoIconName)), GUILayout.ExpandWidth(false), GUILayout.ExpandHeight(true)); + GUILayout.Space(25f); + GUILayout.BeginVertical(); + GUILayout.Space(4f); + GUILayout.Label($"Connected via relay ({m_Region}).\nJoin code: {m_JoinCode}", EditorStyles.miniLabel, GUILayout.ExpandHeight(true)); + + if (GUILayout.Button("Copy code", GUILayout.ExpandHeight(true))) + { + GUIUtility.systemCopyBuffer = m_JoinCode; + } + + GUILayout.Space(4f); + GUILayout.EndVertical(); + GUILayout.Space(2f); + GUILayout.EndHorizontal(); + } +#endif + + if (GUILayout.Button(new GUIContent($"Stop {instanceType}", $"Stops the {instanceType} instance."))) + { +#if RELAY_INTEGRATION_AVAILABLE + m_JoinCode = ""; +#endif + m_NetworkManager.Shutdown(); + } + } + + private const string k_InfoIconName = "console.infoicon"; private static void DrawInstallMultiplayerToolsTip() { const string getToolsText = "Access additional tools for multiplayer development by installing the Multiplayer Tools package in the Package Manager."; const string openDocsButtonText = "Open Docs"; const string dismissButtonText = "Dismiss"; const string targetUrl = "https://docs-multiplayer.unity3d.com/tools/current/install-tools"; - const string infoIconName = "console.infoicon"; + if (NetcodeForGameObjectsEditorSettings.GetNetcodeInstallMultiplayerToolTips() != 0) { @@ -371,7 +497,7 @@ namespace Unity.Netcode.Editor GUILayout.FlexibleSpace(); GUILayout.BeginHorizontal(s_HelpBoxStyle, GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(false), GUILayout.MaxWidth(800)); { - GUILayout.Label(new GUIContent(EditorGUIUtility.IconContent(infoIconName)), GUILayout.ExpandWidth(false), GUILayout.ExpandHeight(true)); + GUILayout.Label(new GUIContent(EditorGUIUtility.IconContent(k_InfoIconName)), GUILayout.ExpandWidth(false), GUILayout.ExpandHeight(true)); GUILayout.Space(4); GUILayout.Label(getToolsText, s_CenteredWordWrappedLabelStyle, GUILayout.ExpandHeight(true)); @@ -404,5 +530,69 @@ namespace Unity.Netcode.Editor GUILayout.Space(10); } + + private void DrawPrefabListField() + { + if (!m_NetworkManager.NetworkConfig.HasOldPrefabList()) + { + if (m_NetworkManager.NetworkConfig.Prefabs.NetworkPrefabsLists.Count == 0) + { + EditorGUILayout.HelpBox("You have no prefab list selected. You will have to add your prefabs manually at runtime for netcode to work.", MessageType.Warning); + } + + EditorGUILayout.PropertyField(m_PrefabsList); + return; + } + + // Old format of prefab list + EditorGUILayout.HelpBox("Network Prefabs serialized in old format. Migrate to new format to edit the list.", MessageType.Info); + if (GUILayout.Button(new GUIContent("Migrate Prefab List", "Converts the old format Network Prefab list to a new Scriptable Object"))) + { + // Default directory + var directory = "Assets/"; + var assetPath = AssetDatabase.GetAssetPath(m_NetworkManager); + if (assetPath == "") + { + assetPath = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(m_NetworkManager); + } + + if (assetPath != "") + { + directory = Path.GetDirectoryName(assetPath); + } + else + { + +#if UNITY_2021_1_OR_NEWER + var prefabStage = UnityEditor.SceneManagement.PrefabStageUtility.GetPrefabStage(m_NetworkManager.gameObject); +#else + var prefabStage = UnityEditor.Experimental.SceneManagement.PrefabStageUtility.GetPrefabStage(m_NetworkManager.gameObject); +#endif + if (prefabStage != null) + { + var prefabPath = prefabStage.assetPath; + if (!string.IsNullOrEmpty(prefabPath)) + { + directory = Path.GetDirectoryName(prefabPath); + } + } + + if (m_NetworkManager.gameObject.scene != null) + { + var scenePath = m_NetworkManager.gameObject.scene.path; + if (!string.IsNullOrEmpty(scenePath)) + { + directory = Path.GetDirectoryName(scenePath); + } + } + } + + var networkPrefabs = m_NetworkManager.NetworkConfig.MigrateOldNetworkPrefabsToNetworkPrefabsList(); + string path = Path.Combine(directory, $"NetworkPrefabs-{m_NetworkManager.GetInstanceID()}.asset"); + Debug.Log("Saving migrated Network Prefabs List to " + path); + AssetDatabase.CreateAsset(networkPrefabs, path); + EditorUtility.SetDirty(m_NetworkManager); + } + } } } diff --git a/Editor/NetworkManagerRelayIntegration.cs b/Editor/NetworkManagerRelayIntegration.cs new file mode 100644 index 0000000..a3cfd36 --- /dev/null +++ b/Editor/NetworkManagerRelayIntegration.cs @@ -0,0 +1,120 @@ +#if UNITY_2022_3_OR_NEWER && (RELAY_SDK_INSTALLED && !UNITY_WEBGL ) || (RELAY_SDK_INSTALLED && UNITY_WEBGL && UTP_TRANSPORT_2_0_ABOVE) +using System; +using System.Threading.Tasks; +using Unity.Netcode.Transports.UTP; +using Unity.Networking.Transport.Relay; +using Unity.Services.Authentication; +using Unity.Services.Core; +using Unity.Services.Relay; +using Unity.Services.Relay.Models; + +namespace Unity.Netcode.Editor +{ + /// + /// Integration with Unity Relay SDK and Unity Transport that support the additional buttons in the NetworkManager inspector. + /// This code could theoretically be used at runtime, but we would like to avoid the additional dependencies in the runtime assembly of netcode for gameobjects. + /// + public static class NetworkManagerRelayIntegration + { + +#if UNITY_WEBGL + private const string k_DefaultConnectionType = "wss"; +#else + private const string k_DefaultConnectionType = "dtls"; +#endif + + /// + /// Easy relay integration (host): it will initialize the unity services, sign in anonymously and start the host with a new relay allocation. + /// Note that this will force the use of Unity Transport. + /// + /// The network manager that will start the connection + /// Maximum number of connections to the created relay. + /// The connection type of the (wss, ws, dtls or udp) + /// The join code that a potential client can use and the allocation + /// Exception when there's an error during services initialization + /// Exception when the project is not linked to a cloud project id + /// Exception when two registered depend on the other + /// The task fails with the exception when the task cannot complete successfully due to Authentication specific errors. + /// See + /// Thrown when the maxConnections argument fails validation in Relay Service SDK. + /// Thrown when the request successfully reach the Relay Allocation Service but results in an error. + internal static async Task<(string, Allocation)> StartHostWithRelay(this NetworkManager networkManager, int maxConnections = 5) + { + var codeAndAllocation = await InitializeAndCreateAllocAsync(networkManager, maxConnections, k_DefaultConnectionType); + return networkManager.StartHost() ? codeAndAllocation : (null, null); + } + + /// + /// Easy relay integration (server): it will initialize the unity services, sign in anonymously and start the server with a new relay allocation. + /// Note that this will force the use of Unity Transport. + /// + /// The network manager that will start the connection + /// Maximum number of connections to the created relay. + /// The join code that a potential client can use and the allocation. + /// Exception when there's an error during services initialization + /// Exception when the project is not linked to a cloud project id + /// Exception when two registered depend on the other + /// The task fails with the exception when the task cannot complete successfully due to Authentication specific errors. + /// See + /// Thrown when the maxConnections argument fails validation in Relay Service SDK. + /// Thrown when the request successfully reach the Relay Allocation Service but results in an error. + internal static async Task<(string, Allocation)> StartServerWithRelay(this NetworkManager networkManager, int maxConnections = 5) + { + var codeAndAllocation = await InitializeAndCreateAllocAsync(networkManager, maxConnections, k_DefaultConnectionType); + return networkManager.StartServer() ? codeAndAllocation : (null, null); + } + + /// + /// Easy relay integration (client): it will initialize the unity services, sign in anonymously, join the relay with the given join code and start the client. + /// Note that this will force the use of Unity Transport. + /// + /// The network manager that will start the connection + /// The join code of the allocation + /// Exception when there's an error during services initialization + /// Exception when the project is not linked to a cloud project id + /// Exception when two registered depend on the other + /// The task fails with the exception when the task cannot complete successfully due to Authentication specific errors. + /// Thrown when the request does not reach the Relay Allocation Service. + /// Thrown if the joinCode has the wrong format. + /// Thrown when the request successfully reach the Relay Allocation Service but results in an error. + /// True if starting the client was successful + internal static async Task StartClientWithRelay(this NetworkManager networkManager, string joinCode) + { + await UnityServices.InitializeAsync(); + if (!AuthenticationService.Instance.IsSignedIn) + { + await AuthenticationService.Instance.SignInAnonymouslyAsync(); + } + var joinAllocation = await RelayService.Instance.JoinAllocationAsync(joinCode: joinCode); + GetUnityTransport(networkManager, k_DefaultConnectionType).SetRelayServerData(new RelayServerData(joinAllocation, k_DefaultConnectionType)); + return networkManager.StartClient() ? joinAllocation : null; + } + + private static async Task<(string, Allocation)> InitializeAndCreateAllocAsync(NetworkManager networkManager, int maxConnections, string connectionType) + { + await UnityServices.InitializeAsync(); + if (!AuthenticationService.Instance.IsSignedIn) + { + await AuthenticationService.Instance.SignInAnonymouslyAsync(); + } + Allocation allocation = await RelayService.Instance.CreateAllocationAsync(maxConnections); + GetUnityTransport(networkManager, connectionType).SetRelayServerData(new RelayServerData(allocation, connectionType)); + var joinCode = await RelayService.Instance.GetJoinCodeAsync(allocation.AllocationId); + return (joinCode, allocation); + } + + private static UnityTransport GetUnityTransport(NetworkManager networkManager, string connectionType) + { + if (!networkManager.TryGetComponent(out var transport)) + { + transport = networkManager.gameObject.AddComponent(); + } +#if UTP_TRANSPORT_2_0_ABOVE + transport.UseWebSockets = connectionType.StartsWith("ws"); // Probably should be part of SetRelayServerData, but not possible at this point +#endif + networkManager.NetworkConfig.NetworkTransport = transport; // Force using UnityTransport + return transport; + } + } +} +#endif diff --git a/Editor/NetworkManagerRelayIntegration.cs.meta b/Editor/NetworkManagerRelayIntegration.cs.meta new file mode 100644 index 0000000..b81b1f6 --- /dev/null +++ b/Editor/NetworkManagerRelayIntegration.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 23b658b1c2e443109a8a131ef3632c9b +timeCreated: 1698673251 \ No newline at end of file diff --git a/Editor/NetworkTransformEditor.cs b/Editor/NetworkTransformEditor.cs index 6a5be78..4e7831d 100644 --- a/Editor/NetworkTransformEditor.cs +++ b/Editor/NetworkTransformEditor.cs @@ -10,6 +10,7 @@ namespace Unity.Netcode.Editor [CustomEditor(typeof(NetworkTransform), true)] public class NetworkTransformEditor : UnityEditor.Editor { + private SerializedProperty m_UseUnreliableDeltas; private SerializedProperty m_SyncPositionXProperty; private SerializedProperty m_SyncPositionYProperty; private SerializedProperty m_SyncPositionZProperty; @@ -39,6 +40,7 @@ namespace Unity.Netcode.Editor /// public void OnEnable() { + m_UseUnreliableDeltas = serializedObject.FindProperty(nameof(NetworkTransform.UseUnreliableDeltas)); m_SyncPositionXProperty = serializedObject.FindProperty(nameof(NetworkTransform.SyncPositionX)); m_SyncPositionYProperty = serializedObject.FindProperty(nameof(NetworkTransform.SyncPositionY)); m_SyncPositionZProperty = serializedObject.FindProperty(nameof(NetworkTransform.SyncPositionZ)); @@ -129,7 +131,9 @@ namespace Unity.Netcode.Editor EditorGUILayout.PropertyField(m_PositionThresholdProperty); EditorGUILayout.PropertyField(m_RotAngleThresholdProperty); EditorGUILayout.PropertyField(m_ScaleThresholdProperty); - + EditorGUILayout.Space(); + EditorGUILayout.LabelField("Delivery", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(m_UseUnreliableDeltas); EditorGUILayout.Space(); EditorGUILayout.LabelField("Configurations", EditorStyles.boldLabel); EditorGUILayout.PropertyField(m_InLocalSpaceProperty); diff --git a/Editor/com.unity.netcode.editor.asmdef b/Editor/com.unity.netcode.editor.asmdef index 29b759d..8eb62d0 100644 --- a/Editor/com.unity.netcode.editor.asmdef +++ b/Editor/com.unity.netcode.editor.asmdef @@ -3,11 +3,21 @@ "rootNamespace": "Unity.Netcode.Editor", "references": [ "Unity.Netcode.Runtime", - "Unity.Netcode.Components" + "Unity.Netcode.Components", + "Unity.Services.Relay", + "Unity.Networking.Transport", + "Unity.Services.Core", + "Unity.Services.Authentication" ], "includePlatforms": [ "Editor" ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], "versionDefines": [ { "name": "com.unity.multiplayer.tools", @@ -33,6 +43,17 @@ "name": "com.unity.modules.physics2d", "expression": "", "define": "COM_UNITY_MODULES_PHYSICS2D" + }, + { + "name": "com.unity.services.relay", + "expression": "1.0", + "define": "RELAY_SDK_INSTALLED" + }, + { + "name": "com.unity.transport", + "expression": "2.0", + "define": "UTP_TRANSPORT_2_0_ABOVE" } - ] -} + ], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Runtime/Configuration/NetworkConfig.cs b/Runtime/Configuration/NetworkConfig.cs index 5970ab1..9b855ec 100644 --- a/Runtime/Configuration/NetworkConfig.cs +++ b/Runtime/Configuration/NetworkConfig.cs @@ -132,7 +132,7 @@ namespace Unity.Netcode /// The amount of time a message should be buffered if the asset or object needed to process it doesn't exist yet. If the asset is not added/object is not spawned within this time, it will be dropped. /// [Tooltip("The amount of time a message should be buffered if the asset or object needed to process it doesn't exist yet. If the asset is not added/object is not spawned within this time, it will be dropped")] - public float SpawnTimeout = 1f; + public float SpawnTimeout = 10f; /// /// Whether or not to enable network logs. diff --git a/Runtime/Configuration/NetworkPrefabs.cs b/Runtime/Configuration/NetworkPrefabs.cs index e47dcc1..42758f7 100644 --- a/Runtime/Configuration/NetworkPrefabs.cs +++ b/Runtime/Configuration/NetworkPrefabs.cs @@ -30,6 +30,12 @@ namespace Unity.Netcode [NonSerialized] public Dictionary NetworkPrefabOverrideLinks = new Dictionary(); + /// + /// This is used for the legacy way of spawning NetworkPrefabs with an override when manually instantiating and spawning. + /// To handle multiple source NetworkPrefab overrides that all point to the same target NetworkPrefab use + /// + /// or + /// [NonSerialized] public Dictionary OverrideToNetworkPrefab = new Dictionary(); @@ -234,7 +240,8 @@ namespace Unity.Netcode { for (int i = 0; i < m_Prefabs.Count; i++) { - if (m_Prefabs[i].Prefab == prefab) + // Check both values as Prefab and be different than SourcePrefabToOverride + if (m_Prefabs[i].Prefab == prefab || m_Prefabs[i].SourcePrefabToOverride == prefab) { return true; } @@ -262,7 +269,7 @@ namespace Unity.Netcode } /// - /// Configures and for the given + /// Configures for the given /// private bool AddPrefabRegistration(NetworkPrefab networkPrefab) { @@ -296,28 +303,16 @@ namespace Unity.Netcode return true; } - // Make sure we don't have several overrides targeting the same prefab. Apparently we don't support that... shame. - if (OverrideToNetworkPrefab.ContainsKey(target)) - { - var networkObject = networkPrefab.Prefab.GetComponent(); - - // This can happen if a user tries to make several GlobalObjectIdHash values point to the same target - Debug.LogError($"{nameof(NetworkPrefab)} (\"{networkObject.name}\") has a duplicate {nameof(NetworkObject.GlobalObjectIdHash)} target entry value of: {target}!"); - return false; - } - switch (networkPrefab.Override) { case NetworkPrefabOverride.Prefab: - { - NetworkPrefabOverrideLinks.Add(source, networkPrefab); - OverrideToNetworkPrefab.Add(target, source); - } - break; case NetworkPrefabOverride.Hash: { NetworkPrefabOverrideLinks.Add(source, networkPrefab); - OverrideToNetworkPrefab.Add(target, source); + if (!OverrideToNetworkPrefab.ContainsKey(target)) + { + OverrideToNetworkPrefab.Add(target, source); + } } break; } diff --git a/Runtime/Connection/NetworkConnectionManager.cs b/Runtime/Connection/NetworkConnectionManager.cs index ccbe775..d737598 100644 --- a/Runtime/Connection/NetworkConnectionManager.cs +++ b/Runtime/Connection/NetworkConnectionManager.cs @@ -10,6 +10,38 @@ using Object = UnityEngine.Object; namespace Unity.Netcode { + + public enum ConnectionEvent + { + ClientConnected, + PeerConnected, + ClientDisconnected, + PeerDisconnected + } + + public struct ConnectionEventData + { + public ConnectionEvent EventType; + + /// + /// The client ID for the client that just connected + /// For the and + /// events on the client side, this will be LocalClientId. + /// On the server side, this will be the ID of the client that just connected. + /// + /// For the and + /// events on the client side, this will be the client ID assigned by the server to the remote peer. + /// + public ulong ClientId; + + /// + /// This is only populated in on the client side, and + /// contains the list of other peers who were present before you connected. In all other situations, + /// this array will be uninitialized. + /// + public NativeArray PeerClientIds; + } + /// /// The NGO connection manager handles: /// - Client Connections @@ -42,7 +74,105 @@ namespace Unity.Netcode /// public event Action OnClientDisconnectCallback = null; - internal void InvokeOnClientConnectedCallback(ulong clientId) => OnClientConnectedCallback?.Invoke(clientId); + /// + /// The callback to invoke once a peer connects. This callback is only ran on the server and on the local client that connects. + /// + public event Action OnConnectionEvent = null; + + + internal void InvokeOnClientConnectedCallback(ulong clientId) + { + try + { + OnClientConnectedCallback?.Invoke(clientId); + } + catch (Exception exception) + { + Debug.LogException(exception); + } + + if (!NetworkManager.IsServer) + { + var peerClientIds = new NativeArray(Math.Max(NetworkManager.ConnectedClientsIds.Count - 1, 0), Allocator.Temp); + // `using var peerClientIds` or `using(peerClientIds)` renders it immutable... + using var sentinel = peerClientIds; + + var idx = 0; + foreach (var peerId in NetworkManager.ConnectedClientsIds) + { + if (peerId == NetworkManager.LocalClientId) + { + continue; + } + + peerClientIds[idx] = peerId; + ++idx; + } + + try + { + OnConnectionEvent?.Invoke(NetworkManager, new ConnectionEventData { ClientId = NetworkManager.LocalClientId, EventType = ConnectionEvent.ClientConnected, PeerClientIds = peerClientIds }); + } + catch (Exception exception) + { + Debug.LogException(exception); + } + } + else + { + try + { + OnConnectionEvent?.Invoke(NetworkManager, new ConnectionEventData { ClientId = clientId, EventType = ConnectionEvent.ClientConnected }); + } + catch (Exception exception) + { + Debug.LogException(exception); + } + } + } + + internal void InvokeOnClientDisconnectCallback(ulong clientId) + { + try + { + OnClientDisconnectCallback?.Invoke(clientId); + } + catch (Exception exception) + { + Debug.LogException(exception); + } + try + { + OnConnectionEvent?.Invoke(NetworkManager, new ConnectionEventData { ClientId = clientId, EventType = ConnectionEvent.ClientDisconnected }); + } + catch (Exception exception) + { + Debug.LogException(exception); + } + } + + internal void InvokeOnPeerConnectedCallback(ulong clientId) + { + try + { + OnConnectionEvent?.Invoke(NetworkManager, new ConnectionEventData { ClientId = clientId, EventType = ConnectionEvent.PeerConnected }); + } + catch (Exception exception) + { + Debug.LogException(exception); + } + } + internal void InvokeOnPeerDisconnectedCallback(ulong clientId) + { + try + { + OnConnectionEvent?.Invoke(NetworkManager, new ConnectionEventData { ClientId = clientId, EventType = ConnectionEvent.PeerDisconnected }); + } + catch (Exception exception) + { + Debug.LogException(exception); + } + } /// /// The callback to invoke if the fails. @@ -217,7 +347,7 @@ namespace Unity.Netcode // When this happens, the client will not have an entry within the m_TransportIdToClientIdMap or m_ClientIdToTransportIdMap lookup tables so we exit early and just return 0 to be used for the disconnect event. if (!LocalClient.IsServer && !TransportIdToClientIdMap.ContainsKey(transportId)) { - return 0; + return NetworkManager.LocalClientId; } var clientId = TransportIdToClientId(transportId); @@ -346,31 +476,38 @@ namespace Unity.Netcode s_TransportDisconnect.Begin(); #endif var clientId = TransportIdCleanUp(transportClientId); - if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) { NetworkLog.LogInfo($"Disconnect Event From {clientId}"); } + // If we are a client and we have gotten the ServerClientId back, then use our assigned local id as the client that was + // disconnected (either the user disconnected or the server disconnected, but the client that disconnected is the LocalClientId) + if (!NetworkManager.IsServer && clientId == NetworkManager.ServerClientId) + { + clientId = NetworkManager.LocalClientId; + } + // Process the incoming message queue so that we get everything from the server disconnecting us or, if we are the server, so we got everything from that client. MessageManager.ProcessIncomingMessageQueue(); - try + InvokeOnClientDisconnectCallback(clientId); + + if (LocalClient.IsHost) { - OnClientDisconnectCallback?.Invoke(clientId); - } - catch (Exception exception) - { - Debug.LogException(exception); + InvokeOnPeerDisconnectedCallback(clientId); } if (LocalClient.IsServer) { OnClientDisconnectFromServer(clientId); } - else + else // As long as we are not in the middle of a shutdown + if (!NetworkManager.ShutdownInProgress) { - // We must pass true here and not process any sends messages as we are no longer connected and thus there is no one to send any messages to and this will cause an exception within UnityTransport as the client ID is no longer valid. + // We must pass true here and not process any sends messages as we are no longer connected. + // Otherwise, attempting to process messages here can cause an exception within UnityTransport + // as the client ID is no longer valid. NetworkManager.Shutdown(true); } #if DEVELOPMENT_BUILD || UNITY_EDITOR @@ -623,8 +760,17 @@ namespace Unity.Netcode var message = new ConnectionApprovedMessage { OwnerClientId = ownerClientId, - NetworkTick = NetworkManager.LocalTime.Tick + NetworkTick = NetworkManager.LocalTime.Tick, + ConnectedClientIds = new NativeArray(ConnectedClientIds.Count, Allocator.Temp) }; + + var i = 0; + foreach (var clientId in ConnectedClientIds) + { + message.ConnectedClientIds[i] = clientId; + ++i; + } + if (!NetworkManager.NetworkConfig.EnableSceneManagement) { // Update the observed spawned NetworkObjects for the newly connected player when scene management is disabled @@ -651,12 +797,17 @@ namespace Unity.Netcode SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, ownerClientId); message.MessageVersions.Dispose(); + message.ConnectedClientIds.Dispose(); // If scene management is disabled, then we are done and notify the local host-server the client is connected if (!NetworkManager.NetworkConfig.EnableSceneManagement) { NetworkManager.ConnectedClients[ownerClientId].IsConnected = true; InvokeOnClientConnectedCallback(ownerClientId); + if (LocalClient.IsHost) + { + InvokeOnPeerConnectedCallback(ownerClientId); + } } else // Otherwise, let NetworkSceneManager handle the initial scene and NetworkObject synchronization { @@ -740,6 +891,8 @@ namespace Unity.Netcode ConnectedClients.Add(clientId, networkClient); ConnectedClientsList.Add(networkClient); + var message = new ClientConnectedMessage { ClientId = clientId }; + NetworkManager.MessageManager.SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, ConnectedClientIds); ConnectedClientIds.Add(clientId); return networkClient; } @@ -757,6 +910,7 @@ namespace Unity.Netcode // If we are shutting down and this is the server or host disconnecting, then ignore // clean up as everything that needs to be destroyed will be during shutdown. + if (NetworkManager.ShutdownInProgress && clientId == NetworkManager.ServerClientId) { return; @@ -780,7 +934,7 @@ namespace Unity.Netcode NetworkManager.SpawnManager.DespawnObject(playerObject, true); } } - else + else if (!NetworkManager.ShutdownInProgress) { playerObject.RemoveOwnership(); } @@ -799,7 +953,7 @@ namespace Unity.Netcode } else { - // Handle changing ownership and prefab handlers + // Handle changing ownership and prefab handlers for (int i = clientOwnedObjects.Count - 1; i >= 0; i--) { var ownedObject = clientOwnedObjects[i]; @@ -816,7 +970,7 @@ namespace Unity.Netcode Object.Destroy(ownedObject.gameObject); } } - else + else if (!NetworkManager.ShutdownInProgress) { ownedObject.RemoveOwnership(); } @@ -837,6 +991,8 @@ namespace Unity.Netcode } ConnectedClientIds.Remove(clientId); + var message = new ClientDisconnectedMessage { ClientId = clientId }; + MessageManager?.SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, ConnectedClientIds); } // If the client ID transport map exists @@ -845,13 +1001,11 @@ namespace Unity.Netcode var transportId = ClientIdToTransportId(clientId); NetworkManager.NetworkConfig.NetworkTransport.DisconnectRemoteClient(transportId); - try + InvokeOnClientDisconnectCallback(clientId); + + if (LocalClient.IsHost) { - OnClientDisconnectCallback?.Invoke(clientId); - } - catch (Exception exception) - { - Debug.LogException(exception); + InvokeOnPeerDisconnectedCallback(clientId); } // Clean up the transport to client (and vice versa) mappings @@ -889,6 +1043,12 @@ namespace Unity.Netcode throw new NotServerException($"Only server can disconnect remote clients. Please use `{nameof(Shutdown)}()` instead."); } + if (clientId == NetworkManager.ServerClientId) + { + Debug.LogWarning($"Disconnecting the local server-host client is not allowed. Use NetworkManager.Shutdown instead."); + return; + } + if (!string.IsNullOrEmpty(reason)) { var disconnectReason = new DisconnectReasonMessage @@ -933,13 +1093,8 @@ namespace Unity.Netcode /// internal void Shutdown() { - LocalClient.IsApproved = false; - LocalClient.IsConnected = false; if (LocalClient.IsServer) { - // make sure all messages are flushed before transport disconnect clients - MessageManager?.ProcessSendQueues(); - // Build a list of all client ids to be disconnected var disconnectedIds = new HashSet(); @@ -975,9 +1130,15 @@ namespace Unity.Netcode { DisconnectRemoteClient(clientId); } + + // make sure all messages are flushed before transport disconnects clients + MessageManager?.ProcessSendQueues(); } else if (NetworkManager != null && NetworkManager.IsListening && LocalClient.IsClient) { + // make sure all messages are flushed before disconnecting + MessageManager?.ProcessSendQueues(); + // Client only, send disconnect and if transport throws and exception, log the exception and continue the shutdown sequence (or forever be shutting down) try { @@ -989,6 +1150,12 @@ namespace Unity.Netcode } } + LocalClient.IsApproved = false; + LocalClient.IsConnected = false; + ConnectedClients.Clear(); + ConnectedClientIds.Clear(); + ConnectedClientsList.Clear(); + if (NetworkManager != null && NetworkManager.NetworkConfig?.NetworkTransport != null) { NetworkManager.NetworkConfig.NetworkTransport.OnTransportEvent -= HandleNetworkEvent; diff --git a/Runtime/Core/NetworkBehaviour.cs b/Runtime/Core/NetworkBehaviour.cs index 7a5bd73..6d099c0 100644 --- a/Runtime/Core/NetworkBehaviour.cs +++ b/Runtime/Core/NetworkBehaviour.cs @@ -6,6 +6,14 @@ using UnityEngine; namespace Unity.Netcode { + public class RpcException : Exception + { + public RpcException(string message) : base(message) + { + + } + } + /// /// The base class to override to write network code. Inherits MonoBehaviour /// @@ -27,16 +35,22 @@ namespace Unity.Netcode // RuntimeAccessModifiersILPP will make this `protected` internal enum __RpcExecStage { + // Technically will overlap with None and Server + // but it doesn't matter since we don't use None and Server + Send = 0, + Execute = 1, + + // Backward compatibility, not used... None = 0, Server = 1, - Client = 2 + Client = 2, } // NetworkBehaviourILPP will override this in derived classes to return the name of the concrete type internal virtual string __getTypeName() => nameof(NetworkBehaviour); [NonSerialized] // RuntimeAccessModifiersILPP will make this `protected` - internal __RpcExecStage __rpc_exec_stage = __RpcExecStage.None; + internal __RpcExecStage __rpc_exec_stage = __RpcExecStage.Send; #pragma warning restore IDE1006 // restore naming rule violation check private const int k_RpcMessageDefaultSize = 1024; // 1k @@ -284,6 +298,99 @@ namespace Unity.Netcode #endif } + +#pragma warning disable IDE1006 // disable naming rule violation check + // RuntimeAccessModifiersILPP will make this `protected` + internal FastBufferWriter __beginSendRpc(uint rpcMethodId, RpcParams rpcParams, RpcAttribute.RpcAttributeParams attributeParams, SendTo defaultTarget, RpcDelivery rpcDelivery) +#pragma warning restore IDE1006 // restore naming rule violation check + { + if (attributeParams.RequireOwnership && !IsOwner) + { + throw new RpcException("This RPC can only be sent by its owner."); + } + return new FastBufferWriter(k_RpcMessageDefaultSize, Allocator.Temp, k_RpcMessageMaximumSize); + } + +#pragma warning disable IDE1006 // disable naming rule violation check + // RuntimeAccessModifiersILPP will make this `protected` + internal void __endSendRpc(ref FastBufferWriter bufferWriter, uint rpcMethodId, RpcParams rpcParams, RpcAttribute.RpcAttributeParams attributeParams, SendTo defaultTarget, RpcDelivery rpcDelivery) +#pragma warning restore IDE1006 // restore naming rule violation check + { + var rpcMessage = new RpcMessage + { + Metadata = new RpcMetadata + { + NetworkObjectId = NetworkObjectId, + NetworkBehaviourId = NetworkBehaviourId, + NetworkRpcMethodId = rpcMethodId, + }, + SenderClientId = NetworkManager.LocalClientId, + WriteBuffer = bufferWriter + }; + + NetworkDelivery networkDelivery; + switch (rpcDelivery) + { + default: + case RpcDelivery.Reliable: + networkDelivery = NetworkDelivery.ReliableFragmentedSequenced; + break; + case RpcDelivery.Unreliable: + if (bufferWriter.Length > NetworkManager.MessageManager.NonFragmentedMessageMaxSize) + { + throw new OverflowException("RPC parameters are too large for unreliable delivery."); + } + networkDelivery = NetworkDelivery.Unreliable; + break; + } + + if (rpcParams.Send.Target == null) + { + switch (defaultTarget) + { + case SendTo.Everyone: + rpcParams.Send.Target = RpcTarget.Everyone; + break; + case SendTo.Owner: + rpcParams.Send.Target = RpcTarget.Owner; + break; + case SendTo.Server: + rpcParams.Send.Target = RpcTarget.Server; + break; + case SendTo.NotServer: + rpcParams.Send.Target = RpcTarget.NotServer; + break; + case SendTo.NotMe: + rpcParams.Send.Target = RpcTarget.NotMe; + break; + case SendTo.NotOwner: + rpcParams.Send.Target = RpcTarget.NotOwner; + break; + case SendTo.Me: + rpcParams.Send.Target = RpcTarget.Me; + break; + case SendTo.ClientsAndHost: + rpcParams.Send.Target = RpcTarget.ClientsAndHost; + break; + case SendTo.SpecifiedInParams: + throw new RpcException("This method requires a runtime-specified send target."); + } + } + else if (defaultTarget != SendTo.SpecifiedInParams && !attributeParams.AllowTargetOverride) + { + throw new RpcException("Target override is not allowed for this method."); + } + + if (rpcParams.Send.LocalDeferMode == LocalDeferMode.Default) + { + rpcParams.Send.LocalDeferMode = attributeParams.DeferLocal ? LocalDeferMode.Defer : LocalDeferMode.SendImmediate; + } + + rpcParams.Send.Target.Send(this, ref rpcMessage, networkDelivery, rpcParams); + + bufferWriter.Dispose(); + } + #pragma warning disable IDE1006 // disable naming rule violation check // RuntimeAccessModifiersILPP will make this `protected` internal static NativeList __createNativeList() where T : unmanaged @@ -315,6 +422,24 @@ namespace Unity.Netcode } } + // This erroneously tries to simplify these method references but the docs do not pick it up correctly + // because they try to resolve it on the field rather than the class of the same name. +#pragma warning disable IDE0001 + /// + /// Provides access to the various targets at runtime, as well as + /// runtime-bound targets like , + /// , + /// , + /// , + /// , , + /// , + /// , + /// , and + /// + /// +#pragma warning restore IDE0001 + public RpcTarget RpcTarget => NetworkManager.RpcTarget; + /// /// If a NetworkObject is assigned, it will return whether or not this NetworkObject /// is the local player object. If no NetworkObject is assigned it will always return false. @@ -331,6 +456,11 @@ namespace Unity.Netcode /// public bool IsServer { get; private set; } + /// + /// Gets if the server (local or remote) is a host - i.e., also a client + /// + public bool ServerIsHost { get; private set; } + /// /// Gets if we are executing as client /// @@ -472,12 +602,13 @@ namespace Unity.Netcode IsHost = NetworkManager.IsListening && NetworkManager.IsHost; IsClient = NetworkManager.IsListening && NetworkManager.IsClient; IsServer = NetworkManager.IsListening && NetworkManager.IsServer; + ServerIsHost = NetworkManager.IsListening && NetworkManager.ServerIsHost; } } else // Shouldn't happen, but if so then set the properties to their default value; { OwnerClientId = NetworkObjectId = default; - IsOwnedByServer = IsOwner = IsHost = IsClient = IsServer = default; + IsOwnedByServer = IsOwner = IsHost = IsClient = IsServer = ServerIsHost = default; NetworkBehaviourId = default; } } diff --git a/Runtime/Core/NetworkManager.cs b/Runtime/Core/NetworkManager.cs index 78301e7..4fd66e3 100644 --- a/Runtime/Core/NetworkManager.cs +++ b/Runtime/Core/NetworkManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Unity.Collections; using UnityEngine; #if UNITY_EDITOR using UnityEditor; @@ -42,6 +43,8 @@ namespace Unity.Netcode ConnectionManager.ProcessPendingApprovals(); ConnectionManager.PollAndHandleNetworkEvents(); + DeferredMessageManager.ProcessTriggers(IDeferredNetworkMessageManager.TriggerType.OnNextFrame, 0); + MessageManager.ProcessIncomingMessageQueue(); MessageManager.CleanupDisconnectedClients(); } @@ -70,13 +73,89 @@ namespace Unity.Netcode if (m_ShuttingDown) { - ShutdownInternal(); + // Host-server will disconnect any connected clients prior to finalizing its shutdown + if (IsServer) + { + ProcessServerShutdown(); + } + else + { + // Clients just disconnect immediately + ShutdownInternal(); + } } } break; } } + /// + /// Used to provide a graceful shutdown sequence + /// + internal enum ServerShutdownStates + { + None, + WaitForClientDisconnects, + InternalShutdown, + ShuttingDown, + }; + + internal ServerShutdownStates ServerShutdownState; + private float m_ShutdownTimeout; + + /// + /// This is a "soft shutdown" where the host or server will disconnect + /// all clients, with a provided reasons, prior to invoking its final + /// internal shutdown. + /// + internal void ProcessServerShutdown() + { + var minClientCount = IsHost ? 2 : 1; + switch (ServerShutdownState) + { + case ServerShutdownStates.None: + { + if (ConnectedClients.Count >= minClientCount) + { + var hostServer = IsHost ? "host" : "server"; + var disconnectReason = $"Disconnected due to {hostServer} shutting down."; + for (int i = ConnectedClientsIds.Count - 1; i >= 0; i--) + { + var clientId = ConnectedClientsIds[i]; + if (clientId == ServerClientId) + { + continue; + } + ConnectionManager.DisconnectClient(clientId, disconnectReason); + } + ServerShutdownState = ServerShutdownStates.WaitForClientDisconnects; + m_ShutdownTimeout = Time.realtimeSinceStartup + 5.0f; + } + else + { + ServerShutdownState = ServerShutdownStates.InternalShutdown; + ProcessServerShutdown(); + } + break; + } + case ServerShutdownStates.WaitForClientDisconnects: + { + if (ConnectedClients.Count < minClientCount || m_ShutdownTimeout < Time.realtimeSinceStartup) + { + ServerShutdownState = ServerShutdownStates.InternalShutdown; + ProcessServerShutdown(); + } + break; + } + case ServerShutdownStates.InternalShutdown: + { + ServerShutdownState = ServerShutdownStates.ShuttingDown; + ShutdownInternal(); + break; + } + } + } + /// /// The client id used to represent the server /// @@ -104,7 +183,7 @@ namespace Unity.Netcode /// /// Gets a list of just the IDs of all connected clients. This is only accessible on the server. /// - public IReadOnlyList ConnectedClientsIds => IsServer ? ConnectionManager.ConnectedClientIds : throw new NotServerException($"{nameof(ConnectionManager.ConnectedClientIds)} should only be accessed on server."); + public IReadOnlyList ConnectedClientsIds => ConnectionManager.ConnectedClientIds; /// /// Gets the local for this client. @@ -122,6 +201,11 @@ namespace Unity.Netcode /// public bool IsServer => ConnectionManager.LocalClient.IsServer; + /// + /// Gets whether or not the current server (local or remote) is a host - i.e., also a client + /// + public bool ServerIsHost => ConnectionManager.ConnectedClientIds.Contains(ServerClientId); + /// /// Gets Whether or not a client is running /// @@ -209,6 +293,8 @@ namespace Unity.Netcode /// /// The callback to invoke once a client connects. This callback is only ran on the server and on the local client that connects. + /// + /// It is recommended to use OnConnectionEvent instead, as this will eventually be deprecated /// public event Action OnClientConnectedCallback { @@ -218,6 +304,8 @@ namespace Unity.Netcode /// /// The callback to invoke when a client disconnects. This callback is only ran on the server and on the local client that disconnects. + /// + /// It is recommended to use OnConnectionEvent instead, as this will eventually be deprecated /// public event Action OnClientDisconnectCallback { @@ -225,6 +313,16 @@ namespace Unity.Netcode remove => ConnectionManager.OnClientDisconnectCallback -= value; } + /// + /// The callback to invoke on any connection event. See and + /// for more info. + /// + public event Action OnConnectionEvent + { + add => ConnectionManager.OnConnectionEvent += value; + remove => ConnectionManager.OnConnectionEvent -= value; + } + /// /// The current host name we are connected to, used to validate certificate /// @@ -379,6 +477,24 @@ namespace Unity.Netcode internal IDeferredNetworkMessageManager DeferredMessageManager { get; private set; } + // This erroneously tries to simplify these method references but the docs do not pick it up correctly + // because they try to resolve it on the field rather than the class of the same name. +#pragma warning disable IDE0001 + /// + /// Provides access to the various targets at runtime, as well as + /// runtime-bound targets like , + /// , + /// , + /// , + /// , , + /// , + /// , + /// , and + /// + /// +#pragma warning restore IDE0001 + public RpcTarget RpcTarget; + /// /// Gets the CustomMessagingManager for this NetworkManager /// @@ -664,6 +780,12 @@ namespace Unity.Netcode internal void Initialize(bool server) { + // Make sure the ServerShutdownState is reset when initializing + if (server) + { + ServerShutdownState = ServerShutdownStates.None; + } + // Don't allow the user to start a network session if the NetworkManager is // still parented under another GameObject if (NetworkManagerCheckForParent(true)) @@ -729,6 +851,8 @@ namespace Unity.Netcode DeferredMessageManager = ComponentFactory.Create(this); + RpcTarget = new RpcTarget(this); + CustomMessagingManager = new CustomMessagingManager(this); SceneManager = new NetworkSceneManager(this); @@ -929,6 +1053,7 @@ namespace Unity.Netcode { LocalClientId = ServerClientId; NetworkMetrics.SetConnectionId(LocalClientId); + MessageManager.SetLocalClientId(LocalClientId); if (NetworkConfig.ConnectionApproval && ConnectionApprovalCallback != null) { @@ -1007,11 +1132,6 @@ namespace Unity.Netcode MessageManager.StopProcessing = discardMessageQueue; } } - - if (NetworkConfig != null && NetworkConfig.NetworkTransport != null) - { - NetworkConfig.NetworkTransport.OnTransportEvent -= ConnectionManager.HandleNetworkEvent; - } } // Ensures that the NetworkManager is cleaned up before OnDestroy is run on NetworkObjects and NetworkBehaviours when unloading a scene with a NetworkManager @@ -1036,6 +1156,9 @@ namespace Unity.Netcode DeferredMessageManager?.CleanupAllTriggers(); CustomMessagingManager = null; + RpcTarget?.Dispose(); + RpcTarget = null; + BehaviourUpdater?.Shutdown(); BehaviourUpdater = null; diff --git a/Runtime/Core/NetworkObject.cs b/Runtime/Core/NetworkObject.cs index f7d6217..4c47753 100644 --- a/Runtime/Core/NetworkObject.cs +++ b/Runtime/Core/NetworkObject.cs @@ -26,6 +26,22 @@ namespace Unity.Netcode [SerializeField] internal uint GlobalObjectIdHash; + /// + /// Used to track the source GlobalObjectIdHash value of the associated network prefab. + /// When an override exists or it is in-scene placed, GlobalObjectIdHash and PrefabGlobalObjectIdHash + /// will be different. The PrefabGlobalObjectIdHash value is what is used when sending a . + /// + internal uint PrefabGlobalObjectIdHash; + + /// + /// This is the source prefab of an in-scene placed NetworkObject. This is not set for in-scene + /// placd NetworkObjects that are not prefab instances, dynamically spawned prefab instances, + /// or for network prefab assets. + /// + [HideInInspector] + [SerializeField] + internal uint InScenePlacedSourceGlobalObjectIdHash; + /// /// Gets the Prefab Hash Id of this object if the object is registerd as a prefab otherwise it returns 0 /// @@ -34,15 +50,7 @@ namespace Unity.Netcode { get { - foreach (var prefab in NetworkManager.NetworkConfig.Prefabs.Prefabs) - { - if (prefab.Prefab == gameObject) - { - return GlobalObjectIdHash; - } - } - - return 0; + return GlobalObjectIdHash; } } @@ -149,6 +157,9 @@ namespace Unity.Netcode EditorUtility.SetDirty(this); } } + + // Always check for in-scene placed to assure any previous version scene assets with in-scene place NetworkObjects gets updated + CheckForInScenePlaced(); } private bool IsEditingPrefab() @@ -164,6 +175,33 @@ namespace Unity.Netcode return true; } + /// + /// This checks to see if this NetworkObject is an in-scene placed prefab instance. If so it will + /// automatically find the source prefab asset's GlobalObjectIdHash value, assign it to + /// InScenePlacedSourceGlobalObjectIdHash and mark this as being in-scene placed. + /// + /// + /// This NetworkObject is considered an in-scene placed prefab asset instance if it is: + /// - Part of a prefab + /// - Not being directly edited + /// - Within a valid scene that is part of the scenes in build list + /// (In-scene defined NetworkObjects that are not part of a prefab instance are excluded.) + /// + private void CheckForInScenePlaced() + { + if (PrefabUtility.IsPartOfAnyPrefab(this) && !IsEditingPrefab() && gameObject.scene.IsValid() && gameObject.scene.isLoaded && gameObject.scene.buildIndex >= 0) + { + var prefab = PrefabUtility.GetCorrespondingObjectFromSource(gameObject); + var assetPath = AssetDatabase.GetAssetPath(prefab); + var sourceAsset = AssetDatabase.LoadAssetAtPath(assetPath); + if (sourceAsset != null && sourceAsset.GlobalObjectIdHash != 0 && InScenePlacedSourceGlobalObjectIdHash != sourceAsset.GlobalObjectIdHash) + { + InScenePlacedSourceGlobalObjectIdHash = sourceAsset.GlobalObjectIdHash; + } + IsSceneObject = true; + } + } + private GlobalObjectId GetGlobalId() { var instanceGlobalId = GlobalObjectId.GetGlobalObjectIdSlow(this); @@ -703,7 +741,7 @@ namespace Unity.Netcode // Since we still have a session connection, log locally and on the server to inform user of this issue. if (NetworkManager.LogLevel <= LogLevel.Error) { - NetworkLog.LogErrorServer($"Destroy a spawned {nameof(NetworkObject)} on a non-host client is not valid. Call {nameof(Destroy)} or {nameof(Despawn)} on the server/host instead."); + NetworkLog.LogErrorServer($"[Invalid Destroy][{gameObject.name}][NetworkObjectId:{NetworkObjectId}] Destroy a spawned {nameof(NetworkObject)} on a non-host client is not valid. Call {nameof(Destroy)} or {nameof(Despawn)} on the server/host instead."); } return; } @@ -720,7 +758,7 @@ namespace Unity.Netcode } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void SpawnInternal(bool destroyWithScene, ulong ownerClientId, bool playerObject) + internal void SpawnInternal(bool destroyWithScene, ulong ownerClientId, bool playerObject) { if (!NetworkManager.IsListening) { @@ -743,6 +781,78 @@ namespace Unity.Netcode } } + /// + /// This invokes . + /// + /// The NetworkPrefab to instantiate and spawn. + /// The local instance of the NetworkManager connected to an session in progress. + /// The owner of the instance (defaults to server). + /// Whether the instance will be destroyed when the scene it is located within is unloaded (default is false). + /// Whether the instance is a player object or not (default is false). + /// Whether you want to force spawning the override when running as a host or server or if you want it to spawn the override for host mode and + /// the source prefab for server. If there is an override, clients always spawn that as opposed to the source prefab (defaults to false). + /// The starting poisiton of the instance. + /// The starting rotation of the instance. + /// The newly instantiated and spawned prefab instance. + public static NetworkObject InstantiateAndSpawn(GameObject networkPrefab, NetworkManager networkManager, ulong ownerClientId = NetworkManager.ServerClientId, bool destroyWithScene = false, bool isPlayerObject = false, bool forceOverride = false, Vector3 position = default, Quaternion rotation = default) + { + var networkObject = networkPrefab.GetComponent(); + if (networkObject == null) + { + Debug.LogError($"The {nameof(NetworkPrefab)} {networkPrefab.name} does not have a {nameof(NetworkObject)} component!"); + return null; + } + return networkObject.InstantiateAndSpawn(networkManager, ownerClientId, destroyWithScene, isPlayerObject, forceOverride, position, rotation); + } + + /// + /// This invokes . + /// + /// The local instance of the NetworkManager connected to an session in progress. + /// The owner of the instance (defaults to server). + /// Whether the instance will be destroyed when the scene it is located within is unloaded (default is false). + /// Whether the instance is a player object or not (default is false). + /// Whether you want to force spawning the override when running as a host or server or if you want it to spawn the override for host mode and + /// the source prefab for server. If there is an override, clients always spawn that as opposed to the source prefab (defaults to false). + /// The starting poisiton of the instance. + /// The starting rotation of the instance. + /// The newly instantiated and spawned prefab instance. + public NetworkObject InstantiateAndSpawn(NetworkManager networkManager, ulong ownerClientId = NetworkManager.ServerClientId, bool destroyWithScene = false, bool isPlayerObject = false, bool forceOverride = false, Vector3 position = default, Quaternion rotation = default) + { + if (networkManager == null) + { + Debug.LogError(NetworkSpawnManager.InstantiateAndSpawnErrors[NetworkSpawnManager.InstantiateAndSpawnErrorTypes.NetworkManagerNull]); + return null; + } + + if (!networkManager.IsListening) + { + Debug.LogError(NetworkSpawnManager.InstantiateAndSpawnErrors[NetworkSpawnManager.InstantiateAndSpawnErrorTypes.NoActiveSession]); + return null; + } + + if (!networkManager.IsServer) + { + Debug.LogError(NetworkSpawnManager.InstantiateAndSpawnErrors[NetworkSpawnManager.InstantiateAndSpawnErrorTypes.NotAuthority]); + return null; + } + + if (NetworkManager.ShutdownInProgress) + { + Debug.LogWarning(NetworkSpawnManager.InstantiateAndSpawnErrors[NetworkSpawnManager.InstantiateAndSpawnErrorTypes.InvokedWhenShuttingDown]); + return null; + } + + // Verify it is actually a valid prefab + if (!NetworkManager.NetworkConfig.Prefabs.Contains(gameObject)) + { + Debug.LogError(NetworkSpawnManager.InstantiateAndSpawnErrors[NetworkSpawnManager.InstantiateAndSpawnErrorTypes.NotRegisteredNetworkPrefab]); + return null; + } + + return NetworkManager.SpawnManager.InstantiateAndSpawnNoParameterChecks(this, ownerClientId, destroyWithScene, isPlayerObject, forceOverride, position, rotation); + } + /// /// Spawns this across the network. Can only be called from the Server /// @@ -969,20 +1079,21 @@ namespace Unity.Netcode return false; } - if (!NetworkManager.IsServer) + if (!NetworkManager.IsServer && !NetworkManager.ShutdownInProgress) { return false; } - if (!IsSpawned) + // If the parent is not null fail only if either of the two is true: + // - This instance is spawned and the parent is not. + // - This instance is not spawned and the parent is. + // Basically, don't allow parenting when either the child or parent is not spawned. + // Caveat: if the parent is null then we can allow parenting whether the instance is or is not spawned. + if (parent != null && (IsSpawned ^ parent.IsSpawned)) { return false; } - if (parent != null && !parent.IsSpawned) - { - return false; - } m_CachedWorldPositionStays = worldPositionStays; if (parent == null) @@ -1018,15 +1129,36 @@ namespace Unity.Netcode if (!NetworkManager.IsServer) { - transform.parent = m_CachedParent; - Debug.LogException(new NotServerException($"Only the server can reparent {nameof(NetworkObject)}s")); + // Log exception if we are a client and not shutting down. + if (!NetworkManager.ShutdownInProgress) + { + transform.parent = m_CachedParent; + Debug.LogException(new NotServerException($"Only the server can reparent {nameof(NetworkObject)}s")); + } + else // Otherwise, if we are removing a parent then go ahead and allow parenting to occur + if (transform.parent == null) + { + m_LatestParent = null; + m_CachedParent = null; + InvokeBehaviourOnNetworkObjectParentChanged(null); + } return; } - + else // Otherwise, on the serer side if this instance is not spawned... if (!IsSpawned) { - transform.parent = m_CachedParent; - Debug.LogException(new SpawnStateException($"{nameof(NetworkObject)} can only be reparented after being spawned")); + // ,,,and we are removing the parent, then go ahead and allow parenting to occur + if (transform.parent == null) + { + m_LatestParent = null; + m_CachedParent = null; + InvokeBehaviourOnNetworkObjectParentChanged(null); + } + else + { + transform.parent = m_CachedParent; + Debug.LogException(new SpawnStateException($"{nameof(NetworkObject)} can only be reparented after being spawned")); + } return; } var removeParent = false; @@ -1869,16 +2001,39 @@ namespace Unity.Netcode /// internal uint HostCheckForGlobalObjectIdHashOverride() { - if (NetworkManager.IsHost) + if (NetworkManager.IsServer) { if (NetworkManager.PrefabHandler.ContainsHandler(this)) { var globalObjectIdHash = NetworkManager.PrefabHandler.GetSourceGlobalObjectIdHash(GlobalObjectIdHash); return globalObjectIdHash == 0 ? GlobalObjectIdHash : globalObjectIdHash; } - if (NetworkManager.NetworkConfig.Prefabs.OverrideToNetworkPrefab.TryGetValue(GlobalObjectIdHash, out uint hash)) + + // If scene management is disabled and this is an in-scene placed NetworkObject then go ahead + // and send the InScenePlacedSourcePrefab's GlobalObjectIdHash value (i.e. what to dynamically spawn) + if (!NetworkManager.NetworkConfig.EnableSceneManagement && IsSceneObject.Value && InScenePlacedSourceGlobalObjectIdHash != 0) { - return hash; + return InScenePlacedSourceGlobalObjectIdHash; + } + + // If the PrefabGlobalObjectIdHash is a non-zero value and the GlobalObjectIdHash value is + // different from the PrefabGlobalObjectIdHash value, then the NetworkObject instance is + // an override for the original network prefab (i.e. PrefabGlobalObjectIdHash) + if (!IsSceneObject.Value && GlobalObjectIdHash != PrefabGlobalObjectIdHash) + { + // If the PrefabGlobalObjectIdHash is already populated (i.e. InstantiateAndSpawn used), then return this + if (PrefabGlobalObjectIdHash != 0) + { + return PrefabGlobalObjectIdHash; + } + else + { + // For legacy manual instantiation and spawning, check the OverrideToNetworkPrefab for a possible match + if (NetworkManager.NetworkConfig.Prefabs.OverrideToNetworkPrefab.ContainsKey(GlobalObjectIdHash)) + { + return NetworkManager.NetworkConfig.Prefabs.OverrideToNetworkPrefab[GlobalObjectIdHash]; + } + } } } diff --git a/Runtime/Messaging/DeferredMessageManager.cs b/Runtime/Messaging/DeferredMessageManager.cs index 227b167..728f0a7 100644 --- a/Runtime/Messaging/DeferredMessageManager.cs +++ b/Runtime/Messaging/DeferredMessageManager.cs @@ -113,6 +113,7 @@ namespace Unity.Netcode // processed before the object is fully spawned. This must be the last thing done in the spawn process. if (triggers.TryGetValue(key, out var triggerInfo)) { + triggers.Remove(key); foreach (var deferredMessage in triggerInfo.TriggerData) { // Reader will be disposed within HandleMessage @@ -120,7 +121,6 @@ namespace Unity.Netcode } triggerInfo.TriggerData.Dispose(); - triggers.Remove(key); } } } diff --git a/Runtime/Messaging/IDeferredNetworkMessageManager.cs b/Runtime/Messaging/IDeferredNetworkMessageManager.cs index 1fc7a4b..0a9ec91 100644 --- a/Runtime/Messaging/IDeferredNetworkMessageManager.cs +++ b/Runtime/Messaging/IDeferredNetworkMessageManager.cs @@ -6,6 +6,7 @@ namespace Unity.Netcode { OnSpawn, OnAddPrefab, + OnNextFrame, } /// diff --git a/Runtime/Messaging/Messages/ClientConnectedMessage.cs b/Runtime/Messaging/Messages/ClientConnectedMessage.cs new file mode 100644 index 0000000..92ed2eb --- /dev/null +++ b/Runtime/Messaging/Messages/ClientConnectedMessage.cs @@ -0,0 +1,35 @@ +namespace Unity.Netcode +{ + internal struct ClientConnectedMessage : INetworkMessage, INetworkSerializeByMemcpy + { + public int Version => 0; + + public ulong ClientId; + + public void Serialize(FastBufferWriter writer, int targetVersion) + { + BytePacker.WriteValueBitPacked(writer, ClientId); + } + + public bool Deserialize(FastBufferReader reader, ref NetworkContext context, int receivedMessageVersion) + { + var networkManager = (NetworkManager)context.SystemOwner; + if (!networkManager.IsClient) + { + return false; + } + ByteUnpacker.ReadValueBitPacked(reader, out ClientId); + return true; + } + + public void Handle(ref NetworkContext context) + { + var networkManager = (NetworkManager)context.SystemOwner; + networkManager.ConnectionManager.ConnectedClientIds.Add(ClientId); + if (networkManager.IsConnectedClient) + { + networkManager.ConnectionManager.InvokeOnPeerConnectedCallback(ClientId); + } + } + } +} diff --git a/Runtime/Messaging/Messages/ClientConnectedMessage.cs.meta b/Runtime/Messaging/Messages/ClientConnectedMessage.cs.meta new file mode 100644 index 0000000..7eafca1 --- /dev/null +++ b/Runtime/Messaging/Messages/ClientConnectedMessage.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 158454105806474cba54a4ea5a0bfb12 +timeCreated: 1697836112 \ No newline at end of file diff --git a/Runtime/Messaging/Messages/ClientDisconnectedMessage.cs b/Runtime/Messaging/Messages/ClientDisconnectedMessage.cs new file mode 100644 index 0000000..9d306cb --- /dev/null +++ b/Runtime/Messaging/Messages/ClientDisconnectedMessage.cs @@ -0,0 +1,35 @@ +namespace Unity.Netcode +{ + internal struct ClientDisconnectedMessage : INetworkMessage, INetworkSerializeByMemcpy + { + public int Version => 0; + + public ulong ClientId; + + public void Serialize(FastBufferWriter writer, int targetVersion) + { + BytePacker.WriteValueBitPacked(writer, ClientId); + } + + public bool Deserialize(FastBufferReader reader, ref NetworkContext context, int receivedMessageVersion) + { + var networkManager = (NetworkManager)context.SystemOwner; + if (!networkManager.IsClient) + { + return false; + } + ByteUnpacker.ReadValueBitPacked(reader, out ClientId); + return true; + } + + public void Handle(ref NetworkContext context) + { + var networkManager = (NetworkManager)context.SystemOwner; + networkManager.ConnectionManager.ConnectedClientIds.Remove(ClientId); + if (networkManager.IsConnectedClient) + { + networkManager.ConnectionManager.InvokeOnPeerDisconnectedCallback(ClientId); + } + } + } +} diff --git a/Runtime/Messaging/Messages/ClientDisconnectedMessage.cs.meta b/Runtime/Messaging/Messages/ClientDisconnectedMessage.cs.meta new file mode 100644 index 0000000..c639def --- /dev/null +++ b/Runtime/Messaging/Messages/ClientDisconnectedMessage.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8f91296c8e5f40b1a2a03d74a31526b6 +timeCreated: 1697836161 \ No newline at end of file diff --git a/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs b/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs index 0a0b398..5635f23 100644 --- a/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs +++ b/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs @@ -5,7 +5,8 @@ namespace Unity.Netcode { internal struct ConnectionApprovedMessage : INetworkMessage { - public int Version => 0; + private const int k_VersionAddClientIds = 1; + public int Version => k_VersionAddClientIds; public ulong OwnerClientId; public int NetworkTick; @@ -17,6 +18,8 @@ namespace Unity.Netcode public NativeArray MessageVersions; + public NativeArray ConnectedClientIds; + public void Serialize(FastBufferWriter writer, int targetVersion) { // ============================================================ @@ -36,6 +39,11 @@ namespace Unity.Netcode BytePacker.WriteValueBitPacked(writer, OwnerClientId); BytePacker.WriteValueBitPacked(writer, NetworkTick); + if (targetVersion >= k_VersionAddClientIds) + { + writer.WriteValueSafe(ConnectedClientIds); + } + uint sceneObjectCount = 0; // When SpawnedObjectsList is not null then scene management is disabled. Provide a list of @@ -106,6 +114,16 @@ namespace Unity.Netcode ByteUnpacker.ReadValueBitPacked(reader, out OwnerClientId); ByteUnpacker.ReadValueBitPacked(reader, out NetworkTick); + + if (receivedMessageVersion >= k_VersionAddClientIds) + { + reader.ReadValueSafe(out ConnectedClientIds, Allocator.TempJob); + } + else + { + ConnectedClientIds = new NativeArray(0, Allocator.TempJob); + } + m_ReceivedSceneObjectData = reader; return true; } @@ -114,6 +132,7 @@ namespace Unity.Netcode { var networkManager = (NetworkManager)context.SystemOwner; networkManager.LocalClientId = OwnerClientId; + networkManager.MessageManager.SetLocalClientId(networkManager.LocalClientId); networkManager.NetworkMetrics.SetConnectionId(networkManager.LocalClientId); var time = new NetworkTime(networkManager.NetworkTickSystem.TickRate, NetworkTick); @@ -126,6 +145,12 @@ namespace Unity.Netcode // Stop the client-side approval timeout coroutine since we are approved. networkManager.ConnectionManager.StopClientApprovalCoroutine(); + networkManager.ConnectionManager.ConnectedClientIds.Clear(); + foreach (var clientId in ConnectedClientIds) + { + networkManager.ConnectionManager.ConnectedClientIds.Add(clientId); + } + // Only if scene management is disabled do we handle NetworkObject synchronization at this point if (!networkManager.NetworkConfig.EnableSceneManagement) { @@ -146,6 +171,8 @@ namespace Unity.Netcode // When scene management is disabled we notify after everything is synchronized networkManager.ConnectionManager.InvokeOnClientConnectedCallback(context.SenderId); } + + ConnectedClientIds.Dispose(); } } } diff --git a/Runtime/Messaging/Messages/CreateObjectMessage.cs b/Runtime/Messaging/Messages/CreateObjectMessage.cs index b285413..809b7c9 100644 --- a/Runtime/Messaging/Messages/CreateObjectMessage.cs +++ b/Runtime/Messaging/Messages/CreateObjectMessage.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; namespace Unity.Netcode { internal struct CreateObjectMessage : INetworkMessage @@ -34,9 +35,29 @@ namespace Unity.Netcode public void Handle(ref NetworkContext context) { var networkManager = (NetworkManager)context.SystemOwner; - var networkObject = NetworkObject.AddSceneObject(ObjectInfo, m_ReceivedNetworkVariableData, networkManager); + // If a client receives a create object message and it is still synchronizing, then defer the object creation until it has finished synchronizing + if (networkManager.SceneManager.ShouldDeferCreateObject()) + { + networkManager.SceneManager.DeferCreateObject(context.SenderId, context.MessageSize, ObjectInfo, m_ReceivedNetworkVariableData); + } + else + { + CreateObject(ref networkManager, context.SenderId, context.MessageSize, ObjectInfo, m_ReceivedNetworkVariableData); + } + } - networkManager.NetworkMetrics.TrackObjectSpawnReceived(context.SenderId, networkObject, context.MessageSize); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void CreateObject(ref NetworkManager networkManager, ulong senderId, uint messageSize, NetworkObject.SceneObject sceneObject, FastBufferReader networkVariableData) + { + try + { + var networkObject = NetworkObject.AddSceneObject(sceneObject, networkVariableData, networkManager); + networkManager.NetworkMetrics.TrackObjectSpawnReceived(senderId, networkObject, messageSize); + } + catch (System.Exception ex) + { + UnityEngine.Debug.LogException(ex); + } } } } diff --git a/Runtime/Messaging/Messages/NamedMessage.cs b/Runtime/Messaging/Messages/NamedMessage.cs index 6177366..5c20688 100644 --- a/Runtime/Messaging/Messages/NamedMessage.cs +++ b/Runtime/Messaging/Messages/NamedMessage.cs @@ -25,9 +25,9 @@ namespace Unity.Netcode public void Handle(ref NetworkContext context) { var networkManager = (NetworkManager)context.SystemOwner; - if (!networkManager.ShutdownInProgress) + if (!networkManager.ShutdownInProgress && networkManager.CustomMessagingManager != null) { - ((NetworkManager)context.SystemOwner).CustomMessagingManager.InvokeNamedMessage(Hash, context.SenderId, m_ReceiveData, context.SerializedHeaderSize); + networkManager.CustomMessagingManager.InvokeNamedMessage(Hash, context.SenderId, m_ReceiveData, context.SerializedHeaderSize); } } } diff --git a/Runtime/Messaging/Messages/ProxyMessage.cs b/Runtime/Messaging/Messages/ProxyMessage.cs new file mode 100644 index 0000000..f47237c --- /dev/null +++ b/Runtime/Messaging/Messages/ProxyMessage.cs @@ -0,0 +1,70 @@ +using System; +using Unity.Collections; + +namespace Unity.Netcode +{ + internal struct ProxyMessage : INetworkMessage + { + public NativeArray TargetClientIds; + public NetworkDelivery Delivery; + public RpcMessage WrappedMessage; + + // Version of ProxyMessage and RpcMessage must always match. + // If ProxyMessage needs to change, increment RpcMessage's version + public int Version => new RpcMessage().Version; + + public void Serialize(FastBufferWriter writer, int targetVersion) + { + writer.WriteValueSafe(TargetClientIds); + BytePacker.WriteValuePacked(writer, Delivery); + WrappedMessage.Serialize(writer, targetVersion); + } + + public bool Deserialize(FastBufferReader reader, ref NetworkContext context, int receivedMessageVersion) + { + reader.ReadValueSafe(out TargetClientIds, Allocator.Temp); + ByteUnpacker.ReadValuePacked(reader, out Delivery); + WrappedMessage = new RpcMessage(); + WrappedMessage.Deserialize(reader, ref context, receivedMessageVersion); + return true; + } + + public unsafe void Handle(ref NetworkContext context) + { + + var networkManager = (NetworkManager)context.SystemOwner; + if (!networkManager.SpawnManager.SpawnedObjects.TryGetValue(WrappedMessage.Metadata.NetworkObjectId, out var networkObject)) + { + throw new InvalidOperationException($"An RPC called on a {nameof(NetworkObject)} that is not in the spawned objects list. Please make sure the {nameof(NetworkObject)} is spawned before calling RPCs."); + } + + var observers = networkObject.Observers; + + var nonServerIds = new NativeList(Allocator.Temp); + for (var i = 0; i < TargetClientIds.Length; ++i) + { + if (!observers.Contains(TargetClientIds[i])) + { + continue; + } + + if (TargetClientIds[i] == NetworkManager.ServerClientId) + { + WrappedMessage.Handle(ref context); + } + else + { + nonServerIds.Add(TargetClientIds[i]); + } + } + + WrappedMessage.WriteBuffer = new FastBufferWriter(WrappedMessage.ReadBuffer.Length, Allocator.Temp); + + using (WrappedMessage.WriteBuffer) + { + WrappedMessage.WriteBuffer.WriteBytesSafe(WrappedMessage.ReadBuffer.GetUnsafePtr(), WrappedMessage.ReadBuffer.Length); + networkManager.MessageManager.SendMessage(ref WrappedMessage, Delivery, nonServerIds); + } + } + } +} diff --git a/Runtime/Messaging/Messages/ProxyMessage.cs.meta b/Runtime/Messaging/Messages/ProxyMessage.cs.meta new file mode 100644 index 0000000..8f291e4 --- /dev/null +++ b/Runtime/Messaging/Messages/ProxyMessage.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e9ee0457d5b740b38dfe6542658fb522 +timeCreated: 1697825043 \ No newline at end of file diff --git a/Runtime/Messaging/Messages/RpcMessages.cs b/Runtime/Messaging/Messages/RpcMessages.cs index 1798c5c..e6961fb 100644 --- a/Runtime/Messaging/Messages/RpcMessages.cs +++ b/Runtime/Messaging/Messages/RpcMessages.cs @@ -159,4 +159,42 @@ namespace Unity.Netcode RpcMessageHelpers.Handle(ref context, ref Metadata, ref ReadBuffer, ref rpcParams); } } + + internal struct RpcMessage : INetworkMessage + { + public int Version => 0; + + public RpcMetadata Metadata; + public ulong SenderClientId; + + public FastBufferWriter WriteBuffer; + public FastBufferReader ReadBuffer; + + public unsafe void Serialize(FastBufferWriter writer, int targetVersion) + { + BytePacker.WriteValuePacked(writer, SenderClientId); + RpcMessageHelpers.Serialize(ref writer, ref Metadata, ref WriteBuffer); + } + + public unsafe bool Deserialize(FastBufferReader reader, ref NetworkContext context, int receivedMessageVersion) + { + ByteUnpacker.ReadValuePacked(reader, out SenderClientId); + return RpcMessageHelpers.Deserialize(ref reader, ref context, ref Metadata, ref ReadBuffer); + } + + public void Handle(ref NetworkContext context) + { + var rpcParams = new __RpcParams + { + Ext = new RpcParams + { + Receive = new RpcReceiveParams + { + SenderClientId = SenderClientId + } + } + }; + RpcMessageHelpers.Handle(ref context, ref Metadata, ref ReadBuffer, ref rpcParams); + } + } } diff --git a/Runtime/Messaging/NetworkManagerHooks.cs b/Runtime/Messaging/NetworkManagerHooks.cs index 3cbd7d6..e8a60b9 100644 --- a/Runtime/Messaging/NetworkManagerHooks.cs +++ b/Runtime/Messaging/NetworkManagerHooks.cs @@ -1,4 +1,5 @@ using System; +using Unity.Netcode.Transports.UTP; namespace Unity.Netcode { @@ -56,7 +57,8 @@ namespace Unity.Netcode { if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) { - NetworkLog.LogError($"A {nameof(ConnectionApprovedMessage)} was received from a client on the server side. This should not happen. Please report this to the Netcode for GameObjects team at https://github.com/Unity-Technologies/com.unity.netcode.gameobjects/issues and include the following data: Message Size: {messageContent.Length}. Message Content: {NetworkMessageManager.ByteArrayToString(messageContent.ToArray(), 0, messageContent.Length)}"); + var transportErrorMsg = GetTransportErrorMessage(messageContent, m_NetworkManager); + NetworkLog.LogError($"A {nameof(ConnectionApprovedMessage)} was received from a client on the server side. {transportErrorMsg}"); } return false; @@ -66,7 +68,7 @@ namespace Unity.Netcode { if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) { - NetworkLog.LogWarning($"Message received from {nameof(senderId)}={senderId} before it has been accepted"); + NetworkLog.LogWarning($"Message received from {nameof(senderId)}={senderId} before it has been accepted."); } return false; @@ -76,7 +78,8 @@ namespace Unity.Netcode { if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) { - NetworkLog.LogError($"A {nameof(ConnectionRequestMessage)} was received from a client when the connection has already been established. This should not happen. Please report this to the Netcode for GameObjects team at https://github.com/Unity-Technologies/com.unity.netcode.gameobjects/issues and include the following data: Message Size: {messageContent.Length}. Message Content: {NetworkMessageManager.ByteArrayToString(messageContent.ToArray(), 0, messageContent.Length)}"); + var transportErrorMsg = GetTransportErrorMessage(messageContent, m_NetworkManager); + NetworkLog.LogError($"A {nameof(ConnectionRequestMessage)} was received from a client when the connection has already been established. {transportErrorMsg}"); } return false; @@ -88,7 +91,8 @@ namespace Unity.Netcode { if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) { - NetworkLog.LogError($"A {nameof(ConnectionRequestMessage)} was received from the server on the client side. This should not happen. Please report this to the Netcode for GameObjects team at https://github.com/Unity-Technologies/com.unity.netcode.gameobjects/issues and include the following data: Message Size: {messageContent.Length}. Message Content: {NetworkMessageManager.ByteArrayToString(messageContent.ToArray(), 0, messageContent.Length)}"); + var transportErrorMsg = GetTransportErrorMessage(messageContent, m_NetworkManager); + NetworkLog.LogError($"A {nameof(ConnectionRequestMessage)} was received from the server on the client side. {transportErrorMsg}"); } return false; @@ -98,7 +102,8 @@ namespace Unity.Netcode { if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) { - NetworkLog.LogError($"A {nameof(ConnectionApprovedMessage)} was received from the server when the connection has already been established. This should not happen. Please report this to the Netcode for GameObjects team at https://github.com/Unity-Technologies/com.unity.netcode.gameobjects/issues and include the following data: Message Size: {messageContent.Length}. Message Content: {NetworkMessageManager.ByteArrayToString(messageContent.ToArray(), 0, messageContent.Length)}"); + var transportErrorMsg = GetTransportErrorMessage(messageContent, m_NetworkManager); + NetworkLog.LogError($"A {nameof(ConnectionApprovedMessage)} was received from the server when the connection has already been established. {transportErrorMsg}"); } return false; @@ -108,6 +113,28 @@ namespace Unity.Netcode return !m_NetworkManager.MessageManager.StopProcessing; } + private static string GetTransportErrorMessage(FastBufferReader messageContent, NetworkManager networkManager) + { + if (networkManager.NetworkConfig.NetworkTransport is not UnityTransport) + { + return $"NetworkTransport: {networkManager.NetworkConfig.NetworkTransport.GetType()}. Please report this to the maintainer of transport layer."; + } + + var transportVersion = GetTransportVersion(networkManager); + return $"{transportVersion}. This should not happen. Please report this to the Netcode for GameObjects team at https://github.com/Unity-Technologies/com.unity.netcode.gameobjects/issues and include the following data: Message Size: {messageContent.Length}. Message Content: {NetworkMessageManager.ByteArrayToString(messageContent.ToArray(), 0, messageContent.Length)}"; + } + + private static string GetTransportVersion(NetworkManager networkManager) + { + var transportVersion = "NetworkTransport: " + networkManager.NetworkConfig.NetworkTransport.GetType(); + if (networkManager.NetworkConfig.NetworkTransport is UnityTransport unityTransport) + { + transportVersion += " UnityTransportProtocol: " + unityTransport.Protocol; + } + + return transportVersion; + } + public void OnBeforeHandleMessage(ref T message, ref NetworkContext context) where T : INetworkMessage { } diff --git a/Runtime/Messaging/NetworkMessageManager.cs b/Runtime/Messaging/NetworkMessageManager.cs index e66b54e..24699eb 100644 --- a/Runtime/Messaging/NetworkMessageManager.cs +++ b/Runtime/Messaging/NetworkMessageManager.cs @@ -85,6 +85,8 @@ namespace Unity.Netcode private INetworkMessageSender m_Sender; private bool m_Disposed; + private ulong m_LocalClientId; + internal Type[] MessageTypes => m_ReverseTypeMap; internal MessageHandler[] MessageHandlers => m_MessageHandlers; @@ -95,6 +97,16 @@ namespace Unity.Netcode return m_MessageTypes[t]; } + internal object GetOwner() + { + return m_Owner; + } + + internal void SetLocalClientId(ulong id) + { + m_LocalClientId = id; + } + public const int DefaultNonFragmentedMessageMaxSize = 1300 & ~7; // Round down to nearest word aligned size (1296) public int NonFragmentedMessageMaxSize = DefaultNonFragmentedMessageMaxSize; public int FragmentedMessageMaxSize = int.MaxValue; @@ -551,7 +563,7 @@ namespace Unity.Netcode // Special cases because these are the messages that carry the version info - thus the version info isn't // populated yet when we get these. The first part of these messages always has to be the version data // and can't change. - if (typeof(T) != typeof(ConnectionRequestMessage) && typeof(T) != typeof(ConnectionApprovedMessage) && typeof(T) != typeof(DisconnectReasonMessage)) + if (typeof(T) != typeof(ConnectionRequestMessage) && typeof(T) != typeof(ConnectionApprovedMessage) && typeof(T) != typeof(DisconnectReasonMessage) && context.SenderId != manager.m_LocalClientId) { messageVersion = manager.GetMessageVersion(typeof(T), context.SenderId, true); if (messageVersion < 0) @@ -808,6 +820,12 @@ namespace Unity.Netcode return SendMessage(ref message, delivery, new PointerListWrapper((ulong*)clientIds.GetUnsafePtr(), clientIds.Length)); } + internal unsafe int SendMessage(ref T message, NetworkDelivery delivery, in NativeList clientIds) + where T : INetworkMessage + { + return SendMessage(ref message, delivery, new PointerListWrapper((ulong*)clientIds.GetUnsafePtr(), clientIds.Length)); + } + internal unsafe void ProcessSendQueues() { if (StopProcessing) diff --git a/Runtime/Messaging/RpcAttributes.cs b/Runtime/Messaging/RpcAttributes.cs index 5d0a501..acb6289 100644 --- a/Runtime/Messaging/RpcAttributes.cs +++ b/Runtime/Messaging/RpcAttributes.cs @@ -21,12 +21,36 @@ namespace Unity.Netcode /// /// Represents the common base class for Rpc attributes. /// - public abstract class RpcAttribute : Attribute + [AttributeUsage(AttributeTargets.Method)] + public class RpcAttribute : Attribute { + // Must match the set of parameters below + public struct RpcAttributeParams + { + public RpcDelivery Delivery; + public bool RequireOwnership; + public bool DeferLocal; + public bool AllowTargetOverride; + } + + // Must match the fields in RemoteAttributeParams /// /// Type of RPC delivery method /// public RpcDelivery Delivery = RpcDelivery.Reliable; + public bool RequireOwnership; + public bool DeferLocal; + public bool AllowTargetOverride; + + public RpcAttribute(SendTo target) + { + } + + // To get around an issue with the release validator, RuntimeAccessModifiersILPP will make this 'public' + private RpcAttribute() + { + + } } /// @@ -36,10 +60,12 @@ namespace Unity.Netcode [AttributeUsage(AttributeTargets.Method)] public class ServerRpcAttribute : RpcAttribute { - /// - /// Whether or not the ServerRpc should only be run if executed by the owner of the object - /// - public bool RequireOwnership = true; + public new bool RequireOwnership; + + public ServerRpcAttribute() : base(SendTo.Server) + { + + } } /// @@ -47,5 +73,11 @@ namespace Unity.Netcode /// A ClientRpc marked method will be fired by the server but executed on clients. /// [AttributeUsage(AttributeTargets.Method)] - public class ClientRpcAttribute : RpcAttribute { } + public class ClientRpcAttribute : RpcAttribute + { + public ClientRpcAttribute() : base(SendTo.NotServer) + { + + } + } } diff --git a/Runtime/Messaging/RpcParams.cs b/Runtime/Messaging/RpcParams.cs index ed49a08..7eb1a18 100644 --- a/Runtime/Messaging/RpcParams.cs +++ b/Runtime/Messaging/RpcParams.cs @@ -3,6 +3,60 @@ using Unity.Collections; namespace Unity.Netcode { + public enum LocalDeferMode + { + Default, + Defer, + SendImmediate + } + /// + /// Generic RPC + /// + public struct RpcSendParams + { + public BaseRpcTarget Target; + + public LocalDeferMode LocalDeferMode; + + public static implicit operator RpcSendParams(BaseRpcTarget target) => new RpcSendParams { Target = target }; + public static implicit operator RpcSendParams(LocalDeferMode deferMode) => new RpcSendParams { LocalDeferMode = deferMode }; + } + + /// + /// The receive parameters for server-side remote procedure calls + /// + public struct RpcReceiveParams + { + /// + /// Server-Side RPC + /// The client identifier of the sender + /// + public ulong SenderClientId; + } + + /// + /// Server-Side RPC + /// Can be used with any sever-side remote procedure call + /// Note: typically this is use primarily for the + /// + public struct RpcParams + { + /// + /// The server RPC send parameters (currently a place holder) + /// + public RpcSendParams Send; + + /// + /// The client RPC receive parameters provides you with the sender's identifier + /// + public RpcReceiveParams Receive; + + public static implicit operator RpcParams(RpcSendParams send) => new RpcParams { Send = send }; + public static implicit operator RpcParams(BaseRpcTarget target) => new RpcParams { Send = new RpcSendParams { Target = target } }; + public static implicit operator RpcParams(LocalDeferMode deferMode) => new RpcParams { Send = new RpcSendParams { LocalDeferMode = deferMode } }; + public static implicit operator RpcParams(RpcReceiveParams receive) => new RpcParams { Receive = receive }; + } + /// /// Server-Side RPC /// Place holder. @@ -99,6 +153,7 @@ namespace Unity.Netcode internal struct __RpcParams #pragma warning restore IDE1006 // restore naming rule violation check { + public RpcParams Ext; public ServerRpcParams Server; public ClientRpcParams Client; } diff --git a/Runtime/Messaging/RpcTargets.meta b/Runtime/Messaging/RpcTargets.meta new file mode 100644 index 0000000..06237e7 --- /dev/null +++ b/Runtime/Messaging/RpcTargets.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b02186acd1144e20acbd0dcb69b14938 +timeCreated: 1697824888 \ No newline at end of file diff --git a/Runtime/Messaging/RpcTargets/BaseRpcTarget.cs b/Runtime/Messaging/RpcTargets/BaseRpcTarget.cs new file mode 100644 index 0000000..1ecb12a --- /dev/null +++ b/Runtime/Messaging/RpcTargets/BaseRpcTarget.cs @@ -0,0 +1,57 @@ +using System; + +namespace Unity.Netcode +{ + public abstract class BaseRpcTarget : IDisposable + { + protected NetworkManager m_NetworkManager; + private bool m_Locked; + + internal void Lock() + { + m_Locked = true; + } + + internal void Unlock() + { + m_Locked = false; + } + + internal BaseRpcTarget(NetworkManager manager) + { + m_NetworkManager = manager; + } + + protected void CheckLockBeforeDispose() + { + if (m_Locked) + { + throw new Exception($"RPC targets obtained through {nameof(RpcTargetUse)}.{RpcTargetUse.Temp} may not be disposed."); + } + } + + public abstract void Dispose(); + + internal abstract void Send(NetworkBehaviour behaviour, ref RpcMessage message, NetworkDelivery delivery, RpcParams rpcParams); + + private protected void SendMessageToClient(NetworkBehaviour behaviour, ulong clientId, ref RpcMessage message, NetworkDelivery delivery) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + var size = +#endif + behaviour.NetworkManager.MessageManager.SendMessage(ref message, delivery, clientId); + +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (NetworkBehaviour.__rpc_name_table[behaviour.GetType()].TryGetValue(message.Metadata.NetworkRpcMethodId, out var rpcMethodName)) + { + behaviour.NetworkManager.NetworkMetrics.TrackRpcSent( + clientId, + behaviour.NetworkObject, + rpcMethodName, + behaviour.__getTypeName(), + size); + } +#endif + } + } +} diff --git a/Runtime/Messaging/RpcTargets/BaseRpcTarget.cs.meta b/Runtime/Messaging/RpcTargets/BaseRpcTarget.cs.meta new file mode 100644 index 0000000..f9d7d4f --- /dev/null +++ b/Runtime/Messaging/RpcTargets/BaseRpcTarget.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 07c2620262e24eb5a426b521c09b3091 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Messaging/RpcTargets/ClientsAndHostRpcTarget.cs b/Runtime/Messaging/RpcTargets/ClientsAndHostRpcTarget.cs new file mode 100644 index 0000000..286b0b0 --- /dev/null +++ b/Runtime/Messaging/RpcTargets/ClientsAndHostRpcTarget.cs @@ -0,0 +1,37 @@ +namespace Unity.Netcode +{ + internal class ClientsAndHostRpcTarget : BaseRpcTarget + { + private BaseRpcTarget m_UnderlyingTarget; + + public override void Dispose() + { + m_UnderlyingTarget = null; + } + + internal override void Send(NetworkBehaviour behaviour, ref RpcMessage message, NetworkDelivery delivery, RpcParams rpcParams) + { + if (m_UnderlyingTarget == null) + { + // NotServer treats a host as being a server and will not send to it + // ClientsAndHost sends to everyone who runs any client logic + // So if the server is a host, this target includes it (as hosts run client logic) + // If the server is not a host, this target leaves it out, ergo the selection of NotServer. + if (behaviour.NetworkManager.ServerIsHost) + { + m_UnderlyingTarget = behaviour.RpcTarget.Everyone; + } + else + { + m_UnderlyingTarget = behaviour.RpcTarget.NotServer; + } + } + + m_UnderlyingTarget.Send(behaviour, ref message, delivery, rpcParams); + } + + internal ClientsAndHostRpcTarget(NetworkManager manager) : base(manager) + { + } + } +} diff --git a/Runtime/Messaging/RpcTargets/ClientsAndHostRpcTarget.cs.meta b/Runtime/Messaging/RpcTargets/ClientsAndHostRpcTarget.cs.meta new file mode 100644 index 0000000..1f79dbe --- /dev/null +++ b/Runtime/Messaging/RpcTargets/ClientsAndHostRpcTarget.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c9f883d678ec4715b160dd9497d5f42d +timeCreated: 1699481382 \ No newline at end of file diff --git a/Runtime/Messaging/RpcTargets/DirectSendRpcTarget.cs b/Runtime/Messaging/RpcTargets/DirectSendRpcTarget.cs new file mode 100644 index 0000000..fef5e91 --- /dev/null +++ b/Runtime/Messaging/RpcTargets/DirectSendRpcTarget.cs @@ -0,0 +1,34 @@ +namespace Unity.Netcode +{ + internal class DirectSendRpcTarget : BaseRpcTarget, IIndividualRpcTarget + { + public BaseRpcTarget Target => this; + + internal ulong ClientId; + + public override void Dispose() + { + CheckLockBeforeDispose(); + } + + internal override void Send(NetworkBehaviour behaviour, ref RpcMessage message, NetworkDelivery delivery, RpcParams rpcParams) + { + SendMessageToClient(behaviour, ClientId, ref message, delivery); + } + + public void SetClientId(ulong clientId) + { + ClientId = clientId; + } + + internal DirectSendRpcTarget(NetworkManager manager) : base(manager) + { + + } + + internal DirectSendRpcTarget(ulong clientId, NetworkManager manager) : base(manager) + { + ClientId = clientId; + } + } +} diff --git a/Runtime/Messaging/RpcTargets/DirectSendRpcTarget.cs.meta b/Runtime/Messaging/RpcTargets/DirectSendRpcTarget.cs.meta new file mode 100644 index 0000000..f1ea3b7 --- /dev/null +++ b/Runtime/Messaging/RpcTargets/DirectSendRpcTarget.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 077544cfd0b94cfc8a2a55d3828b74bb +timeCreated: 1697824873 \ No newline at end of file diff --git a/Runtime/Messaging/RpcTargets/EveryoneRpcTarget.cs b/Runtime/Messaging/RpcTargets/EveryoneRpcTarget.cs new file mode 100644 index 0000000..b0ae738 --- /dev/null +++ b/Runtime/Messaging/RpcTargets/EveryoneRpcTarget.cs @@ -0,0 +1,26 @@ +namespace Unity.Netcode +{ + internal class EveryoneRpcTarget : BaseRpcTarget + { + private NotServerRpcTarget m_NotServerRpcTarget; + private ServerRpcTarget m_ServerRpcTarget; + + public override void Dispose() + { + m_NotServerRpcTarget.Dispose(); + m_ServerRpcTarget.Dispose(); + } + + internal override void Send(NetworkBehaviour behaviour, ref RpcMessage message, NetworkDelivery delivery, RpcParams rpcParams) + { + m_NotServerRpcTarget.Send(behaviour, ref message, delivery, rpcParams); + m_ServerRpcTarget.Send(behaviour, ref message, delivery, rpcParams); + } + + internal EveryoneRpcTarget(NetworkManager manager) : base(manager) + { + m_NotServerRpcTarget = new NotServerRpcTarget(manager); + m_ServerRpcTarget = new ServerRpcTarget(manager); + } + } +} diff --git a/Runtime/Messaging/RpcTargets/EveryoneRpcTarget.cs.meta b/Runtime/Messaging/RpcTargets/EveryoneRpcTarget.cs.meta new file mode 100644 index 0000000..18b2263 --- /dev/null +++ b/Runtime/Messaging/RpcTargets/EveryoneRpcTarget.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 675d4a5c79fc47078092ac15d255745d +timeCreated: 1697824941 \ No newline at end of file diff --git a/Runtime/Messaging/RpcTargets/IGroupRpcTarget.cs b/Runtime/Messaging/RpcTargets/IGroupRpcTarget.cs new file mode 100644 index 0000000..88aac50 --- /dev/null +++ b/Runtime/Messaging/RpcTargets/IGroupRpcTarget.cs @@ -0,0 +1,9 @@ +namespace Unity.Netcode +{ + internal interface IGroupRpcTarget + { + void Add(ulong clientId); + void Clear(); + BaseRpcTarget Target { get; } + } +} diff --git a/Runtime/Messaging/RpcTargets/IGroupRpcTarget.cs.meta b/Runtime/Messaging/RpcTargets/IGroupRpcTarget.cs.meta new file mode 100644 index 0000000..769ed8b --- /dev/null +++ b/Runtime/Messaging/RpcTargets/IGroupRpcTarget.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: beb19a6bb1334252a89b21c8490f7cbe +timeCreated: 1697825109 \ No newline at end of file diff --git a/Runtime/Messaging/RpcTargets/IIndividualRpcTarget.cs b/Runtime/Messaging/RpcTargets/IIndividualRpcTarget.cs new file mode 100644 index 0000000..0e654a8 --- /dev/null +++ b/Runtime/Messaging/RpcTargets/IIndividualRpcTarget.cs @@ -0,0 +1,8 @@ +namespace Unity.Netcode +{ + internal interface IIndividualRpcTarget + { + void SetClientId(ulong clientId); + BaseRpcTarget Target { get; } + } +} diff --git a/Runtime/Messaging/RpcTargets/IIndividualRpcTarget.cs.meta b/Runtime/Messaging/RpcTargets/IIndividualRpcTarget.cs.meta new file mode 100644 index 0000000..f3fa862 --- /dev/null +++ b/Runtime/Messaging/RpcTargets/IIndividualRpcTarget.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c658d9641f564d9890bef4f558f1cea6 +timeCreated: 1697825115 \ No newline at end of file diff --git a/Runtime/Messaging/RpcTargets/LocalSendRpcTarget.cs b/Runtime/Messaging/RpcTargets/LocalSendRpcTarget.cs new file mode 100644 index 0000000..92e2083 --- /dev/null +++ b/Runtime/Messaging/RpcTargets/LocalSendRpcTarget.cs @@ -0,0 +1,67 @@ +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; + +namespace Unity.Netcode +{ + internal class LocalSendRpcTarget : BaseRpcTarget + { + public override void Dispose() + { + + } + + internal override void Send(NetworkBehaviour behaviour, ref RpcMessage message, NetworkDelivery delivery, RpcParams rpcParams) + { + var networkManager = behaviour.NetworkManager; + var context = new NetworkContext + { + SenderId = m_NetworkManager.LocalClientId, + Timestamp = networkManager.RealTimeProvider.RealTimeSinceStartup, + SystemOwner = networkManager, + // header information isn't valid since it's not a real message. + // RpcMessage doesn't access this stuff so it's just left empty. + Header = new NetworkMessageHeader(), + SerializedHeaderSize = 0, + MessageSize = 0 + }; + int length; + if (rpcParams.Send.LocalDeferMode == LocalDeferMode.Defer) + { + using var serializedWriter = new FastBufferWriter(message.WriteBuffer.Length + UnsafeUtility.SizeOf(), Allocator.Temp, int.MaxValue); + message.Serialize(serializedWriter, message.Version); + using var reader = new FastBufferReader(serializedWriter, Allocator.None); + context.Header = new NetworkMessageHeader + { + MessageSize = (uint)reader.Length, + MessageType = m_NetworkManager.MessageManager.GetMessageType(typeof(RpcMessage)) + }; + + behaviour.NetworkManager.DeferredMessageManager.DeferMessage(IDeferredNetworkMessageManager.TriggerType.OnNextFrame, 0, reader, ref context); + length = reader.Length; + } + else + { + using var tempBuffer = new FastBufferReader(message.WriteBuffer, Allocator.None); + message.ReadBuffer = tempBuffer; + message.Handle(ref context); + length = tempBuffer.Length; + } +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (NetworkBehaviour.__rpc_name_table[behaviour.GetType()].TryGetValue(message.Metadata.NetworkRpcMethodId, out var rpcMethodName)) + { + behaviour.NetworkManager.NetworkMetrics.TrackRpcSent( + behaviour.NetworkManager.LocalClientId, + behaviour.NetworkObject, + rpcMethodName, + behaviour.__getTypeName(), + length); + } +#endif + } + + internal LocalSendRpcTarget(NetworkManager manager) : base(manager) + { + + } + } +} diff --git a/Runtime/Messaging/RpcTargets/LocalSendRpcTarget.cs.meta b/Runtime/Messaging/RpcTargets/LocalSendRpcTarget.cs.meta new file mode 100644 index 0000000..1b659b9 --- /dev/null +++ b/Runtime/Messaging/RpcTargets/LocalSendRpcTarget.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c3b290cdc20d4d2293652ec79652962a +timeCreated: 1697824985 \ No newline at end of file diff --git a/Runtime/Messaging/RpcTargets/NotMeRpcTarget.cs b/Runtime/Messaging/RpcTargets/NotMeRpcTarget.cs new file mode 100644 index 0000000..50f81f2 --- /dev/null +++ b/Runtime/Messaging/RpcTargets/NotMeRpcTarget.cs @@ -0,0 +1,67 @@ +namespace Unity.Netcode +{ + internal class NotMeRpcTarget : BaseRpcTarget + { + private IGroupRpcTarget m_GroupSendTarget; + private ServerRpcTarget m_ServerRpcTarget; + + public override void Dispose() + { + m_ServerRpcTarget.Dispose(); + if (m_GroupSendTarget != null) + { + m_GroupSendTarget.Target.Dispose(); + m_GroupSendTarget = null; + } + } + + internal override void Send(NetworkBehaviour behaviour, ref RpcMessage message, NetworkDelivery delivery, RpcParams rpcParams) + { + if (m_GroupSendTarget == null) + { + if (behaviour.IsServer) + { + m_GroupSendTarget = new RpcTargetGroup(m_NetworkManager); + } + else + { + m_GroupSendTarget = new ProxyRpcTargetGroup(m_NetworkManager); + } + } + + m_GroupSendTarget.Clear(); + if (behaviour.IsServer) + { + foreach (var clientId in behaviour.NetworkObject.Observers) + { + if (clientId == behaviour.NetworkManager.LocalClientId) + { + continue; + } + m_GroupSendTarget.Add(clientId); + } + } + else + { + foreach (var clientId in m_NetworkManager.ConnectedClientsIds) + { + if (clientId == behaviour.NetworkManager.LocalClientId) + { + continue; + } + m_GroupSendTarget.Add(clientId); + } + } + m_GroupSendTarget.Target.Send(behaviour, ref message, delivery, rpcParams); + if (!behaviour.IsServer) + { + m_ServerRpcTarget.Send(behaviour, ref message, delivery, rpcParams); + } + } + + internal NotMeRpcTarget(NetworkManager manager) : base(manager) + { + m_ServerRpcTarget = new ServerRpcTarget(manager); + } + } +} diff --git a/Runtime/Messaging/RpcTargets/NotMeRpcTarget.cs.meta b/Runtime/Messaging/RpcTargets/NotMeRpcTarget.cs.meta new file mode 100644 index 0000000..75100da --- /dev/null +++ b/Runtime/Messaging/RpcTargets/NotMeRpcTarget.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 99cd5e8be7bd454bab700ee08b8dad7b +timeCreated: 1697824966 \ No newline at end of file diff --git a/Runtime/Messaging/RpcTargets/NotOwnerRpcTarget.cs b/Runtime/Messaging/RpcTargets/NotOwnerRpcTarget.cs new file mode 100644 index 0000000..ece1ded --- /dev/null +++ b/Runtime/Messaging/RpcTargets/NotOwnerRpcTarget.cs @@ -0,0 +1,83 @@ +namespace Unity.Netcode +{ + internal class NotOwnerRpcTarget : BaseRpcTarget + { + private IGroupRpcTarget m_GroupSendTarget; + private ServerRpcTarget m_ServerRpcTarget; + private LocalSendRpcTarget m_LocalSendRpcTarget; + + public override void Dispose() + { + m_ServerRpcTarget.Dispose(); + m_LocalSendRpcTarget.Dispose(); + if (m_GroupSendTarget != null) + { + m_GroupSendTarget.Target.Dispose(); + m_GroupSendTarget = null; + } + } + + internal override void Send(NetworkBehaviour behaviour, ref RpcMessage message, NetworkDelivery delivery, RpcParams rpcParams) + { + if (m_GroupSendTarget == null) + { + if (behaviour.IsServer) + { + m_GroupSendTarget = new RpcTargetGroup(m_NetworkManager); + } + else + { + m_GroupSendTarget = new ProxyRpcTargetGroup(m_NetworkManager); + } + } + m_GroupSendTarget.Clear(); + + if (behaviour.IsServer) + { + foreach (var clientId in behaviour.NetworkObject.Observers) + { + if (clientId == behaviour.OwnerClientId) + { + continue; + } + if (clientId == behaviour.NetworkManager.LocalClientId) + { + m_LocalSendRpcTarget.Send(behaviour, ref message, delivery, rpcParams); + continue; + } + + m_GroupSendTarget.Add(clientId); + } + } + else + { + foreach (var clientId in m_NetworkManager.ConnectedClientsIds) + { + if (clientId == behaviour.OwnerClientId) + { + continue; + } + if (clientId == behaviour.NetworkManager.LocalClientId) + { + m_LocalSendRpcTarget.Send(behaviour, ref message, delivery, rpcParams); + continue; + } + + m_GroupSendTarget.Add(clientId); + } + } + + m_GroupSendTarget.Target.Send(behaviour, ref message, delivery, rpcParams); + if (behaviour.OwnerClientId != NetworkManager.ServerClientId) + { + m_ServerRpcTarget.Send(behaviour, ref message, delivery, rpcParams); + } + } + + internal NotOwnerRpcTarget(NetworkManager manager) : base(manager) + { + m_ServerRpcTarget = new ServerRpcTarget(manager); + m_LocalSendRpcTarget = new LocalSendRpcTarget(manager); + } + } +} diff --git a/Runtime/Messaging/RpcTargets/NotOwnerRpcTarget.cs.meta b/Runtime/Messaging/RpcTargets/NotOwnerRpcTarget.cs.meta new file mode 100644 index 0000000..09501cf --- /dev/null +++ b/Runtime/Messaging/RpcTargets/NotOwnerRpcTarget.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d7bc66c5253b44d09ad978ea9e51c96f +timeCreated: 1698789420 \ No newline at end of file diff --git a/Runtime/Messaging/RpcTargets/NotServerRpcTarget.cs b/Runtime/Messaging/RpcTargets/NotServerRpcTarget.cs new file mode 100644 index 0000000..d348660 --- /dev/null +++ b/Runtime/Messaging/RpcTargets/NotServerRpcTarget.cs @@ -0,0 +1,72 @@ +namespace Unity.Netcode +{ + internal class NotServerRpcTarget : BaseRpcTarget + { + private IGroupRpcTarget m_GroupSendTarget; + private LocalSendRpcTarget m_LocalSendRpcTarget; + + public override void Dispose() + { + m_LocalSendRpcTarget.Dispose(); + if (m_GroupSendTarget != null) + { + m_GroupSendTarget.Target.Dispose(); + m_GroupSendTarget = null; + } + } + + internal override void Send(NetworkBehaviour behaviour, ref RpcMessage message, NetworkDelivery delivery, RpcParams rpcParams) + { + if (m_GroupSendTarget == null) + { + if (behaviour.IsServer) + { + m_GroupSendTarget = new RpcTargetGroup(m_NetworkManager); + } + else + { + m_GroupSendTarget = new ProxyRpcTargetGroup(m_NetworkManager); + } + } + m_GroupSendTarget.Clear(); + + if (behaviour.IsServer) + { + foreach (var clientId in behaviour.NetworkObject.Observers) + { + if (clientId == NetworkManager.ServerClientId) + { + continue; + } + + m_GroupSendTarget.Add(clientId); + } + } + else + { + foreach (var clientId in m_NetworkManager.ConnectedClientsIds) + { + if (clientId == NetworkManager.ServerClientId) + { + continue; + } + + if (clientId == behaviour.NetworkManager.LocalClientId) + { + m_LocalSendRpcTarget.Send(behaviour, ref message, delivery, rpcParams); + continue; + } + + m_GroupSendTarget.Add(clientId); + } + } + + m_GroupSendTarget.Target.Send(behaviour, ref message, delivery, rpcParams); + } + + internal NotServerRpcTarget(NetworkManager manager) : base(manager) + { + m_LocalSendRpcTarget = new LocalSendRpcTarget(manager); + } + } +} diff --git a/Runtime/Messaging/RpcTargets/NotServerRpcTarget.cs.meta b/Runtime/Messaging/RpcTargets/NotServerRpcTarget.cs.meta new file mode 100644 index 0000000..f9fdd45 --- /dev/null +++ b/Runtime/Messaging/RpcTargets/NotServerRpcTarget.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c63787afe52f45ffbd5d801f78e7c0d6 +timeCreated: 1697824954 \ No newline at end of file diff --git a/Runtime/Messaging/RpcTargets/OwnerRpcTarget.cs b/Runtime/Messaging/RpcTargets/OwnerRpcTarget.cs new file mode 100644 index 0000000..7c78294 --- /dev/null +++ b/Runtime/Messaging/RpcTargets/OwnerRpcTarget.cs @@ -0,0 +1,54 @@ +namespace Unity.Netcode +{ + internal class OwnerRpcTarget : BaseRpcTarget + { + private IIndividualRpcTarget m_UnderlyingTarget; + private LocalSendRpcTarget m_LocalRpcTarget; + private ServerRpcTarget m_ServerRpcTarget; + + public override void Dispose() + { + m_LocalRpcTarget.Dispose(); + if (m_UnderlyingTarget != null) + { + m_UnderlyingTarget.Target.Dispose(); + m_UnderlyingTarget = null; + } + } + + internal override void Send(NetworkBehaviour behaviour, ref RpcMessage message, NetworkDelivery delivery, RpcParams rpcParams) + { + if (behaviour.OwnerClientId == behaviour.NetworkManager.LocalClientId) + { + m_LocalRpcTarget.Send(behaviour, ref message, delivery, rpcParams); + return; + } + + if (behaviour.OwnerClientId == NetworkManager.ServerClientId) + { + m_ServerRpcTarget.Send(behaviour, ref message, delivery, rpcParams); + return; + } + + if (m_UnderlyingTarget == null) + { + if (behaviour.NetworkManager.IsServer) + { + m_UnderlyingTarget = new DirectSendRpcTarget(m_NetworkManager); + } + else + { + m_UnderlyingTarget = new ProxyRpcTarget(behaviour.OwnerClientId, m_NetworkManager); + } + } + m_UnderlyingTarget.SetClientId(behaviour.OwnerClientId); + m_UnderlyingTarget.Target.Send(behaviour, ref message, delivery, rpcParams); + } + + internal OwnerRpcTarget(NetworkManager manager) : base(manager) + { + m_LocalRpcTarget = new LocalSendRpcTarget(manager); + m_ServerRpcTarget = new ServerRpcTarget(manager); + } + } +} diff --git a/Runtime/Messaging/RpcTargets/OwnerRpcTarget.cs.meta b/Runtime/Messaging/RpcTargets/OwnerRpcTarget.cs.meta new file mode 100644 index 0000000..aa85ed8 --- /dev/null +++ b/Runtime/Messaging/RpcTargets/OwnerRpcTarget.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 23c4d52455fc419aaf03094617894257 +timeCreated: 1697824972 \ No newline at end of file diff --git a/Runtime/Messaging/RpcTargets/ProxyRpcTarget.cs b/Runtime/Messaging/RpcTargets/ProxyRpcTarget.cs new file mode 100644 index 0000000..8ae5bfc --- /dev/null +++ b/Runtime/Messaging/RpcTargets/ProxyRpcTarget.cs @@ -0,0 +1,16 @@ +namespace Unity.Netcode +{ + internal class ProxyRpcTarget : ProxyRpcTargetGroup, IIndividualRpcTarget + { + internal ProxyRpcTarget(ulong clientId, NetworkManager manager) : base(manager) + { + Add(clientId); + } + + public void SetClientId(ulong clientId) + { + Clear(); + Add(clientId); + } + } +} diff --git a/Runtime/Messaging/RpcTargets/ProxyRpcTarget.cs.meta b/Runtime/Messaging/RpcTargets/ProxyRpcTarget.cs.meta new file mode 100644 index 0000000..dc192b1 --- /dev/null +++ b/Runtime/Messaging/RpcTargets/ProxyRpcTarget.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 86002805bb9e422e8b71581d1325357f +timeCreated: 1697825007 \ No newline at end of file diff --git a/Runtime/Messaging/RpcTargets/ProxyRpcTargetGroup.cs b/Runtime/Messaging/RpcTargets/ProxyRpcTargetGroup.cs new file mode 100644 index 0000000..4109580 --- /dev/null +++ b/Runtime/Messaging/RpcTargets/ProxyRpcTargetGroup.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using Unity.Collections; + +namespace Unity.Netcode +{ + internal class ProxyRpcTargetGroup : BaseRpcTarget, IDisposable, IGroupRpcTarget + { + public BaseRpcTarget Target => this; + + private ServerRpcTarget m_ServerRpcTarget; + private LocalSendRpcTarget m_LocalSendRpcTarget; + + private bool m_Disposed; + public NativeList TargetClientIds; + internal HashSet Ids = new HashSet(); + + internal override void Send(NetworkBehaviour behaviour, ref RpcMessage message, NetworkDelivery delivery, RpcParams rpcParams) + { + var proxyMessage = new ProxyMessage { Delivery = delivery, TargetClientIds = TargetClientIds.AsArray(), WrappedMessage = message }; +#if DEVELOPMENT_BUILD || UNITY_EDITOR + var size = +#endif + behaviour.NetworkManager.MessageManager.SendMessage(ref proxyMessage, delivery, NetworkManager.ServerClientId); + +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (NetworkBehaviour.__rpc_name_table[behaviour.GetType()].TryGetValue(message.Metadata.NetworkRpcMethodId, out var rpcMethodName)) + { + foreach (var clientId in TargetClientIds) + { + behaviour.NetworkManager.NetworkMetrics.TrackRpcSent( + clientId, + behaviour.NetworkObject, + rpcMethodName, + behaviour.__getTypeName(), + size); + } + } +#endif + if (Ids.Contains(NetworkManager.ServerClientId)) + { + m_ServerRpcTarget.Send(behaviour, ref message, delivery, rpcParams); + } + if (Ids.Contains(m_NetworkManager.LocalClientId)) + { + m_LocalSendRpcTarget.Send(behaviour, ref message, delivery, rpcParams); + } + } + + internal ProxyRpcTargetGroup(NetworkManager manager) : base(manager) + { + TargetClientIds = new NativeList(Allocator.Persistent); + m_ServerRpcTarget = new ServerRpcTarget(manager); + m_LocalSendRpcTarget = new LocalSendRpcTarget(manager); + } + + public override void Dispose() + { + CheckLockBeforeDispose(); + if (!m_Disposed) + { + TargetClientIds.Dispose(); + m_Disposed = true; + m_ServerRpcTarget.Dispose(); + m_LocalSendRpcTarget.Dispose(); + } + } + + public void Add(ulong clientId) + { + if (!Ids.Contains(clientId)) + { + Ids.Add(clientId); + if (clientId != NetworkManager.ServerClientId && clientId != m_NetworkManager.LocalClientId) + { + TargetClientIds.Add(clientId); + } + } + } + + public void Remove(ulong clientId) + { + Ids.Remove(clientId); + for (var i = 0; i < TargetClientIds.Length; ++i) + { + if (TargetClientIds[i] == clientId) + { + TargetClientIds.RemoveAt(i); + break; + } + } + } + + public void Clear() + { + Ids.Clear(); + TargetClientIds.Clear(); + } + } +} diff --git a/Runtime/Messaging/RpcTargets/ProxyRpcTargetGroup.cs.meta b/Runtime/Messaging/RpcTargets/ProxyRpcTargetGroup.cs.meta new file mode 100644 index 0000000..fcc3b05 --- /dev/null +++ b/Runtime/Messaging/RpcTargets/ProxyRpcTargetGroup.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5728dbab532e46a88127510b4ec75af9 +timeCreated: 1697825000 \ No newline at end of file diff --git a/Runtime/Messaging/RpcTargets/RpcTarget.cs b/Runtime/Messaging/RpcTargets/RpcTarget.cs new file mode 100644 index 0000000..8d99c94 --- /dev/null +++ b/Runtime/Messaging/RpcTargets/RpcTarget.cs @@ -0,0 +1,564 @@ +using System.Collections.Generic; +using Unity.Collections; + +namespace Unity.Netcode +{ + /// + /// Configuration for the default method by which an RPC is communicated across the network + /// + public enum SendTo + { + /// + /// Send to the NetworkObject's current owner. + /// Will execute locally if the local process is the owner. + /// + Owner, + /// + /// Send to everyone but the current owner, filtered to the current observer list. + /// Will execute locally if the local process is not the owner. + /// + NotOwner, + /// + /// Send to the server, regardless of ownership. + /// Will execute locally if invoked on the server. + /// + Server, + /// + /// Send to everyone but the server, filtered to the current observer list. + /// Will NOT send to a server running in host mode - it is still treated as a server. + /// If you want to send to servers when they are host, but not when they are dedicated server, use + /// . + ///
+ ///
+ /// Will execute locally if invoked on a client. + /// Will NOT execute locally if invoked on a server running in host mode. + ///
+ NotServer, + /// + /// Execute this RPC locally. + ///
+ ///
+ /// Normally this is no different from a standard function call. + ///
+ ///
+ /// Using the DeferLocal parameter of the attribute or the LocalDeferMode override in RpcSendParams, + /// this can allow an RPC to be processed on localhost with a one-frame delay as if it were sent over + /// the network. + ///
+ Me, + /// + /// Send this RPC to everyone but the local machine, filtered to the current observer list. + /// + NotMe, + /// + /// Send this RPC to everone, filtered to the current observer list. + /// Will execute locally. + /// + Everyone, + /// + /// Send this RPC to all clients, including the host, if a host exists. + /// If the server is running in host mode, this is the same as . + /// If the server is running in dedicated server mode, this is the same as . + /// + ClientsAndHost, + /// + /// This RPC cannot be sent without passing in a target in RpcSendParams. + /// + SpecifiedInParams + } + + public enum RpcTargetUse + { + Temp, + Persistent + } + + /// + /// Implementations of the various options, as well as additional runtime-only options + /// , + /// , + /// , + /// , + /// , , + /// , + /// , + /// , and + /// + /// + public class RpcTarget + { + private NetworkManager m_NetworkManager; + internal RpcTarget(NetworkManager manager) + { + m_NetworkManager = manager; + + Everyone = new EveryoneRpcTarget(manager); + Owner = new OwnerRpcTarget(manager); + NotOwner = new NotOwnerRpcTarget(manager); + Server = new ServerRpcTarget(manager); + NotServer = new NotServerRpcTarget(manager); + NotMe = new NotMeRpcTarget(manager); + Me = new LocalSendRpcTarget(manager); + ClientsAndHost = new ClientsAndHostRpcTarget(manager); + + m_CachedProxyRpcTargetGroup = new ProxyRpcTargetGroup(manager); + m_CachedTargetGroup = new RpcTargetGroup(manager); + m_CachedDirectSendTarget = new DirectSendRpcTarget(manager); + m_CachedProxyRpcTarget = new ProxyRpcTarget(0, manager); + + m_CachedProxyRpcTargetGroup.Lock(); + m_CachedTargetGroup.Lock(); + m_CachedDirectSendTarget.Lock(); + m_CachedProxyRpcTarget.Lock(); + } + + public void Dispose() + { + Everyone.Dispose(); + Owner.Dispose(); + NotOwner.Dispose(); + Server.Dispose(); + NotServer.Dispose(); + NotMe.Dispose(); + Me.Dispose(); + ClientsAndHost.Dispose(); + + m_CachedProxyRpcTargetGroup.Unlock(); + m_CachedTargetGroup.Unlock(); + m_CachedDirectSendTarget.Unlock(); + m_CachedProxyRpcTarget.Unlock(); + + m_CachedProxyRpcTargetGroup.Dispose(); + m_CachedTargetGroup.Dispose(); + m_CachedDirectSendTarget.Dispose(); + m_CachedProxyRpcTarget.Dispose(); + } + + + /// + /// Send to the NetworkObject's current owner. + /// Will execute locally if the local process is the owner. + /// + public BaseRpcTarget Owner; + + /// + /// Send to everyone but the current owner, filtered to the current observer list. + /// Will execute locally if the local process is not the owner. + /// + public BaseRpcTarget NotOwner; + + /// + /// Send to the server, regardless of ownership. + /// Will execute locally if invoked on the server. + /// + public BaseRpcTarget Server; + + /// + /// Send to everyone but the server, filtered to the current observer list. + /// Will NOT send to a server running in host mode - it is still treated as a server. + /// If you want to send to servers when they are host, but not when they are dedicated server, use + /// . + ///
+ ///
+ /// Will execute locally if invoked on a client. + /// Will NOT execute locally if invoked on a server running in host mode. + ///
+ public BaseRpcTarget NotServer; + + /// + /// Execute this RPC locally. + ///
+ ///
+ /// Normally this is no different from a standard function call. + ///
+ ///
+ /// Using the DeferLocal parameter of the attribute or the LocalDeferMode override in RpcSendParams, + /// this can allow an RPC to be processed on localhost with a one-frame delay as if it were sent over + /// the network. + ///
+ public BaseRpcTarget Me; + + /// + /// Send this RPC to everyone but the local machine, filtered to the current observer list. + /// + public BaseRpcTarget NotMe; + + /// + /// Send this RPC to everone, filtered to the current observer list. + /// Will execute locally. + /// + public BaseRpcTarget Everyone; + + /// + /// Send this RPC to all clients, including the host, if a host exists. + /// If the server is running in host mode, this is the same as . + /// If the server is running in dedicated server mode, this is the same as . + /// + public BaseRpcTarget ClientsAndHost; + + /// + /// Send to a specific single client ID. + /// + /// + /// will return a cached target, which should not be stored as it will + /// be overwritten in future calls to Single(). Do not call Dispose() on Temp targets.

will + /// return a new target, which can be stored, but should not be done frequently because it results in a GC allocation. You must call Dispose() on Persistent targets when you are done with them. + /// + public BaseRpcTarget Single(ulong clientId, RpcTargetUse use) + { + if (clientId == m_NetworkManager.LocalClientId) + { + return Me; + } + + if (m_NetworkManager.IsServer || clientId == NetworkManager.ServerClientId) + { + if (use == RpcTargetUse.Persistent) + { + return new DirectSendRpcTarget(clientId, m_NetworkManager); + } + m_CachedDirectSendTarget.SetClientId(clientId); + return m_CachedDirectSendTarget; + } + + if (use == RpcTargetUse.Persistent) + { + return new ProxyRpcTarget(clientId, m_NetworkManager); + } + m_CachedProxyRpcTarget.SetClientId(clientId); + return m_CachedProxyRpcTarget; + } + + /// + /// Send to everyone EXCEPT a specific single client ID. + /// + /// + /// will return a cached target, which should not be stored as it will + /// be overwritten in future calls to Not() or Group(). Do not call Dispose() on Temp targets.

will + /// return a new target, which can be stored, but should not be done frequently because it results in a GC allocation. You must call Dispose() on Persistent targets when you are done with them. + /// + public BaseRpcTarget Not(ulong excludedClientId, RpcTargetUse use) + { + IGroupRpcTarget target; + if (m_NetworkManager.IsServer) + { + if (use == RpcTargetUse.Persistent) + { + target = new RpcTargetGroup(m_NetworkManager); + } + else + { + target = m_CachedTargetGroup; + } + } + else + { + if (use == RpcTargetUse.Persistent) + { + target = new ProxyRpcTargetGroup(m_NetworkManager); + } + else + { + target = m_CachedProxyRpcTargetGroup; + } + } + target.Clear(); + foreach (var clientId in m_NetworkManager.ConnectedClientsIds) + { + if (clientId != excludedClientId) + { + target.Add(clientId); + } + } + + // If ServerIsHost, ConnectedClientIds already contains ServerClientId and this would duplicate it. + if (!m_NetworkManager.ServerIsHost && excludedClientId != NetworkManager.ServerClientId) + { + target.Add(NetworkManager.ServerClientId); + } + + return target.Target; + } + + /// + /// Sends to a group of client IDs. + /// NativeArrays can be trivially constructed using Allocator.Temp, making this an efficient + /// Group method if the group list is dynamically constructed. + /// + /// + /// will return a cached target, which should not be stored as it will + /// be overwritten in future calls to Not() or Group(). Do not call Dispose() on Temp targets.

will + /// return a new target, which can be stored, but should not be done frequently because it results in a GC allocation. You must call Dispose() on Persistent targets when you are done with them. + /// + public BaseRpcTarget Group(NativeArray clientIds, RpcTargetUse use) + { + IGroupRpcTarget target; + if (m_NetworkManager.IsServer) + { + if (use == RpcTargetUse.Persistent) + { + target = new RpcTargetGroup(m_NetworkManager); + } + else + { + target = m_CachedTargetGroup; + } + } + else + { + if (use == RpcTargetUse.Persistent) + { + target = new ProxyRpcTargetGroup(m_NetworkManager); + } + else + { + target = m_CachedProxyRpcTargetGroup; + } + } + target.Clear(); + foreach (var clientId in clientIds) + { + target.Add(clientId); + } + + return target.Target; + } + + /// + /// Sends to a group of client IDs. + /// NativeList can be trivially constructed using Allocator.Temp, making this an efficient + /// Group method if the group list is dynamically constructed. + /// + /// + /// will return a cached target, which should not be stored as it will + /// be overwritten in future calls to Not() or Group(). Do not call Dispose() on Temp targets.

will + /// return a new target, which can be stored, but should not be done frequently because it results in a GC allocation. You must call Dispose() on Persistent targets when you are done with them. + /// + public BaseRpcTarget Group(NativeList clientIds, RpcTargetUse use) + { + var asArray = clientIds.AsArray(); + return Group(asArray, use); + } + + /// + /// Sends to a group of client IDs. + /// Constructing arrays requires garbage collected allocations. This override is only recommended + /// if you either have no strict performance requirements, or have the group of client IDs cached so + /// it is not created each time. + /// + /// + /// will return a cached target, which should not be stored as it will + /// be overwritten in future calls to Not() or Group(). Do not call Dispose() on Temp targets.

will + /// return a new target, which can be stored, but should not be done frequently because it results in a GC allocation. You must call Dispose() on Persistent targets when you are done with them. + /// + public BaseRpcTarget Group(ulong[] clientIds, RpcTargetUse use) + { + return Group(new NativeArray(clientIds, Allocator.Temp), use); + } + + /// + /// Sends to a group of client IDs. + /// This accepts any IEnumerable type, such as List<ulong>, but cannot be called without + /// a garbage collected allocation (even if the type itself is a struct type, due to boxing). + /// This override is only recommended if you either have no strict performance requirements, + /// or have the group of client IDs cached so it is not created each time. + /// + /// + /// will return a cached target, which should not be stored as it will + /// be overwritten in future calls to Not() or Group(). Do not call Dispose() on Temp targets.

will + /// return a new target, which can be stored, but should not be done frequently because it results in a GC allocation. You must call Dispose() on Persistent targets when you are done with them. + /// + public BaseRpcTarget Group(T clientIds, RpcTargetUse use) where T : IEnumerable + { + IGroupRpcTarget target; + if (m_NetworkManager.IsServer) + { + if (use == RpcTargetUse.Persistent) + { + target = new RpcTargetGroup(m_NetworkManager); + } + else + { + target = m_CachedTargetGroup; + } + } + else + { + if (use == RpcTargetUse.Persistent) + { + target = new ProxyRpcTargetGroup(m_NetworkManager); + } + else + { + target = m_CachedProxyRpcTargetGroup; + } + } + target.Clear(); + foreach (var clientId in clientIds) + { + target.Add(clientId); + } + + return target.Target; + } + + /// + /// Sends to everyone EXCEPT a group of client IDs. + /// NativeArrays can be trivially constructed using Allocator.Temp, making this an efficient + /// Group method if the group list is dynamically constructed. + /// + /// + /// will return a cached target, which should not be stored as it will + /// be overwritten in future calls to Not() or Group(). Do not call Dispose() on Temp targets.

will + /// return a new target, which can be stored, but should not be done frequently because it results in a GC allocation. You must call Dispose() on Persistent targets when you are done with them. + /// + public BaseRpcTarget Not(NativeArray excludedClientIds, RpcTargetUse use) + { + IGroupRpcTarget target; + if (m_NetworkManager.IsServer) + { + if (use == RpcTargetUse.Persistent) + { + target = new RpcTargetGroup(m_NetworkManager); + } + else + { + target = m_CachedTargetGroup; + } + } + else + { + if (use == RpcTargetUse.Persistent) + { + target = new ProxyRpcTargetGroup(m_NetworkManager); + } + else + { + target = m_CachedProxyRpcTargetGroup; + } + } + target.Clear(); + + using var asASet = new NativeHashSet(excludedClientIds.Length, Allocator.Temp); + foreach (var clientId in excludedClientIds) + { + asASet.Add(clientId); + } + + foreach (var clientId in m_NetworkManager.ConnectedClientsIds) + { + if (!asASet.Contains(clientId)) + { + target.Add(clientId); + } + } + + // If ServerIsHost, ConnectedClientIds already contains ServerClientId and this would duplicate it. + if (!m_NetworkManager.ServerIsHost && !asASet.Contains(NetworkManager.ServerClientId)) + { + target.Add(NetworkManager.ServerClientId); + } + + return target.Target; + } + + /// + /// Sends to everyone EXCEPT a group of client IDs. + /// NativeList can be trivially constructed using Allocator.Temp, making this an efficient + /// Group method if the group list is dynamically constructed. + /// + /// + /// will return a cached target, which should not be stored as it will + /// be overwritten in future calls to Not() or Group(). Do not call Dispose() on Temp targets.

will + /// return a new target, which can be stored, but should not be done frequently because it results in a GC allocation. You must call Dispose() on Persistent targets when you are done with them. + /// + public BaseRpcTarget Not(NativeList excludedClientIds, RpcTargetUse use) + { + var asArray = excludedClientIds.AsArray(); + return Not(asArray, use); + } + + /// + /// Sends to everyone EXCEPT a group of client IDs. + /// Constructing arrays requires garbage collected allocations. This override is only recommended + /// if you either have no strict performance requirements, or have the group of client IDs cached so + /// it is not created each time. + /// + /// + /// will return a cached target, which should not be stored as it will + /// be overwritten in future calls to Not() or Group(). Do not call Dispose() on Temp targets.

will + /// return a new target, which can be stored, but should not be done frequently because it results in a GC allocation. You must call Dispose() on Persistent targets when you are done with them. + /// + public BaseRpcTarget Not(ulong[] excludedClientIds, RpcTargetUse use) + { + return Not(new NativeArray(excludedClientIds, Allocator.Temp), use); + } + + /// + /// Sends to everyone EXCEPT a group of client IDs. + /// This accepts any IEnumerable type, such as List<ulong>, but cannot be called without + /// a garbage collected allocation (even if the type itself is a struct type, due to boxing). + /// This override is only recommended if you either have no strict performance requirements, + /// or have the group of client IDs cached so it is not created each time. + /// + /// + /// will return a cached target, which should not be stored as it will + /// be overwritten in future calls to Not() or Group(). Do not call Dispose() on Temp targets.

will + /// return a new target, which can be stored, but should not be done frequently because it results in a GC allocation. You must call Dispose() on Persistent targets when you are done with them. + /// + public BaseRpcTarget Not(T excludedClientIds, RpcTargetUse use) where T : IEnumerable + { + IGroupRpcTarget target; + if (m_NetworkManager.IsServer) + { + if (use == RpcTargetUse.Persistent) + { + target = new RpcTargetGroup(m_NetworkManager); + } + else + { + target = m_CachedTargetGroup; + } + } + else + { + if (use == RpcTargetUse.Persistent) + { + target = new ProxyRpcTargetGroup(m_NetworkManager); + } + else + { + target = m_CachedProxyRpcTargetGroup; + } + } + target.Clear(); + + using var asASet = new NativeHashSet(m_NetworkManager.ConnectedClientsIds.Count, Allocator.Temp); + foreach (var clientId in excludedClientIds) + { + asASet.Add(clientId); + } + + foreach (var clientId in m_NetworkManager.ConnectedClientsIds) + { + if (!asASet.Contains(clientId)) + { + target.Add(clientId); + } + } + + // If ServerIsHost, ConnectedClientIds already contains ServerClientId and this would duplicate it. + if (!m_NetworkManager.ServerIsHost && !asASet.Contains(NetworkManager.ServerClientId)) + { + target.Add(NetworkManager.ServerClientId); + } + + return target.Target; + } + + private ProxyRpcTargetGroup m_CachedProxyRpcTargetGroup; + private RpcTargetGroup m_CachedTargetGroup; + private DirectSendRpcTarget m_CachedDirectSendTarget; + private ProxyRpcTarget m_CachedProxyRpcTarget; + } +} diff --git a/Runtime/Messaging/RpcTargets/RpcTarget.cs.meta b/Runtime/Messaging/RpcTargets/RpcTarget.cs.meta new file mode 100644 index 0000000..af39246 --- /dev/null +++ b/Runtime/Messaging/RpcTargets/RpcTarget.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1b26d0227e71408b918ae25ca2a0179b +timeCreated: 1699555535 \ No newline at end of file diff --git a/Runtime/Messaging/RpcTargets/RpcTargetGroup.cs b/Runtime/Messaging/RpcTargets/RpcTargetGroup.cs new file mode 100644 index 0000000..80ec6c9 --- /dev/null +++ b/Runtime/Messaging/RpcTargets/RpcTargetGroup.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; + +namespace Unity.Netcode +{ + internal class RpcTargetGroup : BaseRpcTarget, IGroupRpcTarget + { + public BaseRpcTarget Target => this; + + internal List Targets = new List(); + + private LocalSendRpcTarget m_LocalSendRpcTarget; + private HashSet m_Ids = new HashSet(); + private Stack m_TargetCache = new Stack(); + + public override void Dispose() + { + CheckLockBeforeDispose(); + foreach (var target in Targets) + { + target.Dispose(); + } + foreach (var target in m_TargetCache) + { + target.Dispose(); + } + m_LocalSendRpcTarget.Dispose(); + } + + internal override void Send(NetworkBehaviour behaviour, ref RpcMessage message, NetworkDelivery delivery, RpcParams rpcParams) + { + foreach (var target in Targets) + { + target.Send(behaviour, ref message, delivery, rpcParams); + } + } + + public void Add(ulong clientId) + { + if (!m_Ids.Contains(clientId)) + { + m_Ids.Add(clientId); + if (clientId == m_NetworkManager.LocalClientId) + { + Targets.Add(m_LocalSendRpcTarget); + } + else + { + if (m_TargetCache.Count == 0) + { + Targets.Add(new DirectSendRpcTarget(m_NetworkManager) { ClientId = clientId }); + } + else + { + var target = m_TargetCache.Pop(); + target.ClientId = clientId; + Targets.Add(target); + } + } + } + } + + public void Clear() + { + m_Ids.Clear(); + foreach (var target in Targets) + { + if (target is DirectSendRpcTarget directSendRpcTarget) + { + m_TargetCache.Push(directSendRpcTarget); + } + } + Targets.Clear(); + } + + internal RpcTargetGroup(NetworkManager manager) : base(manager) + { + m_LocalSendRpcTarget = new LocalSendRpcTarget(manager); + } + } +} diff --git a/Runtime/Messaging/RpcTargets/RpcTargetGroup.cs.meta b/Runtime/Messaging/RpcTargets/RpcTargetGroup.cs.meta new file mode 100644 index 0000000..dd7d202 --- /dev/null +++ b/Runtime/Messaging/RpcTargets/RpcTargetGroup.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7f8c0fc053b64a588c99dd7d706d9f0a +timeCreated: 1697824991 \ No newline at end of file diff --git a/Runtime/Messaging/RpcTargets/ServerRpcTarget.cs b/Runtime/Messaging/RpcTargets/ServerRpcTarget.cs new file mode 100644 index 0000000..09b789d --- /dev/null +++ b/Runtime/Messaging/RpcTargets/ServerRpcTarget.cs @@ -0,0 +1,36 @@ +namespace Unity.Netcode +{ + internal class ServerRpcTarget : BaseRpcTarget + { + private BaseRpcTarget m_UnderlyingTarget; + + public override void Dispose() + { + if (m_UnderlyingTarget != null) + { + m_UnderlyingTarget.Dispose(); + m_UnderlyingTarget = null; + } + } + + internal override void Send(NetworkBehaviour behaviour, ref RpcMessage message, NetworkDelivery delivery, RpcParams rpcParams) + { + if (m_UnderlyingTarget == null) + { + if (behaviour.NetworkManager.IsServer) + { + m_UnderlyingTarget = new LocalSendRpcTarget(m_NetworkManager); + } + else + { + m_UnderlyingTarget = new DirectSendRpcTarget(m_NetworkManager) { ClientId = NetworkManager.ServerClientId }; + } + } + m_UnderlyingTarget.Send(behaviour, ref message, delivery, rpcParams); + } + + internal ServerRpcTarget(NetworkManager manager) : base(manager) + { + } + } +} diff --git a/Runtime/Messaging/RpcTargets/ServerRpcTarget.cs.meta b/Runtime/Messaging/RpcTargets/ServerRpcTarget.cs.meta new file mode 100644 index 0000000..a1238cf --- /dev/null +++ b/Runtime/Messaging/RpcTargets/ServerRpcTarget.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c911725afb6d44f3bb1a1d567d9dee0f +timeCreated: 1697824979 \ No newline at end of file diff --git a/Runtime/NetworkVariable/NetworkVariableBase.cs b/Runtime/NetworkVariable/NetworkVariableBase.cs index c72813e..927687c 100644 --- a/Runtime/NetworkVariable/NetworkVariableBase.cs +++ b/Runtime/NetworkVariable/NetworkVariableBase.cs @@ -100,7 +100,15 @@ namespace Unity.Netcode "Are you modifying a NetworkVariable before the NetworkObject is spawned?"); return; } - + if (m_NetworkBehaviour.NetworkManager.ShutdownInProgress) + { + if (m_NetworkBehaviour.NetworkManager.LogLevel <= LogLevel.Developer) + { + Debug.LogWarning($"NetworkVariable is written to during the NetworkManager shutdown! " + + "Are you modifying a NetworkVariable within a NetworkBehaviour.OnDestroy or NetworkBehaviour.OnDespawn method?"); + } + return; + } m_NetworkBehaviour.NetworkManager.BehaviourUpdater.AddForUpdate(m_NetworkBehaviour.NetworkObject); } diff --git a/Runtime/SceneManagement/NetworkSceneManager.cs b/Runtime/SceneManagement/NetworkSceneManager.cs index ffb244e..ba1b0ee 100644 --- a/Runtime/SceneManagement/NetworkSceneManager.cs +++ b/Runtime/SceneManagement/NetworkSceneManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Unity.Collections; using UnityEngine; using UnityEngine.SceneManagement; @@ -597,6 +598,47 @@ namespace Unity.Netcode } } + /// + /// Used for integration tests, normal runtime mode this will always be LoadSceneMode.Single + /// + internal LoadSceneMode DeferLoadingFilter = LoadSceneMode.Single; + /// + /// Determines if a remote client should defer object creation initiated by CreateObjectMessage + /// until a scene event is completed. + /// + /// + /// Deferring object creation should only occur when there is a possibility the objects could be + /// instantiated in a currently active scene that will be unloaded during single mode scene loading + /// to prevent the newly created objects from being destroyed when the scene is unloaded. + /// + internal bool ShouldDeferCreateObject() + { + // This applies only to remote clients and when scene management is enabled + if (!NetworkManager.NetworkConfig.EnableSceneManagement || NetworkManager.IsServer) + { + return false; + } + var synchronizeEventDetected = false; + var loadingEventDetected = false; + foreach (var entry in SceneEventDataStore) + { + if (entry.Value.SceneEventType == SceneEventType.Synchronize) + { + synchronizeEventDetected = true; + } + + // When loading a scene and the load scene mode is single we should defer object creation + if (entry.Value.SceneEventType == SceneEventType.Load && entry.Value.LoadSceneMode == DeferLoadingFilter) + { + loadingEventDetected = true; + } + } + + // Synchronizing while in client synchronization mode single --> Defer + // When not synchronizing but loading a scene in single mode --> Defer + return (synchronizeEventDetected && ClientSynchronizationMode == LoadSceneMode.Single) || (!synchronizeEventDetected && loadingEventDetected); + } + /// /// Gets the scene name from full path to the scene /// @@ -740,12 +782,17 @@ namespace Unity.Netcode // Since NetworkManager is now always migrated to the DDOL we will use this to get the DDOL scene DontDestroyOnLoadScene = networkManager.gameObject.scene; - // Since the server tracks loaded scenes, we need to add the currently active scene - // to the list of scenes that can be unloaded. - if (networkManager.IsServer) + // Since the server tracks loaded scenes, we need to add any currently loaded scenes on the + // server side when the NetworkManager is started and NetworkSceneManager instantiated when + // scene management is enabled. + if (networkManager.IsServer && networkManager.NetworkConfig.EnableSceneManagement) { - var activeScene = SceneManager.GetActiveScene(); - ScenesLoaded.Add(activeScene.handle, activeScene); + for (int i = 0; i < SceneManager.sceneCount; i++) + { + var loadedScene = SceneManager.GetSceneAt(i); + ScenesLoaded.Add(loadedScene.handle, loadedScene); + } + SceneManagerHandler.PopulateLoadedScenes(ref ScenesLoaded, NetworkManager); } // Add to the server to client scene handle table @@ -969,17 +1016,16 @@ namespace Unity.Netcode /// private SceneEventProgress ValidateSceneEventUnloading(Scene scene) { - if (!NetworkManager.IsServer) - { - throw new NotServerException("Only server can start a scene event!"); - } - if (!NetworkManager.NetworkConfig.EnableSceneManagement) { - //Log message about enabling SceneManagement - throw new Exception( - $"{nameof(NetworkConfig.EnableSceneManagement)} flag is not enabled in the {nameof(Netcode.NetworkManager)}'s {nameof(NetworkConfig)}. " + - $"Please set {nameof(NetworkConfig.EnableSceneManagement)} flag to true before calling {nameof(LoadScene)} or {nameof(UnloadScene)}."); + Debug.LogWarning($"{nameof(LoadScene)} was called, but {nameof(NetworkConfig.EnableSceneManagement)} was not enabled! Enable {nameof(NetworkConfig.EnableSceneManagement)} prior to starting a client, host, or server prior to using {nameof(NetworkSceneManager)}!"); + return new SceneEventProgress(null, SceneEventProgressStatus.SceneManagementNotEnabled); + } + + if (!NetworkManager.IsServer) + { + Debug.LogWarning($"[{nameof(SceneEventProgressStatus.ServerOnlyAction)}][Unload] Clients cannot invoke the {nameof(UnloadScene)} method!"); + return new SceneEventProgress(null, SceneEventProgressStatus.ServerOnlyAction); } if (!scene.isLoaded) @@ -998,16 +1044,16 @@ namespace Unity.Netcode /// private SceneEventProgress ValidateSceneEventLoading(string sceneName) { - if (!NetworkManager.IsServer) - { - throw new NotServerException("Only server can start a scene event!"); - } if (!NetworkManager.NetworkConfig.EnableSceneManagement) { - //Log message about enabling SceneManagement - throw new Exception( - $"{nameof(NetworkConfig.EnableSceneManagement)} flag is not enabled in the {nameof(Netcode.NetworkManager)}'s {nameof(NetworkConfig)}. " + - $"Please set {nameof(NetworkConfig.EnableSceneManagement)} flag to true before calling {nameof(LoadScene)} or {nameof(UnloadScene)}."); + Debug.LogWarning($"{nameof(LoadScene)} was called, but {nameof(NetworkConfig.EnableSceneManagement)} was not enabled! Enable {nameof(NetworkConfig.EnableSceneManagement)} prior to starting a client, host, or server prior to using {nameof(NetworkSceneManager)}!"); + return new SceneEventProgress(null, SceneEventProgressStatus.SceneManagementNotEnabled); + } + + if (!NetworkManager.IsServer) + { + Debug.LogWarning($"[{nameof(SceneEventProgressStatus.ServerOnlyAction)}][Load] Clients cannot invoke the {nameof(LoadScene)} method!"); + return new SceneEventProgress(null, SceneEventProgressStatus.ServerOnlyAction); } return ValidateSceneEvent(sceneName); @@ -1112,6 +1158,7 @@ namespace Unity.Netcode { var sceneName = scene.name; var sceneHandle = scene.handle; + if (!scene.isLoaded) { Debug.LogWarning($"{nameof(UnloadScene)} was called, but the scene {scene.name} is not currently loaded!"); @@ -1697,6 +1744,9 @@ namespace Unity.Netcode SendSceneEventData(sceneEventId, new ulong[] { NetworkManager.ServerClientId }); m_IsSceneEventActive = false; + // Process any pending create object messages that the client received while loading a scene + ProcessDeferredCreateObjectMessages(); + // Notify local client that the scene was loaded OnSceneEvent?.Invoke(new SceneEvent() { @@ -2058,6 +2108,9 @@ namespace Unity.Netcode // If needed, migrate dynamically spawned NetworkObjects to the same scene as they are on the server SynchronizeNetworkObjectScene(); + // Process any pending create object messages that the client received during synchronization + ProcessDeferredCreateObjectMessages(); + sceneEventData.SceneEventType = SceneEventType.SynchronizeComplete; SendSceneEventData(sceneEventId, new ulong[] { NetworkManager.ServerClientId }); @@ -2213,6 +2266,11 @@ namespace Unity.Netcode // of the client was persisted since MLAPI) NetworkManager.ConnectionManager.InvokeOnClientConnectedCallback(clientId); + if (NetworkManager.IsHost) + { + NetworkManager.ConnectionManager.InvokeOnPeerConnectedCallback(clientId); + } + // Check to see if the client needs to resynchronize and before sending the message make sure the client is still connected to avoid // a potential crash within the MessageSystem (i.e. sending to a client that no longer exists) if (sceneEventData.ClientNeedsReSynchronization() && !DisableReSynchronization && NetworkManager.ConnectedClients.ContainsKey(clientId)) @@ -2549,5 +2607,50 @@ namespace Unity.Netcode internal Dictionary> ObjectsMigratedTable; } internal List DeferredObjectsMovedEvents = new List(); + + internal struct DeferredObjectCreation + { + internal ulong SenderId; + internal uint MessageSize; + internal NetworkObject.SceneObject SceneObject; + internal FastBufferReader FastBufferReader; + } + + internal List DeferredObjectCreationList = new List(); + internal int DeferredObjectCreationCount; + + internal void DeferCreateObject(ulong senderId, uint messageSize, NetworkObject.SceneObject sceneObject, FastBufferReader fastBufferReader) + { + var deferredObjectCreationEntry = new DeferredObjectCreation() + { + SenderId = senderId, + MessageSize = messageSize, + SceneObject = sceneObject, + }; + + unsafe + { + deferredObjectCreationEntry.FastBufferReader = new FastBufferReader(fastBufferReader.GetUnsafePtrAtCurrentPosition(), Allocator.Persistent, fastBufferReader.Length - fastBufferReader.Position); + } + + DeferredObjectCreationList.Add(deferredObjectCreationEntry); + } + + private void ProcessDeferredCreateObjectMessages() + { + // If no pending create object messages exit early + if (DeferredObjectCreationList.Count == 0) + { + return; + } + var networkManager = NetworkManager; + // Process all deferred create object messages. + foreach (var deferredObjectCreation in DeferredObjectCreationList) + { + CreateObjectMessage.CreateObject(ref networkManager, deferredObjectCreation.SenderId, deferredObjectCreation.MessageSize, deferredObjectCreation.SceneObject, deferredObjectCreation.FastBufferReader); + } + DeferredObjectCreationCount = DeferredObjectCreationList.Count; + DeferredObjectCreationList.Clear(); + } } } diff --git a/Runtime/SceneManagement/SceneEventProgress.cs b/Runtime/SceneManagement/SceneEventProgress.cs index a20ee25..d50c6c7 100644 --- a/Runtime/SceneManagement/SceneEventProgress.cs +++ b/Runtime/SceneManagement/SceneEventProgress.cs @@ -47,6 +47,14 @@ namespace Unity.Netcode /// If you receive this event then it is most likely due to a bug (please open a GitHub issue with steps to replicate).
///
InternalNetcodeError, + /// + /// This is returned when an unload or load action is attempted and scene management is disabled + /// + SceneManagementNotEnabled, + /// + /// This is returned when a client attempts to perform a server only action + /// + ServerOnlyAction, } /// diff --git a/Runtime/Spawning/NetworkSpawnManager.cs b/Runtime/Spawning/NetworkSpawnManager.cs index c11b487..45b3dff 100644 --- a/Runtime/Spawning/NetworkSpawnManager.cs +++ b/Runtime/Spawning/NetworkSpawnManager.cs @@ -322,6 +322,167 @@ namespace Unity.Netcode return networkObject != null; } + internal enum InstantiateAndSpawnErrorTypes + { + NetworkPrefabNull, + NotAuthority, + InvokedWhenShuttingDown, + NotRegisteredNetworkPrefab, + NetworkManagerNull, + NoActiveSession, + } + + internal static readonly Dictionary InstantiateAndSpawnErrors = new Dictionary( + new KeyValuePair[]{ + new KeyValuePair(InstantiateAndSpawnErrorTypes.NetworkPrefabNull, $"The {nameof(NetworkObject)} prefab parameter was null!"), + new KeyValuePair(InstantiateAndSpawnErrorTypes.NotAuthority, $"Only the server has authority to {nameof(InstantiateAndSpawn)}!"), + new KeyValuePair(InstantiateAndSpawnErrorTypes.InvokedWhenShuttingDown, $"Invoking {nameof(InstantiateAndSpawn)} while shutting down! Calls to {nameof(InstantiateAndSpawn)} will be ignored."), + new KeyValuePair(InstantiateAndSpawnErrorTypes.NotRegisteredNetworkPrefab, $"The {nameof(NetworkObject)} parameter is not a registered network prefab. Did you forget to register it or are you trying to instantiate and spawn an instance of a network prefab?"), + new KeyValuePair(InstantiateAndSpawnErrorTypes.NetworkManagerNull, $"The {nameof(NetworkManager)} parameter was null!"), + new KeyValuePair(InstantiateAndSpawnErrorTypes.NoActiveSession, "You can only invoke this method when you are connected to an existing/in-progress network session!") + }); + + /// + /// Use this method to easily instantiate and spawn an instance of a network prefab. + /// InstantiateAndSpawn will: + /// - Find any override associated with the prefab + /// - If there is no override, then the current prefab type is used. + /// - Create an instance of the prefab (or its override). + /// - Spawn the prefab instance + /// + /// The of the pefab asset. + /// The owner of the instance (defaults to server). + /// Whether the instance will be destroyed when the scene it is located within is unloaded (default is false). + /// Whether the instance is a player object or not (default is false). + /// Whether you want to force spawning the override when running as a host or server or if you want it to spawn the override for host mode and + /// the source prefab for server. If there is an override, clients always spawn that as opposed to the source prefab (defaults to false). + /// The starting poisiton of the instance. + /// The starting rotation of the instance. + /// The newly instantiated and spawned prefab instance. + public NetworkObject InstantiateAndSpawn(NetworkObject networkPrefab, ulong ownerClientId = NetworkManager.ServerClientId, bool destroyWithScene = false, bool isPlayerObject = false, bool forceOverride = false, Vector3 position = default, Quaternion rotation = default) + { + if (networkPrefab == null) + { + Debug.LogError(InstantiateAndSpawnErrors[InstantiateAndSpawnErrorTypes.NetworkPrefabNull]); + return null; + } + + if (!NetworkManager.IsServer) + { + Debug.LogError(InstantiateAndSpawnErrors[InstantiateAndSpawnErrorTypes.NotAuthority]); + return null; + } + + if (NetworkManager.ShutdownInProgress) + { + Debug.LogWarning(InstantiateAndSpawnErrors[InstantiateAndSpawnErrorTypes.InvokedWhenShuttingDown]); + return null; + } + + // Verify it is actually a valid prefab + if (!NetworkManager.NetworkConfig.Prefabs.Contains(networkPrefab.gameObject)) + { + Debug.LogError(InstantiateAndSpawnErrors[InstantiateAndSpawnErrorTypes.NotRegisteredNetworkPrefab]); + return null; + } + + return InstantiateAndSpawnNoParameterChecks(networkPrefab, ownerClientId, destroyWithScene, isPlayerObject, forceOverride, position, rotation); + } + + /// + /// !!! Does not perform any parameter checks prior to attempting to instantiate and spawn the NetworkObject !!! + /// + internal NetworkObject InstantiateAndSpawnNoParameterChecks(NetworkObject networkPrefab, ulong ownerClientId = NetworkManager.ServerClientId, bool destroyWithScene = false, bool isPlayerObject = false, bool forceOverride = false, Vector3 position = default, Quaternion rotation = default) + { + + var networkObject = networkPrefab; + // Host spawns the ovveride and server spawns the original prefab unless forceOverride is set to true where both server or host will spawn the override. + if (forceOverride || NetworkManager.IsHost) + { + networkObject = GetNetworkObjectToSpawn(networkPrefab.GlobalObjectIdHash, ownerClientId, position, rotation); + } + if (networkObject == null) + { + Debug.LogError($"Failed to instantiate and spawn {networkPrefab.name}!"); + return null; + } + networkObject.IsPlayerObject = isPlayerObject; + networkObject.transform.position = position; + networkObject.transform.rotation = rotation; + networkObject.SpawnWithOwnership(ownerClientId, destroyWithScene); + return networkObject; + } + + /// + /// Gets the right NetworkObject prefab instance to spawn. If a handler is registered or there is an override assigned to the + /// passed in globalObjectIdHash value, then that is what will be instantiated, spawned, and returned. + /// + internal NetworkObject GetNetworkObjectToSpawn(uint globalObjectIdHash, ulong ownerId, Vector3 position = default, Quaternion rotation = default, bool isScenePlaced = false) + { + NetworkObject networkObject = null; + // If the prefab hash has a registered INetworkPrefabInstanceHandler derived class + if (NetworkManager.PrefabHandler.ContainsHandler(globalObjectIdHash)) + { + // Let the handler spawn the NetworkObject + networkObject = NetworkManager.PrefabHandler.HandleNetworkPrefabSpawn(globalObjectIdHash, ownerId, position, rotation); + networkObject.NetworkManagerOwner = NetworkManager; + } + else + { + // See if there is a valid registered NetworkPrefabOverrideLink associated with the provided prefabHash + var networkPrefabReference = (GameObject)null; + var inScenePlacedWithNoSceneManagement = !NetworkManager.NetworkConfig.EnableSceneManagement && isScenePlaced; + + if (NetworkManager.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks.ContainsKey(globalObjectIdHash)) + { + var networkPrefab = NetworkManager.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks[globalObjectIdHash]; + + switch (networkPrefab.Override) + { + default: + case NetworkPrefabOverride.None: + networkPrefabReference = networkPrefab.Prefab; + break; + case NetworkPrefabOverride.Hash: + case NetworkPrefabOverride.Prefab: + { + // When scene management is disabled and this is an in-scene placed NetworkObject, we want to always use the + // SourcePrefabToOverride and not any possible prefab override as a user might want to spawn overrides dynamically + // but might want to use the same source network prefab as an in-scene placed NetworkObject. + // (When scene management is enabled, clients don't delete their in-scene placed NetworkObjects prior to dynamically + // spawning them so the original prefab placed is preserved and this is not needed) + if (inScenePlacedWithNoSceneManagement) + { + networkPrefabReference = networkPrefab.SourcePrefabToOverride ? networkPrefab.SourcePrefabToOverride : networkPrefab.Prefab; + } + else + { + networkPrefabReference = NetworkManager.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks[globalObjectIdHash].OverridingTargetPrefab; + } + break; + } + } + } + + // If not, then there is an issue (user possibly didn't register the prefab properly?) + if (networkPrefabReference == null) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Error) + { + NetworkLog.LogError($"Failed to create object locally. [{nameof(globalObjectIdHash)}={globalObjectIdHash}]. {nameof(NetworkPrefab)} could not be found. Is the prefab registered with {nameof(NetworkManager)}?"); + } + } + else + { + // Create prefab instance + networkObject = UnityEngine.Object.Instantiate(networkPrefabReference).GetComponent(); + networkObject.NetworkManagerOwner = NetworkManager; + networkObject.PrefabGlobalObjectIdHash = globalObjectIdHash; + } + } + return networkObject; + } + /// /// Creates a local NetowrkObject to be spawned. /// @@ -343,48 +504,7 @@ namespace Unity.Netcode // If scene management is disabled or the NetworkObject was dynamically spawned if (!NetworkManager.NetworkConfig.EnableSceneManagement || !sceneObject.IsSceneObject) { - // If the prefab hash has a registered INetworkPrefabInstanceHandler derived class - if (NetworkManager.PrefabHandler.ContainsHandler(globalObjectIdHash)) - { - // Let the handler spawn the NetworkObject - networkObject = NetworkManager.PrefabHandler.HandleNetworkPrefabSpawn(globalObjectIdHash, sceneObject.OwnerClientId, position, rotation); - networkObject.NetworkManagerOwner = NetworkManager; - isSpawnedByPrefabHandler = true; - } - else - { - // See if there is a valid registered NetworkPrefabOverrideLink associated with the provided prefabHash - GameObject networkPrefabReference = null; - if (NetworkManager.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks.ContainsKey(globalObjectIdHash)) - { - switch (NetworkManager.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks[globalObjectIdHash].Override) - { - default: - case NetworkPrefabOverride.None: - networkPrefabReference = NetworkManager.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks[globalObjectIdHash].Prefab; - break; - case NetworkPrefabOverride.Hash: - case NetworkPrefabOverride.Prefab: - networkPrefabReference = NetworkManager.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks[globalObjectIdHash].OverridingTargetPrefab; - break; - } - } - - // If not, then there is an issue (user possibly didn't register the prefab properly?) - if (networkPrefabReference == null) - { - if (NetworkLog.CurrentLogLevel <= LogLevel.Error) - { - NetworkLog.LogError($"Failed to create object locally. [{nameof(globalObjectIdHash)}={globalObjectIdHash}]. {nameof(NetworkPrefab)} could not be found. Is the prefab registered with {nameof(NetworkManager)}?"); - } - } - else - { - // Create prefab instance - networkObject = UnityEngine.Object.Instantiate(networkPrefabReference).GetComponent(); - networkObject.NetworkManagerOwner = NetworkManager; - } - } + networkObject = GetNetworkObjectToSpawn(sceneObject.Hash, sceneObject.OwnerClientId, position, rotation, sceneObject.IsSceneObject); } else // Get the in-scene placed NetworkObject { @@ -624,6 +744,13 @@ namespace Unity.Netcode { networkObject.SubscribeToActiveSceneForSynch(); } + + // If we are an in-scene placed NetworkObject and our InScenePlacedSourceGlobalObjectIdHash is set + // then assign this to the PrefabGlobalObjectIdHash + if (networkObject.IsSceneObject.Value && networkObject.InScenePlacedSourceGlobalObjectIdHash != 0) + { + networkObject.PrefabGlobalObjectIdHash = networkObject.InScenePlacedSourceGlobalObjectIdHash; + } } internal void SendSpawnCallForObject(ulong clientId, NetworkObject networkObject) @@ -728,17 +855,43 @@ namespace Unity.Netcode // Leave destruction up to the handler NetworkManager.PrefabHandler.HandleNetworkPrefabDestroy(networkObjects[i]); } - else if (networkObjects[i].IsSpawned) + else { - // If it is an in-scene placed NetworkObject then just despawn - // and let it be destroyed when the scene is unloaded. Otherwise, despawn and destroy it. + // If it is an in-scene placed NetworkObject then just despawn and let it be destroyed when the scene + // is unloaded. Otherwise, despawn and destroy it. var shouldDestroy = !(networkObjects[i].IsSceneObject != null && networkObjects[i].IsSceneObject.Value); - OnDespawnObject(networkObjects[i], shouldDestroy); - } - else if (networkObjects[i].IsSceneObject != null && !networkObjects[i].IsSceneObject.Value) - { - UnityEngine.Object.Destroy(networkObjects[i].gameObject); + // If we are going to destroy this NetworkObject, check for any in-scene placed children that need to be removed + if (shouldDestroy) + { + // Check to see if there are any in-scene placed children that are marked to be destroyed with the scene + var childrenObjects = networkObjects[i].GetComponentsInChildren(); + foreach (var childObject in childrenObjects) + { + if (childObject == networkObjects[i]) + { + continue; + } + + // If the child is an in-scene placed NetworkObject then remove the child from the parent (which was dynamically spawned) + // and set its parent to root + if (childObject.IsSceneObject != null && childObject.IsSceneObject.Value) + { + childObject.TryRemoveParent(childObject.WorldPositionStays()); + } + } + } + + // If spawned, then despawn and potentially destroy. + if (networkObjects[i].IsSpawned) + { + OnDespawnObject(networkObjects[i], shouldDestroy); + } + else // Otherwise, if we are not spawned and we should destroy...then destroy. + if (shouldDestroy) + { + UnityEngine.Object.Destroy(networkObjects[i].gameObject); + } } } } @@ -788,7 +941,7 @@ namespace Unity.Netcode { if (networkObjects[i].NetworkManager == NetworkManager) { - if (networkObjects[i].IsSceneObject == null) + if (networkObjects[i].IsSceneObject == null || (networkObjects[i].IsSceneObject.HasValue && networkObjects[i].IsSceneObject.Value)) { networkObjectsToSpawn.Add(networkObjects[i]); } @@ -937,7 +1090,7 @@ namespace Unity.Netcode if (sobj.CheckObjectVisibility == null) { // If the client is not part of the observers and spawn with observers is enabled on this instance or the clientId is the server - if (!sobj.Observers.Contains(clientId) && (sobj.SpawnWithObservers || clientId == NetworkManager.ServerClientId)) + if (sobj.SpawnWithObservers || clientId == NetworkManager.ServerClientId) { sobj.Observers.Add(clientId); } @@ -950,7 +1103,6 @@ namespace Unity.Netcode sobj.Observers.Add(clientId); } else // Otherwise, if the observers contains the clientId (shouldn't happen) then remove it since CheckObjectVisibility returned false - if (sobj.Observers.Contains(clientId)) { sobj.Observers.Remove(clientId); } diff --git a/Runtime/Transports/UTP/BatchedSendQueue.cs b/Runtime/Transports/UTP/BatchedSendQueue.cs index 6bf3e49..b1bd3fa 100644 --- a/Runtime/Transports/UTP/BatchedSendQueue.cs +++ b/Runtime/Transports/UTP/BatchedSendQueue.cs @@ -198,43 +198,69 @@ namespace Unity.Netcode.Transports.UTP /// could lead to a corrupted queue. /// /// The to write to. + /// + /// Maximum number of bytes to copy (0 means writer capacity). This is a soft limit only. + /// If a message is larger than that but fits in the writer, it will be written. In effect, + /// this parameter is the maximum size that small messages can be coalesced together. + /// /// How many bytes were written to the writer. - public int FillWriterWithMessages(ref DataStreamWriter writer) + public int FillWriterWithMessages(ref DataStreamWriter writer, int softMaxBytes = 0) { if (!IsCreated || Length == 0) { return 0; } + softMaxBytes = softMaxBytes == 0 ? writer.Capacity : Math.Min(softMaxBytes, writer.Capacity); + unsafe { var reader = new DataStreamReader(m_Data.AsArray()); - - var writerAvailable = writer.Capacity; var readerOffset = HeadIndex; - while (readerOffset < TailIndex) + reader.SeekSet(readerOffset); + var messageLength = reader.ReadInt(); + var bytesToWrite = messageLength + sizeof(int); + + // Our behavior here depends on the size of the first message in the queue. If it's + // larger than the soft limit, then add only that message to the writer (we want + // large payloads to be fragmented on their own). Otherwise coalesce all small + // messages until we hit the soft limit (which presumably means they won't be + // fragmented, which is the desired behavior for smaller messages). + + if (bytesToWrite > softMaxBytes && bytesToWrite <= writer.Capacity) { - reader.SeekSet(readerOffset); - var messageLength = reader.ReadInt(); + writer.WriteInt(messageLength); + WriteBytes(ref writer, (byte*)m_Data.GetUnsafePtr() + reader.GetBytesRead(), messageLength); - if (writerAvailable < sizeof(int) + messageLength) - { - break; - } - else - { - writer.WriteInt(messageLength); - - var messageOffset = reader.GetBytesRead(); - WriteBytes(ref writer, (byte*)m_Data.GetUnsafePtr() + messageOffset, messageLength); - - writerAvailable -= sizeof(int) + messageLength; - readerOffset += sizeof(int) + messageLength; - } + return bytesToWrite; } + else + { + var bytesWritten = 0; - return writer.Capacity - writerAvailable; + while (readerOffset < TailIndex) + { + reader.SeekSet(readerOffset); + messageLength = reader.ReadInt(); + bytesToWrite = messageLength + sizeof(int); + + if (bytesWritten + bytesToWrite <= softMaxBytes) + { + writer.WriteInt(messageLength); + WriteBytes(ref writer, (byte*)m_Data.GetUnsafePtr() + reader.GetBytesRead(), messageLength); + + readerOffset += bytesToWrite; + bytesWritten += bytesToWrite; + } + else + { + break; + } + } + + return bytesWritten; + } } } diff --git a/Runtime/Transports/UTP/UnityTransport.cs b/Runtime/Transports/UTP/UnityTransport.cs index 64e6033..bc89e15 100644 --- a/Runtime/Transports/UTP/UnityTransport.cs +++ b/Runtime/Transports/UTP/UnityTransport.cs @@ -753,7 +753,7 @@ namespace Unity.Netcode.Transports.UTP // in the stream (the send queue does that automatically) we are sure they'll be // reassembled properly at the other end. This allows us to lift the limit of ~44KB // on reliable payloads (because of the reliable window size). - var written = pipeline == ReliablePipeline ? Queue.FillWriterWithBytes(ref writer, MTU) : Queue.FillWriterWithMessages(ref writer); + var written = pipeline == ReliablePipeline ? Queue.FillWriterWithBytes(ref writer, MTU) : Queue.FillWriterWithMessages(ref writer, MTU); result = Driver.EndSend(writer); if (result == written) @@ -995,6 +995,11 @@ namespace Unity.Netcode.Transports.UTP private void ExtractNetworkMetricsFromPipeline(NetworkPipeline pipeline, NetworkConnection networkConnection) { + if (m_Driver.GetConnectionState(networkConnection) != NetworkConnection.State.Connected) + { + return; + } + //Don't need to dispose of the buffers, they are filled with data pointers. m_Driver.GetPipelineBuffers(pipeline, #if UTP_TRANSPORT_2_0_ABOVE @@ -1229,7 +1234,18 @@ namespace Unity.Netcode.Transports.UTP // Bump the reliable window size to its maximum size of 64. Since NGO makes heavy use of // reliable delivery, we're better off with the increased window size compared to the // extra 4 bytes of header that this costs us. - m_NetworkSettings.WithReliableStageParameters(windowSize: 64); + // + // We also increase the maximum resend timeout since the default one in UTP is very + // aggressive (optimized for latency and low bandwidth). With NGO, it's too low and + // we sometimes notice a lot of useless resends, especially if using Relay. (We can + // only do this with UTP 2.0 because 1.X doesn't support that parameter.) + m_NetworkSettings.WithReliableStageParameters( + windowSize: 64 +#if UTP_TRANSPORT_2_0_ABOVE + , + maximumResendTime: m_ProtocolType == ProtocolType.RelayUnityTransport ? 750 : 500 +#endif + ); #if !UTP_TRANSPORT_2_0_ABOVE && !UNITY_WEBGL m_NetworkSettings.WithBaselibNetworkInterfaceParameters( diff --git a/TestHelpers/Runtime/NetcodeIntegrationTest.cs b/TestHelpers/Runtime/NetcodeIntegrationTest.cs index b30d935..f05ee46 100644 --- a/TestHelpers/Runtime/NetcodeIntegrationTest.cs +++ b/TestHelpers/Runtime/NetcodeIntegrationTest.cs @@ -305,8 +305,12 @@ namespace Unity.Netcode.TestHelpers.Runtime public IEnumerator SetUp() { VerboseDebug($"Entering {nameof(SetUp)}"); - NetcodeLogAssert = new NetcodeLogAssert(); + if (m_EnableTimeTravel) + { + // Setup the frames per tick for time travel advance to next tick + ConfigureFramesPerTick(); + } if (m_SetupIsACoroutine) { yield return OnSetup(); @@ -732,6 +736,16 @@ namespace Unity.Netcode.TestHelpers.Runtime Assert.Fail("Failed to start instances"); } + // When scene management is enabled, we need to re-apply the scenes populated list since we have overriden the ISceneManagerHandler + // imeplementation at this point. This assures any pre-loaded scenes will be automatically assigned to the server and force clients + // to load their own scenes. + if (m_ServerNetworkManager.NetworkConfig.EnableSceneManagement) + { + var scenesLoaded = m_ServerNetworkManager.SceneManager.ScenesLoaded; + m_ServerNetworkManager.SceneManager.SceneManagerHandler.PopulateLoadedScenes(ref scenesLoaded, m_ServerNetworkManager); + } + + if (LogAllMessages) { EnableMessageLogging(); @@ -799,6 +813,12 @@ namespace Unity.Netcode.TestHelpers.Runtime Assert.Fail("Failed to start instances"); } + // Time travel does not play nice with scene loading, clear out server side pre-loaded scenes. + if (m_ServerNetworkManager.NetworkConfig.EnableSceneManagement) + { + m_ServerNetworkManager.SceneManager.ScenesLoaded.Clear(); + } + if (LogAllMessages) { EnableMessageLogging(); @@ -1544,8 +1564,42 @@ namespace Unity.Netcode.TestHelpers.Runtime } } + protected virtual uint GetTickRate() + { + return k_DefaultTickRate; + } + + protected virtual int GetFrameRate() + { + return Application.targetFrameRate == 0 ? 60 : Application.targetFrameRate; + } + + private int m_FramesPerTick = 0; + private float m_TickFrequency = 0; + + /// + /// Recalculates the and that is + /// used in . + /// + protected void ConfigureFramesPerTick() + { + m_TickFrequency = 1.0f / GetTickRate(); + m_FramesPerTick = Math.Max((int)(m_TickFrequency / GetFrameRate()), 1); + } + /// /// Helper function to time travel exactly one tick's worth of time at the current frame and tick rates. + /// This is NetcodeIntegrationTest instance relative and will automatically adjust based on + /// and . + /// + protected void TimeTravelAdvanceTick() + { + TimeTravel(m_TickFrequency, m_FramesPerTick); + } + + /// + /// Helper function to time travel exactly one tick's worth of time at the current frame and tick rates. + /// ** Is based on the global k_DefaultTickRate and is not local to each NetcodeIntegrationTest instance ** /// public static void TimeTravelToNextTick() { @@ -1555,7 +1609,6 @@ namespace Unity.Netcode.TestHelpers.Runtime { frameRate = 60; } - var frames = Math.Max((int)(timePassed / frameRate), 1); TimeTravel(timePassed, frames); } diff --git a/TestHelpers/Runtime/NetcodeIntegrationTestHelpers.cs b/TestHelpers/Runtime/NetcodeIntegrationTestHelpers.cs index bd20732..bf26f9f 100644 --- a/TestHelpers/Runtime/NetcodeIntegrationTestHelpers.cs +++ b/TestHelpers/Runtime/NetcodeIntegrationTestHelpers.cs @@ -813,7 +813,8 @@ namespace Unity.Netcode.TestHelpers.Runtime if (minFrames > 0) { - yield return new WaitUntil(() => Time.frameCount >= minFrames); + var waitForFrameCount = Time.frameCount + minFrames; + yield return new WaitUntil(() => Time.frameCount >= waitForFrameCount); } while (Time.realtimeSinceStartup - startTime < timeout && !predicate()) diff --git a/Tests/Editor/Transports/BatchedSendQueueTests.cs b/Tests/Editor/Transports/BatchedSendQueueTests.cs index 3481480..c1550da 100644 --- a/Tests/Editor/Transports/BatchedSendQueueTests.cs +++ b/Tests/Editor/Transports/BatchedSendQueueTests.cs @@ -237,6 +237,35 @@ namespace Unity.Netcode.EditorTests AssertIsTestMessage(data); } + [Test] + public void BatchedSendQueue_FillWriterWithMessages_StopOnSoftMaxBytes() + { + var smallMessage = new ArraySegment(new byte[10]); + var largeMessage = new ArraySegment(new byte[3000]); + + var smallMessageSize = smallMessage.Count + BatchedSendQueue.PerMessageOverhead; + var largeMessageSize = largeMessage.Count + BatchedSendQueue.PerMessageOverhead; + + using var q = new BatchedSendQueue(k_TestQueueCapacity); + using var data = new NativeArray(largeMessageSize, Allocator.Temp); + + q.PushMessage(smallMessage); + q.PushMessage(largeMessage); + q.PushMessage(smallMessage); + + var writer = new DataStreamWriter(data); + Assert.AreEqual(smallMessageSize, q.FillWriterWithMessages(ref writer, 1000)); + q.Consume(smallMessageSize); + + writer = new DataStreamWriter(data); + Assert.AreEqual(largeMessageSize, q.FillWriterWithMessages(ref writer, 1000)); + q.Consume(largeMessageSize); + + writer = new DataStreamWriter(data); + Assert.AreEqual(smallMessageSize, q.FillWriterWithMessages(ref writer, 1000)); + q.Consume(smallMessageSize); + } + [Test] public void BatchedSendQueue_FillWriterWithBytes_NoopIfNoData() { diff --git a/Tests/Runtime/DeferredMessagingTests.cs b/Tests/Runtime/DeferredMessagingTests.cs index de80a23..f85a9cb 100644 --- a/Tests/Runtime/DeferredMessagingTests.cs +++ b/Tests/Runtime/DeferredMessagingTests.cs @@ -94,6 +94,10 @@ namespace Unity.Netcode.RuntimeTests public override void ProcessTriggers(IDeferredNetworkMessageManager.TriggerType trigger, ulong key) { + if (trigger == IDeferredNetworkMessageManager.TriggerType.OnNextFrame) + { + return; + } ProcessTriggersCalled = true; base.ProcessTriggers(trigger, key); } diff --git a/Tests/Runtime/InvalidConnectionEventsTest.cs b/Tests/Runtime/InvalidConnectionEventsTest.cs index 6504f99..11d3121 100644 --- a/Tests/Runtime/InvalidConnectionEventsTest.cs +++ b/Tests/Runtime/InvalidConnectionEventsTest.cs @@ -79,7 +79,10 @@ namespace Unity.Netcode.RuntimeTests [UnityTest] public IEnumerator WhenSendingConnectionApprovedToAlreadyConnectedClient_ConnectionApprovedMessageIsRejected() { - var message = new ConnectionApprovedMessage(); + var message = new ConnectionApprovedMessage + { + ConnectedClientIds = new NativeArray(0, Allocator.Temp) + }; m_ServerNetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.Reliable, m_ClientNetworkManagers[0].LocalClientId); // Unnamed message is something to wait for. When this one is received, @@ -93,7 +96,7 @@ namespace Unity.Netcode.RuntimeTests m_ClientNetworkManagers[0].ConnectionManager.MessageManager.Hook(new Hooks()); - LogAssert.Expect(LogType.Error, new Regex($"A {nameof(ConnectionApprovedMessage)} was received from the server when the connection has already been established\\. This should not happen\\.")); + LogAssert.Expect(LogType.Error, new Regex($"A {nameof(ConnectionApprovedMessage)} was received from the server when the connection has already been established\\. NetworkTransport: Unity.Netcode.Transports.UTP.UnityTransport UnityTransportProtocol: UnityTransport. This should not happen\\.")); yield return WaitForMessageReceived(m_ClientNetworkManagers.ToList()); } @@ -115,7 +118,7 @@ namespace Unity.Netcode.RuntimeTests m_ClientNetworkManagers[0].ConnectionManager.MessageManager.Hook(new Hooks()); - LogAssert.Expect(LogType.Error, new Regex($"A {nameof(ConnectionRequestMessage)} was received from the server on the client side\\. This should not happen\\.")); + LogAssert.Expect(LogType.Error, new Regex($"A {nameof(ConnectionRequestMessage)} was received from the server on the client side\\. NetworkTransport: Unity.Netcode.Transports.UTP.UnityTransport UnityTransportProtocol: UnityTransport. This should not happen\\.")); yield return WaitForMessageReceived(m_ClientNetworkManagers.ToList()); } @@ -137,7 +140,7 @@ namespace Unity.Netcode.RuntimeTests m_ServerNetworkManager.ConnectionManager.MessageManager.Hook(new Hooks()); - LogAssert.Expect(LogType.Error, new Regex($"A {nameof(ConnectionRequestMessage)} was received from a client when the connection has already been established\\. This should not happen\\.")); + LogAssert.Expect(LogType.Error, new Regex($"A {nameof(ConnectionRequestMessage)} was received from a client when the connection has already been established\\. NetworkTransport: Unity.Netcode.Transports.UTP.UnityTransport UnityTransportProtocol: UnityTransport. This should not happen\\.")); yield return WaitForMessageReceived(new List { m_ServerNetworkManager }); } @@ -145,7 +148,10 @@ namespace Unity.Netcode.RuntimeTests [UnityTest] public IEnumerator WhenSendingConnectionApprovedFromAnyClient_ConnectionApprovedMessageIsRejected() { - var message = new ConnectionApprovedMessage(); + var message = new ConnectionApprovedMessage + { + ConnectedClientIds = new NativeArray(0, Allocator.Temp) + }; m_ClientNetworkManagers[0].ConnectionManager.SendMessage(ref message, NetworkDelivery.Reliable, m_ServerNetworkManager.LocalClientId); // Unnamed message is something to wait for. When this one is received, @@ -159,7 +165,7 @@ namespace Unity.Netcode.RuntimeTests m_ServerNetworkManager.ConnectionManager.MessageManager.Hook(new Hooks()); - LogAssert.Expect(LogType.Error, new Regex($"A {nameof(ConnectionApprovedMessage)} was received from a client on the server side\\. This should not happen\\.")); + LogAssert.Expect(LogType.Error, new Regex($"A {nameof(ConnectionApprovedMessage)} was received from a client on the server side\\. NetworkTransport: Unity.Netcode.Transports.UTP.UnityTransport UnityTransportProtocol: UnityTransport. This should not happen\\.")); yield return WaitForMessageReceived(new List { m_ServerNetworkManager }); } diff --git a/Tests/Runtime/NetworkObject/NetworkObjectDestroyTests.cs b/Tests/Runtime/NetworkObject/NetworkObjectDestroyTests.cs index 6ea6c80..0e6d20e 100644 --- a/Tests/Runtime/NetworkObject/NetworkObjectDestroyTests.cs +++ b/Tests/Runtime/NetworkObject/NetworkObjectDestroyTests.cs @@ -58,6 +58,9 @@ namespace Unity.Netcode.RuntimeTests ShuttingDown, ActiveSession } + + private string m_ClientPlayerName; + private ulong m_ClientNetworkObjectId; /// /// Validates the expected behavior when the client-side destroys a /// @@ -78,7 +81,8 @@ namespace Unity.Netcode.RuntimeTests LogAssert.ignoreFailingMessages = true; NetworkLog.NetworkManagerOverride = m_ClientNetworkManagers[0]; } - + m_ClientPlayerName = clientPlayer.gameObject.name; + m_ClientNetworkObjectId = clientPlayer.NetworkObjectId; Object.DestroyImmediate(clientPlayer.gameObject); // destroying a NetworkObject while a session is active is not allowed @@ -91,12 +95,12 @@ namespace Unity.Netcode.RuntimeTests private bool HaveLogsBeenReceived() { - if (!NetcodeLogAssert.HasLogBeenReceived(LogType.Error, "[Netcode] Destroy a spawned NetworkObject on a non-host client is not valid. Call Destroy or Despawn on the server/host instead.")) + if (!NetcodeLogAssert.HasLogBeenReceived(LogType.Error, $"[Netcode] [Invalid Destroy][{m_ClientPlayerName}][NetworkObjectId:{m_ClientNetworkObjectId}] Destroy a spawned {nameof(NetworkObject)} on a non-host client is not valid. Call Destroy or Despawn on the server/host instead.")) { return false; } - if (!NetcodeLogAssert.HasLogBeenReceived(LogType.Error, $"[Netcode-Server Sender={m_ClientNetworkManagers[0].LocalClientId}] Destroy a spawned NetworkObject on a non-host client is not valid. Call Destroy or Despawn on the server/host instead.")) + if (!NetcodeLogAssert.HasLogBeenReceived(LogType.Error, $"[Netcode-Server Sender={m_ClientNetworkManagers[0].LocalClientId}] [Invalid Destroy][{m_ClientPlayerName}][NetworkObjectId:{m_ClientNetworkObjectId}] Destroy a spawned {nameof(NetworkObject)} on a non-host client is not valid. Call Destroy or Despawn on the server/host instead.")) { return false; } diff --git a/Tests/Runtime/NetworkObject/NetworkObjectOnNetworkDespawnTests.cs b/Tests/Runtime/NetworkObject/NetworkObjectOnNetworkDespawnTests.cs index 726266c..ae1ea07 100644 --- a/Tests/Runtime/NetworkObject/NetworkObjectOnNetworkDespawnTests.cs +++ b/Tests/Runtime/NetworkObject/NetworkObjectOnNetworkDespawnTests.cs @@ -10,111 +10,104 @@ namespace Unity.Netcode.RuntimeTests /// /// Tests that check OnNetworkDespawn being invoked /// - public class NetworkObjectOnNetworkDespawnTests + [TestFixture(HostOrServer.Host)] + [TestFixture(HostOrServer.Server)] + public class NetworkObjectOnNetworkDespawnTests : NetcodeIntegrationTest { - private NetworkManager m_ServerHost; - private NetworkManager[] m_Clients; + private const string k_ObjectName = "TestDespawn"; + public enum InstanceType + { + Server, + Client + } + protected override int NumberOfClients => 1; private GameObject m_ObjectToSpawn; - private NetworkObject m_NetworkObject; + private HostOrServer m_HostOrServer; + public NetworkObjectOnNetworkDespawnTests(HostOrServer hostOrServer) : base(hostOrServer) + { + m_HostOrServer = hostOrServer; + } internal class OnNetworkDespawnTestComponent : NetworkBehaviour { - public bool OnNetworkDespawnCalled { get; internal set; } + public static bool OnServerNetworkDespawnCalled { get; internal set; } + public static bool OnClientNetworkDespawnCalled { get; internal set; } public override void OnNetworkSpawn() { - OnNetworkDespawnCalled = false; + if (IsServer) + { + OnServerNetworkDespawnCalled = false; + } + else + { + OnClientNetworkDespawnCalled = false; + } base.OnNetworkSpawn(); } public override void OnNetworkDespawn() { - OnNetworkDespawnCalled = true; + if (IsServer) + { + OnServerNetworkDespawnCalled = true; + } + else + { + OnClientNetworkDespawnCalled = true; + } base.OnNetworkDespawn(); } } - [UnitySetUp] - public IEnumerator Setup() + protected override void OnServerAndClientsCreated() { - Assert.IsTrue(NetcodeIntegrationTestHelpers.Create(1, out m_ServerHost, out m_Clients)); - - m_ObjectToSpawn = NetcodeIntegrationTestHelpers.CreateNetworkObjectPrefab(nameof(NetworkObjectOnNetworkDespawnTests), m_ServerHost, m_Clients); + m_ObjectToSpawn = CreateNetworkObjectPrefab(k_ObjectName); m_ObjectToSpawn.AddComponent(); - m_NetworkObject = m_ObjectToSpawn.GetComponent(); - - yield return null; - } - - [UnityTearDown] - public IEnumerator Teardown() - { - // Shutdown and clean up both of our NetworkManager instances - if (m_ObjectToSpawn) - { - Object.Destroy(m_ObjectToSpawn); - m_ObjectToSpawn = null; - } - NetcodeIntegrationTestHelpers.Destroy(); - yield return null; - } - - public enum InstanceType - { - Server, - Host, - Client + base.OnServerAndClientsCreated(); } /// - /// Tests that a spawned NetworkObject's associated NetworkBehaviours will have - /// their OnNetworkDespawn invoked during NetworkManager shutdown. + /// This test validates that is invoked when the + /// is shutdown. /// [UnityTest] - public IEnumerator TestNetworkObjectDespawnOnShutdown([Values(InstanceType.Server, InstanceType.Host, InstanceType.Client)] InstanceType despawnCheck) + public IEnumerator TestNetworkObjectDespawnOnShutdown() { - var useHost = despawnCheck != InstanceType.Server; - var networkManager = despawnCheck == InstanceType.Host || despawnCheck == InstanceType.Server ? m_ServerHost : m_Clients[0]; - - // Start the instances - if (!NetcodeIntegrationTestHelpers.Start(useHost, m_ServerHost, m_Clients)) - { - Debug.LogError("Failed to start instances"); - Assert.Fail("Failed to start instances"); - } - - // [Client-Side] Wait for a connection to the server - yield return NetcodeIntegrationTestHelpers.WaitForClientsConnected(m_Clients, null, 512); - - // [Host-Server-Side] Check to make sure all clients are connected - var clientCount = useHost ? m_Clients.Length + 1 : m_Clients.Length; - yield return NetcodeIntegrationTestHelpers.WaitForClientsConnectedToServer(m_ServerHost, clientCount, null, 512); - // Spawn the test object - var spawnedObject = Object.Instantiate(m_NetworkObject); + var spawnedObject = SpawnObject(m_ObjectToSpawn, m_ServerNetworkManager); var spawnedNetworkObject = spawnedObject.GetComponent(); - spawnedNetworkObject.NetworkManagerOwner = m_ServerHost; - spawnedNetworkObject.Spawn(true); - // Get the spawned object relative to which NetworkManager instance we are testing. - var relativeSpawnedObject = new NetcodeIntegrationTestHelpers.ResultWrapper(); - yield return NetcodeIntegrationTestHelpers.GetNetworkObjectByRepresentation((x => x.GetComponent() != null), networkManager, relativeSpawnedObject); - var onNetworkDespawnTestComponent = relativeSpawnedObject.Result.GetComponent(); + // Wait for the client to spawn the object + yield return WaitForConditionOrTimeOut(() => + { + if (!s_GlobalNetworkObjects.ContainsKey(m_ClientNetworkManagers[0].LocalClientId)) + { + return false; + } + if (!s_GlobalNetworkObjects[m_ClientNetworkManagers[0].LocalClientId].ContainsKey(spawnedNetworkObject.NetworkObjectId)) + { + return false; + } + return true; + }); + + AssertOnTimeout($"Timed out waiting for client to spawn {k_ObjectName}!"); // Confirm it is not set before shutting down the NetworkManager - Assert.IsFalse(onNetworkDespawnTestComponent.OnNetworkDespawnCalled); + Assert.IsFalse(OnNetworkDespawnTestComponent.OnClientNetworkDespawnCalled, "[Client-side] despawn state is already set (should not be set at this point)!"); + Assert.IsFalse(OnNetworkDespawnTestComponent.OnServerNetworkDespawnCalled, $"[{m_HostOrServer}-side] despawn state is already set (should not be set at this point)!"); - // Shutdown the NetworkManager instance we are testing. - networkManager.Shutdown(); + // Shutdown the client-side first to validate the client-side instance invokes OnNetworkDespawn + m_ClientNetworkManagers[0].Shutdown(); + yield return WaitForConditionOrTimeOut(() => OnNetworkDespawnTestComponent.OnClientNetworkDespawnCalled); + AssertOnTimeout($"[Client-side] Timed out waiting for {k_ObjectName}'s {nameof(NetworkBehaviour.OnNetworkDespawn)} to be invoked!"); - // Since shutdown is now delayed until the post frame update - // just wait 2 frames before checking to see if OnNetworkDespawnCalled is true - var currentFrame = Time.frameCount + 2; - yield return new WaitUntil(() => Time.frameCount <= currentFrame); - - // Confirm that OnNetworkDespawn is invoked after shutdown - Assert.IsTrue(onNetworkDespawnTestComponent.OnNetworkDespawnCalled); + // Shutdown the servr-host-side second to validate servr-host-side instance invokes OnNetworkDespawn + m_ServerNetworkManager.Shutdown(); + yield return WaitForConditionOrTimeOut(() => OnNetworkDespawnTestComponent.OnClientNetworkDespawnCalled); + AssertOnTimeout($"[{m_HostOrServer}-side]Timed out waiting for {k_ObjectName}'s {nameof(NetworkBehaviour.OnNetworkDespawn)} to be invoked!"); } } } diff --git a/Tests/Runtime/NetworkObject/NetworkObjectSpawnManyObjectsTests.cs b/Tests/Runtime/NetworkObject/NetworkObjectSpawnManyObjectsTests.cs index 70d60ca..8f35c97 100644 --- a/Tests/Runtime/NetworkObject/NetworkObjectSpawnManyObjectsTests.cs +++ b/Tests/Runtime/NetworkObject/NetworkObjectSpawnManyObjectsTests.cs @@ -57,9 +57,9 @@ namespace Unity.Netcode.RuntimeTests serverObject.NetworkManagerOwner = m_ServerNetworkManager; serverObject.Spawn(); } - // ensure all objects are replicated before spawning more - yield return WaitForConditionOrTimeOut(() => SpawnObjecTrackingComponent.SpawnedObjects < k_SpawnedObjects); - Assert.False(s_GlobalTimeoutHelper.TimedOut, $"Timed out waiting for the client to spawn {k_SpawnedObjects} objects!"); + // ensure all objects are replicated + yield return WaitForConditionOrTimeOut(() => SpawnObjecTrackingComponent.SpawnedObjects == k_SpawnedObjects); + AssertOnTimeout($"Timed out waiting for the client to spawn {k_SpawnedObjects} objects!"); } } } diff --git a/Tests/Runtime/NetworkTransform/NetworkTransformBase.cs b/Tests/Runtime/NetworkTransform/NetworkTransformBase.cs new file mode 100644 index 0000000..736d402 --- /dev/null +++ b/Tests/Runtime/NetworkTransform/NetworkTransformBase.cs @@ -0,0 +1,1004 @@ +using System.Collections; +using System.Collections.Generic; +using System.Text; +using NUnit.Framework; +using Unity.Netcode.Components; +using Unity.Netcode.TestHelpers.Runtime; +using UnityEngine; + + +namespace Unity.Netcode.RuntimeTests +{ + public class NetworkTransformBase : IntegrationTestWithApproximation + { + + // The number of iterations to change position, rotation, and scale for NetworkTransformMultipleChangesOverTime + protected const int k_PositionRotationScaleIterations = 3; + protected const int k_PositionRotationScaleIterations3Axis = 8; + + protected float m_CurrentHalfPrecision = 0.0f; + protected const float k_HalfPrecisionPosScale = 0.115f; + protected const float k_HalfPrecisionRot = 0.725f; + + + protected NetworkObject m_AuthoritativePlayer; + protected NetworkObject m_NonAuthoritativePlayer; + protected NetworkObject m_ChildObject; + protected NetworkObject m_SubChildObject; + protected NetworkObject m_ParentObject; + + protected NetworkTransformTestComponent m_AuthoritativeTransform; + protected NetworkTransformTestComponent m_NonAuthoritativeTransform; + protected NetworkTransformTestComponent m_OwnerTransform; + + + protected int m_OriginalTargetFrameRate; + protected Axis m_CurrentAxis; + protected bool m_AxisExcluded; + protected float m_DetectedPotentialInterpolatedTeleport; + + protected StringBuilder m_InfoMessage = new StringBuilder(); + + protected Rotation m_Rotation = Rotation.Euler; + protected Precision m_Precision = Precision.Full; + protected RotationCompression m_RotationCompression = RotationCompression.None; + protected Authority m_Authority; + + // To test that local position, rotation, and scale remain the same when parented. + protected Vector3 m_ChildObjectLocalPosition = new Vector3(5.0f, 0.0f, -5.0f); + protected Vector3 m_ChildObjectLocalRotation = new Vector3(-35.0f, 90.0f, 270.0f); + protected Vector3 m_ChildObjectLocalScale = new Vector3(0.1f, 0.5f, 0.4f); + protected Vector3 m_SubChildObjectLocalPosition = new Vector3(2.0f, 1.0f, -1.0f); + protected Vector3 m_SubChildObjectLocalRotation = new Vector3(5.0f, 15.0f, 124.0f); + protected Vector3 m_SubChildObjectLocalScale = new Vector3(1.0f, 0.15f, 0.75f); + protected NetworkObject m_AuthorityParentObject; + protected NetworkTransformTestComponent m_AuthorityParentNetworkTransform; + protected NetworkObject m_AuthorityChildObject; + protected NetworkObject m_AuthoritySubChildObject; + protected ChildObjectComponent m_AuthorityChildNetworkTransform; + protected ChildObjectComponent m_AuthoritySubChildNetworkTransform; + + public enum Authority + { + ServerAuthority, + OwnerAuthority + } + + public enum Interpolation + { + DisableInterpolate, + EnableInterpolate + } + + public enum Precision + { + Half, + Full + } + + public enum Rotation + { + Euler, + Quaternion + } + + public enum RotationCompression + { + None, + QuaternionCompress + } + + public enum TransformSpace + { + World, + Local + } + + public enum OverrideState + { + Update, + CommitToTransform, + SetState + } + + public enum Axis + { + X, + Y, + Z, + XY, + XZ, + YZ, + XYZ + } + + protected enum ChildrenTransformCheckType + { + Connected_Clients, + Late_Join_Client + } + + protected override int NumberOfClients => OnNumberOfClients(); + + protected override float GetDeltaVarianceThreshold() + { + if (m_Precision == Precision.Half || m_RotationCompression == RotationCompression.QuaternionCompress) + { + return m_CurrentHalfPrecision; + } + return 0.045f; + } + + /// + /// Override to provide the number of clients + /// + /// + protected virtual int OnNumberOfClients() + { + return 1; + } + + /// + /// Determines whether the test will use unreliable delivery for implicit state updates or not + /// + protected virtual bool UseUnreliableDeltas() + { + return false; + } + + protected virtual void Setup() + { + NetworkTransformTestComponent.AuthorityInstance = null; + m_Precision = Precision.Full; + ChildObjectComponent.Reset(); + } + + protected virtual void Teardown() + { + m_EnableVerboseDebug = false; + Object.DestroyImmediate(m_PlayerPrefab); + } + + /// + /// Handles the Setup for time travel enabled child derived tests + /// + protected override void OnInlineSetup() + { + Setup(); + base.OnInlineSetup(); + } + + /// + /// Handles the Teardown for time travel enabled child derived tests + /// + protected override void OnInlineTearDown() + { + Teardown(); + base.OnInlineTearDown(); + } + + /// + /// Handles the Setup for coroutine based derived tests + /// + protected override IEnumerator OnSetup() + { + Setup(); + return base.OnSetup(); + } + + /// + /// Handles the Teardown for coroutine based derived tests + /// + protected override IEnumerator OnTearDown() + { + Teardown(); + return base.OnTearDown(); + } + + /// + /// Constructor + /// + /// Determines if we are running as a server or host + /// Determines if we are using server or owner authority + public NetworkTransformBase(HostOrServer testWithHost, Authority authority, RotationCompression rotationCompression, Rotation rotation, Precision precision) + { + m_UseHost = testWithHost == HostOrServer.Host; + m_Authority = authority; + m_Precision = precision; + m_RotationCompression = rotationCompression; + m_Rotation = rotation; + } + + protected virtual int TargetFrameRate() + { + return 120; + } + + protected override void OnOneTimeSetup() + { + m_OriginalTargetFrameRate = Application.targetFrameRate; + Application.targetFrameRate = TargetFrameRate(); + base.OnOneTimeSetup(); + } + + protected override void OnOneTimeTearDown() + { + Application.targetFrameRate = m_OriginalTargetFrameRate; + base.OnOneTimeTearDown(); + } + + protected override void OnCreatePlayerPrefab() + { + var networkTransformTestComponent = m_PlayerPrefab.AddComponent(); + networkTransformTestComponent.ServerAuthority = m_Authority == Authority.ServerAuthority; + } + + protected override void OnServerAndClientsCreated() + { + var subChildObject = CreateNetworkObjectPrefab("SubChildObject"); + var subChildNetworkTransform = subChildObject.AddComponent(); + subChildNetworkTransform.ServerAuthority = m_Authority == Authority.ServerAuthority; + m_SubChildObject = subChildObject.GetComponent(); + + var childObject = CreateNetworkObjectPrefab("ChildObject"); + var childNetworkTransform = childObject.AddComponent(); + childNetworkTransform.ServerAuthority = m_Authority == Authority.ServerAuthority; + m_ChildObject = childObject.GetComponent(); + + var parentObject = CreateNetworkObjectPrefab("ParentObject"); + var parentNetworkTransform = parentObject.AddComponent(); + parentNetworkTransform.ServerAuthority = m_Authority == Authority.ServerAuthority; + m_ParentObject = parentObject.GetComponent(); + + // Now apply local transform values + m_ChildObject.transform.position = m_ChildObjectLocalPosition; + var childRotation = m_ChildObject.transform.rotation; + childRotation.eulerAngles = m_ChildObjectLocalRotation; + m_ChildObject.transform.rotation = childRotation; + m_ChildObject.transform.localScale = m_ChildObjectLocalScale; + + m_SubChildObject.transform.position = m_SubChildObjectLocalPosition; + var subChildRotation = m_SubChildObject.transform.rotation; + subChildRotation.eulerAngles = m_SubChildObjectLocalRotation; + m_SubChildObject.transform.rotation = childRotation; + m_SubChildObject.transform.localScale = m_SubChildObjectLocalScale; + + if (m_EnableVerboseDebug) + { + m_ServerNetworkManager.LogLevel = LogLevel.Developer; + foreach (var clientNetworkManager in m_ClientNetworkManagers) + { + clientNetworkManager.LogLevel = LogLevel.Developer; + } + } + + m_ServerNetworkManager.NetworkConfig.TickRate = GetTickRate(); + foreach (var clientNetworkManager in m_ClientNetworkManagers) + { + clientNetworkManager.NetworkConfig.TickRate = GetTickRate(); + } + } + + + protected virtual void OnClientsAndServerConnectedSetup() + { + // Get the client player representation on both the server and the client side + var serverSideClientPlayer = m_PlayerNetworkObjects[0][m_ClientNetworkManagers[0].LocalClientId]; + var clientSideClientPlayer = m_PlayerNetworkObjects[m_ClientNetworkManagers[0].LocalClientId][m_ClientNetworkManagers[0].LocalClientId]; + + m_AuthoritativePlayer = m_Authority == Authority.ServerAuthority ? serverSideClientPlayer : clientSideClientPlayer; + m_NonAuthoritativePlayer = m_Authority == Authority.ServerAuthority ? clientSideClientPlayer : serverSideClientPlayer; + + // Get the NetworkTransformTestComponent to make sure the client side is ready before starting test + m_AuthoritativeTransform = m_AuthoritativePlayer.GetComponent(); + m_NonAuthoritativeTransform = m_NonAuthoritativePlayer.GetComponent(); + + // Setup whether we are or are not using unreliable deltas + m_AuthoritativeTransform.UseUnreliableDeltas = UseUnreliableDeltas(); + m_NonAuthoritativeTransform.UseUnreliableDeltas = UseUnreliableDeltas(); + + m_AuthoritativeTransform.UseHalfFloatPrecision = m_Precision == Precision.Half; + m_AuthoritativeTransform.UseQuaternionSynchronization = m_Rotation == Rotation.Quaternion; + m_AuthoritativeTransform.UseQuaternionCompression = m_RotationCompression == RotationCompression.QuaternionCompress; + m_NonAuthoritativeTransform.UseHalfFloatPrecision = m_Precision == Precision.Half; + m_NonAuthoritativeTransform.UseQuaternionSynchronization = m_Rotation == Rotation.Quaternion; + m_NonAuthoritativeTransform.UseQuaternionCompression = m_RotationCompression == RotationCompression.QuaternionCompress; + + + m_OwnerTransform = m_AuthoritativeTransform.IsOwner ? m_AuthoritativeTransform : m_NonAuthoritativeTransform; + } + + protected override void OnTimeTravelServerAndClientsConnected() + { + OnClientsAndServerConnectedSetup(); + + // Wait for the client-side to notify it is finished initializing and spawning. + var success = WaitForConditionOrTimeOutWithTimeTravel(() => m_NonAuthoritativeTransform.ReadyToReceivePositionUpdate == true); + Assert.True(success, "Timed out waiting for client-side to notify it is ready!"); + + Assert.True(m_AuthoritativeTransform.CanCommitToTransform); + Assert.False(m_NonAuthoritativeTransform.CanCommitToTransform); + // Just wait for at least one tick for NetworkTransforms to finish synchronization + TimeTravelAdvanceTick(); + } + + /// + /// Handles the OnServerAndClientsConnected for coroutine based derived tests + /// + protected override IEnumerator OnServerAndClientsConnected() + { + // Wait for the client-side to notify it is finished initializing and spawning. + yield return WaitForClientsConnectedOrTimeOut(); + AssertOnTimeout("Timed out waiting for client-side to notify it is ready!"); + OnClientsAndServerConnectedSetup(); + yield return base.OnServerAndClientsConnected(); + } + + /// + /// Handles setting a new client being connected + /// + protected override void OnNewClientCreated(NetworkManager networkManager) + { + networkManager.NetworkConfig.Prefabs = m_ServerNetworkManager.NetworkConfig.Prefabs; + networkManager.NetworkConfig.TickRate = GetTickRate(); + if (m_EnableVerboseDebug) + { + networkManager.LogLevel = LogLevel.Developer; + } + base.OnNewClientCreated(networkManager); + } + + + /// + /// Returns true when the server-host and all clients have + /// instantiated the child object to be used in + /// + /// + protected bool AllChildObjectInstancesAreSpawned() + { + if (ChildObjectComponent.AuthorityInstance == null) + { + return false; + } + + if (ChildObjectComponent.HasSubChild && ChildObjectComponent.AuthoritySubInstance == null) + { + return false; + } + + foreach (var clientNetworkManager in m_ClientNetworkManagers) + { + if (!ChildObjectComponent.ClientInstances.ContainsKey(clientNetworkManager.LocalClientId)) + { + return false; + } + } + return true; + } + + protected bool AllChildObjectInstancesHaveChild() + { + foreach (var instance in ChildObjectComponent.ClientInstances.Values) + { + if (instance.transform.parent == null) + { + return false; + } + } + if (ChildObjectComponent.HasSubChild) + { + foreach (var instance in ChildObjectComponent.ClientSubChildInstances.Values) + { + if (instance.transform.parent == null) + { + return false; + } + } + } + return true; + } + + /// + /// A wait condition specific method that assures the local space coordinates + /// are not impacted by NetworkTransform when parented. + /// + protected bool AllInstancesKeptLocalTransformValues(bool useSubChild) + { + var authorityObjectLocalPosition = useSubChild ? m_AuthoritySubChildObject.transform.localPosition : m_AuthorityChildObject.transform.localPosition; + var authorityObjectLocalRotation = useSubChild ? m_AuthoritySubChildObject.transform.localRotation.eulerAngles : m_AuthorityChildObject.transform.localRotation.eulerAngles; + var authorityObjectLocalScale = useSubChild ? m_AuthoritySubChildObject.transform.localScale : m_AuthorityChildObject.transform.localScale; + var instances = useSubChild ? ChildObjectComponent.SubInstances : ChildObjectComponent.Instances; + foreach (var childInstance in instances) + { + var childLocalPosition = childInstance.transform.localPosition; + var childLocalRotation = childInstance.transform.localRotation.eulerAngles; + var childLocalScale = childInstance.transform.localScale; + // Adjust approximation based on precision + if (m_Precision == Precision.Half) + { + m_CurrentHalfPrecision = k_HalfPrecisionPosScale; + } + if (!Approximately(childLocalPosition, authorityObjectLocalPosition)) + { + return false; + } + if (!Approximately(childLocalScale, authorityObjectLocalScale)) + { + return false; + } + // Adjust approximation based on precision + if (m_Precision == Precision.Half || m_RotationCompression == RotationCompression.QuaternionCompress) + { + m_CurrentHalfPrecision = k_HalfPrecisionRot; + } + if (!ApproximatelyEuler(childLocalRotation, authorityObjectLocalRotation)) + { + return false; + } + } + return true; + } + + protected bool PostAllChildrenLocalTransformValuesMatch(bool useSubChild) + { + var success = !s_GlobalTimeoutHelper.TimedOut; + var authorityObjectLocalPosition = useSubChild ? m_AuthoritySubChildObject.transform.localPosition : m_AuthorityChildObject.transform.localPosition; + var authorityObjectLocalRotation = useSubChild ? m_AuthoritySubChildObject.transform.localRotation.eulerAngles : m_AuthorityChildObject.transform.localRotation.eulerAngles; + var authorityObjectLocalScale = useSubChild ? m_AuthoritySubChildObject.transform.localScale : m_AuthorityChildObject.transform.localScale; + + if (s_GlobalTimeoutHelper.TimedOut) + { + // If we timed out, then wait for a full range of ticks (plus 1) to assure it sent synchronization data. + for (int j = 0; j < m_ServerNetworkManager.NetworkConfig.TickRate; j++) + { + var instances = useSubChild ? ChildObjectComponent.SubInstances : ChildObjectComponent.Instances; + foreach (var childInstance in instances) + { + var childParentName = "invalid"; + try + { + childParentName = useSubChild ? childInstance.transform.parent.parent.name : childInstance.transform.name; + } + catch (System.Exception ex) + { + Debug.Log(ex.Message); + } + var childLocalPosition = childInstance.transform.localPosition; + var childLocalRotation = childInstance.transform.localRotation.eulerAngles; + var childLocalScale = childInstance.transform.localScale; + // Adjust approximation based on precision + if (m_Precision == Precision.Half || m_RotationCompression == RotationCompression.QuaternionCompress) + { + m_CurrentHalfPrecision = k_HalfPrecisionPosScale; + } + if (!Approximately(childLocalPosition, authorityObjectLocalPosition)) + { + m_InfoMessage.AppendLine($"[{childParentName}][{childInstance.name}] Child's Local Position ({childLocalPosition}) | Authority Local Position ({authorityObjectLocalPosition})"); + success = false; + } + if (!Approximately(childLocalScale, authorityObjectLocalScale)) + { + m_InfoMessage.AppendLine($"[{childParentName}][{childInstance.name}] Child's Local Scale ({childLocalScale}) | Authority Local Scale ({authorityObjectLocalScale})"); + success = false; + } + + // Adjust approximation based on precision + if (m_Precision == Precision.Half || m_RotationCompression == RotationCompression.QuaternionCompress) + { + m_CurrentHalfPrecision = k_HalfPrecisionRot; + } + if (!ApproximatelyEuler(childLocalRotation, authorityObjectLocalRotation)) + { + m_InfoMessage.AppendLine($"[{childParentName}][{childInstance.name}] Child's Local Rotation ({childLocalRotation}) | Authority Local Rotation ({authorityObjectLocalRotation})"); + success = false; + } + } + } + } + return success; + } + + + + /// + /// Validates that moving, rotating, and scaling the authority side with a single + /// tick will properly synchronize the non-authoritative side with the same values. + /// + protected void MoveRotateAndScaleAuthority(Vector3 position, Vector3 rotation, Vector3 scale, OverrideState overrideState) + { + switch (overrideState) + { + case OverrideState.SetState: + { + var authoritativeRotation = m_AuthoritativeTransform.GetSpaceRelativeRotation(); + authoritativeRotation.eulerAngles = rotation; + if (m_Authority == Authority.OwnerAuthority) + { + // Under the scenario where the owner is not the server, and non-auth is the server we set the state from the server + // to be updated to the owner. + if (m_AuthoritativeTransform.IsOwner && !m_AuthoritativeTransform.IsServer && m_NonAuthoritativeTransform.IsServer) + { + m_NonAuthoritativeTransform.SetState(position, authoritativeRotation, scale); + } + else + { + m_AuthoritativeTransform.SetState(position, authoritativeRotation, scale); + } + } + else + { + m_AuthoritativeTransform.SetState(position, authoritativeRotation, scale); + } + + break; + } + case OverrideState.Update: + default: + { + m_AuthoritativeTransform.transform.position = position; + + var authoritativeRotation = m_AuthoritativeTransform.GetSpaceRelativeRotation(); + authoritativeRotation.eulerAngles = rotation; + m_AuthoritativeTransform.transform.rotation = authoritativeRotation; + m_AuthoritativeTransform.transform.localScale = scale; + break; + } + } + } + + /// + /// Randomly determine if an axis should be excluded. + /// If so, then randomly pick one of the axis to be excluded. + /// + protected Vector3 RandomlyExcludeAxis(Vector3 delta) + { + if (Random.Range(0.0f, 1.0f) >= 0.5f) + { + m_AxisExcluded = true; + var axisToIgnore = Random.Range(0, 2); + switch (axisToIgnore) + { + case 0: + { + delta.x = 0; + break; + } + case 1: + { + delta.y = 0; + break; + } + case 2: + { + delta.z = 0; + break; + } + } + } + return delta; + } + + protected bool PositionRotationScaleMatches() + { + return RotationsMatch() && PositionsMatch() && ScaleValuesMatch(); + } + + protected bool PositionRotationScaleMatches(Vector3 position, Vector3 eulerRotation, Vector3 scale) + { + return PositionsMatchesValue(position) && RotationMatchesValue(eulerRotation) && ScaleMatchesValue(scale); + } + + protected bool PositionsMatchesValue(Vector3 positionToMatch) + { + var authorityPosition = m_AuthoritativeTransform.transform.position; + var nonAuthorityPosition = m_NonAuthoritativeTransform.transform.position; + var auhtorityIsEqual = Approximately(authorityPosition, positionToMatch); + var nonauthorityIsEqual = Approximately(nonAuthorityPosition, positionToMatch); + + if (!auhtorityIsEqual) + { + VerboseDebug($"Authority position {authorityPosition} != position to match: {positionToMatch}!"); + } + if (!nonauthorityIsEqual) + { + VerboseDebug($"NonAuthority position {nonAuthorityPosition} != position to match: {positionToMatch}!"); + } + return auhtorityIsEqual && nonauthorityIsEqual; + } + + protected bool RotationMatchesValue(Vector3 rotationEulerToMatch) + { + var authorityRotationEuler = m_AuthoritativeTransform.transform.rotation.eulerAngles; + var nonAuthorityRotationEuler = m_NonAuthoritativeTransform.transform.rotation.eulerAngles; + var auhtorityIsEqual = Approximately(authorityRotationEuler, rotationEulerToMatch); + var nonauthorityIsEqual = Approximately(nonAuthorityRotationEuler, rotationEulerToMatch); + + if (!auhtorityIsEqual) + { + VerboseDebug($"Authority rotation {authorityRotationEuler} != rotation to match: {rotationEulerToMatch}!"); + } + if (!nonauthorityIsEqual) + { + VerboseDebug($"NonAuthority rotation {nonAuthorityRotationEuler} != rotation to match: {rotationEulerToMatch}!"); + } + return auhtorityIsEqual && nonauthorityIsEqual; + } + + protected bool ScaleMatchesValue(Vector3 scaleToMatch) + { + var authorityScale = m_AuthoritativeTransform.transform.localScale; + var nonAuthorityScale = m_NonAuthoritativeTransform.transform.localScale; + var auhtorityIsEqual = Approximately(authorityScale, scaleToMatch); + var nonauthorityIsEqual = Approximately(nonAuthorityScale, scaleToMatch); + + if (!auhtorityIsEqual) + { + VerboseDebug($"Authority scale {authorityScale} != scale to match: {scaleToMatch}!"); + } + if (!nonauthorityIsEqual) + { + VerboseDebug($"NonAuthority scale {nonAuthorityScale} != scale to match: {scaleToMatch}!"); + } + return auhtorityIsEqual && nonauthorityIsEqual; + } + + protected bool TeleportPositionMatches(Vector3 nonAuthorityOriginalPosition) + { + var nonAuthorityPosition = m_NonAuthoritativeTransform.transform.position; + var authorityPosition = m_AuthoritativeTransform.transform.position; + var targetDistance = Mathf.Abs(Vector3.Distance(nonAuthorityOriginalPosition, authorityPosition)); + var nonAuthorityCurrentDistance = Mathf.Abs(Vector3.Distance(nonAuthorityPosition, nonAuthorityOriginalPosition)); + // If we are not within our target distance range + if (!Approximately(targetDistance, nonAuthorityCurrentDistance)) + { + // Apply the non-authority's distance that is checked at the end of the teleport test + m_DetectedPotentialInterpolatedTeleport = nonAuthorityCurrentDistance; + return false; + } + else + { + // Otherwise, if we are within our target distance range then reset any already set value + m_DetectedPotentialInterpolatedTeleport = 0.0f; + } + var xIsEqual = Approximately(authorityPosition.x, nonAuthorityPosition.x); + var yIsEqual = Approximately(authorityPosition.y, nonAuthorityPosition.y); + var zIsEqual = Approximately(authorityPosition.z, nonAuthorityPosition.z); + if (!xIsEqual || !yIsEqual || !zIsEqual) + { + VerboseDebug($"[{m_AuthoritativeTransform.gameObject.name}] Authority position {authorityPosition} != [{m_NonAuthoritativeTransform.gameObject.name}] NonAuthority position {nonAuthorityPosition}"); + } + return xIsEqual && yIsEqual && zIsEqual; + } + + protected bool RotationsMatch(bool printDeltas = false) + { + m_CurrentHalfPrecision = k_HalfPrecisionRot; + var authorityEulerRotation = m_AuthoritativeTransform.GetSpaceRelativeRotation().eulerAngles; + var nonAuthorityEulerRotation = m_NonAuthoritativeTransform.GetSpaceRelativeRotation().eulerAngles; + var xIsEqual = ApproximatelyEuler(authorityEulerRotation.x, nonAuthorityEulerRotation.x) || !m_AuthoritativeTransform.SyncRotAngleX; + var yIsEqual = ApproximatelyEuler(authorityEulerRotation.y, nonAuthorityEulerRotation.y) || !m_AuthoritativeTransform.SyncRotAngleY; + var zIsEqual = ApproximatelyEuler(authorityEulerRotation.z, nonAuthorityEulerRotation.z) || !m_AuthoritativeTransform.SyncRotAngleZ; + if (!xIsEqual || !yIsEqual || !zIsEqual) + { + VerboseDebug($"[{m_AuthoritativeTransform.gameObject.name}][X-{xIsEqual} | Y-{yIsEqual} | Z-{zIsEqual}][{m_CurrentAxis}]" + + $"[Sync: X-{m_AuthoritativeTransform.SyncRotAngleX} | Y-{m_AuthoritativeTransform.SyncRotAngleY} | Z-{m_AuthoritativeTransform.SyncRotAngleZ}] Authority rotation {authorityEulerRotation} != [{m_NonAuthoritativeTransform.gameObject.name}] NonAuthority rotation {nonAuthorityEulerRotation}"); + } + if (printDeltas) + { + Debug.Log($"[Rotation Match] Euler Delta {EulerDelta(authorityEulerRotation, nonAuthorityEulerRotation)}"); + } + return xIsEqual && yIsEqual && zIsEqual; + } + + protected bool PositionsMatch(bool printDeltas = false) + { + m_CurrentHalfPrecision = k_HalfPrecisionPosScale; + var authorityPosition = m_AuthoritativeTransform.GetSpaceRelativePosition(); + var nonAuthorityPosition = m_NonAuthoritativeTransform.GetSpaceRelativePosition(); + var xIsEqual = Approximately(authorityPosition.x, nonAuthorityPosition.x) || !m_AuthoritativeTransform.SyncPositionX; + var yIsEqual = Approximately(authorityPosition.y, nonAuthorityPosition.y) || !m_AuthoritativeTransform.SyncPositionY; + var zIsEqual = Approximately(authorityPosition.z, nonAuthorityPosition.z) || !m_AuthoritativeTransform.SyncPositionZ; + if (!xIsEqual || !yIsEqual || !zIsEqual) + { + VerboseDebug($"[{m_AuthoritativeTransform.gameObject.name}] Authority position {authorityPosition} != [{m_NonAuthoritativeTransform.gameObject.name}] NonAuthority position {nonAuthorityPosition}"); + } + return xIsEqual && yIsEqual && zIsEqual; + } + + protected bool ScaleValuesMatch(bool printDeltas = false) + { + m_CurrentHalfPrecision = k_HalfPrecisionPosScale; + var authorityScale = m_AuthoritativeTransform.transform.localScale; + var nonAuthorityScale = m_NonAuthoritativeTransform.transform.localScale; + var xIsEqual = Approximately(authorityScale.x, nonAuthorityScale.x) || !m_AuthoritativeTransform.SyncScaleX; + var yIsEqual = Approximately(authorityScale.y, nonAuthorityScale.y) || !m_AuthoritativeTransform.SyncScaleY; + var zIsEqual = Approximately(authorityScale.z, nonAuthorityScale.z) || !m_AuthoritativeTransform.SyncScaleZ; + if (!xIsEqual || !yIsEqual || !zIsEqual) + { + VerboseDebug($"[{m_AuthoritativeTransform.gameObject.name}] Authority scale {authorityScale} != [{m_NonAuthoritativeTransform.gameObject.name}] NonAuthority scale {nonAuthorityScale}"); + } + return xIsEqual && yIsEqual && zIsEqual; + } + + private void PrintPositionRotationScaleDeltas() + { + RotationsMatch(true); + PositionsMatch(true); + ScaleValuesMatch(true); + } + } + + /// + /// Helper component for all NetworkTransformTests + /// + public class NetworkTransformTestComponent : NetworkTransform + { + public bool ServerAuthority; + public bool ReadyToReceivePositionUpdate = false; + + public NetworkTransformState AuthorityLastSentState; + public bool StatePushed { get; internal set; } + + public delegate void AuthorityPushedTransformStateDelegateHandler(ref NetworkTransformState networkTransformState); + + public event AuthorityPushedTransformStateDelegateHandler AuthorityPushedTransformState; + + protected override void OnAuthorityPushTransformState(ref NetworkTransformState networkTransformState) + { + StatePushed = true; + AuthorityLastSentState = networkTransformState; + AuthorityPushedTransformState?.Invoke(ref networkTransformState); + base.OnAuthorityPushTransformState(ref networkTransformState); + } + + + public bool StateUpdated { get; internal set; } + protected override void OnNetworkTransformStateUpdated(ref NetworkTransformState oldState, ref NetworkTransformState newState) + { + StateUpdated = true; + base.OnNetworkTransformStateUpdated(ref oldState, ref newState); + } + + protected override bool OnIsServerAuthoritative() + { + return ServerAuthority; + } + + public static NetworkTransformTestComponent AuthorityInstance; + + public override void OnNetworkSpawn() + { + base.OnNetworkSpawn(); + + if (CanCommitToTransform) + { + AuthorityInstance = this; + } + + ReadyToReceivePositionUpdate = true; + } + + public void CommitToTransform() + { + TryCommitTransformToServer(transform, NetworkManager.LocalTime.Time); + } + + public (bool isDirty, bool isPositionDirty, bool isRotationDirty, bool isScaleDirty) ApplyState() + { + var transformState = ApplyLocalNetworkState(transform); + return (transformState.IsDirty, transformState.HasPositionChange, transformState.HasRotAngleChange, transformState.HasScaleChange); + } + } + + /// + /// Helper component for NetworkTransform parenting tests when + /// a child is a parent of another child (i.e. "sub child") + /// + public class SubChildObjectComponent : ChildObjectComponent + { + protected override bool IsSubChild() + { + return true; + } + } + + /// + /// Helper component for NetworkTransform parenting tests + /// + public class ChildObjectComponent : NetworkTransform + { + public static int TestCount; + public static bool EnableChildLog; + public static readonly List Instances = new List(); + public static readonly List SubInstances = new List(); + public static ChildObjectComponent AuthorityInstance { get; internal set; } + public static ChildObjectComponent AuthoritySubInstance { get; internal set; } + public static readonly Dictionary ClientInstances = new Dictionary(); + public static readonly Dictionary ClientSubChildInstances = new Dictionary(); + + public static readonly List InstancesWithLogging = new List(); + + public static bool HasSubChild; + + private StringBuilder m_ChildTransformLog = new StringBuilder(); + private StringBuilder m_ChildStateLog = new StringBuilder(); + + public static void Reset() + { + AuthorityInstance = null; + AuthoritySubInstance = null; + HasSubChild = false; + ClientInstances.Clear(); + ClientSubChildInstances.Clear(); + Instances.Clear(); + SubInstances.Clear(); + } + + public bool ServerAuthority; + + protected virtual bool IsSubChild() + { + return false; + } + + protected override bool OnIsServerAuthoritative() + { + return ServerAuthority; + } + + public override void OnNetworkSpawn() + { + LogTransform(); + + base.OnNetworkSpawn(); + + LogTransform(); + if (CanCommitToTransform) + { + if (!IsSubChild()) + { + AuthorityInstance = this; + } + else + { + AuthoritySubInstance = this; + } + } + else + { + if (!IsSubChild()) + { + Instances.Add(this); + } + else + { + SubInstances.Add(this); + } + } + if (HasSubChild && IsSubChild()) + { + ClientSubChildInstances.Add(NetworkManager.LocalClientId, NetworkObject); + } + else + { + ClientInstances.Add(NetworkManager.LocalClientId, NetworkObject); + } + } + + public override void OnNetworkDespawn() + { + LogToConsole(); + base.OnNetworkDespawn(); + } + + public override void OnNetworkObjectParentChanged(NetworkObject parentNetworkObject) + { + base.OnNetworkObjectParentChanged(parentNetworkObject); + + LogTransform(); + } + + protected override void OnAuthorityPushTransformState(ref NetworkTransformState networkTransformState) + { + base.OnAuthorityPushTransformState(ref networkTransformState); + + LogState(ref networkTransformState, true); + } + + protected override void OnNetworkTransformStateUpdated(ref NetworkTransformState oldState, ref NetworkTransformState newState) + { + base.OnNetworkTransformStateUpdated(ref oldState, ref newState); + LogState(ref newState, false); + } + + protected override void OnSynchronize(ref BufferSerializer serializer) + { + base.OnSynchronize(ref serializer); + var localState = SynchronizeState; + LogState(ref localState, serializer.IsWriter); + } + + private void LogTransform() + { + if (!EnableChildLog) + { + return; + } + if (m_ChildTransformLog.Length == 0) + { + m_ChildTransformLog.AppendLine($"[{TestCount}][{name}] Begin Child Transform Log (Authority: {CanCommitToTransform})-------------->"); + } + m_ChildTransformLog.AppendLine($"POS-SR:{GetSpaceRelativePosition()} POS-W: {transform.position} POS-L: {transform.position}"); + m_ChildTransformLog.AppendLine($"SCA-SR:{GetScale()} SCA-LS: {transform.lossyScale} SCA-L: {transform.localScale}"); + } + + private void LogState(ref NetworkTransformState state, bool isPush) + { + if (!EnableChildLog) + { + return; + } + if (m_ChildStateLog.Length == 0) + { + m_ChildStateLog.AppendLine($"[{TestCount}][{name}] Begin Child State Log (Authority: {CanCommitToTransform})-------------->"); + } + var tick = 0; + if (NetworkManager != null && !NetworkManager.ShutdownInProgress) + { + tick = NetworkManager.ServerTime.Tick; + } + + m_ChildStateLog.AppendLine($"[{state.NetworkTick}][{tick}] Tele:{state.IsTeleportingNextFrame} Sync: {state.IsSynchronizing} Reliable: {state.IsReliableStateUpdate()} IsParented: {state.IsParented} HasPos: {state.HasPositionChange} Pos: {state.GetPosition()}"); + m_ChildStateLog.AppendLine($"Lossy:{state.LossyScale} Scale: {state.GetScale()} Rotation: {state.GetRotation()}"); + } + + private void LogToConsole() + { + if (!EnableChildLog) + { + return; + } + LogBuilder(m_ChildTransformLog); + LogBuilder(m_ChildStateLog); + } + + private void LogBuilder(StringBuilder builder) + { + if (builder.Length == 0) + { + return; + } + var contents = builder.ToString(); + var lines = contents.Split('\n'); + if (lines.Length > 45) + { + var count = 0; + var tempBuilder = new StringBuilder(); + + for (int i = 0; i < lines.Length; i++) + { + if ((i % 45) == 0) + { + if (count > 0) + { + Debug.Log(tempBuilder.ToString()); + tempBuilder.Clear(); + } + tempBuilder.AppendLine($"{count}{lines[i]}"); + count++; + } + else + { + tempBuilder.AppendLine($"{lines[i]}"); + } + } + } + else + { + Debug.Log(builder.ToString()); + } + } + } + + +} // Unity.Netcode.RuntimeTests diff --git a/Tests/Runtime/NetworkTransform/NetworkTransformBase.cs.meta b/Tests/Runtime/NetworkTransform/NetworkTransformBase.cs.meta new file mode 100644 index 0000000..2ffb799 --- /dev/null +++ b/Tests/Runtime/NetworkTransform/NetworkTransformBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ccdf46d0c73f8ac47921ec1be2772fac +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/NetworkTransform/NetworkTransformGeneral.cs b/Tests/Runtime/NetworkTransform/NetworkTransformGeneral.cs new file mode 100644 index 0000000..17475b1 --- /dev/null +++ b/Tests/Runtime/NetworkTransform/NetworkTransformGeneral.cs @@ -0,0 +1,314 @@ +using NUnit.Framework; +using Unity.Netcode.Components; +using UnityEngine; + +namespace Unity.Netcode.RuntimeTests +{ + [TestFixture(HostOrServer.Host, Authority.OwnerAuthority)] + [TestFixture(HostOrServer.Host, Authority.ServerAuthority)] + public class NetworkTransformGeneral : NetworkTransformBase + { + public NetworkTransformGeneral(HostOrServer testWithHost, Authority authority) : + base(testWithHost, authority, RotationCompression.None, Rotation.Euler, Precision.Full) + { } + + protected override bool m_EnableTimeTravel => true; + protected override bool m_SetupIsACoroutine => false; + protected override bool m_TearDownIsACoroutine => false; + + /// + /// Test to verify nonAuthority cannot change the transform directly + /// + [Test] + public void VerifyNonAuthorityCantChangeTransform([Values] Interpolation interpolation) + { + m_AuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; + m_NonAuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; + + Assert.AreEqual(Vector3.zero, m_NonAuthoritativeTransform.transform.position, "other side pos should be zero at first"); // sanity check + + m_NonAuthoritativeTransform.transform.position = new Vector3(4, 5, 6); + + TimeTravelAdvanceTick(); + TimeTravelAdvanceTick(); + + Assert.AreEqual(Vector3.zero, m_NonAuthoritativeTransform.transform.position, "[Position] NonAuthority was able to change the position!"); + + var nonAuthorityRotation = m_NonAuthoritativeTransform.transform.rotation; + var originalNonAuthorityEulerRotation = nonAuthorityRotation.eulerAngles; + var nonAuthorityEulerRotation = originalNonAuthorityEulerRotation; + // Verify rotation is not marked dirty when rotated by half of the threshold + nonAuthorityEulerRotation.y += 20.0f; + nonAuthorityRotation.eulerAngles = nonAuthorityEulerRotation; + m_NonAuthoritativeTransform.transform.rotation = nonAuthorityRotation; + TimeTravelAdvanceTick(); + var nonAuthorityCurrentEuler = m_NonAuthoritativeTransform.transform.rotation.eulerAngles; + Assert.True(originalNonAuthorityEulerRotation.Equals(nonAuthorityCurrentEuler), "[Rotation] NonAuthority was able to change the rotation!"); + + var nonAuthorityScale = m_NonAuthoritativeTransform.transform.localScale; + m_NonAuthoritativeTransform.transform.localScale = nonAuthorityScale * 100; + + TimeTravelAdvanceTick(); + + Assert.True(nonAuthorityScale.Equals(m_NonAuthoritativeTransform.transform.localScale), "[Scale] NonAuthority was able to change the scale!"); + } + + /// + /// Validates that rotation checks don't produce false positive + /// results when rolling over between 0 and 360 degrees + /// + [Test] + public void TestRotationThresholdDeltaCheck([Values] Interpolation interpolation) + { + m_AuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; + m_NonAuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; + m_NonAuthoritativeTransform.RotAngleThreshold = m_AuthoritativeTransform.RotAngleThreshold = 5.0f; + var halfThreshold = m_AuthoritativeTransform.RotAngleThreshold * 0.5001f; + + // Apply the current state prior to getting reference rotations which assures we have + // applied the most current rotation deltas and that all bitset flags are updated + var results = m_AuthoritativeTransform.ApplyState(); + TimeTravelAdvanceTick(); + + // Get the current rotation values; + var authorityRotation = m_AuthoritativeTransform.transform.rotation; + var authorityEulerRotation = authorityRotation.eulerAngles; + + // Verify rotation is not marked dirty when rotated by half of the threshold + authorityEulerRotation.y += halfThreshold; + authorityRotation.eulerAngles = authorityEulerRotation; + m_AuthoritativeTransform.transform.rotation = authorityRotation; + results = m_AuthoritativeTransform.ApplyState(); + Assert.IsFalse(results.isRotationDirty, $"Rotation is dirty when rotation threshold is {m_AuthoritativeTransform.RotAngleThreshold} degrees and only adjusted by {halfThreshold} degrees!"); + // Now allow the delta state to be processed and sent (just allow for two ticks to cover edge cases with time travel and the testing environment) + TimeTravelAdvanceTick(); + TimeTravelAdvanceTick(); + + // Verify rotation is marked dirty when rotated by another half threshold value + authorityEulerRotation.y += halfThreshold; + authorityRotation.eulerAngles = authorityEulerRotation; + m_AuthoritativeTransform.transform.rotation = authorityRotation; + results = m_AuthoritativeTransform.ApplyState(); + Assert.IsTrue(results.isRotationDirty, $"Rotation was not dirty when rotated by the threshold value: {m_AuthoritativeTransform.RotAngleThreshold} degrees!"); + // Now allow the delta state to be processed and sent (just allow for two ticks to cover edge cases with time travel and the testing environment) + TimeTravelAdvanceTick(); + TimeTravelAdvanceTick(); + + //Reset rotation back to zero on all axis + authorityRotation.eulerAngles = authorityEulerRotation = Vector3.zero; + m_AuthoritativeTransform.transform.rotation = authorityRotation; + // Now allow the delta state to be processed and sent (just allow for two ticks to cover edge cases with time travel and the testing environment) + TimeTravelAdvanceTick(); + TimeTravelAdvanceTick(); + + // Rotate by 360 minus halfThreshold (which is really just negative halfThreshold) and verify rotation is not marked dirty + authorityEulerRotation.y = 360 - halfThreshold; + authorityRotation.eulerAngles = authorityEulerRotation; + m_AuthoritativeTransform.transform.rotation = authorityRotation; + results = m_AuthoritativeTransform.ApplyState(); + Assert.IsFalse(results.isRotationDirty, $"Rotation is dirty when rotation threshold is {m_AuthoritativeTransform.RotAngleThreshold} degrees and only adjusted by " + + $"{Mathf.DeltaAngle(0, authorityEulerRotation.y)} degrees!"); + // Now allow the delta state to be processed and sent (just allow for two ticks to cover edge cases with time travel and the testing environment) + TimeTravelAdvanceTick(); + TimeTravelAdvanceTick(); + + // Now apply one more minor decrement that should trigger a dirty flag + authorityEulerRotation.y -= halfThreshold; + authorityRotation.eulerAngles = authorityEulerRotation; + m_AuthoritativeTransform.transform.rotation = authorityRotation; + results = m_AuthoritativeTransform.ApplyState(); + Assert.IsTrue(results.isRotationDirty, $"Rotation was not dirty when rotated by {Mathf.DeltaAngle(0, authorityEulerRotation.y)} degrees!"); + + //Reset rotation back to zero on all axis + authorityRotation.eulerAngles = authorityEulerRotation = Vector3.zero; + m_AuthoritativeTransform.transform.rotation = authorityRotation; + // Now allow the delta state to be processed and sent (just allow for two ticks to cover edge cases with time travel and the testing environment) + TimeTravelAdvanceTick(); + TimeTravelAdvanceTick(); + + // Minor decrement again under the threshold value + authorityEulerRotation.y -= halfThreshold; + authorityRotation.eulerAngles = authorityEulerRotation; + m_AuthoritativeTransform.transform.rotation = authorityRotation; + results = m_AuthoritativeTransform.ApplyState(); + Assert.IsFalse(results.isRotationDirty, $"Rotation is dirty when rotation threshold is {m_AuthoritativeTransform.RotAngleThreshold} degrees and only adjusted by " + + $"{Mathf.DeltaAngle(0, authorityEulerRotation.y)} degrees!"); + // Now allow the delta state to be processed and sent (just allow for two ticks to cover edge cases with time travel and the testing environment) + TimeTravelAdvanceTick(); + TimeTravelAdvanceTick(); + + // Now decrement another half threshold which should trigger the dirty flag + authorityEulerRotation.y -= halfThreshold; + authorityRotation.eulerAngles = authorityEulerRotation; + m_AuthoritativeTransform.transform.rotation = authorityRotation; + results = m_AuthoritativeTransform.ApplyState(); + Assert.IsTrue(results.isRotationDirty, $"Rotation was not dirty when rotated by {Mathf.DeltaAngle(0, authorityEulerRotation.y)} degrees!"); + } + + private bool ValidateBitSetValues() + { + var serverState = m_AuthoritativeTransform.AuthorityLastSentState; + var clientState = m_NonAuthoritativeTransform.LocalAuthoritativeNetworkState; + if (serverState.HasPositionX == clientState.HasPositionX && serverState.HasPositionY == clientState.HasPositionY && serverState.HasPositionZ == clientState.HasPositionZ && + serverState.HasRotAngleX == clientState.HasRotAngleX && serverState.HasRotAngleY == clientState.HasRotAngleY && serverState.HasRotAngleZ == clientState.HasRotAngleZ && + serverState.HasScaleX == clientState.HasScaleX && serverState.HasScaleY == clientState.HasScaleY && serverState.HasScaleZ == clientState.HasScaleZ) + { + return true; + } + return false; + } + + /// + /// Test to make sure that the bitset value is updated properly + /// + [Test] + public void TestBitsetValue([Values] Interpolation interpolation) + { + m_AuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; + m_NonAuthoritativeTransform.RotAngleThreshold = m_AuthoritativeTransform.RotAngleThreshold = 0.1f; + m_AuthoritativeTransform.transform.rotation = Quaternion.Euler(1, 2, 3); + TimeTravelAdvanceTick(); + var success = WaitForConditionOrTimeOutWithTimeTravel(ValidateBitSetValues); + Assert.True(success, $"Timed out waiting for Authoritative Bitset state to equal NonAuthoritative replicated Bitset state!"); + success = WaitForConditionOrTimeOutWithTimeTravel(() => RotationsMatch()); + Assert.True(success, $"[Timed-Out] Authoritative rotation {m_AuthoritativeTransform.transform.rotation.eulerAngles} != Non-Authoritative rotation {m_NonAuthoritativeTransform.transform.rotation.eulerAngles}"); + } + + /// + /// This validates that you can perform multiple explicit calls + /// within the same fractional tick period without the loss of the states applied. + /// + [Test] + public void TestMultipleExplicitSetStates([Values] Interpolation interpolation) + { + var interpolate = interpolation == Interpolation.EnableInterpolate; + m_AuthoritativeTransform.Interpolate = interpolate; + var updatedPosition = GetRandomVector3(-5.0f, 5.0f); + m_AuthoritativeTransform.SetState(updatedPosition, null, null, !interpolate); + // Advance to next frame + TimeTravel(0.001f, 1); + + updatedPosition += GetRandomVector3(-5.0f, 5.0f); + m_AuthoritativeTransform.SetState(updatedPosition, null, null, !interpolate); + TimeTravelAdvanceTick(); + + var success = WaitForConditionOrTimeOutWithTimeTravel(() => PositionsMatch()); + Assert.True(success, $"[Timed-Out] Authoritative position {m_AuthoritativeTransform.transform.position} != Non-Authoritative position {m_NonAuthoritativeTransform.transform.position}"); + Assert.True(Approximately(updatedPosition, m_NonAuthoritativeTransform.transform.position), $"NonAuthority position {m_NonAuthoritativeTransform.transform.position} does not equal the calculated position {updatedPosition}!"); + + var updatedRotation = m_AuthoritativeTransform.transform.rotation; + updatedRotation.eulerAngles += GetRandomVector3(-30.0f, 30.0f); + m_AuthoritativeTransform.SetState(null, updatedRotation, null, !interpolate); + // Advance to next frame + TimeTravel(0.001f, 1); + + updatedRotation.eulerAngles += GetRandomVector3(-30.0f, 30.0f); + m_AuthoritativeTransform.SetState(null, updatedRotation, null, !interpolate); + TimeTravelAdvanceTick(); + + success = WaitForConditionOrTimeOutWithTimeTravel(() => RotationsMatch()); + Assert.True(success, $"[Timed-Out] Authoritative rotation {m_AuthoritativeTransform.transform.rotation.eulerAngles} != Non-Authoritative rotation {m_NonAuthoritativeTransform.transform.rotation.eulerAngles}"); + Assert.True(Approximately(updatedRotation.eulerAngles, m_NonAuthoritativeTransform.transform.rotation.eulerAngles), $"NonAuthority rotation {m_NonAuthoritativeTransform.transform.rotation.eulerAngles} does not equal the calculated rotation {updatedRotation.eulerAngles}!"); + + var updatedScale = m_AuthoritativeTransform.transform.localScale; + updatedScale += GetRandomVector3(-2.0f, 2.0f); + m_AuthoritativeTransform.SetState(null, null, updatedScale, !interpolate); + // Advance to next frame + TimeTravel(0.001f, 1); + + updatedScale += GetRandomVector3(-2.0f, 2.0f); + m_AuthoritativeTransform.SetState(null, null, updatedScale, !interpolate); + TimeTravelAdvanceTick(); + + success = WaitForConditionOrTimeOutWithTimeTravel(() => ScaleValuesMatch()); + Assert.True(success, $"[Timed-Out] Authoritative rotation {m_AuthoritativeTransform.transform.localScale} != Non-Authoritative rotation {m_NonAuthoritativeTransform.transform.localScale}"); + Assert.True(Approximately(updatedScale, m_NonAuthoritativeTransform.transform.localScale), $"NonAuthority scale {m_NonAuthoritativeTransform.transform.localScale} does not equal the calculated scale {updatedScale}!"); + + // Now test explicitly setting all axis of transform multiple times during a fractional tick period + updatedPosition += GetRandomVector3(-5.0f, 5.0f); + updatedRotation.eulerAngles += GetRandomVector3(-30.0f, 30.0f); + updatedScale += GetRandomVector3(-2.0f, 2.0f); + m_AuthoritativeTransform.SetState(updatedPosition, updatedRotation, updatedScale, !interpolate); + // Advance to next frame + TimeTravel(0.001f, 1); + + updatedPosition += GetRandomVector3(-5.0f, 5.0f); + updatedRotation.eulerAngles += GetRandomVector3(-30.0f, 30.0f); + updatedScale += GetRandomVector3(-2.0f, 2.0f); + m_AuthoritativeTransform.SetState(updatedPosition, updatedRotation, updatedScale, !interpolate); + // Advance to next frame + TimeTravel(0.001f, 1); + + updatedPosition += GetRandomVector3(-5.0f, 5.0f); + updatedRotation.eulerAngles += GetRandomVector3(-30.0f, 30.0f); + updatedScale += GetRandomVector3(-2.0f, 2.0f); + m_AuthoritativeTransform.SetState(updatedPosition, updatedRotation, updatedScale, !interpolate); + // Advance to next frame + TimeTravel(0.001f, 1); + + TimeTravelAdvanceTick(); + + success = WaitForConditionOrTimeOutWithTimeTravel(() => PositionsMatch() && RotationsMatch() && ScaleValuesMatch()); + Assert.True(success, $"[Timed-Out] Authoritative transform != Non-Authoritative transform!"); + Assert.True(Approximately(updatedPosition, m_NonAuthoritativeTransform.transform.position), $"NonAuthority position {m_NonAuthoritativeTransform.transform.position} does not equal the calculated position {updatedPosition}!"); + Assert.True(Approximately(updatedRotation.eulerAngles, m_NonAuthoritativeTransform.transform.rotation.eulerAngles), $"NonAuthority rotation {m_NonAuthoritativeTransform.transform.rotation.eulerAngles} does not equal the calculated rotation {updatedRotation.eulerAngles}!"); + Assert.True(Approximately(updatedScale, m_NonAuthoritativeTransform.transform.localScale), $"NonAuthority scale {m_NonAuthoritativeTransform.transform.localScale} does not equal the calculated scale {updatedScale}!"); + } + + /// + /// This test validates the method + /// usage for the non-authoritative side. It will either be the owner or the server making/requesting state changes. + /// This validates that: + /// - The owner authoritative mode can still be controlled by the server (i.e. owner authoritative with server authority override capabilities) + /// - The server authoritative mode can still be directed by the client owner. + /// + /// + /// This also tests that the original server authoritative model with client-owner driven NetworkTransforms is preserved. + /// + [Test] + public void NonAuthorityOwnerSettingStateTest([Values] Interpolation interpolation) + { + var interpolate = interpolation != Interpolation.EnableInterpolate; + m_AuthoritativeTransform.Interpolate = interpolate; + m_NonAuthoritativeTransform.Interpolate = interpolate; + m_NonAuthoritativeTransform.RotAngleThreshold = m_AuthoritativeTransform.RotAngleThreshold = 0.1f; + + // Test one parameter at a time first + var newPosition = new Vector3(125f, 35f, 65f); + var newRotation = Quaternion.Euler(1, 2, 3); + var newScale = new Vector3(2.0f, 2.0f, 2.0f); + m_NonAuthoritativeTransform.SetState(newPosition, null, null, interpolate); + var success = WaitForConditionOrTimeOutWithTimeTravel(() => PositionsMatchesValue(newPosition)); + Assert.True(success, $"Timed out waiting for non-authoritative position state request to be applied!"); + Assert.True(Approximately(newPosition, m_AuthoritativeTransform.transform.position), "Authoritative position does not match!"); + Assert.True(Approximately(newPosition, m_NonAuthoritativeTransform.transform.position), "Non-Authoritative position does not match!"); + + m_NonAuthoritativeTransform.SetState(null, newRotation, null, interpolate); + success = WaitForConditionOrTimeOutWithTimeTravel(() => RotationMatchesValue(newRotation.eulerAngles)); + Assert.True(success, $"Timed out waiting for non-authoritative rotation state request to be applied!"); + Assert.True(Approximately(newRotation.eulerAngles, m_AuthoritativeTransform.transform.rotation.eulerAngles), "Authoritative rotation does not match!"); + Assert.True(Approximately(newRotation.eulerAngles, m_NonAuthoritativeTransform.transform.rotation.eulerAngles), "Non-Authoritative rotation does not match!"); + + m_NonAuthoritativeTransform.SetState(null, null, newScale, interpolate); + success = WaitForConditionOrTimeOutWithTimeTravel(() => ScaleMatchesValue(newScale)); + Assert.True(success, $"Timed out waiting for non-authoritative scale state request to be applied!"); + Assert.True(Approximately(newScale, m_AuthoritativeTransform.transform.localScale), "Authoritative scale does not match!"); + Assert.True(Approximately(newScale, m_NonAuthoritativeTransform.transform.localScale), "Non-Authoritative scale does not match!"); + + // Test all parameters at once + newPosition = new Vector3(55f, 95f, -25f); + newRotation = Quaternion.Euler(20, 5, 322); + newScale = new Vector3(0.5f, 0.5f, 0.5f); + + m_NonAuthoritativeTransform.SetState(newPosition, newRotation, newScale, interpolate); + success = WaitForConditionOrTimeOutWithTimeTravel(() => PositionRotationScaleMatches(newPosition, newRotation.eulerAngles, newScale)); + Assert.True(success, $"Timed out waiting for non-authoritative position, rotation, and scale state request to be applied!"); + Assert.True(Approximately(newPosition, m_AuthoritativeTransform.transform.position), "Authoritative position does not match!"); + Assert.True(Approximately(newPosition, m_NonAuthoritativeTransform.transform.position), "Non-Authoritative position does not match!"); + Assert.True(Approximately(newRotation.eulerAngles, m_AuthoritativeTransform.transform.rotation.eulerAngles), "Authoritative rotation does not match!"); + Assert.True(Approximately(newRotation.eulerAngles, m_NonAuthoritativeTransform.transform.rotation.eulerAngles), "Non-Authoritative rotation does not match!"); + Assert.True(Approximately(newScale, m_AuthoritativeTransform.transform.localScale), "Authoritative scale does not match!"); + Assert.True(Approximately(newScale, m_NonAuthoritativeTransform.transform.localScale), "Non-Authoritative scale does not match!"); + } + } +} diff --git a/Tests/Runtime/NetworkTransform/NetworkTransformGeneral.cs.meta b/Tests/Runtime/NetworkTransform/NetworkTransformGeneral.cs.meta new file mode 100644 index 0000000..88f78f4 --- /dev/null +++ b/Tests/Runtime/NetworkTransform/NetworkTransformGeneral.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7a69eaaf3c4b8464a93520a3514bf5e8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/NetworkTransform/NetworkTransformPacketLossTests.cs b/Tests/Runtime/NetworkTransform/NetworkTransformPacketLossTests.cs new file mode 100644 index 0000000..739fa4f --- /dev/null +++ b/Tests/Runtime/NetworkTransform/NetworkTransformPacketLossTests.cs @@ -0,0 +1,485 @@ +// TODO: Rewrite test to use the tools package. Debug simulator not available in UTP 2.X. +#if !UTP_TRANSPORT_2_0_ABOVE +using System.Collections; +using NUnit.Framework; +using Unity.Netcode.Components; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests +{ + /// + /// Integration tests for NetworkTransform that will test both + /// server and host operating modes and will test both authoritative + /// models for each operating mode when packet loss and latency is + /// present. + /// + [TestFixture(HostOrServer.Host, Authority.ServerAuthority, RotationCompression.None, Rotation.Euler, Precision.Full)] + [TestFixture(HostOrServer.Host, Authority.ServerAuthority, RotationCompression.None, Rotation.Euler, Precision.Half)] + [TestFixture(HostOrServer.Host, Authority.ServerAuthority, RotationCompression.None, Rotation.Quaternion, Precision.Full)] + [TestFixture(HostOrServer.Host, Authority.ServerAuthority, RotationCompression.None, Rotation.Quaternion, Precision.Half)] + [TestFixture(HostOrServer.Host, Authority.ServerAuthority, RotationCompression.QuaternionCompress, Rotation.Quaternion, Precision.Full)] + [TestFixture(HostOrServer.Host, Authority.ServerAuthority, RotationCompression.QuaternionCompress, Rotation.Quaternion, Precision.Half)] + public class NetworkTransformPacketLossTests : NetworkTransformBase + { + private const int k_Latency = 50; + private const int k_PacketLoss = 2; + + private Vector3 m_RandomPosition; + private Vector3 m_TeleportOffset = new Vector3(-1024f, 0f, 0f); + private bool m_Teleported; + + /// + /// Constructor + /// + /// Determines if we are running as a server or host + /// Determines if we are using server or owner authority + public NetworkTransformPacketLossTests(HostOrServer testWithHost, Authority authority, RotationCompression rotationCompression, Rotation rotation, Precision precision) : + base(testWithHost, authority, rotationCompression, rotation, precision) + { } + + protected override void OnServerAndClientsCreated() + { + base.OnServerAndClientsCreated(); + + var unityTransport = m_ServerNetworkManager.NetworkConfig.NetworkTransport as Transports.UTP.UnityTransport; + unityTransport.SetDebugSimulatorParameters(k_Latency, 0, k_PacketLoss); + } + + /// + /// Handles validating all children of the test objects have matching local and global space vaues. + /// + private IEnumerator AllChildrenLocalTransformValuesMatch(bool useSubChild, ChildrenTransformCheckType checkType) + { + // We don't assert on timeout here because we want to log this information during PostAllChildrenLocalTransformValuesMatch + yield return WaitForConditionOrTimeOut(() => AllInstancesKeptLocalTransformValues(useSubChild)); + var success = true; + m_InfoMessage.AppendLine($"[{checkType}][{useSubChild}] Timed out waiting for all children to have the correct local space values:\n"); + if (s_GlobalTimeoutHelper.TimedOut) + { + var waitForMs = new WaitForSeconds(0.001f); + // If we timed out, then wait for a full range of ticks to assure all data has been synchronized before declaring this a failed test. + for (int j = 0; j < m_ServerNetworkManager.NetworkConfig.TickRate; j++) + { + var instances = useSubChild ? ChildObjectComponent.SubInstances : ChildObjectComponent.Instances; + success = PostAllChildrenLocalTransformValuesMatch(useSubChild); + yield return waitForMs; + } + } + + if (!success) + { + Assert.True(success, m_InfoMessage.ToString()); + } + } + + /// + /// Validates that transform values remain the same when a NetworkTransform is + /// parented under another NetworkTransform under all of the possible axial conditions + /// as well as when the parent has a varying scale. + /// + [UnityTest] + public IEnumerator ParentedNetworkTransformTest([Values] Interpolation interpolation, [Values] bool worldPositionStays, [Values(0.5f, 1.0f, 5.0f)] float scale) + { + ChildObjectComponent.EnableChildLog = m_EnableVerboseDebug; + if (m_EnableVerboseDebug) + { + ChildObjectComponent.TestCount++; + } + // Get the NetworkManager that will have authority in order to spawn with the correct authority + var isServerAuthority = m_Authority == Authority.ServerAuthority; + var authorityNetworkManager = m_ServerNetworkManager; + if (!isServerAuthority) + { + authorityNetworkManager = m_ClientNetworkManagers[0]; + } + + // Spawn a parent and children + ChildObjectComponent.HasSubChild = true; + var serverSideParent = SpawnObject(m_ParentObject.gameObject, authorityNetworkManager).GetComponent(); + var serverSideChild = SpawnObject(m_ChildObject.gameObject, authorityNetworkManager).GetComponent(); + var serverSideSubChild = SpawnObject(m_SubChildObject.gameObject, authorityNetworkManager).GetComponent(); + + // Assure all of the child object instances are spawned before proceeding to parenting + yield return WaitForConditionOrTimeOut(AllChildObjectInstancesAreSpawned); + AssertOnTimeout("Timed out waiting for all child instances to be spawned!"); + + // Get the authority parent and child instances + m_AuthorityParentObject = NetworkTransformTestComponent.AuthorityInstance.NetworkObject; + m_AuthorityChildObject = ChildObjectComponent.AuthorityInstance.NetworkObject; + m_AuthoritySubChildObject = ChildObjectComponent.AuthoritySubInstance.NetworkObject; + + // The child NetworkTransform will use world space when world position stays and + // local space when world position does not stay when parenting. + ChildObjectComponent.AuthorityInstance.InLocalSpace = !worldPositionStays; + ChildObjectComponent.AuthorityInstance.UseHalfFloatPrecision = m_Precision == Precision.Half; + ChildObjectComponent.AuthorityInstance.UseQuaternionSynchronization = m_Rotation == Rotation.Quaternion; + ChildObjectComponent.AuthorityInstance.UseQuaternionCompression = m_RotationCompression == RotationCompression.QuaternionCompress; + + ChildObjectComponent.AuthoritySubInstance.InLocalSpace = !worldPositionStays; + ChildObjectComponent.AuthoritySubInstance.UseHalfFloatPrecision = m_Precision == Precision.Half; + ChildObjectComponent.AuthoritySubInstance.UseQuaternionSynchronization = m_Rotation == Rotation.Quaternion; + ChildObjectComponent.AuthoritySubInstance.UseQuaternionCompression = m_RotationCompression == RotationCompression.QuaternionCompress; + + // Set whether we are interpolating or not + m_AuthorityParentNetworkTransform = m_AuthorityParentObject.GetComponent(); + m_AuthorityParentNetworkTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; + m_AuthorityChildNetworkTransform = m_AuthorityChildObject.GetComponent(); + m_AuthorityChildNetworkTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; + m_AuthoritySubChildNetworkTransform = m_AuthoritySubChildObject.GetComponent(); + m_AuthoritySubChildNetworkTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; + + + // Apply a scale to the parent object to make sure the scale on the child is properly updated on + // non-authority instances. + var halfScale = scale * 0.5f; + m_AuthorityParentObject.transform.localScale = GetRandomVector3(scale - halfScale, scale + halfScale); + m_AuthorityChildObject.transform.localScale = GetRandomVector3(scale - halfScale, scale + halfScale); + m_AuthoritySubChildObject.transform.localScale = GetRandomVector3(scale - halfScale, scale + halfScale); + + // Allow one tick for authority to update these changes + + yield return WaitForConditionOrTimeOut(PositionRotationScaleMatches); + + AssertOnTimeout("All transform values did not match prior to parenting!"); + + // Parent the child under the parent with the current world position stays setting + Assert.True(serverSideChild.TrySetParent(serverSideParent.transform, worldPositionStays), "[Server-Side Child] Failed to set child's parent!"); + + // Parent the sub-child under the child with the current world position stays setting + Assert.True(serverSideSubChild.TrySetParent(serverSideChild.transform, worldPositionStays), "[Server-Side SubChild] Failed to set sub-child's parent!"); + + // This waits for all child instances to be parented + yield return WaitForConditionOrTimeOut(AllChildObjectInstancesHaveChild); + AssertOnTimeout("Timed out waiting for all instances to have parented a child!"); + var latencyWait = new WaitForSeconds(k_Latency * 0.003f); + // Wait for at least 3x designated latency period + yield return latencyWait; + + // This validates each child instance has preserved their local space values + yield return AllChildrenLocalTransformValuesMatch(false, ChildrenTransformCheckType.Connected_Clients); + + // This validates each sub-child instance has preserved their local space values + yield return AllChildrenLocalTransformValuesMatch(true, ChildrenTransformCheckType.Connected_Clients); + + // Verify that a late joining client will synchronize to the parented NetworkObjects properly + yield return CreateAndStartNewClient(); + + // Assure all of the child object instances are spawned (basically for the newly connected client) + yield return WaitForConditionOrTimeOut(AllChildObjectInstancesAreSpawned); + AssertOnTimeout("Timed out waiting for all child instances to be spawned!"); + + // This waits for all child instances to be parented + yield return WaitForConditionOrTimeOut(AllChildObjectInstancesHaveChild); + AssertOnTimeout("Timed out waiting for all instances to have parented a child!"); + + // Wait for at least 3x designated latency period + yield return latencyWait; + + // This validates each child instance has preserved their local space values + yield return AllChildrenLocalTransformValuesMatch(false, ChildrenTransformCheckType.Late_Join_Client); + + // This validates each sub-child instance has preserved their local space values + yield return AllChildrenLocalTransformValuesMatch(true, ChildrenTransformCheckType.Late_Join_Client); + } + + /// + /// This validates that multiple changes can occur within the same tick or over + /// several ticks while still keeping non-authoritative instances synchronized. + /// + /// + /// When testing < 3 axis: Interpolation is disabled and only 3 delta updates are applied per unique test + /// When testing 3 axis: Interpolation is enabled, sometimes an axis is intentionally excluded during a + /// delta update, and it runs through 8 delta updates per unique test. + /// + [UnityTest] + public IEnumerator NetworkTransformMultipleChangesOverTime([Values] TransformSpace testLocalTransform, [Values] Axis axis) + { + yield return s_DefaultWaitForTick; + // Just test for OverrideState.Update (they are already being tested for functionality in normal NetworkTransformTests) + var overideState = OverrideState.Update; + var tickRelativeTime = new WaitForSeconds(1.0f / m_ServerNetworkManager.NetworkConfig.TickRate); + m_AuthoritativeTransform.InLocalSpace = testLocalTransform == TransformSpace.Local; + bool axisX = axis == Axis.X || axis == Axis.XY || axis == Axis.XZ || axis == Axis.XYZ; + bool axisY = axis == Axis.Y || axis == Axis.XY || axis == Axis.YZ || axis == Axis.XYZ; + bool axisZ = axis == Axis.Z || axis == Axis.XZ || axis == Axis.YZ || axis == Axis.XYZ; + + var axisCount = axisX ? 1 : 0; + axisCount += axisY ? 1 : 0; + axisCount += axisZ ? 1 : 0; + + m_AuthoritativeTransform.StatePushed = false; + // Enable interpolation when all 3 axis are selected to make sure we are synchronizing properly + // when interpolation is enabled. + m_AuthoritativeTransform.Interpolate = axisCount == 3 ? true : false; + + m_CurrentAxis = axis; + + // Authority dictates what is synchronized and what the precision is going to be + // so we only need to set this on the authoritative side. + m_AuthoritativeTransform.UseHalfFloatPrecision = m_Precision == Precision.Half; + m_AuthoritativeTransform.UseQuaternionSynchronization = m_Rotation == Rotation.Quaternion; + m_AuthoritativeTransform.UseQuaternionCompression = m_RotationCompression == RotationCompression.QuaternionCompress; + + m_AuthoritativeTransform.SyncPositionX = axisX; + m_AuthoritativeTransform.SyncPositionY = axisY; + m_AuthoritativeTransform.SyncPositionZ = axisZ; + + if (!m_AuthoritativeTransform.UseQuaternionSynchronization) + { + m_AuthoritativeTransform.SyncRotAngleX = axisX; + m_AuthoritativeTransform.SyncRotAngleY = axisY; + m_AuthoritativeTransform.SyncRotAngleZ = axisZ; + } + else + { + // This is not required for usage (setting the value should not matter when quaternion synchronization is enabled) + // but is required for this test so we don't get a failure on an axis that is marked to not be synchronized when + // validating the authority's values on non-authority instances. + m_AuthoritativeTransform.SyncRotAngleX = true; + m_AuthoritativeTransform.SyncRotAngleY = true; + m_AuthoritativeTransform.SyncRotAngleZ = true; + } + + m_AuthoritativeTransform.SyncScaleX = axisX; + m_AuthoritativeTransform.SyncScaleY = axisY; + m_AuthoritativeTransform.SyncScaleZ = axisZ; + + var positionStart = GetRandomVector3(0.25f, 1.75f); + var rotationStart = GetRandomVector3(1f, 15f); + var scaleStart = GetRandomVector3(0.25f, 2.0f); + var position = positionStart; + var rotation = rotationStart; + var scale = scaleStart; + var success = false; + + + // Wait for the deltas to be pushed + yield return WaitForConditionOrTimeOut(() => m_AuthoritativeTransform.StatePushed); + + // Just in case we drop the first few state updates + if (s_GlobalTimeoutHelper.TimedOut) + { + // Set the local state to not reflect the authority state's local space settings + // to trigger the state update (it would eventually get there, but this is an integration test) + var state = m_AuthoritativeTransform.LocalAuthoritativeNetworkState; + state.InLocalSpace = !m_AuthoritativeTransform.InLocalSpace; + m_AuthoritativeTransform.LocalAuthoritativeNetworkState = state; + // Wait for the deltas to be pushed + yield return WaitForConditionOrTimeOut(() => m_AuthoritativeTransform.StatePushed); + } + AssertOnTimeout("State was never pushed!"); + + // Allow the precision settings to propagate first as changing precision + // causes a teleport event to occur + yield return s_DefaultWaitForTick; + yield return s_DefaultWaitForTick; + yield return s_DefaultWaitForTick; + yield return s_DefaultWaitForTick; + yield return s_DefaultWaitForTick; + var iterations = axisCount == 3 ? k_PositionRotationScaleIterations3Axis : k_PositionRotationScaleIterations; + + // Move and rotate within the same tick, validate the non-authoritative instance updates + // to each set of changes. Repeat several times. + for (int i = 0; i < iterations; i++) + { + // Always reset this per delta update pass + m_AxisExcluded = false; + var deltaPositionDelta = GetRandomVector3(-1.5f, 1.5f); + var deltaRotationDelta = GetRandomVector3(-3.5f, 3.5f); + var deltaScaleDelta = GetRandomVector3(-0.5f, 0.5f); + + m_NonAuthoritativeTransform.StateUpdated = false; + m_AuthoritativeTransform.StatePushed = false; + + // With two or more axis, excluding one of them while chaging another will validate that + // full precision updates are maintaining their target state value(s) to interpolate towards + if (axisCount == 3) + { + position += RandomlyExcludeAxis(deltaPositionDelta); + rotation += RandomlyExcludeAxis(deltaRotationDelta); + scale += RandomlyExcludeAxis(deltaScaleDelta); + } + else + { + position += deltaPositionDelta; + rotation += deltaRotationDelta; + scale += deltaScaleDelta; + } + + // Apply delta between ticks + MoveRotateAndScaleAuthority(position, rotation, scale, overideState); + + // Wait for the deltas to be pushed (unlike the original test, we don't wait for state to be updated as that could be dropped here) + yield return WaitForConditionOrTimeOut(() => m_AuthoritativeTransform.StatePushed); + AssertOnTimeout($"[Non-Interpolate {i}] Timed out waiting for state to be pushed ({m_AuthoritativeTransform.StatePushed})!"); + + // For 3 axis, we will skip validating that the non-authority interpolates to its target point at least once. + // This will validate that non-authoritative updates are maintaining their target state axis values if only 2 + // of the axis are being updated to assure interpolation maintains the targeted axial value per axis. + // For 2 and 1 axis tests we always validate per delta update + if (m_AxisExcluded || axisCount < 3) + { + // Wait for deltas to synchronize on non-authoritative side + yield return WaitForConditionOrTimeOut(PositionRotationScaleMatches); + // Provide additional debug info about what failed (if it fails) + if (s_GlobalTimeoutHelper.TimedOut) + { + Debug.Log("[Synch Issue Start - 1]"); + // If we timed out, then wait for a full range of ticks (plus 1) to assure it sent synchronization data. + for (int j = 0; j < m_ServerNetworkManager.NetworkConfig.TickRate * 2; j++) + { + success = PositionRotationScaleMatches(); + if (success) + { + // If we matched, then something was dropped and recovered when synchronized + break; + } + yield return s_DefaultWaitForTick; + } + + // Only if we still didn't match + if (!success) + { + m_EnableVerboseDebug = true; + success = PositionRotationScaleMatches(); + m_EnableVerboseDebug = false; + Debug.Log("[Synch Issue END - 1]"); + AssertOnTimeout($"[Non-Interpolate {i}] Timed out waiting for non-authority to match authority's position or rotation"); + } + } + } + } + + if (axisCount == 3) + { + // As a final test, wait for deltas to synchronize on non-authoritative side to assure it interpolates to the correct values + yield return WaitForConditionOrTimeOut(PositionRotationScaleMatches); + // Provide additional debug info about what failed (if it fails) + if (s_GlobalTimeoutHelper.TimedOut) + { + Debug.Log("[Synch Issue Start - 2]"); + // If we timed out, then wait for a full range of ticks (plus 1) to assure it sent synchronization data. + for (int j = 0; j < m_ServerNetworkManager.NetworkConfig.TickRate * 2; j++) + { + success = PositionRotationScaleMatches(); + if (success) + { + // If we matched, then something was dropped and recovered when synchronized + break; + } + yield return s_DefaultWaitForTick; + } + + // Only if we still didn't match + if (!success) + { + m_EnableVerboseDebug = true; + PositionRotationScaleMatches(); + m_EnableVerboseDebug = false; + Debug.Log("[Synch Issue END - 2]"); + AssertOnTimeout("Timed out waiting for non-authority to match authority's position or rotation"); + + } + } + + } + } + + /// + /// Tests changing all axial values one at a time with packet loss + /// These tests are performed: + /// - While in local space and world space + /// - While interpolation is enabled and disabled + /// + [UnityTest] + public IEnumerator TestAuthoritativeTransformChangeOneAtATime([Values] TransformSpace testLocalTransform, [Values] Interpolation interpolation) + { + // Just test for OverrideState.Update (they are already being tested for functionality in normal NetworkTransformTests) + m_AuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; + m_AuthoritativeTransform.InLocalSpace = testLocalTransform == TransformSpace.Local; + m_AuthoritativeTransform.UseQuaternionCompression = m_RotationCompression == RotationCompression.QuaternionCompress; + m_AuthoritativeTransform.UseHalfFloatPrecision = m_Precision == Precision.Half; + m_AuthoritativeTransform.UseQuaternionSynchronization = m_Rotation == Rotation.Quaternion; + m_NonAuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; + + + // test position + var authPlayerTransform = m_AuthoritativeTransform.transform; + + Assert.AreEqual(Vector3.zero, m_NonAuthoritativeTransform.transform.position, "server side pos should be zero at first"); // sanity check + + m_AuthoritativeTransform.transform.position = GetRandomVector3(2f, 30f); + + yield return WaitForConditionOrTimeOut(() => PositionsMatch()); + AssertOnTimeout($"Timed out waiting for positions to match {m_AuthoritativeTransform.transform.position} | {m_NonAuthoritativeTransform.transform.position}"); + + // test rotation + Assert.AreEqual(Quaternion.identity, m_NonAuthoritativeTransform.transform.rotation, "wrong initial value for rotation"); // sanity check + + m_AuthoritativeTransform.transform.rotation = Quaternion.Euler(GetRandomVector3(5, 60)); // using euler angles instead of quaternions directly to really see issues users might encounter + + // Make sure the values match + yield return WaitForConditionOrTimeOut(() => RotationsMatch()); + AssertOnTimeout($"Timed out waiting for rotations to match"); + + m_AuthoritativeTransform.StatePushed = false; + m_AuthoritativeTransform.transform.localScale = GetRandomVector3(1, 6); + + // Make sure the scale values match + yield return WaitForConditionOrTimeOut(() => ScaleValuesMatch()); + AssertOnTimeout($"Timed out waiting for scale values to match"); + } + + [UnityTest] + public IEnumerator TestSameFrameDeltaStateAndTeleport([Values] TransformSpace testLocalTransform, [Values] Interpolation interpolation) + { + m_AuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; + + m_NonAuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; + + m_AuthoritativeTransform.InLocalSpace = testLocalTransform == TransformSpace.Local; + + // test position + var authPlayerTransform = m_AuthoritativeTransform.transform; + + Assert.AreEqual(Vector3.zero, m_NonAuthoritativeTransform.transform.position, "server side pos should be zero at first"); // sanity check + + m_AuthoritativeTransform.AuthorityPushedTransformState += OnAuthorityPushedTransformState; + m_RandomPosition = GetRandomVector3(2f, 30f); + m_AuthoritativeTransform.transform.position = m_RandomPosition; + m_Teleported = false; + yield return WaitForConditionOrTimeOut(() => m_Teleported); + AssertOnTimeout($"Timed out waiting for random position to be pushed!"); + + yield return WaitForConditionOrTimeOut(() => PositionsMatch()); + AssertOnTimeout($"Timed out waiting for positions to match {m_AuthoritativeTransform.transform.position} | {m_NonAuthoritativeTransform.transform.position}"); + + var authPosition = m_AuthoritativeTransform.GetSpaceRelativePosition(); + var nonAuthPosition = m_NonAuthoritativeTransform.GetSpaceRelativePosition(); + + var finalPosition = m_TeleportOffset + m_RandomPosition; + Assert.True(Approximately(authPosition, finalPosition), $"Authority did not set its position ({authPosition}) to the teleport position ({finalPosition})!"); + Assert.True(Approximately(nonAuthPosition, finalPosition), $"NonAuthority did not set its position ({nonAuthPosition}) to the teleport position ({finalPosition})!"); + } + + /// + /// For the TestSameFrameDeltaStateAndTeleport test, we want to teleport on the same frame that we had a delta state update when + /// using unreliable delta state updates (i.e. we want the unreliable packet to be sent first and then the teleport to be sent on + /// the next tick. Store off both states when invoked + /// + /// + private void OnAuthorityPushedTransformState(ref NetworkTransform.NetworkTransformState networkTransformState) + { + // Match the first position update + if (Approximately(m_RandomPosition, networkTransformState.GetPosition())) + { + // Teleport to the m_RandomPosition plus the + m_AuthoritativeTransform.SetState(m_TeleportOffset + m_RandomPosition, null, null, false); + m_AuthoritativeTransform.AuthorityPushedTransformState -= OnAuthorityPushedTransformState; + m_Teleported = true; + } + } + } +} +#endif diff --git a/Tests/Runtime/NetworkTransform/NetworkTransformPacketLossTests.cs.meta b/Tests/Runtime/NetworkTransform/NetworkTransformPacketLossTests.cs.meta new file mode 100644 index 0000000..c363dc6 --- /dev/null +++ b/Tests/Runtime/NetworkTransform/NetworkTransformPacketLossTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e0f584e8eb891d5459373e96e54fe821 +timeCreated: 1620872927 \ No newline at end of file diff --git a/Tests/Runtime/NetworkTransform/NetworkTransformStateTests.cs b/Tests/Runtime/NetworkTransform/NetworkTransformStateTests.cs index 2669c23..f2512ae 100644 --- a/Tests/Runtime/NetworkTransform/NetworkTransformStateTests.cs +++ b/Tests/Runtime/NetworkTransform/NetworkTransformStateTests.cs @@ -230,6 +230,11 @@ namespace Unity.Netcode.RuntimeTests var gameObject = new GameObject($"Test-{nameof(NetworkTransformStateTests)}.{nameof(TestSyncAxes)}"); var networkObject = gameObject.AddComponent(); var networkTransform = gameObject.AddComponent(); + + var manager = new GameObject($"Test-{nameof(NetworkManager)}.{nameof(TestSyncAxes)}"); + var networkManager = manager.AddComponent(); + networkObject.NetworkManagerOwner = networkManager; + networkTransform.enabled = false; // do not tick `FixedUpdate()` or `Update()` var initialPosition = Vector3.zero; @@ -269,6 +274,7 @@ namespace Unity.Netcode.RuntimeTests if (syncPosX || syncPosY || syncPosZ || syncRotX || syncRotY || syncRotZ || syncScaX || syncScaY || syncScaZ) { + Assert.NotNull(networkTransform.NetworkManager, "NetworkManager is NULL!"); Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); } } @@ -714,6 +720,7 @@ namespace Unity.Netcode.RuntimeTests } Object.DestroyImmediate(gameObject); + Object.DestroyImmediate(manager); } diff --git a/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs b/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs index a29c963..0b7be8a 100644 --- a/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs +++ b/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs @@ -1,451 +1,44 @@ -using System.Collections.Generic; -using System.Text; using NUnit.Framework; -using Unity.Netcode.Components; -using Unity.Netcode.TestHelpers.Runtime; using UnityEngine; namespace Unity.Netcode.RuntimeTests { - /// - /// Helper component for all NetworkTransformTests - /// - public class NetworkTransformTestComponent : NetworkTransform - { - public bool ServerAuthority; - public bool ReadyToReceivePositionUpdate = false; - - public NetworkTransformState AuthorityLastSentState; - public bool StatePushed { get; internal set; } - - protected override void OnAuthorityPushTransformState(ref NetworkTransformState networkTransformState) - { - StatePushed = true; - AuthorityLastSentState = networkTransformState; - base.OnAuthorityPushTransformState(ref networkTransformState); - } - - - public bool StateUpdated { get; internal set; } - protected override void OnNetworkTransformStateUpdated(ref NetworkTransformState oldState, ref NetworkTransformState newState) - { - StateUpdated = true; - base.OnNetworkTransformStateUpdated(ref oldState, ref newState); - } - - protected override bool OnIsServerAuthoritative() - { - return ServerAuthority; - } - - public static NetworkTransformTestComponent AuthorityInstance; - - public override void OnNetworkSpawn() - { - base.OnNetworkSpawn(); - - if (CanCommitToTransform) - { - AuthorityInstance = this; - } - - ReadyToReceivePositionUpdate = true; - } - - public void CommitToTransform() - { - TryCommitTransformToServer(transform, NetworkManager.LocalTime.Time); - } - - public (bool isDirty, bool isPositionDirty, bool isRotationDirty, bool isScaleDirty) ApplyState() - { - var transformState = ApplyLocalNetworkState(transform); - return (transformState.IsDirty, transformState.HasPositionChange, transformState.HasRotAngleChange, transformState.HasScaleChange); - } - } - - /// - /// Helper component for NetworkTransform parenting tests when - /// a child is a parent of another child (i.e. "sub child") - /// - public class SubChildObjectComponent : ChildObjectComponent - { - protected override bool IsSubChild() - { - return true; - } - } - - /// - /// Helper component for NetworkTransform parenting tests - /// - public class ChildObjectComponent : NetworkTransform - { - public static readonly List Instances = new List(); - public static readonly List SubInstances = new List(); - public static ChildObjectComponent AuthorityInstance { get; internal set; } - public static ChildObjectComponent AuthoritySubInstance { get; internal set; } - public static readonly Dictionary ClientInstances = new Dictionary(); - public static readonly Dictionary ClientSubChildInstances = new Dictionary(); - - public static bool HasSubChild; - - public static void Reset() - { - AuthorityInstance = null; - AuthoritySubInstance = null; - HasSubChild = false; - ClientInstances.Clear(); - ClientSubChildInstances.Clear(); - Instances.Clear(); - SubInstances.Clear(); - } - - public bool ServerAuthority; - - protected virtual bool IsSubChild() - { - return false; - } - - protected override bool OnIsServerAuthoritative() - { - return ServerAuthority; - } - - public override void OnNetworkSpawn() - { - base.OnNetworkSpawn(); - if (CanCommitToTransform) - { - if (!IsSubChild()) - { - AuthorityInstance = this; - } - else - { - AuthoritySubInstance = this; - } - } - else - { - if (!IsSubChild()) - { - Instances.Add(this); - } - else - { - SubInstances.Add(this); - } - } - if (HasSubChild && IsSubChild()) - { - ClientSubChildInstances.Add(NetworkManager.LocalClientId, NetworkObject); - } - else - { - ClientInstances.Add(NetworkManager.LocalClientId, NetworkObject); - } - } - } - /// /// Integration tests for NetworkTransform that will test both /// server and host operating modes and will test both authoritative /// models for each operating mode. /// - [TestFixture(HostOrServer.Host, Authority.ServerAuthority)] - [TestFixture(HostOrServer.Host, Authority.OwnerAuthority)] - [TestFixture(HostOrServer.Server, Authority.ServerAuthority)] - [TestFixture(HostOrServer.Server, Authority.OwnerAuthority)] - - public class NetworkTransformTests : IntegrationTestWithApproximation + [TestFixture(HostOrServer.Host, Authority.ServerAuthority, RotationCompression.None, Rotation.Euler, Precision.Full)] + [TestFixture(HostOrServer.Host, Authority.ServerAuthority, RotationCompression.None, Rotation.Euler, Precision.Half)] + [TestFixture(HostOrServer.Host, Authority.ServerAuthority, RotationCompression.None, Rotation.Quaternion, Precision.Full)] + [TestFixture(HostOrServer.Host, Authority.ServerAuthority, RotationCompression.None, Rotation.Quaternion, Precision.Half)] + [TestFixture(HostOrServer.Host, Authority.ServerAuthority, RotationCompression.QuaternionCompress, Rotation.Quaternion, Precision.Full)] + [TestFixture(HostOrServer.Host, Authority.ServerAuthority, RotationCompression.QuaternionCompress, Rotation.Quaternion, Precision.Half)] + [TestFixture(HostOrServer.Host, Authority.OwnerAuthority, RotationCompression.None, Rotation.Euler, Precision.Full)] + [TestFixture(HostOrServer.Host, Authority.OwnerAuthority, RotationCompression.None, Rotation.Euler, Precision.Half)] + [TestFixture(HostOrServer.Host, Authority.OwnerAuthority, RotationCompression.None, Rotation.Quaternion, Precision.Full)] + [TestFixture(HostOrServer.Host, Authority.OwnerAuthority, RotationCompression.None, Rotation.Quaternion, Precision.Half)] + [TestFixture(HostOrServer.Host, Authority.OwnerAuthority, RotationCompression.QuaternionCompress, Rotation.Quaternion, Precision.Full)] + [TestFixture(HostOrServer.Host, Authority.OwnerAuthority, RotationCompression.QuaternionCompress, Rotation.Quaternion, Precision.Half)] + public class NetworkTransformTests : NetworkTransformBase { - private NetworkObject m_AuthoritativePlayer; - private NetworkObject m_NonAuthoritativePlayer; - private NetworkObject m_ChildObject; - private NetworkObject m_SubChildObject; - private NetworkObject m_ParentObject; - - private NetworkTransformTestComponent m_AuthoritativeTransform; - private NetworkTransformTestComponent m_NonAuthoritativeTransform; - private NetworkTransformTestComponent m_OwnerTransform; - - private readonly Authority m_Authority; - - public enum Authority - { - ServerAuthority, - OwnerAuthority - } - - public enum Interpolation - { - DisableInterpolate, - EnableInterpolate - } - - public enum Precision - { - Half, - Full - } - - public enum Rotation - { - Euler, - Quaternion - } - - public enum RotationCompression - { - None, - QuaternionCompress - } - - - public enum TransformSpace - { - World, - Local - } - - public enum OverrideState - { - Update, - CommitToTransform, - SetState - } - - public enum Axis - { - X, - Y, - Z, - XY, - XZ, - YZ, - XYZ - } - + protected const int k_TickRate = 60; /// /// Constructor /// /// Determines if we are running as a server or host /// Determines if we are using server or owner authority - public NetworkTransformTests(HostOrServer testWithHost, Authority authority) - { - m_UseHost = testWithHost == HostOrServer.Host ? true : false; - m_Authority = authority; - } + public NetworkTransformTests(HostOrServer testWithHost, Authority authority, RotationCompression rotationCompression, Rotation rotation, Precision precision) : + base(testWithHost, authority, rotationCompression, rotation, precision) + { } - protected override int NumberOfClients => 1; protected override bool m_EnableTimeTravel => true; protected override bool m_SetupIsACoroutine => false; protected override bool m_TearDownIsACoroutine => false; - private const int k_TickRate = 60; - private int m_OriginalTargetFrameRate; - protected override void OnOneTimeSetup() + protected override uint GetTickRate() { - m_OriginalTargetFrameRate = Application.targetFrameRate; - Application.targetFrameRate = 120; - base.OnOneTimeSetup(); - } - - protected override void OnOneTimeTearDown() - { - Application.targetFrameRate = m_OriginalTargetFrameRate; - base.OnOneTimeTearDown(); - } - - protected override void OnInlineSetup() - { - NetworkTransformTestComponent.AuthorityInstance = null; - m_Precision = Precision.Full; - ChildObjectComponent.Reset(); - } - - protected override void OnInlineTearDown() - { - m_EnableVerboseDebug = false; - Object.DestroyImmediate(m_PlayerPrefab); - } - - protected override void OnCreatePlayerPrefab() - { - var networkTransformTestComponent = m_PlayerPrefab.AddComponent(); - networkTransformTestComponent.ServerAuthority = m_Authority == Authority.ServerAuthority; - } - - protected override void OnServerAndClientsCreated() - { - var subChildObject = CreateNetworkObjectPrefab("SubChildObject"); - var subChildNetworkTransform = subChildObject.AddComponent(); - subChildNetworkTransform.ServerAuthority = m_Authority == Authority.ServerAuthority; - m_SubChildObject = subChildObject.GetComponent(); - - var childObject = CreateNetworkObjectPrefab("ChildObject"); - var childNetworkTransform = childObject.AddComponent(); - childNetworkTransform.ServerAuthority = m_Authority == Authority.ServerAuthority; - m_ChildObject = childObject.GetComponent(); - - var parentObject = CreateNetworkObjectPrefab("ParentObject"); - var parentNetworkTransform = parentObject.AddComponent(); - parentNetworkTransform.ServerAuthority = m_Authority == Authority.ServerAuthority; - m_ParentObject = parentObject.GetComponent(); - - // Now apply local transform values - m_ChildObject.transform.position = m_ChildObjectLocalPosition; - var childRotation = m_ChildObject.transform.rotation; - childRotation.eulerAngles = m_ChildObjectLocalRotation; - m_ChildObject.transform.rotation = childRotation; - m_ChildObject.transform.localScale = m_ChildObjectLocalScale; - - m_SubChildObject.transform.position = m_SubChildObjectLocalPosition; - var subChildRotation = m_SubChildObject.transform.rotation; - subChildRotation.eulerAngles = m_SubChildObjectLocalRotation; - m_SubChildObject.transform.rotation = childRotation; - m_SubChildObject.transform.localScale = m_SubChildObjectLocalScale; - - if (m_EnableVerboseDebug) - { - m_ServerNetworkManager.LogLevel = LogLevel.Developer; - foreach (var clientNetworkManager in m_ClientNetworkManagers) - { - clientNetworkManager.LogLevel = LogLevel.Developer; - } - } - - m_ServerNetworkManager.NetworkConfig.TickRate = k_TickRate; - foreach (var clientNetworkManager in m_ClientNetworkManagers) - { - clientNetworkManager.NetworkConfig.TickRate = k_TickRate; - } - } - - protected override void OnTimeTravelServerAndClientsConnected() - { - // Get the client player representation on both the server and the client side - var serverSideClientPlayer = m_ServerNetworkManager.ConnectedClients[m_ClientNetworkManagers[0].LocalClientId].PlayerObject; - var clientSideClientPlayer = m_ClientNetworkManagers[0].LocalClient.PlayerObject; - - m_AuthoritativePlayer = m_Authority == Authority.ServerAuthority ? serverSideClientPlayer : clientSideClientPlayer; - m_NonAuthoritativePlayer = m_Authority == Authority.ServerAuthority ? clientSideClientPlayer : serverSideClientPlayer; - - // Get the NetworkTransformTestComponent to make sure the client side is ready before starting test - m_AuthoritativeTransform = m_AuthoritativePlayer.GetComponent(); - m_NonAuthoritativeTransform = m_NonAuthoritativePlayer.GetComponent(); - - m_OwnerTransform = m_AuthoritativeTransform.IsOwner ? m_AuthoritativeTransform : m_NonAuthoritativeTransform; - - // Wait for the client-side to notify it is finished initializing and spawning. - var success = WaitForConditionOrTimeOutWithTimeTravel(() => m_NonAuthoritativeTransform.ReadyToReceivePositionUpdate == true); - Assert.True(success, "Timed out waiting for client-side to notify it is ready!"); - - Assert.True(m_AuthoritativeTransform.CanCommitToTransform); - Assert.False(m_NonAuthoritativeTransform.CanCommitToTransform); - // Just wait for at least one tick for NetworkTransforms to finish synchronization - WaitForNextTick(); - } - - /// - /// Returns true when the server-host and all clients have - /// instantiated the child object to be used in - /// - /// - private bool AllChildObjectInstancesAreSpawned() - { - if (ChildObjectComponent.AuthorityInstance == null) - { - return false; - } - - if (ChildObjectComponent.HasSubChild && ChildObjectComponent.AuthoritySubInstance == null) - { - return false; - } - - foreach (var clientNetworkManager in m_ClientNetworkManagers) - { - if (!ChildObjectComponent.ClientInstances.ContainsKey(clientNetworkManager.LocalClientId)) - { - return false; - } - } - return true; - } - - private bool AllChildObjectInstancesHaveChild() - { - foreach (var instance in ChildObjectComponent.ClientInstances.Values) - { - if (instance.transform.parent == null) - { - return false; - } - } - if (ChildObjectComponent.HasSubChild) - { - foreach (var instance in ChildObjectComponent.ClientSubChildInstances.Values) - { - if (instance.transform.parent == null) - { - return false; - } - } - } - return true; - } - - // To test that local position, rotation, and scale remain the same when parented. - private Vector3 m_ChildObjectLocalPosition = new Vector3(5.0f, 0.0f, -5.0f); - private Vector3 m_ChildObjectLocalRotation = new Vector3(-35.0f, 90.0f, 270.0f); - private Vector3 m_ChildObjectLocalScale = new Vector3(0.1f, 0.5f, 0.4f); - private Vector3 m_SubChildObjectLocalPosition = new Vector3(2.0f, 1.0f, -1.0f); - private Vector3 m_SubChildObjectLocalRotation = new Vector3(5.0f, 15.0f, 124.0f); - private Vector3 m_SubChildObjectLocalScale = new Vector3(1.0f, 0.15f, 0.75f); - - - /// - /// A wait condition specific method that assures the local space coordinates - /// are not impacted by NetworkTransform when parented. - /// - private bool AllInstancesKeptLocalTransformValues() - { - var authorityObjectLocalPosition = m_AuthorityChildObject.transform.localPosition; - var authorityObjectLocalRotation = m_AuthorityChildObject.transform.localRotation.eulerAngles; - var authorityObjectLocalScale = m_AuthorityChildObject.transform.localScale; - - foreach (var childInstance in ChildObjectComponent.Instances) - { - var childLocalPosition = childInstance.transform.localPosition; - var childLocalRotation = childInstance.transform.localRotation.eulerAngles; - var childLocalScale = childInstance.transform.localScale; - // Adjust approximation based on precision - if (m_Precision == Precision.Half) - { - m_CurrentHalfPrecision = k_HalfPrecisionPosScale; - } - if (!Approximately(childLocalPosition, authorityObjectLocalPosition)) - { - return false; - } - if (!Approximately(childLocalScale, authorityObjectLocalScale)) - { - return false; - } - // Adjust approximation based on precision - if (m_Precision == Precision.Half) - { - m_CurrentHalfPrecision = k_HalfPrecisionRot; - } - if (!ApproximatelyEuler(childLocalRotation, authorityObjectLocalRotation)) - { - return false; - } - } - return true; + return k_TickRate; } /// @@ -453,77 +46,37 @@ namespace Unity.Netcode.RuntimeTests /// If not, it generates a message containing the axial values that did not match /// the target/start local space values. /// - private void AllChildrenLocalTransformValuesMatch(bool useSubChild) + private void AllChildrenLocalTransformValuesMatch(bool useSubChild, ChildrenTransformCheckType checkType) { - var success = WaitForConditionOrTimeOutWithTimeTravel(AllInstancesKeptLocalTransformValues); - var infoMessage = new StringBuilder($"Timed out waiting for all children to have the correct local space values:\n"); - var authorityObjectLocalPosition = useSubChild ? m_AuthoritySubChildObject.transform.localPosition : m_AuthorityChildObject.transform.localPosition; - var authorityObjectLocalRotation = useSubChild ? m_AuthoritySubChildObject.transform.localRotation.eulerAngles : m_AuthorityChildObject.transform.localRotation.eulerAngles; - var authorityObjectLocalScale = useSubChild ? m_AuthoritySubChildObject.transform.localScale : m_AuthorityChildObject.transform.localScale; - - if (s_GlobalTimeoutHelper.TimedOut || !success) + // We don't assert on timeout here because we want to log this information during PostAllChildrenLocalTransformValuesMatch + var success = WaitForConditionOrTimeOutWithTimeTravel(() => AllInstancesKeptLocalTransformValues(useSubChild), (int)GetTickRate() * 2); + m_InfoMessage.Clear(); + m_InfoMessage.AppendLine($"[{checkType}][{useSubChild}] Timed out waiting for all children to have the correct local space values:\n"); + if (!success) { - var instances = useSubChild ? ChildObjectComponent.SubInstances : ChildObjectComponent.Instances; - foreach (var childInstance in ChildObjectComponent.Instances) + // If we timed out, then wait for 4 ticks to assure all data has been synchronized before declaring this a failed test. + for (int j = 0; j < 4; j++) { - var childLocalPosition = childInstance.transform.localPosition; - var childLocalRotation = childInstance.transform.localRotation.eulerAngles; - var childLocalScale = childInstance.transform.localScale; - // Adjust approximation based on precision - if (m_Precision == Precision.Half) - { - m_CurrentHalfPrecision = k_HalfPrecisionPosScale; - } - if (!Approximately(childLocalPosition, authorityObjectLocalPosition)) - { - infoMessage.AppendLine($"[{childInstance.name}] Child's Local Position ({childLocalPosition}) | Authority Local Position ({authorityObjectLocalPosition})"); - success = false; - } - if (!Approximately(childLocalScale, authorityObjectLocalScale)) - { - infoMessage.AppendLine($"[{childInstance.name}] Child's Local Scale ({childLocalScale}) | Authority Local Scale ({authorityObjectLocalScale})"); - success = false; - } - - // Adjust approximation based on precision - if (m_Precision == Precision.Half) - { - m_CurrentHalfPrecision = k_HalfPrecisionRot; - } - if (!ApproximatelyEuler(childLocalRotation, authorityObjectLocalRotation)) - { - infoMessage.AppendLine($"[{childInstance.name}] Child's Local Rotation ({childLocalRotation}) | Authority Local Rotation ({authorityObjectLocalRotation})"); - success = false; - } - } - if (!success) - { - Assert.True(success, infoMessage.ToString()); + var instances = useSubChild ? ChildObjectComponent.SubInstances : ChildObjectComponent.Instances; + success = PostAllChildrenLocalTransformValuesMatch(useSubChild); + TimeTravelAdvanceTick(); } } + + if (!success) + { + Assert.True(success, m_InfoMessage.ToString()); + } } - private NetworkObject m_AuthorityParentObject; - private NetworkTransformTestComponent m_AuthorityParentNetworkTransform; - private NetworkObject m_AuthorityChildObject; - private NetworkObject m_AuthoritySubChildObject; - private ChildObjectComponent m_AuthorityChildNetworkTransform; - - private ChildObjectComponent m_AuthoritySubChildNetworkTransform; - /// /// Validates that transform values remain the same when a NetworkTransform is /// parented under another NetworkTransform under all of the possible axial conditions /// as well as when the parent has a varying scale. /// [Test] - public void ParentedNetworkTransformTest([Values] Precision precision, [Values] Rotation rotation, - [Values] RotationCompression rotationCompression, [Values] Interpolation interpolation, [Values] bool worldPositionStays, - [Values(0.5f, 1.0f, 5.0f)] float scale) + public void ParentedNetworkTransformTest([Values] Interpolation interpolation, [Values] bool worldPositionStays, [Values(0.5f, 1.0f, 5.0f)] float scale) { - // Set the precision being used for threshold adjustments - m_Precision = precision; - // Get the NetworkManager that will have authority in order to spawn with the correct authority var isServerAuthority = m_Authority == Authority.ServerAuthority; var authorityNetworkManager = m_ServerNetworkManager; @@ -550,14 +103,14 @@ namespace Unity.Netcode.RuntimeTests // The child NetworkTransform will use world space when world position stays and // local space when world position does not stay when parenting. ChildObjectComponent.AuthorityInstance.InLocalSpace = !worldPositionStays; - ChildObjectComponent.AuthorityInstance.UseHalfFloatPrecision = precision == Precision.Half; - ChildObjectComponent.AuthorityInstance.UseQuaternionSynchronization = rotation == Rotation.Quaternion; - ChildObjectComponent.AuthorityInstance.UseQuaternionCompression = rotationCompression == RotationCompression.QuaternionCompress; + ChildObjectComponent.AuthorityInstance.UseHalfFloatPrecision = m_Precision == Precision.Half; + ChildObjectComponent.AuthorityInstance.UseQuaternionSynchronization = m_Rotation == Rotation.Quaternion; + ChildObjectComponent.AuthorityInstance.UseQuaternionCompression = m_RotationCompression == RotationCompression.QuaternionCompress; ChildObjectComponent.AuthoritySubInstance.InLocalSpace = !worldPositionStays; - ChildObjectComponent.AuthoritySubInstance.UseHalfFloatPrecision = precision == Precision.Half; - ChildObjectComponent.AuthoritySubInstance.UseQuaternionSynchronization = rotation == Rotation.Quaternion; - ChildObjectComponent.AuthoritySubInstance.UseQuaternionCompression = rotationCompression == RotationCompression.QuaternionCompress; + ChildObjectComponent.AuthoritySubInstance.UseHalfFloatPrecision = m_Precision == Precision.Half; + ChildObjectComponent.AuthoritySubInstance.UseQuaternionSynchronization = m_Rotation == Rotation.Quaternion; + ChildObjectComponent.AuthoritySubInstance.UseQuaternionCompression = m_RotationCompression == RotationCompression.QuaternionCompress; // Set whether we are interpolating or not m_AuthorityParentNetworkTransform = m_AuthorityParentObject.GetComponent(); @@ -576,7 +129,10 @@ namespace Unity.Netcode.RuntimeTests m_AuthoritySubChildObject.transform.localScale = GetRandomVector3(scale - halfScale, scale + halfScale); // Allow one tick for authority to update these changes - TimeTravelToNextTick(); + TimeTravelAdvanceTick(); + success = WaitForConditionOrTimeOutWithTimeTravel(PositionRotationScaleMatches); + + Assert.True(success, "All transform values did not match prior to parenting!"); // Parent the child under the parent with the current world position stays setting Assert.True(serverSideChild.TrySetParent(serverSideParent.transform, worldPositionStays), "[Server-Side Child] Failed to set child's parent!"); @@ -588,12 +144,15 @@ namespace Unity.Netcode.RuntimeTests success = WaitForConditionOrTimeOutWithTimeTravel(AllChildObjectInstancesHaveChild); Assert.True(success, "Timed out waiting for all instances to have parented a child!"); - TimeTravelToNextTick(); + // Provide two network ticks for interpolation to finalize + TimeTravelAdvanceTick(); + TimeTravelAdvanceTick(); + // This validates each child instance has preserved their local space values - AllChildrenLocalTransformValuesMatch(false); + AllChildrenLocalTransformValuesMatch(false, ChildrenTransformCheckType.Connected_Clients); // This validates each sub-child instance has preserved their local space values - AllChildrenLocalTransformValuesMatch(true); + AllChildrenLocalTransformValuesMatch(true, ChildrenTransformCheckType.Connected_Clients); // Verify that a late joining client will synchronize to the parented NetworkObjects properly CreateAndStartNewClientWithTimeTravel(); @@ -607,136 +166,10 @@ namespace Unity.Netcode.RuntimeTests Assert.True(success, "Timed out waiting for all instances to have parented a child!"); // This validates each child instance has preserved their local space values - AllChildrenLocalTransformValuesMatch(false); + AllChildrenLocalTransformValuesMatch(false, ChildrenTransformCheckType.Late_Join_Client); // This validates each sub-child instance has preserved their local space values - AllChildrenLocalTransformValuesMatch(true); - } - - /// - /// Validates that moving, rotating, and scaling the authority side with a single - /// tick will properly synchronize the non-authoritative side with the same values. - /// - private void MoveRotateAndScaleAuthority(Vector3 position, Vector3 rotation, Vector3 scale, OverrideState overrideState) - { - switch (overrideState) - { - case OverrideState.SetState: - { - var authoritativeRotation = m_AuthoritativeTransform.GetSpaceRelativeRotation(); - authoritativeRotation.eulerAngles = rotation; - if (m_Authority == Authority.OwnerAuthority) - { - // Under the scenario where the owner is not the server, and non-auth is the server we set the state from the server - // to be updated to the owner. - if (m_AuthoritativeTransform.IsOwner && !m_AuthoritativeTransform.IsServer && m_NonAuthoritativeTransform.IsServer) - { - m_NonAuthoritativeTransform.SetState(position, authoritativeRotation, scale); - } - else - { - m_AuthoritativeTransform.SetState(position, authoritativeRotation, scale); - } - } - else - { - m_AuthoritativeTransform.SetState(position, authoritativeRotation, scale); - } - - break; - } - case OverrideState.Update: - default: - { - m_AuthoritativeTransform.transform.position = position; - - var authoritativeRotation = m_AuthoritativeTransform.GetSpaceRelativeRotation(); - authoritativeRotation.eulerAngles = rotation; - m_AuthoritativeTransform.transform.rotation = authoritativeRotation; - m_AuthoritativeTransform.transform.localScale = scale; - break; - } - } - } - - /// - /// Waits until the next tick - /// - private void WaitForNextTick() - { - var currentTick = m_AuthoritativeTransform.NetworkManager.LocalTime.Tick; - while (m_AuthoritativeTransform.NetworkManager.LocalTime.Tick == currentTick) - { - var frameRate = Application.targetFrameRate; - if (frameRate <= 0) - { - frameRate = 60; - } - var frameDuration = 1f / frameRate; - TimeTravel(frameDuration, 1); - } - } - - // The number of iterations to change position, rotation, and scale for NetworkTransformMultipleChangesOverTime - private const int k_PositionRotationScaleIterations = 3; - private const int k_PositionRotationScaleIterations3Axis = 8; - - protected override void OnNewClientCreated(NetworkManager networkManager) - { - networkManager.NetworkConfig.Prefabs = m_ServerNetworkManager.NetworkConfig.Prefabs; - networkManager.NetworkConfig.TickRate = k_TickRate; - base.OnNewClientCreated(networkManager); - } - - private Precision m_Precision = Precision.Full; - private float m_CurrentHalfPrecision = 0.0f; - private const float k_HalfPrecisionPosScale = 0.041f; - private const float k_HalfPrecisionRot = 0.725f; - - protected override float GetDeltaVarianceThreshold() - { - if (m_Precision == Precision.Half) - { - return m_CurrentHalfPrecision; - } - return base.GetDeltaVarianceThreshold(); - } - - - private Axis m_CurrentAxis; - - private bool m_AxisExcluded; - - /// - /// Randomly determine if an axis should be excluded. - /// If so, then randomly pick one of the axis to be excluded. - /// - private Vector3 RandomlyExcludeAxis(Vector3 delta) - { - if (Random.Range(0.0f, 1.0f) >= 0.5f) - { - m_AxisExcluded = true; - var axisToIgnore = Random.Range(0, 2); - switch (axisToIgnore) - { - case 0: - { - delta.x = 0; - break; - } - case 1: - { - delta.y = 0; - break; - } - case 2: - { - delta.z = 0; - break; - } - } - } - return delta; + AllChildrenLocalTransformValuesMatch(true, ChildrenTransformCheckType.Late_Join_Client); } /// @@ -749,8 +182,7 @@ namespace Unity.Netcode.RuntimeTests /// delta update, and it runs through 8 delta updates per unique test. /// [Test] - public void NetworkTransformMultipleChangesOverTime([Values] TransformSpace testLocalTransform, [Values] OverrideState overideState, - [Values] Precision precision, [Values] Rotation rotationSynch, [Values] Axis axis) + public void NetworkTransformMultipleChangesOverTime([Values] TransformSpace testLocalTransform, [Values] OverrideState overideState, [Values] Axis axis) { m_AuthoritativeTransform.InLocalSpace = testLocalTransform == TransformSpace.Local; bool axisX = axis == Axis.X || axis == Axis.XY || axis == Axis.XZ || axis == Axis.XYZ; @@ -767,12 +199,6 @@ namespace Unity.Netcode.RuntimeTests m_CurrentAxis = axis; - // Authority dictates what is synchronized and what the precision is going to be - // so we only need to set this on the authoritative side. - m_AuthoritativeTransform.UseHalfFloatPrecision = precision == Precision.Half; - m_AuthoritativeTransform.UseQuaternionSynchronization = rotationSynch == Rotation.Quaternion; - m_Precision = precision; - m_AuthoritativeTransform.SyncPositionX = axisX; m_AuthoritativeTransform.SyncPositionY = axisY; m_AuthoritativeTransform.SyncPositionZ = axisZ; @@ -810,7 +236,7 @@ namespace Unity.Netcode.RuntimeTests WaitForConditionOrTimeOutWithTimeTravel(() => m_AuthoritativeTransform.StatePushed); // Allow the precision settings to propagate first as changing precision // causes a teleport event to occur - WaitForNextTick(); + TimeTravelAdvanceTick(); var iterations = axisCount == 3 ? k_PositionRotationScaleIterations3Axis : k_PositionRotationScaleIterations; // Move and rotate within the same tick, validate the non-authoritative instance updates @@ -938,7 +364,6 @@ namespace Unity.Netcode.RuntimeTests var overrideUpdate = overideState == OverrideState.CommitToTransform; m_AuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; m_NonAuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; - m_AuthoritativeTransform.InLocalSpace = testLocalTransform == TransformSpace.Local; // test position @@ -946,28 +371,40 @@ namespace Unity.Netcode.RuntimeTests Assert.AreEqual(Vector3.zero, m_NonAuthoritativeTransform.transform.position, "server side pos should be zero at first"); // sanity check + TimeTravelAdvanceTick(); + m_AuthoritativeTransform.StatePushed = false; var nextPosition = GetRandomVector3(2f, 30f); - m_AuthoritativeTransform.transform.position = nextPosition; - if (overideState != OverrideState.SetState) + + switch (overideState) { - authPlayerTransform.position = nextPosition; - m_OwnerTransform.CommitToTransform(); - } - else - { - m_OwnerTransform.SetState(nextPosition, null, null, m_AuthoritativeTransform.Interpolate); + case OverrideState.Update: + { + m_AuthoritativeTransform.transform.position = nextPosition; + break; + } + case OverrideState.SetState: + { + m_OwnerTransform.SetState(nextPosition, null, null); + break; + } + case OverrideState.CommitToTransform: + { + m_OwnerTransform.transform.position = nextPosition; + m_OwnerTransform.CommitToTransform(); + break; + } } bool success; - if (overideState != OverrideState.Update) + if (overideState == OverrideState.CommitToTransform) { // Wait for the deltas to be pushed - success = WaitForConditionOrTimeOutWithTimeTravel(() => m_AuthoritativeTransform.StatePushed); + success = WaitForConditionOrTimeOutWithTimeTravel(() => m_AuthoritativeTransform.StatePushed, 600); Assert.True(success, $"[Position] Timed out waiting for state to be pushed ({m_AuthoritativeTransform.StatePushed})!"); } - success = WaitForConditionOrTimeOutWithTimeTravel(() => PositionsMatch()); + success = WaitForConditionOrTimeOutWithTimeTravel(() => PositionsMatch(), 600); Assert.True(success, $"Timed out waiting for positions to match {m_AuthoritativeTransform.transform.position} | {m_NonAuthoritativeTransform.transform.position}"); // test rotation @@ -975,214 +412,80 @@ namespace Unity.Netcode.RuntimeTests m_AuthoritativeTransform.StatePushed = false; var nextRotation = Quaternion.Euler(GetRandomVector3(5, 60)); // using euler angles instead of quaternions directly to really see issues users might encounter - if (overideState != OverrideState.SetState) + switch (overideState) { - authPlayerTransform.rotation = nextRotation; - m_OwnerTransform.CommitToTransform(); + case OverrideState.Update: + { + m_AuthoritativeTransform.transform.rotation = nextRotation; + break; + } + case OverrideState.SetState: + { + m_OwnerTransform.SetState(null, nextRotation, null); + break; + } + case OverrideState.CommitToTransform: + { + m_OwnerTransform.transform.rotation = nextRotation; + m_OwnerTransform.CommitToTransform(); + break; + } } - else - { - m_OwnerTransform.SetState(null, nextRotation, null, m_AuthoritativeTransform.Interpolate); - } - if (overideState != OverrideState.Update) + + if (overideState == OverrideState.CommitToTransform) { // Wait for the deltas to be pushed - success = WaitForConditionOrTimeOutWithTimeTravel(() => m_AuthoritativeTransform.StatePushed); + success = WaitForConditionOrTimeOutWithTimeTravel(() => m_AuthoritativeTransform.StatePushed, 600); Assert.True(success, $"[Rotation] Timed out waiting for state to be pushed ({m_AuthoritativeTransform.StatePushed})!"); } // Make sure the values match - success = WaitForConditionOrTimeOutWithTimeTravel(() => RotationsMatch()); + success = WaitForConditionOrTimeOutWithTimeTravel(() => RotationsMatch(), 600); Assert.True(success, $"Timed out waiting for rotations to match"); m_AuthoritativeTransform.StatePushed = false; var nextScale = GetRandomVector3(1, 6); - if (overrideUpdate) + + switch (overideState) { - authPlayerTransform.localScale = nextScale; - m_OwnerTransform.CommitToTransform(); + case OverrideState.Update: + { + m_AuthoritativeTransform.transform.localScale = nextScale; + break; + } + case OverrideState.SetState: + { + m_OwnerTransform.SetState(null, null, nextScale); + break; + } + case OverrideState.CommitToTransform: + { + m_OwnerTransform.transform.localScale = nextScale; + m_OwnerTransform.CommitToTransform(); + break; + } } - else - { - m_OwnerTransform.SetState(null, null, nextScale, m_AuthoritativeTransform.Interpolate); - } - if (overideState != OverrideState.Update) + + if (overideState == OverrideState.CommitToTransform) { // Wait for the deltas to be pushed - success = WaitForConditionOrTimeOutWithTimeTravel(() => m_AuthoritativeTransform.StatePushed); + success = WaitForConditionOrTimeOutWithTimeTravel(() => m_AuthoritativeTransform.StatePushed, 600); Assert.True(success, $"[Rotation] Timed out waiting for state to be pushed ({m_AuthoritativeTransform.StatePushed})!"); } // Make sure the scale values match - success = WaitForConditionOrTimeOutWithTimeTravel(() => ScaleValuesMatch()); + success = WaitForConditionOrTimeOutWithTimeTravel(() => ScaleValuesMatch(), 600); Assert.True(success, $"Timed out waiting for scale values to match"); } - /// - /// Test to verify nonAuthority cannot change the transform directly - /// - [Test] - public void VerifyNonAuthorityCantChangeTransform([Values] Interpolation interpolation, [Values] Precision precision) - { - m_AuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; - m_AuthoritativeTransform.UseHalfFloatPrecision = precision == Precision.Half; - m_AuthoritativeTransform.UseQuaternionSynchronization = true; - m_NonAuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; - m_NonAuthoritativeTransform.UseHalfFloatPrecision = precision == Precision.Half; - m_NonAuthoritativeTransform.UseQuaternionSynchronization = true; - - - Assert.AreEqual(Vector3.zero, m_NonAuthoritativeTransform.transform.position, "other side pos should be zero at first"); // sanity check - - m_NonAuthoritativeTransform.transform.position = new Vector3(4, 5, 6); - - WaitForNextTick(); - WaitForNextTick(); - - Assert.AreEqual(Vector3.zero, m_NonAuthoritativeTransform.transform.position, "[Position] NonAuthority was able to change the position!"); - - var nonAuthorityRotation = m_NonAuthoritativeTransform.transform.rotation; - var originalNonAuthorityEulerRotation = nonAuthorityRotation.eulerAngles; - var nonAuthorityEulerRotation = originalNonAuthorityEulerRotation; - // Verify rotation is not marked dirty when rotated by half of the threshold - nonAuthorityEulerRotation.y += 20.0f; - nonAuthorityRotation.eulerAngles = nonAuthorityEulerRotation; - m_NonAuthoritativeTransform.transform.rotation = nonAuthorityRotation; - WaitForNextTick(); - var nonAuthorityCurrentEuler = m_NonAuthoritativeTransform.transform.rotation.eulerAngles; - Assert.True(originalNonAuthorityEulerRotation.Equals(nonAuthorityCurrentEuler), "[Rotation] NonAuthority was able to change the rotation!"); - - var nonAuthorityScale = m_NonAuthoritativeTransform.transform.localScale; - m_NonAuthoritativeTransform.transform.localScale = nonAuthorityScale * 100; - - WaitForNextTick(); - - Assert.True(nonAuthorityScale.Equals(m_NonAuthoritativeTransform.transform.localScale), "[Scale] NonAuthority was able to change the scale!"); - } - - /// - /// Validates that rotation checks don't produce false positive - /// results when rolling over between 0 and 360 degrees - /// - [Test] - public void TestRotationThresholdDeltaCheck([Values] Interpolation interpolation, [Values] Precision precision) - { - m_AuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; - m_AuthoritativeTransform.UseHalfFloatPrecision = precision == Precision.Half; - m_AuthoritativeTransform.UseQuaternionSynchronization = true; - m_NonAuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; - m_NonAuthoritativeTransform.UseHalfFloatPrecision = precision == Precision.Half; - m_NonAuthoritativeTransform.UseQuaternionSynchronization = true; - m_NonAuthoritativeTransform.RotAngleThreshold = m_AuthoritativeTransform.RotAngleThreshold = 5.0f; - - var halfThreshold = m_AuthoritativeTransform.RotAngleThreshold * 0.5001f; - var authorityRotation = m_AuthoritativeTransform.transform.rotation; - var authorityEulerRotation = authorityRotation.eulerAngles; - - // Apply the current state which assures all bitset flags are updated - var results = m_AuthoritativeTransform.ApplyState(); - - // Verify rotation is not marked dirty when rotated by half of the threshold - authorityEulerRotation.y += halfThreshold; - authorityRotation.eulerAngles = authorityEulerRotation; - m_AuthoritativeTransform.transform.rotation = authorityRotation; - results = m_AuthoritativeTransform.ApplyState(); - Assert.IsFalse(results.isRotationDirty, $"Rotation is dirty when rotation threshold is {m_AuthoritativeTransform.RotAngleThreshold} degrees and only adjusted by {halfThreshold} degrees!"); - WaitForNextTick(); - - // Verify rotation is marked dirty when rotated by another half threshold value - authorityEulerRotation.y += halfThreshold; - authorityRotation.eulerAngles = authorityEulerRotation; - m_AuthoritativeTransform.transform.rotation = authorityRotation; - results = m_AuthoritativeTransform.ApplyState(); - Assert.IsTrue(results.isRotationDirty, $"Rotation was not dirty when rotated by the threshold value: {m_AuthoritativeTransform.RotAngleThreshold} degrees!"); - WaitForNextTick(); - - //Reset rotation back to zero on all axis - authorityRotation.eulerAngles = authorityEulerRotation = Vector3.zero; - m_AuthoritativeTransform.transform.rotation = authorityRotation; - WaitForNextTick(); - - // Rotate by 360 minus halfThreshold (which is really just negative halfThreshold) and verify rotation is not marked dirty - authorityEulerRotation.y = 360 - halfThreshold; - authorityRotation.eulerAngles = authorityEulerRotation; - m_AuthoritativeTransform.transform.rotation = authorityRotation; - results = m_AuthoritativeTransform.ApplyState(); - - Assert.IsFalse(results.isRotationDirty, $"Rotation is dirty when rotation threshold is {m_AuthoritativeTransform.RotAngleThreshold} degrees and only adjusted by " + - $"{Mathf.DeltaAngle(0, authorityEulerRotation.y)} degrees!"); - - authorityEulerRotation.y -= halfThreshold; - authorityRotation.eulerAngles = authorityEulerRotation; - m_AuthoritativeTransform.transform.rotation = authorityRotation; - results = m_AuthoritativeTransform.ApplyState(); - - Assert.IsTrue(results.isRotationDirty, $"Rotation was not dirty when rotated by {Mathf.DeltaAngle(0, authorityEulerRotation.y)} degrees!"); - - //Reset rotation back to zero on all axis - authorityRotation.eulerAngles = authorityEulerRotation = Vector3.zero; - m_AuthoritativeTransform.transform.rotation = authorityRotation; - WaitForNextTick(); - - authorityEulerRotation.y -= halfThreshold; - authorityRotation.eulerAngles = authorityEulerRotation; - m_AuthoritativeTransform.transform.rotation = authorityRotation; - results = m_AuthoritativeTransform.ApplyState(); - Assert.IsFalse(results.isRotationDirty, $"Rotation is dirty when rotation threshold is {m_AuthoritativeTransform.RotAngleThreshold} degrees and only adjusted by " + - $"{Mathf.DeltaAngle(0, authorityEulerRotation.y)} degrees!"); - - authorityEulerRotation.y -= halfThreshold; - authorityRotation.eulerAngles = authorityEulerRotation; - m_AuthoritativeTransform.transform.rotation = authorityRotation; - results = m_AuthoritativeTransform.ApplyState(); - - Assert.IsTrue(results.isRotationDirty, $"Rotation was not dirty when rotated by {Mathf.DeltaAngle(0, authorityEulerRotation.y)} degrees!"); - } - - private bool ValidateBitSetValues(NetworkTransform.NetworkTransformState serverState, NetworkTransform.NetworkTransformState clientState) - { - if (serverState.HasPositionX == clientState.HasPositionX && serverState.HasPositionY == clientState.HasPositionY && serverState.HasPositionZ == clientState.HasPositionZ && - serverState.HasRotAngleX == clientState.HasRotAngleX && serverState.HasRotAngleY == clientState.HasRotAngleY && serverState.HasRotAngleZ == clientState.HasRotAngleZ && - serverState.HasScaleX == clientState.HasScaleX && serverState.HasScaleY == clientState.HasScaleY && serverState.HasScaleZ == clientState.HasScaleZ) - { - return true; - } - return false; - } - - /// - /// Test to make sure that the bitset value is updated properly - /// - [Test] - public void TestBitsetValue([Values] Interpolation interpolation) - { - m_AuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; - m_NonAuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; - m_NonAuthoritativeTransform.RotAngleThreshold = m_AuthoritativeTransform.RotAngleThreshold = 0.1f; - WaitForNextTick(); - - m_AuthoritativeTransform.transform.rotation = Quaternion.Euler(1, 2, 3); - var serverLastSentState = m_AuthoritativeTransform.AuthorityLastSentState; - var clientReplicatedState = m_NonAuthoritativeTransform.LocalAuthoritativeNetworkState; - var success = WaitForConditionOrTimeOutWithTimeTravel(() => ValidateBitSetValues(serverLastSentState, clientReplicatedState)); - Assert.True(success, $"Timed out waiting for Authoritative Bitset state to equal NonAuthoritative replicated Bitset state!"); - - success = WaitForConditionOrTimeOutWithTimeTravel(() => RotationsMatch()); - Assert.True(success, $"[Timed-Out] Authoritative rotation {m_AuthoritativeTransform.transform.rotation.eulerAngles} != Non-Authoritative rotation {m_NonAuthoritativeTransform.transform.rotation.eulerAngles}"); - } - - private float m_DetectedPotentialInterpolatedTeleport; - /// /// The tests teleporting with and without interpolation /// [Test] - public void TeleportTest([Values] Interpolation interpolation, [Values] Precision precision) + public void TeleportTest([Values] Interpolation interpolation) { m_AuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; m_NonAuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; - m_AuthoritativeTransform.UseHalfFloatPrecision = precision == Precision.Half; - m_Precision = precision; var authTransform = m_AuthoritativeTransform.transform; var nonAuthPosition = m_NonAuthoritativeTransform.transform.position; var currentTick = m_AuthoritativeTransform.NetworkManager.ServerTime.Tick; @@ -1207,210 +510,5 @@ namespace Unity.Netcode.RuntimeTests Assert.IsTrue(Approximately(m_DetectedPotentialInterpolatedTeleport, 0.0f), $"Detected possible interpolation on non-authority side! NonAuthority distance: {m_DetectedPotentialInterpolatedTeleport} | Target distance: {targetDistance}"); } - /// - /// This test validates the method - /// usage for the non-authoritative side. It will either be the owner or the server making/requesting state changes. - /// This validates that: - /// - The owner authoritative mode can still be controlled by the server (i.e. owner authoritative with server authority override capabilities) - /// - The server authoritative mode can still be directed by the client owner. - /// - /// - /// This also tests that the original server authoritative model with client-owner driven NetworkTransforms is preserved. - /// - [Test] - public void NonAuthorityOwnerSettingStateTest([Values] Interpolation interpolation) - { - var interpolate = interpolation == Interpolation.EnableInterpolate; - m_AuthoritativeTransform.Interpolate = interpolate; - m_NonAuthoritativeTransform.Interpolate = interpolate; - m_NonAuthoritativeTransform.RotAngleThreshold = m_AuthoritativeTransform.RotAngleThreshold = 0.1f; - - // Test one parameter at a time first - var newPosition = new Vector3(125f, 35f, 65f); - var newRotation = Quaternion.Euler(1, 2, 3); - var newScale = new Vector3(2.0f, 2.0f, 2.0f); - m_NonAuthoritativeTransform.SetState(newPosition, null, null, interpolate); - var success = WaitForConditionOrTimeOutWithTimeTravel(() => PositionsMatchesValue(newPosition)); - Assert.True(success, $"Timed out waiting for non-authoritative position state request to be applied!"); - Assert.True(Approximately(newPosition, m_AuthoritativeTransform.transform.position), "Authoritative position does not match!"); - Assert.True(Approximately(newPosition, m_NonAuthoritativeTransform.transform.position), "Non-Authoritative position does not match!"); - - m_NonAuthoritativeTransform.SetState(null, newRotation, null, interpolate); - success = WaitForConditionOrTimeOutWithTimeTravel(() => RotationMatchesValue(newRotation.eulerAngles)); - Assert.True(success, $"Timed out waiting for non-authoritative rotation state request to be applied!"); - Assert.True(Approximately(newRotation.eulerAngles, m_AuthoritativeTransform.transform.rotation.eulerAngles), "Authoritative rotation does not match!"); - Assert.True(Approximately(newRotation.eulerAngles, m_NonAuthoritativeTransform.transform.rotation.eulerAngles), "Non-Authoritative rotation does not match!"); - - m_NonAuthoritativeTransform.SetState(null, null, newScale, interpolate); - success = WaitForConditionOrTimeOutWithTimeTravel(() => ScaleMatchesValue(newScale)); - Assert.True(success, $"Timed out waiting for non-authoritative scale state request to be applied!"); - Assert.True(Approximately(newScale, m_AuthoritativeTransform.transform.localScale), "Authoritative scale does not match!"); - Assert.True(Approximately(newScale, m_NonAuthoritativeTransform.transform.localScale), "Non-Authoritative scale does not match!"); - - // Test all parameters at once - newPosition = new Vector3(55f, 95f, -25f); - newRotation = Quaternion.Euler(20, 5, 322); - newScale = new Vector3(0.5f, 0.5f, 0.5f); - - m_NonAuthoritativeTransform.SetState(newPosition, newRotation, newScale, interpolate); - success = WaitForConditionOrTimeOutWithTimeTravel(() => PositionRotationScaleMatches(newPosition, newRotation.eulerAngles, newScale)); - Assert.True(success, $"Timed out waiting for non-authoritative position, rotation, and scale state request to be applied!"); - Assert.True(Approximately(newPosition, m_AuthoritativeTransform.transform.position), "Authoritative position does not match!"); - Assert.True(Approximately(newPosition, m_NonAuthoritativeTransform.transform.position), "Non-Authoritative position does not match!"); - Assert.True(Approximately(newRotation.eulerAngles, m_AuthoritativeTransform.transform.rotation.eulerAngles), "Authoritative rotation does not match!"); - Assert.True(Approximately(newRotation.eulerAngles, m_NonAuthoritativeTransform.transform.rotation.eulerAngles), "Non-Authoritative rotation does not match!"); - Assert.True(Approximately(newScale, m_AuthoritativeTransform.transform.localScale), "Authoritative scale does not match!"); - Assert.True(Approximately(newScale, m_NonAuthoritativeTransform.transform.localScale), "Non-Authoritative scale does not match!"); - } - - private const float k_AproximateDeltaVariance = 0.025f; - private bool PositionsMatchesValue(Vector3 positionToMatch) - { - var authorityPosition = m_AuthoritativeTransform.transform.position; - var nonAuthorityPosition = m_NonAuthoritativeTransform.transform.position; - var auhtorityIsEqual = Approximately(authorityPosition, positionToMatch); - var nonauthorityIsEqual = Approximately(nonAuthorityPosition, positionToMatch); - - if (!auhtorityIsEqual) - { - VerboseDebug($"Authority position {authorityPosition} != position to match: {positionToMatch}!"); - } - if (!nonauthorityIsEqual) - { - VerboseDebug($"NonAuthority position {nonAuthorityPosition} != position to match: {positionToMatch}!"); - } - return auhtorityIsEqual && nonauthorityIsEqual; - } - - private bool RotationMatchesValue(Vector3 rotationEulerToMatch) - { - var authorityRotationEuler = m_AuthoritativeTransform.transform.rotation.eulerAngles; - var nonAuthorityRotationEuler = m_NonAuthoritativeTransform.transform.rotation.eulerAngles; - var auhtorityIsEqual = Approximately(authorityRotationEuler, rotationEulerToMatch); - var nonauthorityIsEqual = Approximately(nonAuthorityRotationEuler, rotationEulerToMatch); - - if (!auhtorityIsEqual) - { - VerboseDebug($"Authority rotation {authorityRotationEuler} != rotation to match: {rotationEulerToMatch}!"); - } - if (!nonauthorityIsEqual) - { - VerboseDebug($"NonAuthority rotation {nonAuthorityRotationEuler} != rotation to match: {rotationEulerToMatch}!"); - } - return auhtorityIsEqual && nonauthorityIsEqual; - } - - private bool ScaleMatchesValue(Vector3 scaleToMatch) - { - var authorityScale = m_AuthoritativeTransform.transform.localScale; - var nonAuthorityScale = m_NonAuthoritativeTransform.transform.localScale; - var auhtorityIsEqual = Approximately(authorityScale, scaleToMatch); - var nonauthorityIsEqual = Approximately(nonAuthorityScale, scaleToMatch); - - if (!auhtorityIsEqual) - { - VerboseDebug($"Authority scale {authorityScale} != scale to match: {scaleToMatch}!"); - } - if (!nonauthorityIsEqual) - { - VerboseDebug($"NonAuthority scale {nonAuthorityScale} != scale to match: {scaleToMatch}!"); - } - return auhtorityIsEqual && nonauthorityIsEqual; - } - - private bool TeleportPositionMatches(Vector3 nonAuthorityOriginalPosition) - { - var nonAuthorityPosition = m_NonAuthoritativeTransform.transform.position; - var authorityPosition = m_AuthoritativeTransform.transform.position; - var targetDistance = Mathf.Abs(Vector3.Distance(nonAuthorityOriginalPosition, authorityPosition)); - var nonAuthorityCurrentDistance = Mathf.Abs(Vector3.Distance(nonAuthorityPosition, nonAuthorityOriginalPosition)); - // If we are not within our target distance range - if (!Approximately(targetDistance, nonAuthorityCurrentDistance)) - { - // Apply the non-authority's distance that is checked at the end of the teleport test - m_DetectedPotentialInterpolatedTeleport = nonAuthorityCurrentDistance; - return false; - } - else - { - // Otherwise, if we are within our target distance range then reset any already set value - m_DetectedPotentialInterpolatedTeleport = 0.0f; - } - var xIsEqual = Approximately(authorityPosition.x, nonAuthorityPosition.x); - var yIsEqual = Approximately(authorityPosition.y, nonAuthorityPosition.y); - var zIsEqual = Approximately(authorityPosition.z, nonAuthorityPosition.z); - if (!xIsEqual || !yIsEqual || !zIsEqual) - { - VerboseDebug($"[{m_AuthoritativeTransform.gameObject.name}] Authority position {authorityPosition} != [{m_NonAuthoritativeTransform.gameObject.name}] NonAuthority position {nonAuthorityPosition}"); - } - return xIsEqual && yIsEqual && zIsEqual; - } - - private bool PositionRotationScaleMatches(Vector3 position, Vector3 eulerRotation, Vector3 scale) - { - return PositionsMatchesValue(position) && RotationMatchesValue(eulerRotation) && ScaleMatchesValue(scale); - } - - private bool PositionRotationScaleMatches() - { - return RotationsMatch() && PositionsMatch() && ScaleValuesMatch(); - } - - private void PrintPositionRotationScaleDeltas() - { - RotationsMatch(true); - PositionsMatch(true); - ScaleValuesMatch(true); - } - - private bool RotationsMatch(bool printDeltas = false) - { - m_CurrentHalfPrecision = k_HalfPrecisionRot; - var authorityEulerRotation = m_AuthoritativeTransform.GetSpaceRelativeRotation().eulerAngles; - var nonAuthorityEulerRotation = m_NonAuthoritativeTransform.GetSpaceRelativeRotation().eulerAngles; - var xIsEqual = ApproximatelyEuler(authorityEulerRotation.x, nonAuthorityEulerRotation.x) || !m_AuthoritativeTransform.SyncRotAngleX; - var yIsEqual = ApproximatelyEuler(authorityEulerRotation.y, nonAuthorityEulerRotation.y) || !m_AuthoritativeTransform.SyncRotAngleY; - var zIsEqual = ApproximatelyEuler(authorityEulerRotation.z, nonAuthorityEulerRotation.z) || !m_AuthoritativeTransform.SyncRotAngleZ; - if (!xIsEqual || !yIsEqual || !zIsEqual) - { - VerboseDebug($"[{m_AuthoritativeTransform.gameObject.name}][X-{xIsEqual} | Y-{yIsEqual} | Z-{zIsEqual}][{m_CurrentAxis}]" + - $"[Sync: X-{m_AuthoritativeTransform.SyncRotAngleX} | X-{m_AuthoritativeTransform.SyncRotAngleY} | X-{m_AuthoritativeTransform.SyncRotAngleZ}] Authority rotation {authorityEulerRotation} != [{m_NonAuthoritativeTransform.gameObject.name}] NonAuthority rotation {nonAuthorityEulerRotation}"); - } - if (printDeltas) - { - Debug.Log($"[Rotation Match] Euler Delta {EulerDelta(authorityEulerRotation, nonAuthorityEulerRotation)}"); - } - return xIsEqual && yIsEqual && zIsEqual; - } - - private bool PositionsMatch(bool printDeltas = false) - { - m_CurrentHalfPrecision = k_HalfPrecisionPosScale; - var authorityPosition = m_AuthoritativeTransform.GetSpaceRelativePosition(); - var nonAuthorityPosition = m_NonAuthoritativeTransform.GetSpaceRelativePosition(); - var xIsEqual = Approximately(authorityPosition.x, nonAuthorityPosition.x) || !m_AuthoritativeTransform.SyncPositionX; - var yIsEqual = Approximately(authorityPosition.y, nonAuthorityPosition.y) || !m_AuthoritativeTransform.SyncPositionY; - var zIsEqual = Approximately(authorityPosition.z, nonAuthorityPosition.z) || !m_AuthoritativeTransform.SyncPositionZ; - if (!xIsEqual || !yIsEqual || !zIsEqual) - { - VerboseDebug($"[{m_AuthoritativeTransform.gameObject.name}] Authority position {authorityPosition} != [{m_NonAuthoritativeTransform.gameObject.name}] NonAuthority position {nonAuthorityPosition}"); - } - return xIsEqual && yIsEqual && zIsEqual; - } - - private bool ScaleValuesMatch(bool printDeltas = false) - { - m_CurrentHalfPrecision = k_HalfPrecisionPosScale; - var authorityScale = m_AuthoritativeTransform.transform.localScale; - var nonAuthorityScale = m_NonAuthoritativeTransform.transform.localScale; - var xIsEqual = Approximately(authorityScale.x, nonAuthorityScale.x) || !m_AuthoritativeTransform.SyncScaleX; - var yIsEqual = Approximately(authorityScale.y, nonAuthorityScale.y) || !m_AuthoritativeTransform.SyncScaleY; - var zIsEqual = Approximately(authorityScale.z, nonAuthorityScale.z) || !m_AuthoritativeTransform.SyncScaleZ; - if (!xIsEqual || !yIsEqual || !zIsEqual) - { - VerboseDebug($"[{m_AuthoritativeTransform.gameObject.name}] Authority scale {authorityScale} != [{m_NonAuthoritativeTransform.gameObject.name}] NonAuthority scale {nonAuthorityScale}"); - } - return xIsEqual && yIsEqual && zIsEqual; - } } } diff --git a/Tests/Runtime/NetworkVariableTests.cs b/Tests/Runtime/NetworkVariableTests.cs index 02afdc8..8c17d1b 100644 --- a/Tests/Runtime/NetworkVariableTests.cs +++ b/Tests/Runtime/NetworkVariableTests.cs @@ -2769,4 +2769,71 @@ namespace Unity.Netcode.RuntimeTests Assert.IsFalse(s_GlobalTimeoutHelper.TimedOut, nameof(CheckTestObjectComponentValuesOnAll)); } } + + public class NetvarDespawnShutdown : NetworkBehaviour + { + private NetworkVariable m_IntNetworkVariable = new NetworkVariable(); + private NetworkList m_IntList; + + private void Awake() + { + m_IntList = new NetworkList(); + } + + public override void OnNetworkDespawn() + { + if (IsServer) + { + m_IntNetworkVariable.Value = 5; + for (int i = 0; i < 10; i++) + { + m_IntList.Add(i); + } + } + base.OnNetworkDespawn(); + } + } + + /// + /// Validates that setting values for NetworkVariable or NetworkList during the + /// OnNetworkDespawn method will not cause an exception to occur. + /// + public class NetworkVariableModifyOnNetworkDespawn : NetcodeIntegrationTest + { + protected override int NumberOfClients => 1; + + private GameObject m_TestPrefab; + + protected override void OnServerAndClientsCreated() + { + m_TestPrefab = CreateNetworkObjectPrefab("NetVarDespawn"); + m_TestPrefab.AddComponent(); + base.OnServerAndClientsCreated(); + } + + private bool OnClientSpawnedTestPrefab(ulong networkObjectId) + { + var clientId = m_ClientNetworkManagers[0].LocalClientId; + if (!s_GlobalNetworkObjects.ContainsKey(clientId)) + { + return false; + } + + if (!s_GlobalNetworkObjects[clientId].ContainsKey(networkObjectId)) + { + return false; + } + + return true; + } + + [UnityTest] + public IEnumerator ModifyNetworkVariableOrListOnNetworkDespawn() + { + var instance = SpawnObject(m_TestPrefab, m_ServerNetworkManager); + yield return WaitForConditionOrTimeOut(() => OnClientSpawnedTestPrefab(instance.GetComponent().NetworkObjectId)); + m_ServerNetworkManager.Shutdown(); + // As long as no excetptions occur, the test passes. + } + } } diff --git a/Tests/Runtime/PeerDisconnectCallbackTests.cs b/Tests/Runtime/PeerDisconnectCallbackTests.cs new file mode 100644 index 0000000..227b72e --- /dev/null +++ b/Tests/Runtime/PeerDisconnectCallbackTests.cs @@ -0,0 +1,185 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using Unity.Netcode.TestHelpers.Runtime; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests +{ + /// + /// Validates the client disconnection process. + /// This assures that: + /// - When a client disconnects from the server that the server: + /// -- Detects the client disconnected. + /// -- Cleans up the transport to NGO client (and vice versa) mappings. + /// - When a server disconnects a client that: + /// -- The client detects this disconnection. + /// -- The server cleans up the transport to NGO client (and vice versa) mappings. + /// - When the server-side player object is destroyed + /// - When the server-side player object ownership is transferred back to the server + /// + [TestFixture(HostOrServer.Server)] + [TestFixture(HostOrServer.Host)] + public class PeerDisconnectCallbackTests : NetcodeIntegrationTest + { + + public enum ClientDisconnectType + { + ServerDisconnectsClient, + ClientDisconnectsFromServer + } + + protected override int NumberOfClients => 3; + + private int m_ClientDisconnectCount; + private int m_PeerDisconnectCount; + + + public PeerDisconnectCallbackTests(HostOrServer hostOrServer) + : base(hostOrServer) + { + } + + protected override void OnServerAndClientsCreated() + { + // Adjusting client and server timeout periods to reduce test time + // Get the tick frequency in milliseconds and triple it for the heartbeat timeout + var heartBeatTimeout = (int)(300 * (1.0f / m_ServerNetworkManager.NetworkConfig.TickRate)); + var unityTransport = m_ServerNetworkManager.NetworkConfig.NetworkTransport as Transports.UTP.UnityTransport; + if (unityTransport != null) + { + unityTransport.HeartbeatTimeoutMS = heartBeatTimeout; + } + + unityTransport = m_ClientNetworkManagers[0].NetworkConfig.NetworkTransport as Transports.UTP.UnityTransport; + if (unityTransport != null) + { + unityTransport.HeartbeatTimeoutMS = heartBeatTimeout; + } + + base.OnServerAndClientsCreated(); + } + + protected override IEnumerator OnSetup() + { + m_ClientDisconnectCount = 0; + m_PeerDisconnectCount = 0; + return base.OnSetup(); + } + + private void OnConnectionEventCallback(NetworkManager networkManager, ConnectionEventData data) + { + switch (data.EventType) + { + case ConnectionEvent.ClientDisconnected: + Assert.IsFalse(data.PeerClientIds.IsCreated); + ++m_ClientDisconnectCount; + break; + case ConnectionEvent.PeerDisconnected: + Assert.IsFalse(data.PeerClientIds.IsCreated); + ++m_PeerDisconnectCount; + break; + } + } + + [UnityTest] + public IEnumerator TestPeerDisconnectCallback([Values] ClientDisconnectType clientDisconnectType, [Values(1ul, 2ul, 3ul)] ulong disconnectedClient) + { + foreach (var client in m_ClientNetworkManagers) + { + client.OnConnectionEvent += OnConnectionEventCallback; + if (m_UseHost) + { + Assert.IsTrue(client.ConnectedClientsIds.Contains(0ul)); + } + Assert.IsTrue(client.ConnectedClientsIds.Contains(1ul)); + Assert.IsTrue(client.ConnectedClientsIds.Contains(2ul)); + Assert.IsTrue(client.ConnectedClientsIds.Contains(3ul)); + Assert.AreEqual(client.ServerIsHost, m_UseHost); + } + m_ServerNetworkManager.OnConnectionEvent += OnConnectionEventCallback; + if (m_UseHost) + { + Assert.IsTrue(m_ServerNetworkManager.ConnectedClientsIds.Contains(0ul)); + } + Assert.IsTrue(m_ServerNetworkManager.ConnectedClientsIds.Contains(1ul)); + Assert.IsTrue(m_ServerNetworkManager.ConnectedClientsIds.Contains(2ul)); + Assert.IsTrue(m_ServerNetworkManager.ConnectedClientsIds.Contains(3ul)); + Assert.AreEqual(m_ServerNetworkManager.ServerIsHost, m_UseHost); + + // Set up a WaitForMessageReceived hook. + // In some cases the message will be received during StopOneClient, but it is not guaranteed + // So we start the listener before we call Stop so it will be noticed regardless of whether it happens + // during StopOneClient or whether we have to wait for it + var messageHookEntriesForSpawn = new List(); + foreach (var clientNetworkManager in m_ClientNetworkManagers.Where(c => c.LocalClientId != disconnectedClient)) + { + var messageHook = new MessageHookEntry(clientNetworkManager); + messageHook.AssignMessageType(); + messageHookEntriesForSpawn.Add(messageHook); + } + + // Used to determine if all clients received the CreateObjectMessage + var hooks = new MessageHooksConditional(messageHookEntriesForSpawn); + + if (clientDisconnectType == ClientDisconnectType.ServerDisconnectsClient) + { + m_ServerNetworkManager.DisconnectClient(disconnectedClient); + } + else + { + yield return StopOneClient(m_ClientNetworkManagers[disconnectedClient - 1]); + } + + yield return WaitForConditionOrTimeOut(hooks); + Assert.False(s_GlobalTimeoutHelper.TimedOut); + + foreach (var client in m_ClientNetworkManagers) + { + if (!client.IsConnectedClient) + { + Assert.IsEmpty(client.ConnectedClientsIds); + continue; + } + if (m_UseHost) + { + Assert.IsTrue(client.ConnectedClientsIds.Contains(0ul)); + } + + for (var i = 1ul; i < 3ul; ++i) + { + if (i == disconnectedClient) + { + Assert.IsFalse(client.ConnectedClientsIds.Contains(i)); + } + else + { + Assert.IsTrue(client.ConnectedClientsIds.Contains(i)); + } + } + } + if (m_UseHost) + { + Assert.IsTrue(m_ServerNetworkManager.ConnectedClientsIds.Contains(0ul)); + } + + for (var i = 1ul; i < 3ul; ++i) + { + if (i == disconnectedClient) + { + Assert.IsFalse(m_ServerNetworkManager.ConnectedClientsIds.Contains(i)); + } + else + { + Assert.IsTrue(m_ServerNetworkManager.ConnectedClientsIds.Contains(i)); + } + } + + // If disconnected, the server and the client that disconnected will be notified + Assert.AreEqual(2, m_ClientDisconnectCount); + // Host receives peer disconnect, dedicated server does not + Assert.AreEqual(m_UseHost ? 3 : 2, m_PeerDisconnectCount); + } + } +} diff --git a/Tests/Runtime/PeerDisconnectCallbackTests.cs.meta b/Tests/Runtime/PeerDisconnectCallbackTests.cs.meta new file mode 100644 index 0000000..2e6247f --- /dev/null +++ b/Tests/Runtime/PeerDisconnectCallbackTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 283ae7cc9a0641c8b49dacad79242287 +timeCreated: 1699978638 \ No newline at end of file diff --git a/Tests/Runtime/RpcTests.cs b/Tests/Runtime/RpcTests.cs index 16d206a..b26d5b2 100644 --- a/Tests/Runtime/RpcTests.cs +++ b/Tests/Runtime/RpcTests.cs @@ -62,7 +62,6 @@ namespace Unity.Netcode.RuntimeTests } #endif - [ClientRpc] public void MyClientRpc() { diff --git a/Tests/Runtime/StopStartRuntimeTests.cs b/Tests/Runtime/StopStartRuntimeTests.cs index 16fa90e..8d52f1f 100644 --- a/Tests/Runtime/StopStartRuntimeTests.cs +++ b/Tests/Runtime/StopStartRuntimeTests.cs @@ -1,7 +1,6 @@ using System.Collections; using NUnit.Framework; using Unity.Netcode.TestHelpers.Runtime; -using UnityEngine; using UnityEngine.TestTools; namespace Unity.Netcode.RuntimeTests @@ -16,15 +15,19 @@ namespace Unity.Netcode.RuntimeTests base.OnOneTimeSetup(); } + + private bool m_ServerStopped; [UnityTest] public IEnumerator WhenShuttingDownAndRestarting_SDKRestartsSuccessfullyAndStaysRunning() { // shutdown the server + m_ServerNetworkManager.OnServerStopped += OnServerStopped; m_ServerNetworkManager.Shutdown(); - // wait 1 frame because shutdowns are delayed - var nextFrameNumber = Time.frameCount + 1; - yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber); + // wait until the OnServerStopped is invoked + m_ServerStopped = false; + yield return WaitForConditionOrTimeOut(() => m_ServerStopped); + AssertOnTimeout("Timed out waiting for the server to stop!"); // Verify the shutdown occurred Assert.IsFalse(m_ServerNetworkManager.IsServer); @@ -45,15 +48,23 @@ namespace Unity.Netcode.RuntimeTests Assert.IsTrue(m_ServerNetworkManager.IsListening); } + private void OnServerStopped(bool obj) + { + m_ServerNetworkManager.OnServerStopped -= OnServerStopped; + m_ServerStopped = true; + } + [UnityTest] public IEnumerator WhenShuttingDownTwiceAndRestarting_SDKRestartsSuccessfullyAndStaysRunning() { // shutdown the server + m_ServerNetworkManager.OnServerStopped += OnServerStopped; m_ServerNetworkManager.Shutdown(); - // wait 1 frame because shutdowns are delayed - var nextFrameNumber = Time.frameCount + 1; - yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber); + // wait until the OnServerStopped is invoked + m_ServerStopped = false; + yield return WaitForConditionOrTimeOut(() => m_ServerStopped); + AssertOnTimeout("Timed out waiting for the server to stop!"); // Verify the shutdown occurred Assert.IsFalse(m_ServerNetworkManager.IsServer); diff --git a/Tests/Runtime/Transports/UnityTransportTests.cs b/Tests/Runtime/Transports/UnityTransportTests.cs index 85bdea8..d2dcddf 100644 --- a/Tests/Runtime/Transports/UnityTransportTests.cs +++ b/Tests/Runtime/Transports/UnityTransportTests.cs @@ -192,19 +192,31 @@ namespace Unity.Netcode.RuntimeTests yield return WaitForNetworkEvent(NetworkEvent.Connect, m_Client1Events); - var data1 = new ArraySegment(new byte[] { 11 }); - m_Client1.Send(m_Client1.ServerClientId, data1, delivery); + var data1 = new byte[10]; + data1[0] = 11; + m_Client1.Send(m_Client1.ServerClientId, new ArraySegment(data1), delivery); - var data2 = new ArraySegment(new byte[] { 22 }); - m_Client1.Send(m_Client1.ServerClientId, data2, delivery); + var data2 = new byte[3000]; + data2[0] = 22; + m_Client1.Send(m_Client1.ServerClientId, new ArraySegment(data2), delivery); + + var data3 = new byte[10]; + data3[0] = 33; + m_Client1.Send(m_Client1.ServerClientId, new ArraySegment(data3), delivery); yield return WaitForNetworkEvent(NetworkEvent.Data, m_ServerEvents); - Assert.AreEqual(3, m_ServerEvents.Count); - Assert.AreEqual(NetworkEvent.Data, m_ServerEvents[2].Type); + Assert.AreEqual(4, m_ServerEvents.Count); + Assert.AreEqual(NetworkEvent.Data, m_ServerEvents[3].Type); Assert.AreEqual(11, m_ServerEvents[1].Data.First()); + Assert.AreEqual(10, m_ServerEvents[1].Data.Count); + Assert.AreEqual(22, m_ServerEvents[2].Data.First()); + Assert.AreEqual(3000, m_ServerEvents[2].Data.Count); + + Assert.AreEqual(33, m_ServerEvents[3].Data.First()); + Assert.AreEqual(10, m_ServerEvents[3].Data.Count); yield return null; } diff --git a/Tests/Runtime/UniversalRpcTests.cs b/Tests/Runtime/UniversalRpcTests.cs new file mode 100644 index 0000000..49a1e47 --- /dev/null +++ b/Tests/Runtime/UniversalRpcTests.cs @@ -0,0 +1,1917 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using NUnit.Framework; +using Unity.Collections; +using Unity.Netcode.TestHelpers.Runtime; +using UnityEngine; +using Object = UnityEngine.Object; +using Random = System.Random; + +// NOTE: +// Unity's test runner cannot handle a single test fixture with thousands of tests in it. +// Since this file contains thousands of tests (once all parameters have been taken into account), +// I had to split up the tests into separate fixtures for each test case. +// That was the only way to get Unity to actually be able to handle this number of tests. +// I put them in their own namespace so they would be easier to navigate in the test list. +namespace Unity.Netcode.RuntimeTests.UniversalRpcTests +{ + public class UniversalRpcNetworkBehaviour : NetworkBehaviour + { + public bool Stop = false; + public string Received = string.Empty; + public Tuple ReceivedParams = null; + public ulong ReceivedFrom = ulong.MaxValue; + + public void OnRpcReceived() + { + var st = new StackTrace(); + var sf = st.GetFrame(1); + + var currentMethod = sf.GetMethod(); + Received = currentMethod.Name; + } + public void OnRpcReceivedWithParams(int a, bool b, float f, string s) + { + var st = new StackTrace(); + var sf = st.GetFrame(1); + + var currentMethod = sf.GetMethod(); + Received = currentMethod.Name; + ReceivedParams = new Tuple(a, b, f, s); + } + + // Basic RPCs + + [Rpc(SendTo.Everyone)] + public void DefaultToEveryoneRpc() + { + OnRpcReceived(); + } + + [Rpc(SendTo.Me)] + public void DefaultToMeRpc() + { + OnRpcReceived(); + } + + [Rpc(SendTo.Owner)] + public void DefaultToOwnerRpc() + { + OnRpcReceived(); + } + + [Rpc(SendTo.NotOwner)] + public void DefaultToNotOwnerRpc() + { + OnRpcReceived(); + } + + [Rpc(SendTo.Server)] + public void DefaultToServerRpc() + { + OnRpcReceived(); + } + + [Rpc(SendTo.NotMe)] + public void DefaultToNotMeRpc() + { + OnRpcReceived(); + } + + [Rpc(SendTo.NotServer)] + public void DefaultToNotServerRpc() + { + OnRpcReceived(); + } + + [Rpc(SendTo.ClientsAndHost)] + public void DefaultToClientsAndHostRpc() + { + OnRpcReceived(); + } + + // RPCs with parameters + + [Rpc(SendTo.Everyone)] + public void DefaultToEveryoneWithParamsRpc(int i, bool b, float f, string s) + { + OnRpcReceivedWithParams(i, b, f, s); + } + + [Rpc(SendTo.Me)] + public void DefaultToMeWithParamsRpc(int i, bool b, float f, string s) + { + OnRpcReceivedWithParams(i, b, f, s); + } + + [Rpc(SendTo.Owner)] + public void DefaultToOwnerWithParamsRpc(int i, bool b, float f, string s) + { + OnRpcReceivedWithParams(i, b, f, s); + } + + [Rpc(SendTo.NotOwner)] + public void DefaultToNotOwnerWithParamsRpc(int i, bool b, float f, string s) + { + OnRpcReceivedWithParams(i, b, f, s); + } + + [Rpc(SendTo.Server)] + public void DefaultToServerWithParamsRpc(int i, bool b, float f, string s) + { + OnRpcReceivedWithParams(i, b, f, s); + } + + [Rpc(SendTo.NotMe)] + public void DefaultToNotMeWithParamsRpc(int i, bool b, float f, string s) + { + OnRpcReceivedWithParams(i, b, f, s); + } + + [Rpc(SendTo.NotServer)] + public void DefaultToNotServerWithParamsRpc(int i, bool b, float f, string s) + { + OnRpcReceivedWithParams(i, b, f, s); + } + + [Rpc(SendTo.ClientsAndHost)] + public void DefaultToClientsAndHostWithParamsRpc(int i, bool b, float f, string s) + { + OnRpcReceivedWithParams(i, b, f, s); + } + + // RPCs with RPC parameters + + [Rpc(SendTo.Everyone)] + public void DefaultToEveryoneWithRpcParamsRpc(RpcParams rpcParams) + { + OnRpcReceived(); + ReceivedFrom = rpcParams.Receive.SenderClientId; + } + + [Rpc(SendTo.Me)] + public void DefaultToMeWithRpcParamsRpc(RpcParams rpcParams) + { + OnRpcReceived(); + ReceivedFrom = rpcParams.Receive.SenderClientId; + } + + [Rpc(SendTo.Owner)] + public void DefaultToOwnerWithRpcParamsRpc(RpcParams rpcParams) + { + OnRpcReceived(); + ReceivedFrom = rpcParams.Receive.SenderClientId; + } + + [Rpc(SendTo.NotOwner)] + public void DefaultToNotOwnerWithRpcParamsRpc(RpcParams rpcParams) + { + OnRpcReceived(); + ReceivedFrom = rpcParams.Receive.SenderClientId; + } + + [Rpc(SendTo.Server)] + public void DefaultToServerWithRpcParamsRpc(RpcParams rpcParams) + { + OnRpcReceived(); + ReceivedFrom = rpcParams.Receive.SenderClientId; + } + + [Rpc(SendTo.NotMe)] + public void DefaultToNotMeWithRpcParamsRpc(RpcParams rpcParams) + { + OnRpcReceived(); + ReceivedFrom = rpcParams.Receive.SenderClientId; + } + + [Rpc(SendTo.NotServer)] + public void DefaultToNotServerWithRpcParamsRpc(RpcParams rpcParams) + { + OnRpcReceived(); + ReceivedFrom = rpcParams.Receive.SenderClientId; + } + + [Rpc(SendTo.ClientsAndHost)] + public void DefaultToClientsAndHostWithRpcParamsRpc(RpcParams rpcParams) + { + OnRpcReceived(); + ReceivedFrom = rpcParams.Receive.SenderClientId; + } + + + // RPCs with parameters and RPC parameters + + [Rpc(SendTo.Everyone)] + public void DefaultToEveryoneWithParamsAndRpcParamsRpc(int i, bool b, float f, string s, RpcParams rpcParams) + { + OnRpcReceivedWithParams(i, b, f, s); + } + + [Rpc(SendTo.Me)] + public void DefaultToMeWithParamsAndRpcParamsRpc(int i, bool b, float f, string s, RpcParams rpcParams) + { + OnRpcReceivedWithParams(i, b, f, s); + } + + [Rpc(SendTo.Owner)] + public void DefaultToOwnerWithParamsAndRpcParamsRpc(int i, bool b, float f, string s, RpcParams rpcParams) + { + OnRpcReceivedWithParams(i, b, f, s); + } + + [Rpc(SendTo.NotOwner)] + public void DefaultToNotOwnerWithParamsAndRpcParamsRpc(int i, bool b, float f, string s, RpcParams rpcParams) + { + OnRpcReceivedWithParams(i, b, f, s); + } + + [Rpc(SendTo.Server)] + public void DefaultToServerWithParamsAndRpcParamsRpc(int i, bool b, float f, string s, RpcParams rpcParams) + { + OnRpcReceivedWithParams(i, b, f, s); + } + + [Rpc(SendTo.NotMe)] + public void DefaultToNotMeWithParamsAndRpcParamsRpc(int i, bool b, float f, string s, RpcParams rpcParams) + { + OnRpcReceivedWithParams(i, b, f, s); + } + + [Rpc(SendTo.NotServer)] + public void DefaultToNotServerWithParamsAndRpcParamsRpc(int i, bool b, float f, string s, RpcParams rpcParams) + { + OnRpcReceivedWithParams(i, b, f, s); + } + + [Rpc(SendTo.ClientsAndHost)] + public void DefaultToClientsAndHostWithParamsAndRpcParamsRpc(int i, bool b, float f, string s, RpcParams rpcParams) + { + OnRpcReceivedWithParams(i, b, f, s); + } + + // RPCs with AllowTargetOverride = true + + // AllowTargetOverried is implied with SpecifiedInParams and does not need to be stated + // Including it will cause a compiler warning + [Rpc(SendTo.SpecifiedInParams)] + public void DefaultToSpecifiedInParamsAllowOverrideRpc(RpcParams rpcParams) + { + OnRpcReceived(); + } + + [Rpc(SendTo.Everyone, AllowTargetOverride = true)] + public void DefaultToEveryoneAllowOverrideRpc(RpcParams rpcParams) + { + OnRpcReceived(); + } + + [Rpc(SendTo.Me, AllowTargetOverride = true)] + public void DefaultToMeAllowOverrideRpc(RpcParams rpcParams) + { + OnRpcReceived(); + } + + [Rpc(SendTo.Owner, AllowTargetOverride = true)] + public void DefaultToOwnerAllowOverrideRpc(RpcParams rpcParams) + { + OnRpcReceived(); + } + + [Rpc(SendTo.NotOwner, AllowTargetOverride = true)] + public void DefaultToNotOwnerAllowOverrideRpc(RpcParams rpcParams) + { + OnRpcReceived(); + } + + [Rpc(SendTo.Server, AllowTargetOverride = true)] + public void DefaultToServerAllowOverrideRpc(RpcParams rpcParams) + { + OnRpcReceived(); + } + + [Rpc(SendTo.NotMe, AllowTargetOverride = true)] + public void DefaultToNotMeAllowOverrideRpc(RpcParams rpcParams) + { + OnRpcReceived(); + } + + [Rpc(SendTo.NotServer, AllowTargetOverride = true)] + public void DefaultToNotServerAllowOverrideRpc(RpcParams rpcParams) + { + OnRpcReceived(); + } + + [Rpc(SendTo.ClientsAndHost, AllowTargetOverride = true)] + public void DefaultToClientsAndHostAllowOverrideRpc(RpcParams rpcParams) + { + OnRpcReceived(); + } + + // RPCs with DeferLocal = true + + [Rpc(SendTo.Everyone, DeferLocal = true)] + public void DefaultToEveryoneDeferLocalRpc(RpcParams rpcParams) + { + OnRpcReceived(); + } + + [Rpc(SendTo.Me, DeferLocal = true)] + public void DefaultToMeDeferLocalRpc(RpcParams rpcParams) + { + OnRpcReceived(); + } + + [Rpc(SendTo.Owner, DeferLocal = true)] + public void DefaultToOwnerDeferLocalRpc(RpcParams rpcParams) + { + OnRpcReceived(); + } + + [Rpc(SendTo.NotOwner, DeferLocal = true)] + public void DefaultToNotOwnerDeferLocalRpc(RpcParams rpcParams) + { + OnRpcReceived(); + } + + [Rpc(SendTo.Server, DeferLocal = true)] + public void DefaultToServerDeferLocalRpc(RpcParams rpcParams) + { + OnRpcReceived(); + } + + [Rpc(SendTo.NotServer, DeferLocal = true)] + public void DefaultToNotServerDeferLocalRpc(RpcParams rpcParams) + { + OnRpcReceived(); + } + + [Rpc(SendTo.ClientsAndHost, DeferLocal = true)] + public void DefaultToClientsAndHostDeferLocalRpc(RpcParams rpcParams) + { + OnRpcReceived(); + } + + // RPCs with RequireOwnership = true + + [Rpc(SendTo.Everyone, RequireOwnership = true)] + public void DefaultToEveryoneRequireOwnershipRpc() + { + OnRpcReceived(); + } + + [Rpc(SendTo.Me, RequireOwnership = true)] + public void DefaultToMeRequireOwnershipRpc() + { + OnRpcReceived(); + } + + [Rpc(SendTo.Owner, RequireOwnership = true)] + public void DefaultToOwnerRequireOwnershipRpc() + { + OnRpcReceived(); + } + + [Rpc(SendTo.NotOwner, RequireOwnership = true)] + public void DefaultToNotOwnerRequireOwnershipRpc() + { + OnRpcReceived(); + } + + [Rpc(SendTo.Server, RequireOwnership = true)] + public void DefaultToServerRequireOwnershipRpc() + { + OnRpcReceived(); + } + + [Rpc(SendTo.NotMe, RequireOwnership = true)] + public void DefaultToNotMeRequireOwnershipRpc() + { + OnRpcReceived(); + } + + [Rpc(SendTo.NotServer, RequireOwnership = true)] + public void DefaultToNotServerRequireOwnershipRpc() + { + OnRpcReceived(); + } + + [Rpc(SendTo.ClientsAndHost, RequireOwnership = true)] + public void DefaultToClientsAndHostRequireOwnershipRpc() + { + OnRpcReceived(); + } + + [Rpc(SendTo.SpecifiedInParams, RequireOwnership = true)] + public void SpecifiedInParamsRequireOwnershipRpc(RpcParams rpcParams) + { + OnRpcReceived(); + } + + + // Mutual RPC Recursion + + [Rpc(SendTo.Server, DeferLocal = true)] + public void MutualRecursionServerRpc() + { + if (Stop) + { + Stop = false; + return; + } + OnRpcReceived(); + MutualRecursionClientRpc(); + } + + [Rpc(SendTo.NotServer, DeferLocal = true)] + public void MutualRecursionClientRpc() + { + OnRpcReceived(); + MutualRecursionServerRpc(); + } + + // Self recursion + [Rpc(SendTo.Server, DeferLocal = true)] + public void SelfRecursiveRpc() + { + if (Stop) + { + Stop = false; + return; + } + OnRpcReceived(); + SelfRecursiveRpc(); + } + } + + public class UniversalRpcTestsBase : NetcodeIntegrationTest + { + protected override int NumberOfClients => 2; + + public UniversalRpcTestsBase(HostOrServer hostOrServer) : base(hostOrServer) + { + } + + + protected override NetworkManagerInstatiationMode OnSetIntegrationTestMode() + { + return NetworkManagerInstatiationMode.AllTests; + } + + protected override bool m_EnableTimeTravel => true; + + protected override bool m_SetupIsACoroutine => false; + protected override bool m_TearDownIsACoroutine => false; + + protected GameObject m_ServerObject; + + protected override void OnCreatePlayerPrefab() + { + m_PlayerPrefab.AddComponent(); + } + + protected override void OnServerAndClientsCreated() + { + m_ServerObject = new GameObject { name = "Server Object" }; + var networkObject = m_ServerObject.AddComponent(); + m_ServerObject.AddComponent(); + networkObject.NetworkManagerOwner = m_ServerNetworkManager; + NetcodeIntegrationTestHelpers.MakeNetworkObjectTestPrefab(networkObject); + m_ServerNetworkManager.AddNetworkPrefab(m_ServerObject); + foreach (var client in m_ClientNetworkManagers) + { + client.AddNetworkPrefab(m_ServerObject); + } + } + + protected override void OnInlineTearDown() + { + Clear(); + } + + protected void Clear() + { + foreach (var obj in Object.FindObjectsByType(FindObjectsSortMode.None)) + { + obj.Received = string.Empty; + obj.ReceivedParams = null; + obj.ReceivedFrom = ulong.MaxValue; + } + } + + protected override void OnOneTimeTearDown() + { + Object.DestroyImmediate(m_ServerObject); + } + + protected override void OnTimeTravelServerAndClientsConnected() + { + m_ServerObject.GetComponent().Spawn(); + WaitForMessageReceivedWithTimeTravel(m_ClientNetworkManagers.ToList()); + } + + protected UniversalRpcNetworkBehaviour GetPlayerObject(ulong ownerClientId, ulong onClient) + { + if (ownerClientId == NetworkManager.ServerClientId && !m_ServerNetworkManager.IsHost) + { + foreach (var obj in Object.FindObjectsByType(FindObjectsSortMode.None)) + { + if (obj.name.StartsWith("Server Object") && obj.OwnerClientId == ownerClientId && obj.NetworkManager.LocalClientId == onClient) + { + return obj; + } + } + } + + return m_PlayerNetworkObjects[onClient][ownerClientId].GetComponent(); + } + + protected void VerifyLocalReceived(ulong objectOwner, ulong sender, string name, bool verifyReceivedFrom) + { + var obj = GetPlayerObject(objectOwner, sender); + Assert.AreEqual(name, obj.Received); + Assert.IsNull(obj.ReceivedParams); + if (verifyReceivedFrom) + { + Assert.AreEqual(sender, obj.ReceivedFrom); + } + } + + protected void VerifyLocalReceivedWithParams(ulong objectOwner, ulong sender, string name, int i, bool b, float f, string s) + { + var obj = GetPlayerObject(objectOwner, sender); + Assert.AreEqual(name, obj.Received); + Assert.IsNotNull(obj.ReceivedParams); + Assert.AreEqual(i, obj.ReceivedParams.Item1); + Assert.AreEqual(b, obj.ReceivedParams.Item2); + Assert.AreEqual(f, obj.ReceivedParams.Item3); + Assert.AreEqual(s, obj.ReceivedParams.Item4); + } + + protected void VerifyNotReceived(ulong objectOwner, ulong[] receivedBy) + { + foreach (var client in receivedBy) + { + UniversalRpcNetworkBehaviour playerObject = GetPlayerObject(objectOwner, client); + Assert.AreEqual(string.Empty, playerObject.Received); + Assert.IsNull(playerObject.ReceivedParams); + } + } + + protected void VerifyRemoteReceived(ulong objectOwner, ulong sender, string message, ulong[] receivedBy, bool verifyReceivedFrom, bool waitForMessages = true) + { + foreach (var client in receivedBy) + { + if (client == sender) + { + VerifyLocalReceived(objectOwner, sender, message, verifyReceivedFrom); + + break; + } + } + + if (waitForMessages) + { + var needsProxyMessage = false; + var needsServerRpcMessage = false; + if (sender != 0) + { + foreach (var client in receivedBy) + { + if (client == sender) + { + continue; + } + + if (client != 0) + { + needsProxyMessage = true; + } + else + { + needsServerRpcMessage = true; + } + } + } + + if (needsProxyMessage) + { + var messages = new List { typeof(ProxyMessage) }; + if (needsServerRpcMessage) + { + messages.Add(typeof(RpcMessage)); + } + + WaitForMessagesReceivedWithTimeTravel(messages, new[] { m_ServerNetworkManager }.ToList()); + } + + var managersThatNeedToWaitForRpc = new List(); + if (needsServerRpcMessage && !needsProxyMessage) + { + managersThatNeedToWaitForRpc.Add(m_ServerNetworkManager); + } + + foreach (var client in receivedBy) + { + if (client != sender && client != 0) + { + managersThatNeedToWaitForRpc.Add(m_ClientNetworkManagers[client - 1]); + } + } + + WaitForMessageReceivedWithTimeTravel(managersThatNeedToWaitForRpc); + } + + foreach (var client in receivedBy) + { + UniversalRpcNetworkBehaviour playerObject = GetPlayerObject(objectOwner, client); + Assert.AreEqual(message, playerObject.Received); + Assert.IsNull(playerObject.ReceivedParams); + if (verifyReceivedFrom) + { + Assert.AreEqual(sender, playerObject.ReceivedFrom); + } + } + } + + protected void VerifyRemoteReceivedWithParams(ulong objectOwner, ulong sender, string message, ulong[] receivedBy, int i, bool b, float f, string s) + { + foreach (var client in receivedBy) + { + if (client == sender) + { + VerifyLocalReceivedWithParams(objectOwner, sender, message, i, b, f, s); + + break; + } + } + + var needsProxyMessage = false; + var needsServerRpcMessage = false; + if (sender != 0) + { + foreach (var client in receivedBy) + { + if (client == sender) + { + continue; + } + + if (client != 0) + { + needsProxyMessage = true; + } + else + { + needsServerRpcMessage = true; + } + } + } + + if (needsProxyMessage) + { + var messages = new List { typeof(ProxyMessage) }; + if (needsServerRpcMessage) + { + messages.Add(typeof(RpcMessage)); + } + + WaitForMessagesReceivedWithTimeTravel(messages, new[] { m_ServerNetworkManager }.ToList()); + } + + var managersThatNeedToWaitForRpc = new List(); + if (needsServerRpcMessage && !needsProxyMessage) + { + managersThatNeedToWaitForRpc.Add(m_ServerNetworkManager); + } + + foreach (var client in receivedBy) + { + if (client != sender && client != 0) + { + managersThatNeedToWaitForRpc.Add(m_ClientNetworkManagers[client - 1]); + } + } + + WaitForMessageReceivedWithTimeTravel(managersThatNeedToWaitForRpc); + + foreach (var client in receivedBy) + { + UniversalRpcNetworkBehaviour playerObject = GetPlayerObject(objectOwner, client); + Assert.AreEqual(message, playerObject.Received); + + Assert.IsNotNull(playerObject.ReceivedParams); + Assert.AreEqual(i, playerObject.ReceivedParams.Item1); + Assert.AreEqual(b, playerObject.ReceivedParams.Item2); + Assert.AreEqual(f, playerObject.ReceivedParams.Item3); + Assert.AreEqual(s, playerObject.ReceivedParams.Item4); + } + } + + protected static ulong[] s_ClientIds = new[] { 0ul, 1ul, 2ul }; + + public void VerifySentToEveryone(ulong objectOwner, ulong sender, string methodName) + { + VerifyRemoteReceived(objectOwner, sender, methodName, s_ClientIds, false); + } + + public void VerifySentToEveryoneWithReceivedFrom(ulong objectOwner, ulong sender, string methodName) + { + VerifyRemoteReceived(objectOwner, sender, methodName, s_ClientIds, true); + } + + public void VerifySentToEveryoneWithParams(ulong objectOwner, ulong sender, string methodName, int i, bool b, float f, string s) + { + VerifyRemoteReceivedWithParams(objectOwner, sender, methodName, s_ClientIds, i, b, f, s); + } + + public void VerifySentToId(ulong objectOwner, ulong sender, ulong receiver, string methodName, bool verifyReceivedFrom) + { + VerifyRemoteReceived(objectOwner, sender, methodName, new[] { receiver }, verifyReceivedFrom); + VerifyNotReceived(objectOwner, s_ClientIds.Where(c => c != receiver).ToArray()); + + // Pass some time to make sure that no other client ever receives this + TimeTravel(1f, 30); + VerifyNotReceived(objectOwner, s_ClientIds.Where(c => c != receiver).ToArray()); + } + + public void VerifySentToNotId(ulong objectOwner, ulong sender, ulong notReceiver, string methodName, bool verifyReceivedFrom) + { + VerifyNotReceived(objectOwner, new[] { notReceiver }); + VerifyRemoteReceived(objectOwner, sender, methodName, s_ClientIds.Where(c => c != notReceiver).ToArray(), verifyReceivedFrom); + // Verify again after all the waiting is finished + VerifyNotReceived(objectOwner, new[] { notReceiver }); + } + + public void VerifySentToIdWithParams(ulong objectOwner, ulong sender, ulong receiver, string methodName, int i, bool b, float f, string s) + { + VerifyRemoteReceivedWithParams(objectOwner, sender, methodName, new[] { receiver }, i, b, f, s); + VerifyNotReceived(objectOwner, s_ClientIds.Where(c => c != receiver).ToArray()); + + // Pass some time to make sure that no other client ever receives this + TimeTravel(1f, 30); + VerifyNotReceived(objectOwner, s_ClientIds.Where(c => c != receiver).ToArray()); + } + + public void VerifySentToNotIdWithParams(ulong objectOwner, ulong sender, ulong notReceiver, string methodName, int i, bool b, float f, string s) + { + VerifyNotReceived(objectOwner, new[] { notReceiver }); + VerifyRemoteReceivedWithParams(objectOwner, sender, methodName, s_ClientIds.Where(c => c != notReceiver).ToArray(), i, b, f, s); + // Verify again after all the waiting is finished + VerifyNotReceived(objectOwner, new[] { notReceiver }); + } + + public void VerifySentToOwner(ulong objectOwner, ulong sender, string methodName) + { + VerifySentToId(objectOwner, sender, objectOwner, methodName, false); + } + + public void VerifySentToNotOwner(ulong objectOwner, ulong sender, string methodName) + { + VerifySentToNotId(objectOwner, sender, objectOwner, methodName, false); + } + + public void VerifySentToServer(ulong objectOwner, ulong sender, string methodName) + { + VerifySentToId(objectOwner, sender, NetworkManager.ServerClientId, methodName, false); + } + + public void VerifySentToNotServer(ulong objectOwner, ulong sender, string methodName) + { + VerifySentToNotId(objectOwner, sender, NetworkManager.ServerClientId, methodName, false); + } + + public void VerifySentToClientsAndHost(ulong objectOwner, ulong sender, string methodName) + { + if (m_ServerNetworkManager.IsHost) + { + VerifySentToEveryone(objectOwner, sender, methodName); + } + else + { + VerifySentToNotServer(objectOwner, sender, methodName); + } + } + + public void VerifySentToMe(ulong objectOwner, ulong sender, string methodName) + { + VerifySentToId(objectOwner, sender, sender, methodName, false); + } + + public void VerifySentToNotMe(ulong objectOwner, ulong sender, string methodName) + { + VerifySentToNotId(objectOwner, sender, sender, methodName, false); + } + + public void VerifySentToOwnerWithReceivedFrom(ulong objectOwner, ulong sender, string methodName) + { + VerifySentToId(objectOwner, sender, objectOwner, methodName, true); + } + + public void VerifySentToNotOwnerWithReceivedFrom(ulong objectOwner, ulong sender, string methodName) + { + VerifySentToNotId(objectOwner, sender, objectOwner, methodName, true); + } + + public void VerifySentToServerWithReceivedFrom(ulong objectOwner, ulong sender, string methodName) + { + VerifySentToId(objectOwner, sender, NetworkManager.ServerClientId, methodName, true); + } + + public void VerifySentToNotServerWithReceivedFrom(ulong objectOwner, ulong sender, string methodName) + { + VerifySentToNotId(objectOwner, sender, NetworkManager.ServerClientId, methodName, true); + } + + public void VerifySentToClientsAndHostWithReceivedFrom(ulong objectOwner, ulong sender, string methodName) + { + if (m_ServerNetworkManager.IsHost) + { + VerifySentToEveryoneWithReceivedFrom(objectOwner, sender, methodName); + } + else + { + VerifySentToNotServerWithReceivedFrom(objectOwner, sender, methodName); + } + } + + public void VerifySentToMeWithReceivedFrom(ulong objectOwner, ulong sender, string methodName) + { + VerifySentToId(objectOwner, sender, sender, methodName, true); + } + + public void VerifySentToNotMeWithReceivedFrom(ulong objectOwner, ulong sender, string methodName) + { + VerifySentToNotId(objectOwner, sender, sender, methodName, true); + } + + public void VerifySentToOwnerWithParams(ulong objectOwner, ulong sender, string methodName, int i, bool b, float f, string s) + { + VerifySentToIdWithParams(objectOwner, sender, objectOwner, methodName, i, b, f, s); + } + + public void VerifySentToNotOwnerWithParams(ulong objectOwner, ulong sender, string methodName, int i, bool b, float f, string s) + { + VerifySentToNotIdWithParams(objectOwner, sender, objectOwner, methodName, i, b, f, s); + } + + public void VerifySentToServerWithParams(ulong objectOwner, ulong sender, string methodName, int i, bool b, float f, string s) + { + VerifySentToIdWithParams(objectOwner, sender, NetworkManager.ServerClientId, methodName, i, b, f, s); + } + + public void VerifySentToNotServerWithParams(ulong objectOwner, ulong sender, string methodName, int i, bool b, float f, string s) + { + VerifySentToNotIdWithParams(objectOwner, sender, NetworkManager.ServerClientId, methodName, i, b, f, s); + } + + public void VerifySentToClientsAndHostWithParams(ulong objectOwner, ulong sender, string methodName, int i, bool b, float f, string s) + { + if (m_ServerNetworkManager.IsHost) + { + VerifySentToEveryoneWithParams(objectOwner, sender, methodName, i, b, f, s); + } + else + { + VerifySentToNotServerWithParams(objectOwner, sender, methodName, i, b, f, s); + } + } + + public void VerifySentToMeWithParams(ulong objectOwner, ulong sender, string methodName, int i, bool b, float f, string s) + { + VerifySentToIdWithParams(objectOwner, sender, sender, methodName, i, b, f, s); + } + + public void VerifySentToNotMeWithParams(ulong objectOwner, ulong sender, string methodName, int i, bool b, float f, string s) + { + VerifySentToNotIdWithParams(objectOwner, sender, sender, methodName, i, b, f, s); + } + + public void RethrowTargetInvocationException(Action action) + { + try + { + action.Invoke(); + } + catch (TargetInvocationException e) + { + throw e.InnerException; + } + } + } + + [TestFixture(HostOrServer.Host)] + [TestFixture(HostOrServer.Server)] + internal class UniversalRpcTestSendingNoOverride : UniversalRpcTestsBase + { + public UniversalRpcTestSendingNoOverride(HostOrServer hostOrServer) : base(hostOrServer) + { + + } + + [Test] + public void TestSendingNoOverride( + // Excludes SendTo.SpecifiedInParams + [Values(SendTo.Everyone, SendTo.Me, SendTo.Owner, SendTo.Server, SendTo.NotMe, SendTo.NotOwner, SendTo.NotServer, SendTo.ClientsAndHost)] SendTo sendTo, + [Values(0u, 1u, 2u)] ulong objectOwner, + [Values(0u, 1u, 2u)] ulong sender + ) + { + var sendMethodName = $"DefaultTo{sendTo}Rpc"; + var verifyMethodName = $"VerifySentTo{sendTo}"; + + var senderObject = GetPlayerObject(objectOwner, sender); + var sendMethod = senderObject.GetType().GetMethod(sendMethodName); + sendMethod.Invoke(senderObject, new object[] { }); + + var verifyMethod = GetType().GetMethod(verifyMethodName); + verifyMethod.Invoke(this, new object[] { objectOwner, sender, sendMethodName }); + } + + } + + [TestFixture(HostOrServer.Host)] + [TestFixture(HostOrServer.Server)] + internal class UniversalRpcTestSenderClientId : UniversalRpcTestsBase + { + public UniversalRpcTestSenderClientId(HostOrServer hostOrServer) : base(hostOrServer) + { + + } + + [Test] + public void TestSenderClientId( + // Excludes SendTo.SpecifiedInParams + [Values(SendTo.Everyone, SendTo.Me, SendTo.Owner, SendTo.Server, SendTo.NotMe, SendTo.NotOwner, SendTo.NotServer, SendTo.ClientsAndHost)] SendTo sendTo, + [Values(0u, 1u, 2u)] ulong objectOwner, + [Values(0u, 1u, 2u)] ulong sender + ) + { + var sendMethodName = $"DefaultTo{sendTo}WithRpcParamsRpc"; + var verifyMethodName = $"VerifySentTo{sendTo}WithReceivedFrom"; + + var senderObject = GetPlayerObject(objectOwner, sender); + var sendMethod = senderObject.GetType().GetMethod(sendMethodName); + sendMethod.Invoke(senderObject, new object[] { new RpcParams() }); + + var verifyMethod = GetType().GetMethod(verifyMethodName); + verifyMethod.Invoke(this, new object[] { objectOwner, sender, sendMethodName }); + } + + } + + [TestFixture(HostOrServer.Host)] + [TestFixture(HostOrServer.Server)] + internal class UniversalRpcTestSendingNoOverrideWithParams : UniversalRpcTestsBase + { + public UniversalRpcTestSendingNoOverrideWithParams(HostOrServer hostOrServer) : base(hostOrServer) + { + + } + + [Test] + public void TestSendingNoOverrideWithParams( + // Excludes SendTo.SpecifiedInParams + [Values(SendTo.Everyone, SendTo.Me, SendTo.Owner, SendTo.Server, SendTo.NotMe, SendTo.NotOwner, SendTo.NotServer, SendTo.ClientsAndHost)] SendTo sendTo, + [Values(0u, 1u, 2u)] ulong objectOwner, + [Values(0u, 1u, 2u)] ulong sender + ) + { + var rand = new Random(); + var i = rand.Next(); + var f = (float)rand.NextDouble(); + var b = rand.Next() % 2 == 1; + var s = ""; + var numChars = rand.Next() % 5 + 5; + const string chars = "abcdefghijklmnopqrstuvwxycABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-=_+[]{}\\|;':\",./<>?"; + for (var j = 0; j < numChars; ++j) + { + s += chars[rand.Next(chars.Length)]; + } + + var sendMethodName = $"DefaultTo{sendTo}WithParamsRpc"; + var verifyMethodName = $"VerifySentTo{sendTo}WithParams"; + + var senderObject = GetPlayerObject(objectOwner, sender); + var sendMethod = senderObject.GetType().GetMethod(sendMethodName); + sendMethod.Invoke(senderObject, new object[] { i, b, f, s }); + + var verifyMethod = GetType().GetMethod(verifyMethodName); + verifyMethod.Invoke(this, new object[] { objectOwner, sender, sendMethodName, i, b, f, s }); + } + + } + + [TestFixture(HostOrServer.Host)] + [TestFixture(HostOrServer.Server)] + internal class UniversalRpcTestSendingNoOverrideWithParamsAndRpcParams : UniversalRpcTestsBase + { + public UniversalRpcTestSendingNoOverrideWithParamsAndRpcParams(HostOrServer hostOrServer) : base(hostOrServer) + { + + } + + [Test] + public void TestSendingNoOverrideWithParamsAndRpcParams( + // Excludes SendTo.SpecifiedInParams + [Values(SendTo.Everyone, SendTo.Me, SendTo.Owner, SendTo.Server, SendTo.NotMe, SendTo.NotOwner, SendTo.NotServer, SendTo.ClientsAndHost)] SendTo sendTo, + [Values(0u, 1u, 2u)] ulong objectOwner, + [Values(0u, 1u, 2u)] ulong sender + ) + { + var rand = new Random(); + var i = rand.Next(); + var f = (float)rand.NextDouble(); + var b = rand.Next() % 2 == 1; + var s = ""; + var numChars = rand.Next() % 5 + 5; + const string chars = "abcdefghijklmnopqrstuvwxycABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-=_+[]{}\\|;':\",./<>?"; + for (var j = 0; j < numChars; ++j) + { + s += chars[rand.Next(chars.Length)]; + } + + var sendMethodName = $"DefaultTo{sendTo}WithParamsAndRpcParamsRpc"; + var verifyMethodName = $"VerifySentTo{sendTo}WithParams"; + + var senderObject = GetPlayerObject(objectOwner, sender); + var sendMethod = senderObject.GetType().GetMethod(sendMethodName); + sendMethod.Invoke(senderObject, new object[] { i, b, f, s, new RpcParams() }); + + var verifyMethod = GetType().GetMethod(verifyMethodName); + verifyMethod.Invoke(this, new object[] { objectOwner, sender, sendMethodName, i, b, f, s }); + } + + } + + [TestFixture(HostOrServer.Host)] + [TestFixture(HostOrServer.Server)] + internal class UniversalRpcTestRequireOwnership : UniversalRpcTestsBase + { + public UniversalRpcTestRequireOwnership(HostOrServer hostOrServer) : base(hostOrServer) + { + + } + + [Test] + public void TestRequireOwnership( + // Excludes SendTo.SpecifiedInParams + [Values(SendTo.Everyone, SendTo.Me, SendTo.Owner, SendTo.Server, SendTo.NotMe, SendTo.NotOwner, SendTo.NotServer, SendTo.ClientsAndHost)] SendTo sendTo, + [Values(0u, 1u, 2u)] ulong objectOwner, + [Values(0u, 1u, 2u)] ulong sender + ) + { + var sendMethodName = $"DefaultTo{sendTo}RequireOwnershipRpc"; + + var senderObject = GetPlayerObject(objectOwner, sender); + var sendMethod = senderObject.GetType().GetMethod(sendMethodName); + if (sender != objectOwner) + { + Assert.Throws(() => RethrowTargetInvocationException(() => sendMethod.Invoke(senderObject, new object[] { }))); + } + else + { + var verifyMethodName = $"VerifySentTo{sendTo}"; + sendMethod.Invoke(senderObject, new object[] { }); + + var verifyMethod = GetType().GetMethod(verifyMethodName); + verifyMethod.Invoke(this, new object[] { objectOwner, sender, sendMethodName }); + } + } + } + + [TestFixture(HostOrServer.Host)] + [TestFixture(HostOrServer.Server)] + internal class UniversalRpcTestDisallowedOverride : UniversalRpcTestsBase + { + public UniversalRpcTestDisallowedOverride(HostOrServer hostOrServer) : base(hostOrServer) + { + + } + + [Test] + public void TestDisallowedOverride( + // Excludes SendTo.SpecifiedInParams + [Values(SendTo.Everyone, SendTo.Me, SendTo.Owner, SendTo.Server, SendTo.NotMe, SendTo.NotOwner, SendTo.NotServer, SendTo.ClientsAndHost)] SendTo sendTo, + [Values(0u, 1u, 2u)] ulong objectOwner, + [Values(0u, 1u, 2u)] ulong sender) + { + var senderObject = GetPlayerObject(objectOwner, sender); + var methodName = $"DefaultTo{sendTo}WithRpcParamsRpc"; + var method = senderObject.GetType().GetMethod(methodName); + Assert.Throws(() => RethrowTargetInvocationException(() => method.Invoke(senderObject, new object[] { (RpcParams)senderObject.RpcTarget.Everyone }))); + Assert.Throws(() => RethrowTargetInvocationException(() => method.Invoke(senderObject, new object[] { (RpcParams)senderObject.RpcTarget.Owner }))); + Assert.Throws(() => RethrowTargetInvocationException(() => method.Invoke(senderObject, new object[] { (RpcParams)senderObject.RpcTarget.NotOwner }))); + Assert.Throws(() => RethrowTargetInvocationException(() => method.Invoke(senderObject, new object[] { (RpcParams)senderObject.RpcTarget.Server }))); + Assert.Throws(() => RethrowTargetInvocationException(() => method.Invoke(senderObject, new object[] { (RpcParams)senderObject.RpcTarget.NotServer }))); + Assert.Throws(() => RethrowTargetInvocationException(() => method.Invoke(senderObject, new object[] { (RpcParams)senderObject.RpcTarget.ClientsAndHost }))); + Assert.Throws(() => RethrowTargetInvocationException(() => method.Invoke(senderObject, new object[] { (RpcParams)senderObject.RpcTarget.Me }))); + Assert.Throws(() => RethrowTargetInvocationException(() => method.Invoke(senderObject, new object[] { (RpcParams)senderObject.RpcTarget.NotMe }))); + Assert.Throws(() => RethrowTargetInvocationException(() => method.Invoke(senderObject, new object[] { (RpcParams)senderObject.RpcTarget.Single(0, RpcTargetUse.Temp) }))); + Assert.Throws(() => RethrowTargetInvocationException(() => method.Invoke(senderObject, new object[] { (RpcParams)senderObject.RpcTarget.Not(0, RpcTargetUse.Temp) }))); + Assert.Throws(() => RethrowTargetInvocationException(() => method.Invoke(senderObject, new object[] { (RpcParams)senderObject.RpcTarget.Group(new[] { 0ul, 1ul, 2ul }, RpcTargetUse.Temp) }))); + Assert.Throws(() => RethrowTargetInvocationException(() => method.Invoke(senderObject, new object[] { (RpcParams)senderObject.RpcTarget.Not(new[] { 0ul, 1ul, 2ul }, RpcTargetUse.Temp) }))); + } + + } + + [TestFixture(HostOrServer.Host)] + [TestFixture(HostOrServer.Server)] + internal class UniversalRpcTestSendingWithTargetOverride : UniversalRpcTestsBase + { + public UniversalRpcTestSendingWithTargetOverride(HostOrServer hostOrServer) : base(hostOrServer) + { + + } + + [Test] + public void TestSendingWithTargetOverride( + [Values] SendTo defaultSendTo, + [Values(SendTo.Everyone, SendTo.Me, SendTo.Owner, SendTo.Server, SendTo.NotMe, SendTo.NotOwner, SendTo.NotServer, SendTo.ClientsAndHost)] SendTo overrideSendTo, + [Values(0u, 1u, 2u)] ulong objectOwner, + [Values(0u, 1u, 2u)] ulong sender + ) + { + var sendMethodName = $"DefaultTo{defaultSendTo}AllowOverrideRpc"; + var targetField = typeof(RpcTarget).GetField(overrideSendTo.ToString()); + var verifyMethodName = $"VerifySentTo{overrideSendTo}"; + + var senderObject = GetPlayerObject(objectOwner, sender); + var target = (BaseRpcTarget)targetField.GetValue(senderObject.RpcTarget); + var sendMethod = senderObject.GetType().GetMethod(sendMethodName); + sendMethod.Invoke(senderObject, new object[] { (RpcParams)target }); + + var verifyMethod = GetType().GetMethod(verifyMethodName); + verifyMethod.Invoke(this, new object[] { objectOwner, sender, sendMethodName }); + } + + + } + + [TestFixture(HostOrServer.Host)] + [TestFixture(HostOrServer.Server)] + internal class UniversalRpcTestSendingWithSingleOverride : UniversalRpcTestsBase + { + public UniversalRpcTestSendingWithSingleOverride(HostOrServer hostOrServer) : base(hostOrServer) + { + + } + + [Test] + public void TestSendingWithSingleOverride( + [Values] SendTo defaultSendTo, + [Values(0u, 1u, 2u)] ulong recipient, + [Values(0u, 1u, 2u)] ulong objectOwner, + [Values(0u, 1u, 2u)] ulong sender + ) + { + var sendMethodName = $"DefaultTo{defaultSendTo}AllowOverrideRpc"; + + var senderObject = GetPlayerObject(objectOwner, sender); + var target = senderObject.RpcTarget.Single(recipient, RpcTargetUse.Temp); + var sendMethod = senderObject.GetType().GetMethod(sendMethodName); + sendMethod.Invoke(senderObject, new object[] { (RpcParams)target }); + + VerifyRemoteReceived(objectOwner, sender, sendMethodName, new[] { recipient }, false); + VerifyNotReceived(objectOwner, s_ClientIds.Where(c => recipient != c).ToArray()); + + // Pass some time to make sure that no other client ever receives this + TimeTravel(1f, 30); + VerifyNotReceived(objectOwner, s_ClientIds.Where(c => recipient != c).ToArray()); + } + + } + + [TestFixture(HostOrServer.Host)] + [TestFixture(HostOrServer.Server)] + internal class UniversalRpcTestSendingWithSingleNotOverride : UniversalRpcTestsBase + { + public UniversalRpcTestSendingWithSingleNotOverride(HostOrServer hostOrServer) : base(hostOrServer) + { + + } + + [Test] + public void TestSendingWithSingleNotOverride( + [Values] SendTo defaultSendTo, + [Values(0u, 1u, 2u)] ulong recipient, + [Values(0u, 1u, 2u)] ulong objectOwner, + [Values(0u, 1u, 2u)] ulong sender + ) + { + var sendMethodName = $"DefaultTo{defaultSendTo}AllowOverrideRpc"; + + var senderObject = GetPlayerObject(objectOwner, sender); + var target = senderObject.RpcTarget.Not(recipient, RpcTargetUse.Temp); + var sendMethod = senderObject.GetType().GetMethod(sendMethodName); + sendMethod.Invoke(senderObject, new object[] { (RpcParams)target }); + + VerifyRemoteReceived(objectOwner, sender, sendMethodName, s_ClientIds.Where(c => recipient != c).ToArray(), false); + VerifyNotReceived(objectOwner, new[] { recipient }); + + // Pass some time to make sure that no other client ever receives this + TimeTravel(1f, 30); + VerifyNotReceived(objectOwner, new[] { recipient }); + } + + } + + [TestFixture(HostOrServer.Host)] + [TestFixture(HostOrServer.Server)] + internal class UniversalRpcTestSendingWithGroupOverride : UniversalRpcTestsBase + { + public UniversalRpcTestSendingWithGroupOverride(HostOrServer hostOrServer) : base(hostOrServer) + { + + } + + public static ulong[][] RecipientGroups = new[] + { + new[] { 0ul }, + new[] { 1ul }, + new[] { 0ul, 1ul }, + new[] { 0ul, 1ul, 2ul } + }; + + public enum AllocationType + { + Array, + NativeArray, + NativeList, + List + } + + [Test] + public void TestSendingWithGroupOverride( + [Values] SendTo defaultSendTo, + [ValueSource(nameof(RecipientGroups))] ulong[] recipient, + [Values(0u, 1u, 2u)] ulong objectOwner, + [Values(0u, 1u, 2u)] ulong sender, + [Values] AllocationType allocationType + ) + { + var sendMethodName = $"DefaultTo{defaultSendTo}AllowOverrideRpc"; + + var senderObject = GetPlayerObject(objectOwner, sender); + BaseRpcTarget target = null; + switch (allocationType) + { + case AllocationType.Array: + target = senderObject.RpcTarget.Group(recipient, RpcTargetUse.Temp); + break; + case AllocationType.List: + target = senderObject.RpcTarget.Group(recipient.ToList(), RpcTargetUse.Temp); + break; + case AllocationType.NativeArray: + var arr = new NativeArray(recipient, Allocator.Temp); + target = senderObject.RpcTarget.Group(arr, RpcTargetUse.Temp); + arr.Dispose(); + break; + case AllocationType.NativeList: + // For some reason on 2020.3, calling list.AsArray() and passing that to the next function + // causes Allocator.Temp allocations to become invalid somehow. This is not an issue on later + // versions of Unity. + var list = new NativeList(recipient.Length, Allocator.TempJob); + foreach (var id in recipient) + { + list.Add(id); + } + target = senderObject.RpcTarget.Group(list, RpcTargetUse.Temp); + list.Dispose(); + break; + } + var sendMethod = senderObject.GetType().GetMethod(sendMethodName); + sendMethod.Invoke(senderObject, new object[] { (RpcParams)target }); + + VerifyRemoteReceived(objectOwner, sender, sendMethodName, s_ClientIds.Where(c => recipient.Contains(c)).ToArray(), false); + VerifyNotReceived(objectOwner, s_ClientIds.Where(c => !recipient.Contains(c)).ToArray()); + + // Pass some time to make sure that no other client ever receives this + TimeTravel(1f, 30); + VerifyNotReceived(objectOwner, s_ClientIds.Where(c => !recipient.Contains(c)).ToArray()); + } + + } + + [TestFixture(HostOrServer.Host)] + [TestFixture(HostOrServer.Server)] + internal class UniversalRpcTestSendingWithGroupNotOverride : UniversalRpcTestsBase + { + public UniversalRpcTestSendingWithGroupNotOverride(HostOrServer hostOrServer) : base(hostOrServer) + { + + } + + public static ulong[][] RecipientGroups = new[] + { + new ulong[] {}, + new[] { 0ul }, + new[] { 1ul }, + new[] { 0ul, 1ul }, + }; + + public enum AllocationType + { + Array, + NativeArray, + NativeList, + List + } + + [Test] + public void TestSendingWithGroupNotOverride( + [Values] SendTo defaultSendTo, + [ValueSource(nameof(RecipientGroups))] ulong[] recipient, + [Values(0u, 1u, 2u)] ulong objectOwner, + [Values(0u, 1u, 2u)] ulong sender, + [Values] AllocationType allocationType + ) + { + var sendMethodName = $"DefaultTo{defaultSendTo}AllowOverrideRpc"; + + var senderObject = GetPlayerObject(objectOwner, sender); + BaseRpcTarget target = null; + switch (allocationType) + { + case AllocationType.Array: + target = senderObject.RpcTarget.Not(recipient, RpcTargetUse.Temp); + break; + case AllocationType.List: + target = senderObject.RpcTarget.Not(recipient.ToList(), RpcTargetUse.Temp); + break; + case AllocationType.NativeArray: + var arr = new NativeArray(recipient, Allocator.Temp); + target = senderObject.RpcTarget.Not(arr, RpcTargetUse.Temp); + arr.Dispose(); + break; + case AllocationType.NativeList: + // For some reason on 2020.3, calling list.AsArray() and passing that to the next function + // causes Allocator.Temp allocations to become invalid somehow. This is not an issue on later + // versions of Unity. + var list = new NativeList(recipient.Length, Allocator.TempJob); + foreach (var id in recipient) + { + list.Add(id); + } + target = senderObject.RpcTarget.Not(list, RpcTargetUse.Temp); + list.Dispose(); + break; + } + var sendMethod = senderObject.GetType().GetMethod(sendMethodName); + sendMethod.Invoke(senderObject, new object[] { (RpcParams)target }); + + VerifyRemoteReceived(objectOwner, sender, sendMethodName, s_ClientIds.Where(c => !recipient.Contains(c)).ToArray(), false); + VerifyNotReceived(objectOwner, s_ClientIds.Where(c => recipient.Contains(c)).ToArray()); + + // Pass some time to make sure that no other client ever receives this + TimeTravel(1f, 30); + VerifyNotReceived(objectOwner, s_ClientIds.Where(c => recipient.Contains(c)).ToArray()); + } + + } + + [TestFixture(HostOrServer.Host)] + [TestFixture(HostOrServer.Server)] + internal class UniversalRpcTestDefaultSendToSpecifiedInParamsSendingToServerAndOwner : UniversalRpcTestsBase + { + public UniversalRpcTestDefaultSendToSpecifiedInParamsSendingToServerAndOwner(HostOrServer hostOrServer) : base(hostOrServer) + { + + } + } + + [TestFixture(HostOrServer.Host)] + [TestFixture(HostOrServer.Server)] + internal class UniversalRpcTestDeferLocal : UniversalRpcTestsBase + { + public UniversalRpcTestDeferLocal(HostOrServer hostOrServer) : base(hostOrServer) + { + + } + + [Test] + // All the test cases that involve sends that will be delivered locally + [TestCase(SendTo.Everyone, 0u, 0u)] + [TestCase(SendTo.Everyone, 0u, 1u)] + [TestCase(SendTo.Everyone, 0u, 2u)] + [TestCase(SendTo.Everyone, 1u, 0u)] + [TestCase(SendTo.Everyone, 1u, 1u)] + [TestCase(SendTo.Everyone, 1u, 2u)] + [TestCase(SendTo.Everyone, 2u, 0u)] + [TestCase(SendTo.Everyone, 2u, 1u)] + [TestCase(SendTo.Everyone, 2u, 2u)] + [TestCase(SendTo.Me, 0u, 0u)] + [TestCase(SendTo.Me, 0u, 1u)] + [TestCase(SendTo.Me, 0u, 2u)] + [TestCase(SendTo.Me, 1u, 0u)] + [TestCase(SendTo.Me, 1u, 1u)] + [TestCase(SendTo.Me, 1u, 2u)] + [TestCase(SendTo.Me, 2u, 0u)] + [TestCase(SendTo.Me, 2u, 1u)] + [TestCase(SendTo.Me, 2u, 2u)] + [TestCase(SendTo.Owner, 0u, 0u)] + [TestCase(SendTo.Owner, 1u, 1u)] + [TestCase(SendTo.Owner, 2u, 2u)] + [TestCase(SendTo.Server, 0u, 0u)] + [TestCase(SendTo.Server, 1u, 0u)] + [TestCase(SendTo.Server, 2u, 0u)] + [TestCase(SendTo.NotOwner, 0u, 1u)] + [TestCase(SendTo.NotOwner, 0u, 2u)] + [TestCase(SendTo.NotOwner, 1u, 0u)] + [TestCase(SendTo.NotOwner, 1u, 2u)] + [TestCase(SendTo.NotOwner, 2u, 0u)] + [TestCase(SendTo.NotOwner, 2u, 1u)] + [TestCase(SendTo.NotServer, 0u, 1u)] + [TestCase(SendTo.NotServer, 0u, 2u)] + [TestCase(SendTo.NotServer, 1u, 1u)] + [TestCase(SendTo.NotServer, 1u, 2u)] + [TestCase(SendTo.NotServer, 2u, 1u)] + [TestCase(SendTo.NotServer, 2u, 2u)] + [TestCase(SendTo.ClientsAndHost, 0u, 0u)] + [TestCase(SendTo.ClientsAndHost, 0u, 1u)] + [TestCase(SendTo.ClientsAndHost, 0u, 2u)] + [TestCase(SendTo.ClientsAndHost, 1u, 0u)] + [TestCase(SendTo.ClientsAndHost, 1u, 1u)] + [TestCase(SendTo.ClientsAndHost, 1u, 2u)] + [TestCase(SendTo.ClientsAndHost, 2u, 0u)] + [TestCase(SendTo.ClientsAndHost, 2u, 1u)] + [TestCase(SendTo.ClientsAndHost, 2u, 2u)] + public void TestDeferLocal( + SendTo defaultSendTo, + ulong objectOwner, + ulong sender + ) + { + if (defaultSendTo == SendTo.ClientsAndHost && sender == 0u && !m_ServerNetworkManager.IsHost) + { + // Not calling Assert.Ignore() because Unity will mark the whole block of tests as ignored + // Just consider this case a success... + return; + } + var sendMethodName = $"DefaultTo{defaultSendTo}DeferLocalRpc"; + var verifyMethodName = $"VerifySentTo{defaultSendTo}"; + var senderObject = GetPlayerObject(objectOwner, sender); + var sendMethod = senderObject.GetType().GetMethod(sendMethodName); + sendMethod.Invoke(senderObject, new object[] { new RpcParams() }); + + VerifyNotReceived(objectOwner, new[] { sender }); + // Should be received on the next frame + SimulateOneFrame(); + VerifyLocalReceived(objectOwner, sender, sendMethodName, false); + + var verifyMethod = GetType().GetMethod(verifyMethodName); + verifyMethod.Invoke(this, new object[] { objectOwner, sender, sendMethodName }); + } + + [Test] + // All the test cases that involve sends that will be delivered locally + [TestCase(SendTo.Everyone, 0u, 0u)] + [TestCase(SendTo.Everyone, 0u, 1u)] + [TestCase(SendTo.Everyone, 0u, 2u)] + [TestCase(SendTo.Everyone, 1u, 0u)] + [TestCase(SendTo.Everyone, 1u, 1u)] + [TestCase(SendTo.Everyone, 1u, 2u)] + [TestCase(SendTo.Everyone, 2u, 0u)] + [TestCase(SendTo.Everyone, 2u, 1u)] + [TestCase(SendTo.Everyone, 2u, 2u)] + [TestCase(SendTo.Me, 0u, 0u)] + [TestCase(SendTo.Me, 0u, 1u)] + [TestCase(SendTo.Me, 0u, 2u)] + [TestCase(SendTo.Me, 1u, 0u)] + [TestCase(SendTo.Me, 1u, 1u)] + [TestCase(SendTo.Me, 1u, 2u)] + [TestCase(SendTo.Me, 2u, 0u)] + [TestCase(SendTo.Me, 2u, 1u)] + [TestCase(SendTo.Me, 2u, 2u)] + [TestCase(SendTo.Owner, 0u, 0u)] + [TestCase(SendTo.Owner, 1u, 1u)] + [TestCase(SendTo.Owner, 2u, 2u)] + [TestCase(SendTo.Server, 0u, 0u)] + [TestCase(SendTo.Server, 1u, 0u)] + [TestCase(SendTo.Server, 2u, 0u)] + [TestCase(SendTo.NotOwner, 0u, 1u)] + [TestCase(SendTo.NotOwner, 0u, 2u)] + [TestCase(SendTo.NotOwner, 1u, 0u)] + [TestCase(SendTo.NotOwner, 1u, 2u)] + [TestCase(SendTo.NotOwner, 2u, 0u)] + [TestCase(SendTo.NotOwner, 2u, 1u)] + [TestCase(SendTo.NotServer, 0u, 1u)] + [TestCase(SendTo.NotServer, 0u, 2u)] + [TestCase(SendTo.NotServer, 1u, 1u)] + [TestCase(SendTo.NotServer, 1u, 2u)] + [TestCase(SendTo.NotServer, 2u, 1u)] + [TestCase(SendTo.NotServer, 2u, 2u)] + [TestCase(SendTo.ClientsAndHost, 0u, 0u)] + [TestCase(SendTo.ClientsAndHost, 0u, 1u)] + [TestCase(SendTo.ClientsAndHost, 0u, 2u)] + [TestCase(SendTo.ClientsAndHost, 1u, 0u)] + [TestCase(SendTo.ClientsAndHost, 1u, 1u)] + [TestCase(SendTo.ClientsAndHost, 1u, 2u)] + [TestCase(SendTo.ClientsAndHost, 2u, 0u)] + [TestCase(SendTo.ClientsAndHost, 2u, 1u)] + [TestCase(SendTo.ClientsAndHost, 2u, 2u)] + public void TestDeferLocalOverrideToTrue( + SendTo defaultSendTo, + ulong objectOwner, + ulong sender + ) + { + if (defaultSendTo == SendTo.ClientsAndHost && sender == 0u && !m_ServerNetworkManager.IsHost) + { + // Not calling Assert.Ignore() because Unity will mark the whole block of tests as ignored + // Just consider this case a success... + return; + } + var sendMethodName = $"DefaultTo{defaultSendTo}WithRpcParamsRpc"; + var verifyMethodName = $"VerifySentTo{defaultSendTo}"; + var senderObject = GetPlayerObject(objectOwner, sender); + var sendMethod = senderObject.GetType().GetMethod(sendMethodName); + sendMethod.Invoke(senderObject, new object[] { (RpcParams)LocalDeferMode.Defer }); + + VerifyNotReceived(objectOwner, new[] { sender }); + // Should be received on the next frame + SimulateOneFrame(); + VerifyLocalReceived(objectOwner, sender, sendMethodName, false); + + var verifyMethod = GetType().GetMethod(verifyMethodName); + verifyMethod.Invoke(this, new object[] { objectOwner, sender, sendMethodName }); + } + + [Test] + // All the test cases that involve sends that will be delivered locally + [TestCase(SendTo.Everyone, 0u, 0u)] + [TestCase(SendTo.Everyone, 0u, 1u)] + [TestCase(SendTo.Everyone, 0u, 2u)] + [TestCase(SendTo.Everyone, 1u, 0u)] + [TestCase(SendTo.Everyone, 1u, 1u)] + [TestCase(SendTo.Everyone, 1u, 2u)] + [TestCase(SendTo.Everyone, 2u, 0u)] + [TestCase(SendTo.Everyone, 2u, 1u)] + [TestCase(SendTo.Everyone, 2u, 2u)] + [TestCase(SendTo.Me, 0u, 0u)] + [TestCase(SendTo.Me, 0u, 1u)] + [TestCase(SendTo.Me, 0u, 2u)] + [TestCase(SendTo.Me, 1u, 0u)] + [TestCase(SendTo.Me, 1u, 1u)] + [TestCase(SendTo.Me, 1u, 2u)] + [TestCase(SendTo.Me, 2u, 0u)] + [TestCase(SendTo.Me, 2u, 1u)] + [TestCase(SendTo.Me, 2u, 2u)] + [TestCase(SendTo.Owner, 0u, 0u)] + [TestCase(SendTo.Owner, 1u, 1u)] + [TestCase(SendTo.Owner, 2u, 2u)] + [TestCase(SendTo.Server, 0u, 0u)] + [TestCase(SendTo.Server, 1u, 0u)] + [TestCase(SendTo.Server, 2u, 0u)] + [TestCase(SendTo.NotOwner, 0u, 1u)] + [TestCase(SendTo.NotOwner, 0u, 2u)] + [TestCase(SendTo.NotOwner, 1u, 0u)] + [TestCase(SendTo.NotOwner, 1u, 2u)] + [TestCase(SendTo.NotOwner, 2u, 0u)] + [TestCase(SendTo.NotOwner, 2u, 1u)] + [TestCase(SendTo.NotServer, 0u, 1u)] + [TestCase(SendTo.NotServer, 0u, 2u)] + [TestCase(SendTo.NotServer, 1u, 1u)] + [TestCase(SendTo.NotServer, 1u, 2u)] + [TestCase(SendTo.NotServer, 2u, 1u)] + [TestCase(SendTo.NotServer, 2u, 2u)] + [TestCase(SendTo.ClientsAndHost, 0u, 0u)] + [TestCase(SendTo.ClientsAndHost, 0u, 1u)] + [TestCase(SendTo.ClientsAndHost, 0u, 2u)] + [TestCase(SendTo.ClientsAndHost, 1u, 0u)] + [TestCase(SendTo.ClientsAndHost, 1u, 1u)] + [TestCase(SendTo.ClientsAndHost, 1u, 2u)] + [TestCase(SendTo.ClientsAndHost, 2u, 0u)] + [TestCase(SendTo.ClientsAndHost, 2u, 1u)] + [TestCase(SendTo.ClientsAndHost, 2u, 2u)] + public void TestDeferLocalOverrideToFalse( + SendTo defaultSendTo, + ulong objectOwner, + ulong sender + ) + { + if (defaultSendTo == SendTo.ClientsAndHost && sender == 0u && !m_ServerNetworkManager.IsHost) + { + // Not calling Assert.Ignore() because Unity will mark the whole block of tests as ignored + // Just consider this case a success... + return; + } + var sendMethodName = $"DefaultTo{defaultSendTo}DeferLocalRpc"; + var verifyMethodName = $"VerifySentTo{defaultSendTo}"; + var senderObject = GetPlayerObject(objectOwner, sender); + var sendMethod = senderObject.GetType().GetMethod(sendMethodName); + sendMethod.Invoke(senderObject, new object[] { (RpcParams)LocalDeferMode.SendImmediate }); + + VerifyLocalReceived(objectOwner, sender, sendMethodName, false); + + var verifyMethod = GetType().GetMethod(verifyMethodName); + verifyMethod.Invoke(this, new object[] { objectOwner, sender, sendMethodName }); + } + + } + + [TestFixture(HostOrServer.Host)] + [TestFixture(HostOrServer.Server)] + internal class UniversalRpcTestMutualRecursion : UniversalRpcTestsBase + { + public UniversalRpcTestMutualRecursion(HostOrServer hostOrServer) : base(hostOrServer) + { + + } + + [Test] + public void TestMutualRecursion() + { + var serverObj = GetPlayerObject(NetworkManager.ServerClientId, NetworkManager.ServerClientId); + + serverObj.MutualRecursionClientRpc(); + + var serverIdArray = new[] { NetworkManager.ServerClientId }; + var clientIdArray = s_ClientIds.Where(c => c != NetworkManager.ServerClientId).ToArray(); + + var clientList = m_ClientNetworkManagers.ToList(); + var serverList = new List { m_ServerNetworkManager }; + + VerifyNotReceived(NetworkManager.ServerClientId, s_ClientIds); + + for (var i = 0; i < 10; ++i) + { + WaitForMessageReceivedWithTimeTravel(clientList); + VerifyRemoteReceived(NetworkManager.ServerClientId, NetworkManager.ServerClientId, nameof(UniversalRpcNetworkBehaviour.MutualRecursionClientRpc), clientIdArray, false, false); + VerifyNotReceived(NetworkManager.ServerClientId, serverIdArray); + + Clear(); + + WaitForMessageReceivedWithTimeTravel(serverList); + VerifyRemoteReceived(NetworkManager.ServerClientId, NetworkManager.ServerClientId, nameof(UniversalRpcNetworkBehaviour.MutualRecursionServerRpc), serverIdArray, false, false); + VerifyNotReceived(NetworkManager.ServerClientId, clientIdArray); + + Clear(); + } + serverObj.Stop = true; + WaitForMessageReceivedWithTimeTravel(serverList); + Assert.IsFalse(serverObj.Stop); + } + + + } + + [TestFixture(HostOrServer.Host)] + [TestFixture(HostOrServer.Server)] + internal class UniversalRpcTestSelfRecursion : UniversalRpcTestsBase + { + public UniversalRpcTestSelfRecursion(HostOrServer hostOrServer) : base(hostOrServer) + { + + } + + [Test] + public void TestSelfRecursion() + { + var serverObj = GetPlayerObject(NetworkManager.ServerClientId, NetworkManager.ServerClientId); + + serverObj.SelfRecursiveRpc(); + + var serverIdArray = new[] { NetworkManager.ServerClientId }; + var clientIdArray = s_ClientIds.Where(c => c != NetworkManager.ServerClientId).ToArray(); + + var clientList = m_ClientNetworkManagers.ToList(); + var serverList = new List { m_ServerNetworkManager }; + + for (var i = 0; i < 10; ++i) + { + VerifyNotReceived(NetworkManager.ServerClientId, s_ClientIds); + SimulateOneFrame(); + VerifyLocalReceived(NetworkManager.ServerClientId, NetworkManager.ServerClientId, nameof(UniversalRpcNetworkBehaviour.SelfRecursiveRpc), false); + + Clear(); + } + + serverObj.Stop = true; + SimulateOneFrame(); + Assert.IsFalse(serverObj.Stop); + VerifyNotReceived(NetworkManager.ServerClientId, s_ClientIds); + } + + } + + [TestFixture(ObjType.Server)] + [TestFixture(ObjType.Client)] + internal class UniversalRpcTestRpcTargetUse : UniversalRpcTestsBase + { + private ObjType m_ObjType; + + public UniversalRpcTestRpcTargetUse(ObjType objType) : base(HostOrServer.Server) + { + m_ObjType = objType; + } + + public enum AllocationType + { + Array, + NativeArray, + NativeList, + List + } + private BaseRpcTarget GetGroup(NetworkBehaviour senderObject, ulong[] recipient, AllocationType allocationType, RpcTargetUse use) + { + switch (allocationType) + { + case AllocationType.Array: + return senderObject.RpcTarget.Group(recipient, use); + case AllocationType.List: + return senderObject.RpcTarget.Group(recipient.ToList(), use); + case AllocationType.NativeArray: + var arr = new NativeArray(recipient, Allocator.Temp); + var naTarget = senderObject.RpcTarget.Group(arr, use); + arr.Dispose(); + return naTarget; + case AllocationType.NativeList: + // For some reason on 2020.3, calling list.AsArray() and passing that to the next function + // causes Allocator.Temp allocations to become invalid somehow. This is not an issue on later + // versions of Unity. + var list = new NativeList(recipient.Length, Allocator.TempJob); + foreach (var id in recipient) + { + list.Add(id); + } + var nlTarget = senderObject.RpcTarget.Group(list, use); + list.Dispose(); + return nlTarget; + } + + return null; + } + private BaseRpcTarget GetNot(NetworkBehaviour senderObject, ulong[] recipient, AllocationType allocationType, RpcTargetUse use) + { + switch (allocationType) + { + case AllocationType.Array: + return senderObject.RpcTarget.Not(recipient, use); + case AllocationType.List: + return senderObject.RpcTarget.Not(recipient.ToList(), use); + case AllocationType.NativeArray: + var arr = new NativeArray(recipient, Allocator.Temp); + var naTarget = senderObject.RpcTarget.Not(arr, use); + arr.Dispose(); + return naTarget; + case AllocationType.NativeList: + // For some reason on 2020.3, calling list.AsArray() and passing that to the next function + // causes Allocator.Temp allocations to become invalid somehow. This is not an issue on later + // versions of Unity. + var list = new NativeList(recipient.Length, Allocator.TempJob); + foreach (var id in recipient) + { + list.Add(id); + } + var nlTarget = senderObject.RpcTarget.Not(list, use); + list.Dispose(); + return nlTarget; + } + + return null; + } + + public enum ObjType + { + Client, + Server + } + + private NetworkBehaviour m_Obj; + + protected override void OnTimeTravelServerAndClientsConnected() + { + base.OnTimeTravelServerAndClientsConnected(); + + if (m_ObjType == ObjType.Server) + { + m_Obj = GetPlayerObject(NetworkManager.ServerClientId, NetworkManager.ServerClientId); + } + else + { + m_Obj = GetPlayerObject(1, 1); + } + } + + [Test] + public void TestRpcTargetUseGroup([Values] AllocationType allocationType) + { + var group1 = GetGroup(m_Obj, new[] { 1ul, 2ul }, allocationType, RpcTargetUse.Temp); + var group2 = GetGroup(m_Obj, new[] { 2ul, 3ul }, allocationType, RpcTargetUse.Temp); + var group3 = GetGroup(m_Obj, new[] { 1ul, 2ul }, allocationType, RpcTargetUse.Persistent); + var group4 = GetGroup(m_Obj, new[] { 2ul, 3ul }, allocationType, RpcTargetUse.Persistent); + + Assert.AreSame(group1, group2); + Assert.AreNotSame(group1, group3); + Assert.AreNotSame(group1, group4); + Assert.AreNotSame(group2, group3); + Assert.AreNotSame(group2, group4); + Assert.AreNotSame(group3, group4); + + Assert.Throws(() => + { + group1.Dispose(); + }); + + Assert.Throws(() => + { + group2.Dispose(); + }); + + group3.Dispose(); + group4.Dispose(); + } + + [Test] + public void TestRpcTargetUseNotGroup([Values] AllocationType allocationType) + { + var not1 = GetNot(m_Obj, new[] { 1ul, 2ul }, allocationType, RpcTargetUse.Temp); + var not2 = GetNot(m_Obj, new[] { 2ul, 3ul }, allocationType, RpcTargetUse.Temp); + var not3 = GetNot(m_Obj, new[] { 1ul, 2ul }, allocationType, RpcTargetUse.Persistent); + var not4 = GetNot(m_Obj, new[] { 2ul, 3ul }, allocationType, RpcTargetUse.Persistent); + + Assert.AreSame(not1, not2); + Assert.AreNotSame(not1, not3); + Assert.AreNotSame(not1, not4); + Assert.AreNotSame(not2, not3); + Assert.AreNotSame(not2, not4); + Assert.AreNotSame(not3, not4); + + Assert.Throws(() => + { + not1.Dispose(); + }); + + Assert.Throws(() => + { + not2.Dispose(); + }); + + not3.Dispose(); + not4.Dispose(); + } + + [Test] + public void TestRpcTargetUseSingle() + { + // Not using 1 here because 1 is a special case that returns a LocalSendTarget for the client + // because the client versin of this test uses m_Obj from client ID 1 (ergo 1 is localhost in this test). + // So 1 will always be different from 2 and we want to verify the first two are the same. + var single1 = m_Obj.RpcTarget.Single(2ul, RpcTargetUse.Temp); + var single2 = m_Obj.RpcTarget.Single(3ul, RpcTargetUse.Temp); + var single3 = m_Obj.RpcTarget.Single(2ul, RpcTargetUse.Persistent); + var single4 = m_Obj.RpcTarget.Single(3ul, RpcTargetUse.Persistent); + Assert.AreSame(single1, single2); + Assert.AreNotSame(single1, single3); + Assert.AreNotSame(single1, single4); + Assert.AreNotSame(single2, single3); + Assert.AreNotSame(single2, single4); + Assert.AreNotSame(single3, single4); + + Assert.Throws(() => + { + single1.Dispose(); + }); + + Assert.Throws(() => + { + single2.Dispose(); + }); + + single3.Dispose(); + single4.Dispose(); + } + + [Test] + public void TestRpcTargetUseNotSingle() + { + var singleNot1 = m_Obj.RpcTarget.Not(1ul, RpcTargetUse.Temp); + var singleNot2 = m_Obj.RpcTarget.Not(2ul, RpcTargetUse.Temp); + var singleNot3 = m_Obj.RpcTarget.Not(1ul, RpcTargetUse.Persistent); + var singleNot4 = m_Obj.RpcTarget.Not(2ul, RpcTargetUse.Persistent); + Assert.AreSame(singleNot1, singleNot2); + Assert.AreNotSame(singleNot1, singleNot3); + Assert.AreNotSame(singleNot1, singleNot4); + Assert.AreNotSame(singleNot2, singleNot3); + Assert.AreNotSame(singleNot2, singleNot4); + Assert.AreNotSame(singleNot3, singleNot4); + + Assert.Throws(() => + { + singleNot1.Dispose(); + }); + + Assert.Throws(() => + { + singleNot2.Dispose(); + }); + + singleNot3.Dispose(); + singleNot4.Dispose(); + } + + } +} diff --git a/Tests/Runtime/UniversalRpcTests.cs.meta b/Tests/Runtime/UniversalRpcTests.cs.meta new file mode 100644 index 0000000..2564551 --- /dev/null +++ b/Tests/Runtime/UniversalRpcTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: af2b96a4f4d34fa798385b487fc5c97d +timeCreated: 1698158457 \ No newline at end of file diff --git a/package.json b/package.json index 904609d..064fe51 100644 --- a/package.json +++ b/package.json @@ -2,23 +2,23 @@ "name": "com.unity.netcode.gameobjects", "displayName": "Netcode for GameObjects", "description": "Netcode for GameObjects is a high-level netcode SDK that provides networking capabilities to GameObject/MonoBehaviour workflows within Unity and sits on top of underlying transport layer.", - "version": "1.7.1", - "unity": "2020.3", + "version": "1.8.0", + "unity": "2021.3", "dependencies": { "com.unity.nuget.mono-cecil": "1.10.1", "com.unity.transport": "1.4.0" }, "_upm": { - "changelog": "### Added\n\n### Fixed\n\n- Fixed a bug where having a class with Rpcs that inherits from a class without Rpcs that inherits from NetworkVariable would cause a compile error. (#2751)\n- Fixed issue where `NetworkBehaviour.Synchronize` was not truncating the write buffer if nothing was serialized during `NetworkBehaviour.OnSynchronize` causing an additional 6 bytes to be written per `NetworkBehaviour` component instance. (#2749)\n\n### Changed" + "changelog": "### Added\n\n- Added a new RPC attribute, which is simply `Rpc`. (#2762)\n - This is a generic attribute that can perform the functions of both Server and Client RPCs, as well as enabling client-to-client RPCs. Includes several default targets: `Server`, `NotServer`, `Owner`, `NotOwner`, `Me`, `NotMe`, `ClientsAndHost`, and `Everyone`. Runtime overrides are available for any of these targets, as well as for sending to a specific ID or groups of IDs.\n - This attribute also includes the ability to defer RPCs that are sent to the local process to the start of the next frame instead of executing them immediately, treating them as if they had gone across the network. The default behavior is to execute immediately.\n - This attribute effectively replaces `ServerRpc` and `ClientRpc`. `ServerRpc` and `ClientRpc` remain in their existing forms for backward compatibility, but `Rpc` will be the recommended and most supported option.\n- Added `NetworkManager.OnConnectionEvent` as a unified connection event callback to notify clients and servers of all client connections and disconnections within the session (#2762)\n- Added `NetworkManager.ServerIsHost` and `NetworkBehaviour.ServerIsHost` to allow a client to tell if it is connected to a host or to a dedicated server (#2762)\n- Added `SceneEventProgress.SceneManagementNotEnabled` return status to be returned when a `NetworkSceneManager` method is invoked and scene management is not enabled. (#2735)\n- Added `SceneEventProgress.ServerOnlyAction` return status to be returned when a `NetworkSceneManager` method is invoked by a client. (#2735)\n- Added `NetworkObject.InstantiateAndSpawn` and `NetworkSpawnManager.InstantiateAndSpawn` methods to simplify prefab spawning by assuring that the prefab is valid and applies any override prior to instantiating the `GameObject` and spawning the `NetworkObject` instance. (#2710)\n\n### Fixed\n\n- Fixed issue where a client disconnected by a server-host would not receive a local notification. (#2789)\n- Fixed issue where a server-host could shutdown during a relay connection but periodically the transport disconnect message sent to any connected clients could be dropped. (#2789)\n- Fixed issue where a host could disconnect its local client but remain running as a server. (#2789)\n- Fixed issue where `OnClientDisconnectedCallback` was not being invoked under certain conditions. (#2789)\n- Fixed issue where `OnClientDisconnectedCallback` was always returning 0 as the client identifier. (#2789)\n- Fixed issue where if a host or server shutdown while a client owned NetworkObjects (other than the player) it would throw an exception. (#2789)\n- Fixed issue where setting values on a `NetworkVariable` or `NetworkList` within `OnNetworkDespawn` during a shutdown sequence would throw an exception. (#2789)\n- Fixed issue where a teleport state could potentially be overridden by a previous unreliable delta state. (#2777)\n- Fixed issue where `NetworkTransform` was using the `NetworkManager.ServerTime.Tick` as opposed to `NetworkManager.NetworkTickSystem.ServerTime.Tick` during the authoritative side's tick update where it performed a delta state check. (#2777)\n- Fixed issue where a parented in-scene placed NetworkObject would be destroyed upon a client or server exiting a network session but not unloading the original scene in which the NetworkObject was placed. (#2737)\n- Fixed issue where during client synchronization and scene loading, when client synchronization or the scene loading mode are set to `LoadSceneMode.Single`, a `CreateObjectMessage` could be received, processed, and the resultant spawned `NetworkObject` could be instantiated in the client's currently active scene that could, towards the end of the client synchronization or loading process, be unloaded and cause the newly created `NetworkObject` to be destroyed (and throw and exception). (#2735)\n- Fixed issue where a `NetworkTransform` instance with interpolation enabled would result in wide visual motion gaps (stuttering) under above normal latency conditions and a 1-5% or higher packet are drop rate. (#2713)\n- Fixed issue where you could not have multiple source network prefab overrides targeting the same network prefab as their override. (#2710)\n\n### Changed\n- Changed the server or host shutdown so it will now perform a \"soft shutdown\" when `NetworkManager.Shutdown` is invoked. This will send a disconnect notification to all connected clients and the server-host will wait for all connected clients to disconnect or timeout after a 5 second period before completing the shutdown process. (#2789)\n- Changed `OnClientDisconnectedCallback` will now return the assigned client identifier on the local client side if the client was approved and assigned one prior to being disconnected. (#2789)\n- Changed `NetworkTransform.SetState` (and related methods) now are cumulative during a fractional tick period and sent on the next pending tick. (#2777)\n- `NetworkManager.ConnectedClientsIds` is now accessible on the client side and will contain the list of all clients in the session, including the host client if the server is operating in host mode (#2762)\n- Changed `NetworkSceneManager` to return a `SceneEventProgress` status and not throw exceptions for methods invoked when scene management is disabled and when a client attempts to access a `NetworkSceneManager` method by a client. (#2735)\n- Changed `NetworkTransform` authoritative instance tick registration so a single `NetworkTransform` specific tick event update will update all authoritative instances to improve perofmance. (#2713)\n- Changed `NetworkPrefabs.OverrideToNetworkPrefab` dictionary is no longer used/populated due to it ending up being related to a regression bug and not allowing more than one override to be assigned to a network prefab asset. (#2710)\n- Changed in-scene placed `NetworkObject`s now store their source network prefab asset's `GlobalObjectIdHash` internally that is used, when scene management is disabled, by clients to spawn the correct prefab even if the `NetworkPrefab` entry has an override. This does not impact dynamically spawning the same prefab which will yield the override on both host and client. (#2710)\n- Changed in-scene placed `NetworkObject`s no longer require a `NetworkPrefab` entry with `GlobalObjectIdHash` override in order for clients to properly synchronize. (#2710)\n- Changed in-scene placed `NetworkObject`s now set their `IsSceneObject` value when generating their `GlobalObjectIdHash` value. (#2710)\n- Changed the default `NetworkConfig.SpawnTimeout` value from 1.0s to 10.0s. (#2710)" }, "upmCi": { - "footprint": "0aa1a9720f4e4850c481cc1bb159b646494808e1" + "footprint": "d2e1d4664bac8ee7f790f7869f550b2f3e65977d" }, - "documentationUrl": "https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@1.7/manual/index.html", + "documentationUrl": "https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@1.8/manual/index.html", "repository": { "url": "https://github.com/Unity-Technologies/com.unity.netcode.gameobjects.git", "type": "git", - "revision": "5df824c7588b43c29238a927d14642d5f94129ff" + "revision": "53b735c70fba0c5476fe04b6440320bcd61ed195" }, "samples": [ {