diff --git a/CHANGELOG.md b/CHANGELOG.md index c29397c..3acbe8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,60 @@ 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.1.0] - 2022-10-21 + +### Added + +- Added `NetworkManager.IsApproved` flag that is set to `true` a client has been approved.(#2261) +- `UnityTransport` now provides a way to set the Relay server data directly from the `RelayServerData` structure (provided by the Unity Transport package) throuh its `SetRelayServerData` method. This allows making use of the new APIs in UTP 1.3 that simplify integration of the Relay SDK. (#2235) +- IPv6 is now supported for direct connections when using `UnityTransport`. (#2232) +- Added WebSocket support when using UTP 2.0 with `UseWebSockets` property in the `UnityTransport` component of the `NetworkManager` allowing to pick WebSockets for communication. When building for WebGL, this selection happens automatically. (#2201) +- Added position, rotation, and scale to the `ParentSyncMessage` which provides users the ability to specify the final values on the server-side when `OnNetworkObjectParentChanged` is invoked just before the message is created (when the `Transform` values are applied to the message). (#2146) +- Added `NetworkObject.TryRemoveParent` method for convenience purposes opposed to having to cast null to either `GameObject` or `NetworkObject`. (#2146) + +### Changed + +- Updated `UnityTransport` dependency on `com.unity.transport` to 1.3.0. (#2231) +- The send queues of `UnityTransport` are now dynamically-sized. This means that there shouldn't be any need anymore to tweak the 'Max Send Queue Size' value. In fact, this field is now removed from the inspector and will not be serialized anymore. It is still possible to set it manually using the `MaxSendQueueSize` property, but it is not recommended to do so aside from some specific needs (e.g. limiting the amount of memory used by the send queues in very constrained environments). (#2212) +- As a consequence of the above change, the `UnityTransport.InitialMaxSendQueueSize` field is now deprecated. There is no default value anymore since send queues are dynamically-sized. (#2212) +- The debug simulator in `UnityTransport` is now non-deterministic. Its random number generator used to be seeded with a constant value, leading to the same pattern of packet drops, delays, and jitter in every run. (#2196) +- `NetworkVariable<>` now supports managed `INetworkSerializable` types, as well as other managed types with serialization/deserialization delegates registered to `UserNetworkVariableSerialization.WriteValue` and `UserNetworkVariableSerialization.ReadValue` (#2219) +- `NetworkVariable<>` and `BufferSerializer` now deserialize `INetworkSerializable` types in-place, rather than constructing new ones. (#2219) + +### Fixed + +- Fixed `NetworkManager.ApprovalTimeout` will not timeout due to slower client synchronization times as it now uses the added `NetworkManager.IsApproved` flag to determined if the client has been approved or not.(#2261) +- Fixed issue caused when changing ownership of objects hidden to some clients (#2242) +- Fixed issue where an in-scene placed NetworkObject would not invoke NetworkBehaviour.OnNetworkSpawn if the GameObject was disabled when it was despawned. (#2239) +- Fixed issue where clients were not rebuilding the `NetworkConfig` hash value for each unique connection request. (#2226) +- Fixed the issue where player objects were not taking the `DontDestroyWithOwner` property into consideration when a client disconnected. (#2225) +- Fixed issue where `SceneEventProgress` would not complete if a client late joins while it is still in progress. (#2222) +- Fixed issue where `SceneEventProgress` would not complete if a client disconnects. (#2222) +- Fixed issues with detecting if a `SceneEventProgress` has timed out. (#2222) +- Fixed issue #1924 where `UnityTransport` would fail to restart after a first failure (even if what caused the initial failure was addressed). (#2220) +- Fixed issue where `NetworkTransform.SetStateServerRpc` and `NetworkTransform.SetStateClientRpc` were not honoring local vs world space settings when applying the position and rotation. (#2203) +- Fixed ILPP `TypeLoadException` on WebGL on MacOS Editor and potentially other platforms. (#2199) +- Implicit conversion of NetworkObjectReference to GameObject will now return null instead of throwing an exception if the referenced object could not be found (i.e., was already despawned) (#2158) +- Fixed warning resulting from a stray NetworkAnimator.meta file (#2153) +- Fixed Connection Approval Timeout not working client side. (#2164) +- Fixed issue where the `WorldPositionStays` parenting parameter was not being synchronized with clients. (#2146) +- Fixed issue where parented in-scene placed `NetworkObject`s would fail for late joining clients. (#2146) +- Fixed issue where scale was not being synchronized which caused issues with nested parenting and scale when `WorldPositionStays` was true. (#2146) +- Fixed issue with `NetworkTransform.ApplyTransformToNetworkStateWithInfo` where it was not honoring axis sync settings when `NetworkTransformState.IsTeleportingNextFrame` was true. (#2146) +- Fixed issue with `NetworkTransform.TryCommitTransformToServer` where it was not honoring the `InLocalSpace` setting. (#2146) +- Fixed ClientRpcs always reporting in the profiler view as going to all clients, even when limited to a subset of clients by `ClientRpcParams`. (#2144) +- Fixed RPC codegen failing to choose the correct extension methods for `FastBufferReader` and `FastBufferWriter` when the parameters were a generic type (i.e., List) and extensions for multiple instantiations of that type have been defined (i.e., List and List) (#2142) +- Fixed the issue where running a server (i.e. not host) the second player would not receive updates (unless a third player joined). (#2127) +- Fixed issue where late-joining client transition synchronization could fail when more than one transition was occurring.(#2127) +- Fixed throwing an exception in `OnNetworkUpdate` causing other `OnNetworkUpdate` calls to not be executed. (#1739) +- Fixed synchronization when Time.timeScale is set to 0. This changes timing update to use unscaled deltatime. Now network updates rate are independent from the local time scale. (#2171) +- Fixed not sending all NetworkVariables to all clients when a client connects to a server. (#1987) +- Fixed IsOwner/IsOwnedByServer being wrong on the server after calling RemoveOwnership (#2211) ## [1.0.2] - 2022-09-12 +### Fixed + - Fixed issue where `NetworkTransform` was not honoring the InLocalSpace property on the authority side during OnNetworkSpawn. (#2170) - Fixed issue where `NetworkTransform` was not ending extrapolation for the previous state causing non-authoritative instances to become out of synch. (#2170) - Fixed issue where `NetworkTransform` was not continuing to interpolate for the remainder of the associated tick period. (#2170) @@ -22,7 +73,7 @@ Additional documentation and release notes are available at [Multiplayer Documen - Changed version to 1.0.1. (#2131) - Updated dependency on `com.unity.transport` to 1.2.0. (#2129) - When using `UnityTransport`, _reliable_ payloads are now allowed to exceed the configured 'Max Payload Size'. Unreliable payloads remain bounded by this setting. (#2081) -- Performance improvements for cases with large number of NetworkObjects, by not iterating over all unchanged NetworkObjects +- Performance improvements for cases with large number of NetworkObjects, by not iterating over all unchanged NetworkObjects ### Fixed @@ -239,7 +290,7 @@ Additional documentation and release notes are available at [Multiplayer Documen - ResetTrigger function to NetworkAnimator (#1327) -### Fixed +### Fixed - Overflow exception when syncing Animator state. (#1327) - Added `try`/`catch` around RPC calls, preventing exception from causing further RPC calls to fail (#1329) @@ -264,7 +315,7 @@ Additional documentation and release notes are available at [Multiplayer Documen - Added `ClientNetworkTransform` sample to the SDK package (#1168) - Added `Bootstrap` sample to the SDK package (#1140) - Enhanced `NetworkSceneManager` implementation with additive scene loading capabilities (#1080, #955, #913) - - `NetworkSceneManager.OnSceneEvent` provides improved scene event notificaitons + - `NetworkSceneManager.OnSceneEvent` provides improved scene event notificaitons - Enhanced `NetworkTransform` implementation with per axis/component based and threshold based state replication (#1042, #1055, #1061, #1084, #1101) - Added a jitter-resistent `BufferedLinearInterpolator` for `NetworkTransform` (#1060) - Implemented `NetworkPrefabHandler` that provides support for object pooling and `NetworkPrefab` overrides (#1073, #1004, #977, #905,#749, #727) @@ -321,7 +372,7 @@ Additional documentation and release notes are available at [Multiplayer Documen - Removed `NetworkDictionary`, `NetworkSet` (#1149) - Removed `NetworkVariableSettings` (#1097) - Removed predefined `NetworkVariable` types (#1093) - - Removed `NetworkVariableBool`, `NetworkVariableByte`, `NetworkVariableSByte`, `NetworkVariableUShort`, `NetworkVariableShort`, `NetworkVariableUInt`, `NetworkVariableInt`, `NetworkVariableULong`, `NetworkVariableLong`, `NetworkVariableFloat`, `NetworkVariableDouble`, `NetworkVariableVector2`, `NetworkVariableVector3`, `NetworkVariableVector4`, `NetworkVariableColor`, `NetworkVariableColor32`, `NetworkVariableRay`, `NetworkVariableQuaternion` + - Removed `NetworkVariableBool`, `NetworkVariableByte`, `NetworkVariableSByte`, `NetworkVariableUShort`, `NetworkVariableShort`, `NetworkVariableUInt`, `NetworkVariableInt`, `NetworkVariableULong`, `NetworkVariableLong`, `NetworkVariableFloat`, `NetworkVariableDouble`, `NetworkVariableVector2`, `NetworkVariableVector3`, `NetworkVariableVector4`, `NetworkVariableColor`, `NetworkVariableColor32`, `NetworkVariableRay`, `NetworkVariableQuaternion` - Removed `NetworkChannel` and `MultiplexTransportAdapter` (#1133) - Removed ILPP backend for 2019.4, minimum required version is 2020.3+ (#895) - `NetworkManager.NetworkConfig` had the following properties removed: (#1080) @@ -393,14 +444,14 @@ This is the initial experimental Unity MLAPI Package, v0.1.0. - Integrated MLAPI with the Unity Profiler for versions 2020.2 and later: - Added new profiler modules for MLAPI that report important network data. - Attached the profiler to a remote player to view network data over the wire. -- A test project is available for building and experimenting with MLAPI features. This project is available in the MLAPI GitHub [testproject folder](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/tree/release/0.1.0/testproject). +- A test project is available for building and experimenting with MLAPI features. This project is available in the MLAPI GitHub [testproject folder](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/tree/release/0.1.0/testproject). - Added a [MLAPI Community Contributions](https://github.com/Unity-Technologies/mlapi-community-contributions/tree/master/com.mlapi.contrib.extensions) new GitHub repository to accept extensions from the MLAPI community. Current extensions include moved MLAPI features for lag compensation (useful for Server Authoritative actions) and `TrackedObject`. ### Changed - [GitHub 520](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/520): MLAPI now uses the Unity Package Manager for installation management. -- Added functionality and usability to `NetworkVariable`, previously called `NetworkVar`. Updates enhance options and fully replace the need for `SyncedVar`s. -- [GitHub 507](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/507): Reimplemented `NetworkAnimator`, which synchronizes animation states for networked objects. +- Added functionality and usability to `NetworkVariable`, previously called `NetworkVar`. Updates enhance options and fully replace the need for `SyncedVar`s. +- [GitHub 507](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/507): Reimplemented `NetworkAnimator`, which synchronizes animation states for networked objects. - GitHub [444](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/444) and [455](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/455): Channels are now represented as bytes instead of strings. For users of previous versions of MLAPI, this release renames APIs due to refactoring. All obsolete marked APIs have been removed as per [GitHub 513](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/513) and [GitHub 514](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/514). @@ -433,7 +484,7 @@ For users of previous versions of MLAPI, this release renames APIs due to refact ### Fixed -- [GitHub 460](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/460): Fixed an issue for RPC where the host-server was not receiving RPCs from the host-client and vice versa without the loopback flag set in `NetworkingManager`. +- [GitHub 460](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/460): Fixed an issue for RPC where the host-server was not receiving RPCs from the host-client and vice versa without the loopback flag set in `NetworkingManager`. - Fixed an issue where data in the Profiler was incorrectly aggregated and drawn, which caused the profiler data to increment indefinitely instead of resetting each frame. - Fixed an issue the client soft-synced causing PlayMode client-only scene transition issues, caused when running the client in the editor and the host as a release build. Users may have encountered a soft sync of `NetworkedInstanceId` issues in the `SpawnManager.ClientCollectSoftSyncSceneObjectSweep` method. - [GitHub 458](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/458): Fixed serialization issues in `NetworkList` and `NetworkDictionary` when running in Server mode. @@ -448,10 +499,10 @@ With a new release of MLAPI in Unity, some features have been removed: - SyncVars have been removed from MLAPI. Use `NetworkVariable`s in place of this functionality. - [GitHub 527](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/527): Lag compensation systems and `TrackedObject` have moved to the new [MLAPI Community Contributions](https://github.com/Unity-Technologies/mlapi-community-contributions/tree/master/com.mlapi.contrib.extensions) repo. - [GitHub 509](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/509): Encryption has been removed from MLAPI. The `Encryption` option in `NetworkConfig` on the `NetworkingManager` is not available in this release. This change will not block game creation or running. A current replacement for this functionality is not available, and may be developed in future releases. See the following changes: - - Removed `SecuritySendFlags` from all APIs. - - Removed encryption, cryptography, and certificate configurations from APIs including `NetworkManager` and `NetworkConfig`. - - Removed "hail handshake", including `NetworkManager` implementation and `NetworkConstants` entries. - - Modified `RpcQueue` and `RpcBatcher` internals to remove encryption and authentication from reading and writing. + - Removed `SecuritySendFlags` from all APIs. + - Removed encryption, cryptography, and certificate configurations from APIs including `NetworkManager` and `NetworkConfig`. + - Removed "hail handshake", including `NetworkManager` implementation and `NetworkConstants` entries. + - Modified `RpcQueue` and `RpcBatcher` internals to remove encryption and authentication from reading and writing. - Removed the previous MLAPI Profiler editor window from Unity versions 2020.2 and later. - Removed previous MLAPI Convenience and Performance RPC APIs with the new standard RPC API. See [RFC #1](https://github.com/Unity-Technologies/com.unity.multiplayer.rfcs/blob/master/text/0001-std-rpc-api.md) for details. - [GitHub 520](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/520): Removed the MLAPI Installer. @@ -464,7 +515,7 @@ With a new release of MLAPI in Unity, some features have been removed: - For `NetworkVariable`, the `NetworkDictionary` `List` and `Set` must use the `reliableSequenced` channel. - `NetworkObjects`s are supported but when spawning a prefab with nested child network objects you have to manually call spawn on them - `NetworkTransform` have the following issues: - - Replicated objects may have jitter. + - Replicated objects may have jitter. - The owner is always authoritative about the object's position. - Scale is not synchronized. - Connection Approval is not called on the host client. diff --git a/Components/NetworkAnimator.cs b/Components/NetworkAnimator.cs index 41ab79d..258b64e 100644 --- a/Components/NetworkAnimator.cs +++ b/Components/NetworkAnimator.cs @@ -1,14 +1,19 @@ #if COM_UNITY_MODULES_ANIMATION +using System; using System.Collections.Generic; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using UnityEngine; +#if UNITY_EDITOR +using UnityEditor.Animations; +#endif namespace Unity.Netcode.Components { internal class NetworkAnimatorStateChangeHandler : INetworkUpdateSystem { private NetworkAnimator m_NetworkAnimator; + private bool m_IsServer; /// /// This removes sending RPCs from within RPCs when the @@ -32,7 +37,14 @@ private void FlushMessages() foreach (var sendEntry in m_SendTriggerUpdates) { - m_NetworkAnimator.SendAnimTriggerClientRpc(sendEntry.AnimationTriggerMessage, sendEntry.ClientRpcParams); + if (!sendEntry.SendToServer) + { + m_NetworkAnimator.SendAnimTriggerClientRpc(sendEntry.AnimationTriggerMessage, sendEntry.ClientRpcParams); + } + else + { + m_NetworkAnimator.SendAnimTriggerServerRpc(sendEntry.AnimationTriggerMessage); + } } m_SendTriggerUpdates.Clear(); } @@ -44,8 +56,8 @@ public void NetworkUpdate(NetworkUpdateStage updateStage) { case NetworkUpdateStage.PreUpdate: { - // Only the server forwards messages and synchronizes players - if (m_NetworkAnimator.NetworkManager.IsServer) + // Only the owner or the server send messages + if (m_NetworkAnimator.IsOwner || m_IsServer) { // Flush any pending messages FlushMessages(); @@ -125,6 +137,7 @@ internal void ProcessParameterUpdate(NetworkAnimator.ParametersUpdateMessage par private struct TriggerUpdate { + public bool SendToServer; public ClientRpcParams ClientRpcParams; public NetworkAnimator.AnimationTriggerMessage AnimationTriggerMessage; } @@ -134,11 +147,23 @@ private struct TriggerUpdate /// /// Invoked when a server needs to forward an update to a Trigger state /// - internal void SendTriggerUpdate(NetworkAnimator.AnimationTriggerMessage animationTriggerMessage, ClientRpcParams clientRpcParams = default) + internal void QueueTriggerUpdateToClient(NetworkAnimator.AnimationTriggerMessage animationTriggerMessage, ClientRpcParams clientRpcParams = default) { m_SendTriggerUpdates.Add(new TriggerUpdate() { ClientRpcParams = clientRpcParams, AnimationTriggerMessage = animationTriggerMessage }); } + internal void QueueTriggerUpdateToServer(NetworkAnimator.AnimationTriggerMessage animationTriggerMessage) + { + m_SendTriggerUpdates.Add(new TriggerUpdate() { AnimationTriggerMessage = animationTriggerMessage, SendToServer = true }); + } + + private Queue m_AnimationMessageQueue = new Queue(); + + internal void AddAnimationMessageToProcessQueue(NetworkAnimator.AnimationMessage message) + { + m_AnimationMessageQueue.Enqueue(message); + } + internal void DeregisterUpdate() { NetworkUpdateLoop.UnregisterNetworkUpdate(this, NetworkUpdateStage.PreUpdate); @@ -147,34 +172,266 @@ internal void DeregisterUpdate() internal NetworkAnimatorStateChangeHandler(NetworkAnimator networkAnimator) { m_NetworkAnimator = networkAnimator; + m_IsServer = networkAnimator.NetworkManager.IsServer; NetworkUpdateLoop.RegisterNetworkUpdate(this, NetworkUpdateStage.PreUpdate); } } - - /// /// NetworkAnimator enables remote synchronization of state for on network objects. /// - [AddComponentMenu("Netcode/" + nameof(NetworkAnimator))] + [AddComponentMenu("Netcode/Network Animator")] [RequireComponent(typeof(Animator))] - public class NetworkAnimator : NetworkBehaviour + public class NetworkAnimator : NetworkBehaviour, ISerializationCallbackReceiver + { - internal struct AnimationMessage : INetworkSerializable + [Serializable] + internal class TransitionStateinfo { - // state hash per layer. if non-zero, then Play() this animation, skipping transitions - internal bool Transition; + public int Layer; + public int OriginatingState; + public int DestinationState; + public float TransitionDuration; + public int TriggerNameHash; + public int TransitionIndex; + } + + /// + /// Used to build the destination state to transition info table + /// + [HideInInspector] + [SerializeField] + internal List TransitionStateInfoList; + + // Used to get the associated transition information required to synchronize late joining clients with transitions + // [Layer][DestinationState][TransitionStateInfo] + private Dictionary> m_DestinationStateToTransitioninfo = new Dictionary>(); + + /// + /// Builds the m_DestinationStateToTransitioninfo lookup table + /// + private void BuildDestinationToTransitionInfoTable() + { + foreach (var entry in TransitionStateInfoList) + { + if (!m_DestinationStateToTransitioninfo.ContainsKey(entry.Layer)) + { + m_DestinationStateToTransitioninfo.Add(entry.Layer, new Dictionary()); + } + var destinationStateTransitionInfo = m_DestinationStateToTransitioninfo[entry.Layer]; + if (!destinationStateTransitionInfo.ContainsKey(entry.DestinationState)) + { + destinationStateTransitionInfo.Add(entry.DestinationState, entry); + } + } + } + + /// + /// Creates the TransitionStateInfoList table + /// + private void BuildTransitionStateInfoList() + { +#if UNITY_EDITOR + if (UnityEditor.EditorApplication.isUpdating) + { + return; + } + TransitionStateInfoList = new List(); + var animatorController = m_Animator.runtimeAnimatorController as AnimatorController; + if (animatorController == null) + { + return; + } + + for (int x = 0; x < animatorController.layers.Length; x++) + { + var layer = animatorController.layers[x]; + + for (int y = 0; y < layer.stateMachine.states.Length; y++) + { + var animatorState = layer.stateMachine.states[y].state; + var transitions = layer.stateMachine.GetStateMachineTransitions(layer.stateMachine); + for (int z = 0; z < animatorState.transitions.Length; z++) + { + var transition = animatorState.transitions[z]; + if (transition.conditions.Length == 0 && transition.isExit) + { + // We don't need to worry about exit transitions with no conditions + continue; + } + + foreach (var condition in transition.conditions) + { + var parameterName = condition.parameter; + var parameters = animatorController.parameters; + foreach (var parameter in parameters) + { + switch (parameter.type) + { + case AnimatorControllerParameterType.Trigger: + { + // Match the condition with an existing trigger + if (parameterName == parameter.name) + { + var transitionInfo = new TransitionStateinfo() + { + Layer = x, + OriginatingState = animatorState.nameHash, + DestinationState = transition.destinationState.nameHash, + TransitionDuration = transition.duration, + TriggerNameHash = parameter.nameHash, + TransitionIndex = z + }; + TransitionStateInfoList.Add(transitionInfo); + } + break; + } + default: + break; + } + } + } + } + } + } +#endif + } + + public void OnAfterDeserialize() + { + BuildDestinationToTransitionInfoTable(); + } + + public void OnBeforeSerialize() + { + BuildTransitionStateInfoList(); + } + + internal struct AnimationState : INetworkSerializable + { + // Not to be serialized, used for processing the animation state + internal bool HasBeenProcessed; internal int StateHash; internal float NormalizedTime; internal int Layer; internal float Weight; + // For synchronizing transitions + internal bool Transition; + // The StateHash is where the transition starts + // and the DestinationStateHash is the destination state + internal int DestinationStateHash; + public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter { - serializer.SerializeValue(ref StateHash); - serializer.SerializeValue(ref NormalizedTime); - serializer.SerializeValue(ref Layer); - serializer.SerializeValue(ref Weight); + if (serializer.IsWriter) + { + var writer = serializer.GetFastBufferWriter(); + var writeSize = FastBufferWriter.GetWriteSize(Transition); + writeSize += FastBufferWriter.GetWriteSize(StateHash); + writeSize += FastBufferWriter.GetWriteSize(NormalizedTime); + writeSize += FastBufferWriter.GetWriteSize(Layer); + writeSize += FastBufferWriter.GetWriteSize(Weight); + if (Transition) + { + writeSize += FastBufferWriter.GetWriteSize(DestinationStateHash); + } + + if (!writer.TryBeginWrite(writeSize)) + { + throw new OverflowException($"[{GetType().Name}] Could not serialize: Out of buffer space."); + } + + writer.WriteValue(Transition); + writer.WriteValue(StateHash); + writer.WriteValue(NormalizedTime); + writer.WriteValue(Layer); + writer.WriteValue(Weight); + if (Transition) + { + writer.WriteValue(DestinationStateHash); + } + } + else + { + var reader = serializer.GetFastBufferReader(); + // Begin reading the Transition flag + if (!reader.TryBeginRead(FastBufferWriter.GetWriteSize(Transition))) + { + throw new OverflowException($"[{GetType().Name}] Could not deserialize: Out of buffer space."); + } + reader.ReadValue(out Transition); + + // Now determine what remains to be read + var readSize = FastBufferWriter.GetWriteSize(StateHash); + readSize += FastBufferWriter.GetWriteSize(NormalizedTime); + readSize += FastBufferWriter.GetWriteSize(Layer); + readSize += FastBufferWriter.GetWriteSize(Weight); + if (Transition) + { + readSize += FastBufferWriter.GetWriteSize(DestinationStateHash); + } + + // Now read the remaining information about this AnimationState + if (!reader.TryBeginRead(readSize)) + { + throw new OverflowException($"[{GetType().Name}] Could not deserialize: Out of buffer space."); + } + + reader.ReadValue(out StateHash); + reader.ReadValue(out NormalizedTime); + reader.ReadValue(out Layer); + reader.ReadValue(out Weight); + if (Transition) + { + reader.ReadValue(out DestinationStateHash); + } + } + } + } + + internal struct AnimationMessage : INetworkSerializable + { + // Not to be serialized, used for processing the animation message + internal bool HasBeenProcessed; + + // This is preallocated/populated in OnNetworkSpawn for all instances in the event ownership or + // authority changes. When serializing, IsDirtyCount determines how many AnimationState entries + // should be serialized from the list. When deserializing the list is created and populated with + // only the number of AnimationStates received which is dictated by the deserialized IsDirtyCount. + internal List AnimationStates; + + // Used to determine how many AnimationState entries we are sending or receiving + internal int IsDirtyCount; + + public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter + { + var animationState = new AnimationState(); + if (serializer.IsReader) + { + AnimationStates = new List(); + + serializer.SerializeValue(ref IsDirtyCount); + // Since we create a new AnimationMessage when deserializing + // we need to create new animation states for each incoming + // AnimationState being updated + for (int i = 0; i < IsDirtyCount; i++) + { + animationState = new AnimationState(); + serializer.SerializeValue(ref animationState); + AnimationStates.Add(animationState); + } + } + else + { + // When writing, only send the counted dirty animation states + serializer.SerializeValue(ref IsDirtyCount); + for (int i = 0; i < IsDirtyCount; i++) + { + animationState = AnimationStates[i]; + serializer.SerializeNetworkSerializable(ref animationState); + } + } } } @@ -223,7 +480,8 @@ protected virtual bool OnIsServerAuthoritative() return true; } - // Animators only support up to 32 params + // Animators only support up to 32 parameters + // TODO: Look into making this a range limited property private const int k_MaxAnimationParams = 32; private int[] m_TransitionHash; @@ -269,9 +527,9 @@ private void Cleanup() m_NetworkAnimatorStateChangeHandler = null; } - if (IsServer) + if (m_CachedNetworkManager != null) { - NetworkManager.OnClientConnectedCallback -= OnClientConnectedCallback; + m_CachedNetworkManager.OnClientConnectedCallback -= OnClientConnectedCallback; } if (m_CachedAnimatorParameters != null && m_CachedAnimatorParameters.IsCreated) @@ -293,40 +551,64 @@ public override void OnDestroy() private List m_ParametersToUpdate; private List m_ClientSendList; private ClientRpcParams m_ClientRpcParams; + private AnimationMessage m_AnimationMessage; + + /// + /// Used for integration test to validate that the + /// AnimationMessage.AnimationStates remains the same + /// size as the layer count. + /// + internal AnimationMessage GetAnimationMessage() + { + return m_AnimationMessage; + } + // Only used in Cleanup + private NetworkManager m_CachedNetworkManager; + + /// public override void OnNetworkSpawn() { - if (IsOwner || IsServer) - { - int layers = m_Animator.layerCount; - m_TransitionHash = new int[layers]; - m_AnimationHash = new int[layers]; - m_LayerWeights = new float[layers]; + int layers = m_Animator.layerCount; - if (IsServer) - { - NetworkManager.OnClientConnectedCallback += OnClientConnectedCallback; - } + // Initializing the below arrays for everyone handles an issue + // when running in owner authoritative mode and the owner changes. + m_TransitionHash = new int[layers]; + m_AnimationHash = new int[layers]; + m_LayerWeights = new float[layers]; - // Store off our current layer weights - for (int layer = 0; layer < m_Animator.layerCount; layer++) - { - float layerWeightNow = m_Animator.GetLayerWeight(layer); - if (layerWeightNow != m_LayerWeights[layer]) - { - m_LayerWeights[layer] = layerWeightNow; - } - } + if (IsServer) + { + m_ClientSendList = new List(128); + m_ClientRpcParams = new ClientRpcParams(); + m_ClientRpcParams.Send = new ClientRpcSendParams(); + m_ClientRpcParams.Send.TargetClientIds = m_ClientSendList; + + // Cache the NetworkManager instance to remove the OnClientConnectedCallback subscription + m_CachedNetworkManager = NetworkManager; + NetworkManager.OnClientConnectedCallback += OnClientConnectedCallback; + } + // We initialize the m_AnimationMessage for all instances in the event that + // ownership or authority changes during runtime. + m_AnimationMessage = new AnimationMessage(); + m_AnimationMessage.AnimationStates = new List(); - if (IsServer) + // Store off our current layer weights and create our animation + // state entries per layer. + for (int layer = 0; layer < m_Animator.layerCount; layer++) + { + // We create an AnimationState per layer to preallocate the maximum + // number of possible AnimationState changes we could send in one + // AnimationMessage. + m_AnimationMessage.AnimationStates.Add(new AnimationState()); + float layerWeightNow = m_Animator.GetLayerWeight(layer); + if (layerWeightNow != m_LayerWeights[layer]) { - m_ClientSendList = new List(128); - m_ClientRpcParams = new ClientRpcParams(); - m_ClientRpcParams.Send = new ClientRpcSendParams(); - m_ClientRpcParams.Send.TargetClientIds = m_ClientSendList; + m_LayerWeights[layer] = layerWeightNow; } } + // Build our reference parameter values to detect when they change var parameters = m_Animator.parameters; m_CachedAnimatorParameters = new NativeArray(parameters.Length, Allocator.Persistent); m_ParametersToUpdate = new List(parameters.Length); @@ -373,6 +655,7 @@ public override void OnNetworkSpawn() m_NetworkAnimatorStateChangeHandler = new NetworkAnimatorStateChangeHandler(this); } + /// public override void OnNetworkDespawn() { Cleanup(); @@ -393,18 +676,25 @@ internal void ServerSynchronizeNewPlayer(ulong playerId) m_ParametersToUpdate.Add(i); } SendParametersUpdate(m_ClientRpcParams); + + // Reset the dirty count before synchronizing the newly connected client with all layers + m_AnimationMessage.IsDirtyCount = 0; + for (int layer = 0; layer < m_Animator.layerCount; layer++) { AnimatorStateInfo st = m_Animator.GetCurrentAnimatorStateInfo(layer); - var stateHash = st.fullPathHash; var normalizedTime = st.normalizedTime; - var totalSpeed = st.speed * st.speedMultiplier; - var adjustedNormalizedMaxTime = totalSpeed > 0.0f ? 1.0f / totalSpeed : 0.0f; - // NOTE: - // When synchronizing, for now we will just complete the transition and - // synchronize the player to the next state being transitioned into - if (m_Animator.IsInTransition(layer)) + var isInTransition = m_Animator.IsInTransition(layer); + + // Grab one of the available AnimationState entries so we can fill it with the current + // layer's animation state. + var animationState = m_AnimationMessage.AnimationStates[layer]; + + // Synchronizing transitions with trigger conditions for late joining clients is now + // handled by cross fading between the late joining client's current layer's AnimationState + // and the transition's destination AnimationState. + if (isInTransition) { var tt = m_Animator.GetAnimatorTransitionInfo(layer); var nextState = m_Animator.GetNextAnimatorStateInfo(layer); @@ -422,23 +712,39 @@ internal void ServerSynchronizeNewPlayer(ulong playerId) { normalizedTime = 0.0f; } - stateHash = nextState.fullPathHash; + + // Use the destination state to transition info lookup table to see if this is a transition we can + // synchronize using cross fading + if (m_DestinationStateToTransitioninfo.ContainsKey(layer)) + { + if (m_DestinationStateToTransitioninfo[layer].ContainsKey(nextState.shortNameHash)) + { + var destinationInfo = m_DestinationStateToTransitioninfo[layer][nextState.shortNameHash]; + stateHash = destinationInfo.OriginatingState; + // Set the destination state to cross fade to from the originating state + animationState.DestinationStateHash = destinationInfo.DestinationState; + } + } } - var animMsg = new AnimationMessage - { - Transition = m_Animator.IsInTransition(layer), - StateHash = stateHash, - NormalizedTime = normalizedTime, - Layer = layer, - Weight = m_LayerWeights[layer] - }; - // Server always send via client RPC - SendAnimStateClientRpc(animMsg, m_ClientRpcParams); + animationState.Transition = isInTransition; // The only time this could be set to true + animationState.StateHash = stateHash; // When a transition, this is the originating/starting state + animationState.NormalizedTime = normalizedTime; + animationState.Layer = layer; + animationState.Weight = m_LayerWeights[layer]; + + // Apply the changes + m_AnimationMessage.AnimationStates[layer] = animationState; } + // Send all animation states + m_AnimationMessage.IsDirtyCount = m_Animator.layerCount; + SendAnimStateClientRpc(m_AnimationMessage, m_ClientRpcParams); } + /// + /// Required for the server to synchronize newly joining players + /// private void OnClientConnectedCallback(ulong playerId) { m_NetworkAnimatorStateChangeHandler.SynchronizeClient(playerId); @@ -461,46 +767,57 @@ internal void CheckForAnimatorChanges() if (m_Animator.runtimeAnimatorController == null) { + if (NetworkManager.LogLevel == LogLevel.Developer) + { + Debug.LogError($"[{GetType().Name}] Could not find an assigned {nameof(RuntimeAnimatorController)}! Cannot check {nameof(Animator)} for changes in state!"); + } return; } int stateHash; float normalizedTime; - // This sends updates only if a layer change or transition is happening + // Reset the dirty count before checking for AnimationState updates + m_AnimationMessage.IsDirtyCount = 0; + + // This sends updates only if a layer's state has changed for (int layer = 0; layer < m_Animator.layerCount; layer++) { AnimatorStateInfo st = m_Animator.GetCurrentAnimatorStateInfo(layer); var totalSpeed = st.speed * st.speedMultiplier; var adjustedNormalizedMaxTime = totalSpeed > 0.0f ? 1.0f / totalSpeed : 0.0f; - // determine if we have reached the end of our state time, if so we can skip - if (st.normalizedTime >= adjustedNormalizedMaxTime) - { - continue; - } - if (!CheckAnimStateChanged(out stateHash, out normalizedTime, layer)) { continue; } - var animMsg = new AnimationMessage - { - Transition = m_Animator.IsInTransition(layer), - StateHash = stateHash, - NormalizedTime = normalizedTime, - Layer = layer, - Weight = m_LayerWeights[layer] - }; + // If we made it here, then we need to synchronize this layer's animation state. + // Get one of the preallocated AnimationState entries and populate it with the + // current layer's state. + var animationState = m_AnimationMessage.AnimationStates[m_AnimationMessage.IsDirtyCount]; + + animationState.Transition = false; // Only used during synchronization + animationState.StateHash = stateHash; + animationState.NormalizedTime = normalizedTime; + animationState.Layer = layer; + animationState.Weight = m_LayerWeights[layer]; + // Apply the changes + m_AnimationMessage.AnimationStates[m_AnimationMessage.IsDirtyCount] = animationState; + m_AnimationMessage.IsDirtyCount++; + } + + // Send an AnimationMessage only if there are dirty AnimationStates to send + if (m_AnimationMessage.IsDirtyCount > 0) + { if (!IsServer && IsOwner) { - SendAnimStateServerRpc(animMsg); + SendAnimStateServerRpc(m_AnimationMessage); } else { - SendAnimStateClientRpc(animMsg); + SendAnimStateClientRpc(m_AnimationMessage); } } } @@ -596,7 +913,7 @@ unsafe private bool CheckParametersChanged() /// /// Checks if any of the Animator's states have changed /// - private unsafe bool CheckAnimStateChanged(out int stateHash, out float normalizedTime, int layer) + private bool CheckAnimStateChanged(out int stateHash, out float normalizedTime, int layer) { stateHash = 0; normalizedTime = 0; @@ -746,9 +1063,9 @@ internal unsafe void UpdateParameters(ParametersUpdateMessage parametersUpdate) } /// - /// Applies the AnimationMessage state to the Animator + /// Applies the AnimationState state to the Animator /// - private unsafe void UpdateAnimationState(AnimationMessage animationState) + internal void UpdateAnimationState(AnimationState animationState) { if (animationState.StateHash == 0) { @@ -756,9 +1073,46 @@ private unsafe void UpdateAnimationState(AnimationMessage animationState) } var currentState = m_Animator.GetCurrentAnimatorStateInfo(animationState.Layer); - if (currentState.fullPathHash != animationState.StateHash || m_Animator.IsInTransition(animationState.Layer) != animationState.Transition) + // If it is a transition, then we are synchronizing transitions in progress when a client late joins + if (animationState.Transition) { - m_Animator.Play(animationState.StateHash, animationState.Layer, animationState.NormalizedTime); + // We should have all valid entries for any animation state transition update + // Verify the AnimationState's assigned Layer exists + if (m_DestinationStateToTransitioninfo.ContainsKey(animationState.Layer)) + { + // Verify the inner-table has the destination AnimationState name hash + if (m_DestinationStateToTransitioninfo[animationState.Layer].ContainsKey(animationState.DestinationStateHash)) + { + // Make sure we are on the originating/starting state we are going to cross fade into + if (currentState.shortNameHash == animationState.StateHash) + { + // Get the transition state information + var transitionStateInfo = m_DestinationStateToTransitioninfo[animationState.Layer][animationState.DestinationStateHash]; + + // Cross fade from the current to the destination state for the transitions duration while starting at the server's current normalized time of the transition + m_Animator.CrossFade(transitionStateInfo.DestinationState, transitionStateInfo.TransitionDuration, transitionStateInfo.Layer, 0.0f, animationState.NormalizedTime); + } + else if (NetworkManager.LogLevel == LogLevel.Developer) + { + NetworkLog.LogWarning($"Current State Hash ({currentState.fullPathHash}) != AnimationState.StateHash ({animationState.StateHash})"); + } + } + else if (NetworkManager.LogLevel == LogLevel.Developer) + { + NetworkLog.LogError($"[DestinationState To Transition Info] Layer ({animationState.Layer}) sub-table does not contain destination state ({animationState.DestinationStateHash})!"); + } + } + else if (NetworkManager.LogLevel == LogLevel.Developer) + { + NetworkLog.LogError($"[DestinationState To Transition Info] Layer ({animationState.Layer}) does not exist!"); + } + } + else + { + if (currentState.fullPathHash != animationState.StateHash) + { + m_Animator.Play(animationState.StateHash, animationState.Layer, animationState.NormalizedTime); + } } m_Animator.SetLayerWeight(animationState.Layer, animationState.Weight); } @@ -781,7 +1135,7 @@ private unsafe void SendParametersUpdateServerRpc(ParametersUpdateMessage parame return; } UpdateParameters(parametersUpdate); - if (NetworkManager.ConnectedClientsIds.Count - 2 > 0) + if (NetworkManager.ConnectedClientsIds.Count > (IsHost ? 2 : 1)) { m_ClientSendList.Clear(); m_ClientSendList.AddRange(NetworkManager.ConnectedClientsIds); @@ -811,11 +1165,11 @@ internal unsafe void SendParametersUpdateClientRpc(ParametersUpdateMessage param /// The server sets its local state and then forwards the message to the remaining clients /// [ServerRpc] - private unsafe void SendAnimStateServerRpc(AnimationMessage animSnapshot, ServerRpcParams serverRpcParams = default) + private unsafe void SendAnimStateServerRpc(AnimationMessage animationMessage, ServerRpcParams serverRpcParams = default) { if (IsServerAuthoritative()) { - m_NetworkAnimatorStateChangeHandler.SendAnimationUpdate(animSnapshot); + m_NetworkAnimatorStateChangeHandler.SendAnimationUpdate(animationMessage); } else { @@ -823,15 +1177,21 @@ private unsafe void SendAnimStateServerRpc(AnimationMessage animSnapshot, Server { return; } - UpdateAnimationState(animSnapshot); - if (NetworkManager.ConnectedClientsIds.Count - 2 > 0) + + foreach (var animationState in animationMessage.AnimationStates) + { + UpdateAnimationState(animationState); + } + + m_NetworkAnimatorStateChangeHandler.AddAnimationMessageToProcessQueue(animationMessage); + if (NetworkManager.ConnectedClientsIds.Count > (IsHost ? 2 : 1)) { m_ClientSendList.Clear(); m_ClientSendList.AddRange(NetworkManager.ConnectedClientsIds); m_ClientSendList.Remove(serverRpcParams.Receive.SenderClientId); m_ClientSendList.Remove(NetworkManager.ServerClientId); m_ClientRpcParams.Send.TargetClientIds = m_ClientSendList; - m_NetworkAnimatorStateChangeHandler.SendAnimationUpdate(animSnapshot, m_ClientRpcParams); + m_NetworkAnimatorStateChangeHandler.SendAnimationUpdate(animationMessage, m_ClientRpcParams); } } } @@ -840,16 +1200,25 @@ private unsafe void SendAnimStateServerRpc(AnimationMessage animSnapshot, Server /// Internally-called RPC client receiving function to update some animation state on a client /// [ClientRpc] - private unsafe void SendAnimStateClientRpc(AnimationMessage animSnapshot, ClientRpcParams clientRpcParams = default) + private unsafe void SendAnimStateClientRpc(AnimationMessage animationMessage, ClientRpcParams clientRpcParams = default) { - if (IsServer) + // This should never happen + if (IsHost) { + if (NetworkManager.LogLevel == LogLevel.Developer) + { + NetworkLog.LogWarning("Detected the Host is sending itself animation updates! Please report this issue."); + } return; } + var isServerAuthoritative = IsServerAuthoritative(); if (!isServerAuthoritative && !IsOwner || isServerAuthoritative) { - UpdateAnimationState(animSnapshot); + foreach (var animationState in animationMessage.AnimationStates) + { + UpdateAnimationState(animationState); + } } } @@ -858,44 +1227,67 @@ private unsafe void SendAnimStateClientRpc(AnimationMessage animSnapshot, Client /// The server sets its local state and then forwards the message to the remaining clients /// [ServerRpc] - private void SendAnimTriggerServerRpc(AnimationTriggerMessage animationTriggerMessage, ServerRpcParams serverRpcParams = default) + internal void SendAnimTriggerServerRpc(AnimationTriggerMessage animationTriggerMessage, ServerRpcParams serverRpcParams = default) { + // If it is server authoritative if (IsServerAuthoritative()) { - m_NetworkAnimatorStateChangeHandler.SendTriggerUpdate(animationTriggerMessage); + // The only condition where this should (be allowed to) happen is when the owner sends the server a trigger message + if (OwnerClientId == serverRpcParams.Receive.SenderClientId) + { + m_NetworkAnimatorStateChangeHandler.QueueTriggerUpdateToClient(animationTriggerMessage); + } + else if (NetworkManager.LogLevel == LogLevel.Developer) + { + NetworkLog.LogWarning($"[Server Authoritative] Detected the a non-authoritative client is sending the server animation trigger updates. If you recently changed ownership of the {name} object, then this could be the reason."); + } } else { + // Ignore if a non-owner sent this. if (serverRpcParams.Receive.SenderClientId != OwnerClientId) { + if (NetworkManager.LogLevel == LogLevel.Developer) + { + NetworkLog.LogWarning($"[Owner Authoritative] Detected the a non-authoritative client is sending the server animation trigger updates. If you recently changed ownership of the {name} object, then this could be the reason."); + } return; } - // trigger the animation locally on the server... - m_Animator.SetBool(animationTriggerMessage.Hash, animationTriggerMessage.IsTriggerSet); - if (NetworkManager.ConnectedClientsIds.Count - 2 > 0) + // set the trigger locally on the server + InternalSetTrigger(animationTriggerMessage.Hash, animationTriggerMessage.IsTriggerSet); + + // send the message to all non-authority clients excluding the server and the owner + if (NetworkManager.ConnectedClientsIds.Count > (IsHost ? 2 : 1)) { m_ClientSendList.Clear(); m_ClientSendList.AddRange(NetworkManager.ConnectedClientsIds); m_ClientSendList.Remove(serverRpcParams.Receive.SenderClientId); m_ClientSendList.Remove(NetworkManager.ServerClientId); m_ClientRpcParams.Send.TargetClientIds = m_ClientSendList; - m_NetworkAnimatorStateChangeHandler.SendTriggerUpdate(animationTriggerMessage, m_ClientRpcParams); + m_NetworkAnimatorStateChangeHandler.QueueTriggerUpdateToClient(animationTriggerMessage, m_ClientRpcParams); } } + } + /// + /// See above + /// + private void InternalSetTrigger(int hash, bool isSet = true) + { + m_Animator.SetBool(hash, isSet); } /// /// Internally-called RPC client receiving function to update a trigger when the server wants to forward /// a trigger for a client to play / reset /// - /// the payload containing the trigger data to apply + /// the payload containing the trigger data to apply /// unused [ClientRpc] internal void SendAnimTriggerClientRpc(AnimationTriggerMessage animationTriggerMessage, ClientRpcParams clientRpcParams = default) { - m_Animator.SetBool(animationTriggerMessage.Hash, animationTriggerMessage.IsTriggerSet); + InternalSetTrigger(animationTriggerMessage.Hash, animationTriggerMessage.IsTriggerSet); } /// @@ -923,14 +1315,20 @@ public void SetTrigger(int hash, bool setTrigger = true) var animTriggerMessage = new AnimationTriggerMessage() { Hash = hash, IsTriggerSet = setTrigger }; if (IsServer) { - SendAnimTriggerClientRpc(animTriggerMessage); + /// as to why we queue + m_NetworkAnimatorStateChangeHandler.QueueTriggerUpdateToClient(animTriggerMessage); + if (!IsHost) + { + InternalSetTrigger(hash); + } } else { - SendAnimTriggerServerRpc(animTriggerMessage); + /// as to why we queue + m_NetworkAnimatorStateChangeHandler.QueueTriggerUpdateToServer(animTriggerMessage); if (!IsServerAuthoritative()) { - m_Animator.SetTrigger(hash); + InternalSetTrigger(hash); } } } diff --git a/Components/NetworkRigidbody.cs b/Components/NetworkRigidbody.cs index 6515f7e..0569aa9 100644 --- a/Components/NetworkRigidbody.cs +++ b/Components/NetworkRigidbody.cs @@ -9,6 +9,7 @@ namespace Unity.Netcode.Components /// [RequireComponent(typeof(Rigidbody))] [RequireComponent(typeof(NetworkTransform))] + [AddComponentMenu("Netcode/Network Rigidbody")] public class NetworkRigidbody : NetworkBehaviour { /// diff --git a/Components/NetworkRigidbody2D.cs b/Components/NetworkRigidbody2D.cs index 1ac82bb..246519c 100644 --- a/Components/NetworkRigidbody2D.cs +++ b/Components/NetworkRigidbody2D.cs @@ -9,6 +9,7 @@ namespace Unity.Netcode.Components /// [RequireComponent(typeof(Rigidbody2D))] [RequireComponent(typeof(NetworkTransform))] + [AddComponentMenu("Netcode/Network Rigidbody 2D")] public class NetworkRigidbody2D : NetworkBehaviour { private Rigidbody2D m_Rigidbody; diff --git a/Components/NetworkTransform.cs b/Components/NetworkTransform.cs index b1ff99e..f8d4dbd 100644 --- a/Components/NetworkTransform.cs +++ b/Components/NetworkTransform.cs @@ -10,7 +10,7 @@ namespace Unity.Netcode.Components /// The replicated value will be automatically be interpolated (if active) and applied to the underlying GameObject's transform. /// [DisallowMultipleComponent] - [AddComponentMenu("Netcode/" + nameof(NetworkTransform))] + [AddComponentMenu("Netcode/Network Transform")] [DefaultExecutionOrder(100000)] // this is needed to catch the update time after the transform was updated by user scripts public class NetworkTransform : NetworkBehaviour { @@ -282,14 +282,7 @@ public void NetworkSerialize(BufferSerializer serializer) where T : IReade { // Go ahead and mark the local state dirty or not dirty as well /// - if (HasPositionChange || HasRotAngleChange || HasScaleChange) - { - IsDirty = true; - } - else - { - IsDirty = false; - } + IsDirty = HasPositionChange || HasRotAngleChange || HasScaleChange; } } } @@ -458,19 +451,6 @@ internal NetworkTransformState GetLastSentState() return m_LastSentState; } - /// - /// Calculated when spawned, this is used to offset a newly received non-authority side state by 1 tick duration - /// in order to end the extrapolation for that state's values. - /// - /// - /// Example: - /// NetworkState-A is received, processed, and measurements added - /// NetworkState-A is duplicated (NetworkState-A-Post) and its sent time is offset by the tick frequency - /// One tick later, NetworkState-A-Post is applied to end that delta's extrapolation. - /// to see how NetworkState-A-Post doesn't get excluded/missed - /// - private double m_TickFrequency; - /// /// This will try to send/commit the current transform delta states (if any) /// @@ -495,14 +475,16 @@ protected void TryCommitTransformToServer(Transform transformToCommit, double di } else // Non-Authority { + var position = InLocalSpace ? transformToCommit.localPosition : transformToCommit.position; + var rotation = InLocalSpace ? transformToCommit.localRotation : transformToCommit.rotation; // We are an owner requesting to update our state if (!m_CachedIsServer) { - SetStateServerRpc(transformToCommit.position, transformToCommit.rotation, transformToCommit.localScale, false); + SetStateServerRpc(position, rotation, transformToCommit.localScale, false); } else // Server is always authoritative (including owner authoritative) { - SetStateClientRpc(transformToCommit.position, transformToCommit.rotation, transformToCommit.localScale, false); + SetStateClientRpc(position, rotation, transformToCommit.localScale, false); } } } @@ -528,37 +510,24 @@ private void TryCommitTransform(Transform transformToCommit, double dirtyTime) } } + /// + /// Initializes the interpolators with the current transform values + /// private void ResetInterpolatedStateToCurrentAuthoritativeState() { var serverTime = NetworkManager.ServerTime.Time; - - // TODO: Look into a better way to communicate the entire state for late joining clients. - // Since the replicated network state will just be the most recent deltas and not the entire state. - //m_PositionXInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.PositionX, serverTime); - //m_PositionYInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.PositionY, serverTime); - //m_PositionZInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.PositionZ, serverTime); - - //m_RotationInterpolator.ResetTo(Quaternion.Euler(m_LocalAuthoritativeNetworkState.RotAngleX, m_LocalAuthoritativeNetworkState.RotAngleY, m_LocalAuthoritativeNetworkState.RotAngleZ), serverTime); - - //m_ScaleXInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.ScaleX, serverTime); - //m_ScaleYInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.ScaleY, serverTime); - //m_ScaleZInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.ScaleZ, serverTime); - - // NOTE ABOUT THIS CHANGE: - // !!! This will exclude any scale changes because we currently do not spawn network objects with scale !!! - // Regarding Scale: It will be the same scale as the default scale for the object being spawned. var position = InLocalSpace ? transform.localPosition : transform.position; m_PositionXInterpolator.ResetTo(position.x, serverTime); m_PositionYInterpolator.ResetTo(position.y, serverTime); m_PositionZInterpolator.ResetTo(position.z, serverTime); + var rotation = InLocalSpace ? transform.localRotation : transform.rotation; m_RotationInterpolator.ResetTo(rotation, serverTime); - // TODO: (Create Jira Ticket) Synchronize local scale during NetworkObject synchronization - // (We will probably want to byte pack TransformData to offset the 3 float addition) - m_ScaleXInterpolator.ResetTo(transform.localScale.x, serverTime); - m_ScaleYInterpolator.ResetTo(transform.localScale.y, serverTime); - m_ScaleZInterpolator.ResetTo(transform.localScale.z, serverTime); + var scale = transform.localScale; + m_ScaleXInterpolator.ResetTo(scale.x, serverTime); + m_ScaleYInterpolator.ResetTo(scale.y, serverTime); + m_ScaleZInterpolator.ResetTo(scale.z, serverTime); } /// @@ -609,63 +578,63 @@ private bool ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState netw isDirty = true; } - if (SyncPositionX && Mathf.Abs(networkState.PositionX - position.x) >= PositionThreshold || networkState.IsTeleportingNextFrame) + if (SyncPositionX && (Mathf.Abs(networkState.PositionX - position.x) >= PositionThreshold || networkState.IsTeleportingNextFrame)) { 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)) { 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)) { networkState.PositionZ = position.z; networkState.HasPositionZ = true; isPositionDirty = true; } - 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)) { 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)) { 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)) { networkState.RotAngleZ = rotAngles.z; networkState.HasRotAngleZ = true; isRotationDirty = true; } - if (SyncScaleX && Mathf.Abs(networkState.ScaleX - scale.x) >= ScaleThreshold || networkState.IsTeleportingNextFrame) + if (SyncScaleX && (Mathf.Abs(networkState.ScaleX - scale.x) >= ScaleThreshold || networkState.IsTeleportingNextFrame)) { 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)) { 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)) { networkState.ScaleZ = scale.z; networkState.HasScaleZ = true; @@ -1014,7 +983,6 @@ public override void OnNetworkSpawn() { m_CachedIsServer = IsServer; m_CachedNetworkManager = NetworkManager; - m_TickFrequency = 1.0 / NetworkManager.NetworkConfig.TickRate; Initialize(); @@ -1150,8 +1118,7 @@ private void SetStateInternal(Vector3 pos, Quaternion rot, Vector3 scale, bool s } else { - transform.position = pos; - transform.rotation = rot; + transform.SetPositionAndRotation(pos, rot); } transform.localScale = scale; m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = shouldTeleport; @@ -1169,11 +1136,7 @@ private void SetStateInternal(Vector3 pos, Quaternion rot, Vector3 scale, bool s private void SetStateClientRpc(Vector3 pos, Quaternion rot, Vector3 scale, bool shouldTeleport, ClientRpcParams clientRpcParams = default) { // Server dictated state is always applied - transform.position = pos; - transform.rotation = rot; - transform.localScale = scale; - m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = shouldTeleport; - TryCommitTransform(transform, m_CachedNetworkManager.LocalTime.Time); + SetStateInternal(pos, rot, scale, shouldTeleport); } /// @@ -1190,12 +1153,7 @@ private void SetStateServerRpc(Vector3 pos, Quaternion rot, Vector3 scale, bool { (pos, rot, scale) = OnClientRequestChange(pos, rot, scale); } - - transform.position = pos; - transform.rotation = rot; - transform.localScale = scale; - m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = shouldTeleport; - TryCommitTransform(transform, m_CachedNetworkManager.LocalTime.Time); + SetStateInternal(pos, rot, scale, shouldTeleport); } /// diff --git a/Editor/CodeGen/CodeGenHelpers.cs b/Editor/CodeGen/CodeGenHelpers.cs index 24e6315..7571857 100644 --- a/Editor/CodeGen/CodeGenHelpers.cs +++ b/Editor/CodeGen/CodeGenHelpers.cs @@ -15,6 +15,10 @@ namespace Unity.Netcode.Editor.CodeGen { internal static class CodeGenHelpers { + public const string DotnetModuleName = "netstandard.dll"; + public const string UnityModuleName = "UnityEngine.CoreModule.dll"; + public const string NetcodeModuleName = "Unity.Netcode.Runtime.dll"; + public const string RuntimeAssemblyName = "Unity.Netcode.Runtime"; public static readonly string NetworkBehaviour_FullName = typeof(NetworkBehaviour).FullName; @@ -119,6 +123,19 @@ public static bool HasInterface(this TypeReference typeReference, string interfa try { var typeDef = typeReference.Resolve(); + // Note: this won't catch generics correctly. + // + // class Foo: IInterface {} + // class Bar: Foo {} + // + // Bar.HasInterface(IInterface) -> returns false even though it should be true. + // + // This can be fixed (see GetAllFieldsAndResolveGenerics() in NetworkBehaviourILPP to understand how) + // but right now we don't need that to work so it's left alone to reduce complexity + if (typeDef.BaseType.HasInterface(interfaceTypeFullName)) + { + return true; + } var typeFaces = typeDef.Interfaces; return typeFaces.Any(iface => iface.InterfaceType.FullName == interfaceTypeFullName); } @@ -380,5 +397,74 @@ public static AssemblyDefinition AssemblyDefinitionFor(ICompiledAssembly compile return assemblyDefinition; } + + private static void SearchForBaseModulesRecursive(AssemblyDefinition assemblyDefinition, PostProcessorAssemblyResolver assemblyResolver, ref ModuleDefinition unityModule, ref ModuleDefinition netcodeModule, HashSet visited) + { + + foreach (var module in assemblyDefinition.Modules) + { + if (module == null) + { + continue; + } + + if (unityModule != null && netcodeModule != null) + { + return; + } + + if (unityModule == null && module.Name == UnityModuleName) + { + unityModule = module; + continue; + } + + if (netcodeModule == null && module.Name == NetcodeModuleName) + { + netcodeModule = module; + continue; + } + } + if (unityModule != null && netcodeModule != null) + { + return; + } + + foreach (var assemblyNameReference in assemblyDefinition.MainModule.AssemblyReferences) + { + if (assemblyNameReference == null) + { + continue; + } + if (visited.Contains(assemblyNameReference.Name)) + { + continue; + } + + visited.Add(assemblyNameReference.Name); + + var assembly = assemblyResolver.Resolve(assemblyNameReference); + if (assembly == null) + { + continue; + } + SearchForBaseModulesRecursive(assembly, assemblyResolver, ref unityModule, ref netcodeModule, visited); + + if (unityModule != null && netcodeModule != null) + { + return; + } + } + } + + public static (ModuleDefinition UnityModule, ModuleDefinition NetcodeModule) FindBaseModules(AssemblyDefinition assemblyDefinition, PostProcessorAssemblyResolver assemblyResolver) + { + ModuleDefinition unityModule = null; + ModuleDefinition netcodeModule = null; + var visited = new HashSet(); + SearchForBaseModulesRecursive(assemblyDefinition, assemblyResolver, ref unityModule, ref netcodeModule, visited); + + return (unityModule, netcodeModule); + } } } diff --git a/Editor/CodeGen/INetworkMessageILPP.cs b/Editor/CodeGen/INetworkMessageILPP.cs index 309e268..30b6f0b 100644 --- a/Editor/CodeGen/INetworkMessageILPP.cs +++ b/Editor/CodeGen/INetworkMessageILPP.cs @@ -2,7 +2,6 @@ using System.IO; using System.Linq; using System.Collections.Generic; -using System.Reflection; using Mono.Cecil; using Mono.Cecil.Cil; using Mono.Cecil.Rocks; @@ -17,8 +16,7 @@ internal sealed class INetworkMessageILPP : ILPPInterface { public override ILPPInterface GetInstance() => this; - public override bool WillProcess(ICompiledAssembly compiledAssembly) => - compiledAssembly.Name == CodeGenHelpers.RuntimeAssemblyName; + public override bool WillProcess(ICompiledAssembly compiledAssembly) => compiledAssembly.Name == CodeGenHelpers.RuntimeAssemblyName; private readonly List m_Diagnostics = new List(); @@ -32,13 +30,22 @@ public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly) m_Diagnostics.Clear(); // read - var assemblyDefinition = CodeGenHelpers.AssemblyDefinitionFor(compiledAssembly, out var resolver); + var assemblyDefinition = CodeGenHelpers.AssemblyDefinitionFor(compiledAssembly, out m_AssemblyResolver); if (assemblyDefinition == null) { m_Diagnostics.AddError($"Cannot read assembly definition: {compiledAssembly.Name}"); return null; } + // modules + (_, m_NetcodeModule) = CodeGenHelpers.FindBaseModules(assemblyDefinition, m_AssemblyResolver); + + if (m_NetcodeModule == null) + { + m_Diagnostics.AddError($"Cannot find Netcode module: {CodeGenHelpers.NetcodeModuleName}"); + return null; + } + // process var mainModule = assemblyDefinition.MainModule; if (mainModule != null) @@ -60,7 +67,7 @@ public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly) } catch (Exception e) { - m_Diagnostics.AddError((e.ToString() + e.StackTrace.ToString()).Replace("\n", "|").Replace("\r", "|")); + m_Diagnostics.AddError((e.ToString() + e.StackTrace).Replace("\n", "|").Replace("\r", "|")); } } else @@ -91,6 +98,8 @@ public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly) return new ILPostProcessResult(new InMemoryAssembly(pe.ToArray(), pdb.ToArray()), m_Diagnostics); } + private ModuleDefinition m_NetcodeModule; + private PostProcessorAssemblyResolver m_AssemblyResolver; private MethodReference m_MessagingSystem_ReceiveMessage_MethodRef; private TypeReference m_MessagingSystem_MessageWithHandler_TypeRef; @@ -105,63 +114,102 @@ public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly) private bool ImportReferences(ModuleDefinition moduleDefinition) { - m_MessagingSystem_MessageHandler_Constructor_TypeRef = moduleDefinition.ImportReference(typeof(MessagingSystem.MessageHandler).GetConstructors()[0]); + // Different environments seem to have different situations... + // Some have these definitions in netstandard.dll... + // some seem to have them elsewhere... + // Since they're standard .net classes they're not going to cause + // the same issues as referencing other assemblies, in theory, since + // the definitions should be standard and consistent across platforms + // (i.e., there's no #if UNITY_EDITOR in them that could create + // invalid IL code) + TypeDefinition typeTypeDef = moduleDefinition.ImportReference(typeof(Type)).Resolve(); + TypeDefinition listTypeDef = moduleDefinition.ImportReference(typeof(List<>)).Resolve(); + + TypeDefinition messageHandlerTypeDef = null; + TypeDefinition messageWithHandlerTypeDef = null; + TypeDefinition ilppMessageProviderTypeDef = null; + TypeDefinition messagingSystemTypeDef = null; + foreach (var netcodeTypeDef in m_NetcodeModule.GetAllTypes()) + { + if (messageHandlerTypeDef == null && netcodeTypeDef.Name == nameof(MessagingSystem.MessageHandler)) + { + messageHandlerTypeDef = netcodeTypeDef; + continue; + } + + if (messageWithHandlerTypeDef == null && netcodeTypeDef.Name == nameof(MessagingSystem.MessageWithHandler)) + { + messageWithHandlerTypeDef = netcodeTypeDef; + continue; + } + + if (ilppMessageProviderTypeDef == null && netcodeTypeDef.Name == nameof(ILPPMessageProvider)) + { + ilppMessageProviderTypeDef = netcodeTypeDef; + continue; + } + + if (messagingSystemTypeDef == null && netcodeTypeDef.Name == nameof(MessagingSystem)) + { + messagingSystemTypeDef = netcodeTypeDef; + continue; + } + } + + m_MessagingSystem_MessageHandler_Constructor_TypeRef = moduleDefinition.ImportReference(messageHandlerTypeDef.GetConstructors().First()); - var messageWithHandlerType = typeof(MessagingSystem.MessageWithHandler); - m_MessagingSystem_MessageWithHandler_TypeRef = moduleDefinition.ImportReference(messageWithHandlerType); - foreach (var fieldInfo in messageWithHandlerType.GetFields()) + m_MessagingSystem_MessageWithHandler_TypeRef = moduleDefinition.ImportReference(messageWithHandlerTypeDef); + foreach (var fieldDef in messageWithHandlerTypeDef.Fields) { - switch (fieldInfo.Name) + switch (fieldDef.Name) { case nameof(MessagingSystem.MessageWithHandler.MessageType): - m_MessagingSystem_MessageWithHandler_MessageType_FieldRef = moduleDefinition.ImportReference(fieldInfo); + m_MessagingSystem_MessageWithHandler_MessageType_FieldRef = moduleDefinition.ImportReference(fieldDef); break; case nameof(MessagingSystem.MessageWithHandler.Handler): - m_MessagingSystem_MessageWithHandler_Handler_FieldRef = moduleDefinition.ImportReference(fieldInfo); + m_MessagingSystem_MessageWithHandler_Handler_FieldRef = moduleDefinition.ImportReference(fieldDef); break; } } - var typeType = typeof(Type); - foreach (var methodInfo in typeType.GetMethods()) + foreach (var methodDef in typeTypeDef.Methods) { - switch (methodInfo.Name) + switch (methodDef.Name) { case nameof(Type.GetTypeFromHandle): - m_Type_GetTypeFromHandle_MethodRef = moduleDefinition.ImportReference(methodInfo); + m_Type_GetTypeFromHandle_MethodRef = moduleDefinition.ImportReference(methodDef); break; } } - var ilppMessageProviderType = typeof(ILPPMessageProvider); - foreach (var fieldInfo in ilppMessageProviderType.GetFields(BindingFlags.Static | BindingFlags.NonPublic)) + foreach (var fieldDef in ilppMessageProviderTypeDef.Fields) { - switch (fieldInfo.Name) + switch (fieldDef.Name) { case nameof(ILPPMessageProvider.__network_message_types): - m_ILPPMessageProvider___network_message_types_FieldRef = moduleDefinition.ImportReference(fieldInfo); + m_ILPPMessageProvider___network_message_types_FieldRef = moduleDefinition.ImportReference(fieldDef); break; } } - var listType = typeof(List); - foreach (var methodInfo in listType.GetMethods()) + foreach (var methodDef in listTypeDef.Methods) { - switch (methodInfo.Name) + switch (methodDef.Name) { - case nameof(List.Add): - m_List_Add_MethodRef = moduleDefinition.ImportReference(methodInfo); + case "Add": + m_List_Add_MethodRef = methodDef; + m_List_Add_MethodRef.DeclaringType = listTypeDef.MakeGenericInstanceType(messageWithHandlerTypeDef); + m_List_Add_MethodRef = moduleDefinition.ImportReference(m_List_Add_MethodRef); break; } } - var messagingSystemType = typeof(MessagingSystem); - foreach (var methodInfo in messagingSystemType.GetMethods(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public)) + foreach (var methodDef in messagingSystemTypeDef.Methods) { - switch (methodInfo.Name) + switch (methodDef.Name) { case k_ReceiveMessageName: - m_MessagingSystem_ReceiveMessage_MethodRef = moduleDefinition.ImportReference(methodInfo); + m_MessagingSystem_ReceiveMessage_MethodRef = moduleDefinition.ImportReference(methodDef); break; } } @@ -217,10 +265,8 @@ private void CreateInstructionsToRegisterType(ILProcessor processor, List networkMessageTypes) diff --git a/Editor/CodeGen/INetworkSerializableILPP.cs b/Editor/CodeGen/INetworkSerializableILPP.cs index 31d7f34..22b2f14 100644 --- a/Editor/CodeGen/INetworkSerializableILPP.cs +++ b/Editor/CodeGen/INetworkSerializableILPP.cs @@ -10,7 +10,6 @@ namespace Unity.Netcode.Editor.CodeGen { - internal sealed class INetworkSerializableILPP : ILPPInterface { public override ILPPInterface GetInstance() => this; @@ -92,7 +91,7 @@ public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly) } catch (Exception e) { - m_Diagnostics.AddError((e.ToString() + e.StackTrace.ToString()).Replace("\n", "|").Replace("\r", "|")); + m_Diagnostics.AddError((e.ToString() + e.StackTrace).Replace("\n", "|").Replace("\r", "|")); } } else diff --git a/Editor/CodeGen/NetworkBehaviourILPP.cs b/Editor/CodeGen/NetworkBehaviourILPP.cs index 8f8305c..ce7d3bb 100644 --- a/Editor/CodeGen/NetworkBehaviourILPP.cs +++ b/Editor/CodeGen/NetworkBehaviourILPP.cs @@ -2,7 +2,6 @@ using System.IO; using System.Linq; using System.Collections.Generic; -using System.Reflection; using System.Runtime.CompilerServices; using Mono.Cecil; using Mono.Cecil.Cil; @@ -23,8 +22,7 @@ internal sealed class NetworkBehaviourILPP : ILPPInterface public override ILPPInterface GetInstance() => this; - public override bool WillProcess(ICompiledAssembly compiledAssembly) => - compiledAssembly.References.Any(filePath => Path.GetFileNameWithoutExtension(filePath) == CodeGenHelpers.RuntimeAssemblyName); + public override bool WillProcess(ICompiledAssembly compiledAssembly) => compiledAssembly.References.Any(filePath => Path.GetFileNameWithoutExtension(filePath) == CodeGenHelpers.RuntimeAssemblyName); private readonly List m_Diagnostics = new List(); @@ -35,7 +33,6 @@ public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly) return null; } - m_Diagnostics.Clear(); // read @@ -46,11 +43,27 @@ public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly) return null; } + // modules + (m_UnityModule, m_NetcodeModule) = CodeGenHelpers.FindBaseModules(assemblyDefinition, m_AssemblyResolver); + + if (m_UnityModule == null) + { + m_Diagnostics.AddError($"Cannot find Unity module: {CodeGenHelpers.UnityModuleName}"); + return null; + } + + if (m_NetcodeModule == null) + { + m_Diagnostics.AddError($"Cannot find Netcode module: {CodeGenHelpers.NetcodeModuleName}"); + return null; + } + // process var mainModule = assemblyDefinition.MainModule; if (mainModule != null) { m_MainModule = mainModule; + if (ImportReferences(mainModule)) { // process `NetworkBehaviour` types @@ -60,10 +73,12 @@ public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly) .Where(t => t.IsSubclassOf(CodeGenHelpers.NetworkBehaviour_FullName)) .ToList() .ForEach(b => ProcessNetworkBehaviour(b, compiledAssembly.Defines)); + + CreateNetworkVariableTypeInitializers(assemblyDefinition); } catch (Exception e) { - m_Diagnostics.AddError((e.ToString() + e.StackTrace.ToString()).Replace("\n", "|").Replace("\r", "|")); + m_Diagnostics.AddError((e.ToString() + e.StackTrace).Replace("\n", "|").Replace("\r", "|")); } } else @@ -92,7 +107,117 @@ public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly) return new ILPostProcessResult(new InMemoryAssembly(pe.ToArray(), pdb.ToArray()), m_Diagnostics); } + private MethodDefinition GetOrCreateStaticConstructor(TypeDefinition typeDefinition) + { + var staticCtorMethodDef = typeDefinition.GetStaticConstructor(); + if (staticCtorMethodDef == null) + { + staticCtorMethodDef = new MethodDefinition( + ".cctor", // Static Constructor (constant-constructor) + MethodAttributes.HideBySig | + MethodAttributes.SpecialName | + MethodAttributes.RTSpecialName | + MethodAttributes.Static, + typeDefinition.Module.TypeSystem.Void); + staticCtorMethodDef.Body.Instructions.Add(Instruction.Create(OpCodes.Ret)); + typeDefinition.Methods.Add(staticCtorMethodDef); + } + + return staticCtorMethodDef; + } + + private bool IsMemcpyableType(TypeReference type) + { + foreach (var supportedType in BaseSupportedTypes) + { + if (type.FullName == supportedType.FullName) + { + return true; + } + } + + return false; + } + + private void CreateNetworkVariableTypeInitializers(AssemblyDefinition assembly) + { + foreach (var typeDefinition in assembly.MainModule.Types) + { + if (typeDefinition.FullName == "") + { + var staticCtorMethodDef = GetOrCreateStaticConstructor(typeDefinition); + + var processor = staticCtorMethodDef.Body.GetILProcessor(); + + var instructions = new List(); + + foreach (var type in m_WrappedNetworkVariableTypes) + { + // If a serializable type isn't found, FallbackSerializer will be used automatically, which will + // call into UserNetworkVariableSerialization, giving the user a chance to define their own serializaiton + // for types that aren't in our official supported types list. + GenericInstanceMethod serializeMethod = null; + GenericInstanceMethod equalityMethod; + + if (type.IsValueType) + { + if (type.HasInterface(typeof(INetworkSerializeByMemcpy).FullName) || type.Resolve().IsEnum || IsMemcpyableType(type)) + { + serializeMethod = new GenericInstanceMethod(m_NetworkVariableSerializationTypes_InitializeSerializer_UnmanagedByMemcpy_MethodRef); + } + else if (type.HasInterface(typeof(INetworkSerializable).FullName)) + { + serializeMethod = new GenericInstanceMethod(m_NetworkVariableSerializationTypes_InitializeSerializer_UnmanagedINetworkSerializable_MethodRef); + } + else if (type.HasInterface(CodeGenHelpers.IUTF8Bytes_FullName) && type.HasInterface(k_INativeListBool_FullName)) + { + serializeMethod = new GenericInstanceMethod(m_NetworkVariableSerializationTypes_InitializeSerializer_FixedString_MethodRef); + } + + if (type.HasInterface(typeof(IEquatable<>).FullName + "<" + type.FullName + ">")) + { + equalityMethod = new GenericInstanceMethod(m_NetworkVariableSerializationTypes_InitializeEqualityChecker_UnmanagedIEquatable_MethodRef); + } + else + { + equalityMethod = new GenericInstanceMethod(m_NetworkVariableSerializationTypes_InitializeEqualityChecker_UnmanagedValueEquals_MethodRef); + } + } + else + { + if (type.HasInterface(typeof(INetworkSerializable).FullName)) + { + serializeMethod = new GenericInstanceMethod(m_NetworkVariableSerializationTypes_InitializeSerializer_ManagedINetworkSerializable_MethodRef); + } + + if (type.HasInterface(typeof(IEquatable<>).FullName + "<" + type.FullName + ">")) + { + equalityMethod = new GenericInstanceMethod(m_NetworkVariableSerializationTypes_InitializeEqualityChecker_ManagedIEquatable_MethodRef); + } + else + { + equalityMethod = new GenericInstanceMethod(m_NetworkVariableSerializationTypes_InitializeEqualityChecker_ManagedClassEquals_MethodRef); + } + } + + if (serializeMethod != null) + { + serializeMethod.GenericArguments.Add(type); + instructions.Add(processor.Create(OpCodes.Call, m_MainModule.ImportReference(serializeMethod))); + } + equalityMethod.GenericArguments.Add(type); + instructions.Add(processor.Create(OpCodes.Call, m_MainModule.ImportReference(equalityMethod))); + } + + instructions.ForEach(instruction => processor.Body.Instructions.Insert(processor.Body.Instructions.Count - 1, instruction)); + break; + } + } + } + private ModuleDefinition m_MainModule; + private ModuleDefinition m_UnityModule; + private ModuleDefinition m_NetcodeModule; private PostProcessorAssemblyResolver m_AssemblyResolver; private MethodReference m_Debug_LogError_MethodRef; @@ -123,14 +248,51 @@ public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly) private FieldReference m_ServerRpcParams_Receive_FieldRef; private FieldReference m_ServerRpcParams_Receive_SenderClientId_FieldRef; private TypeReference m_ClientRpcParams_TypeRef; + private MethodReference m_NetworkVariableSerializationTypes_InitializeSerializer_UnmanagedByMemcpy_MethodRef; + private MethodReference m_NetworkVariableSerializationTypes_InitializeSerializer_UnmanagedINetworkSerializable_MethodRef; + private MethodReference m_NetworkVariableSerializationTypes_InitializeSerializer_ManagedINetworkSerializable_MethodRef; + private MethodReference m_NetworkVariableSerializationTypes_InitializeSerializer_FixedString_MethodRef; + private MethodReference m_NetworkVariableSerializationTypes_InitializeEqualityChecker_ManagedIEquatable_MethodRef; + private MethodReference m_NetworkVariableSerializationTypes_InitializeEqualityChecker_UnmanagedIEquatable_MethodRef; + private MethodReference m_NetworkVariableSerializationTypes_InitializeEqualityChecker_UnmanagedValueEquals_MethodRef; + private MethodReference m_NetworkVariableSerializationTypes_InitializeEqualityChecker_ManagedClassEquals_MethodRef; private TypeReference m_FastBufferWriter_TypeRef; - private Dictionary m_FastBufferWriter_WriteValue_MethodRefs = new Dictionary(); - private List m_FastBufferWriter_ExtensionMethodRefs = new List(); + private readonly Dictionary m_FastBufferWriter_WriteValue_MethodRefs = new Dictionary(); + private readonly List m_FastBufferWriter_ExtensionMethodRefs = new List(); private TypeReference m_FastBufferReader_TypeRef; - private Dictionary m_FastBufferReader_ReadValue_MethodRefs = new Dictionary(); - private List m_FastBufferReader_ExtensionMethodRefs = new List(); + private readonly Dictionary m_FastBufferReader_ReadValue_MethodRefs = new Dictionary(); + private readonly List m_FastBufferReader_ExtensionMethodRefs = new List(); + + private HashSet m_WrappedNetworkVariableTypes = new HashSet(); + + internal static readonly Type[] BaseSupportedTypes = new[] + { + typeof(bool), + typeof(byte), + typeof(sbyte), + typeof(char), + typeof(decimal), + typeof(double), + typeof(float), + typeof(int), + typeof(uint), + typeof(long), + typeof(ulong), + typeof(short), + typeof(ushort), + typeof(Vector2), + typeof(Vector3), + typeof(Vector2Int), + typeof(Vector3Int), + typeof(Vector4), + typeof(Quaternion), + typeof(Color), + typeof(Color32), + typeof(Ray), + typeof(Ray2D) + }; private const string k_Debug_LogError = nameof(Debug.LogError); private const string k_NetworkManager_LocalClientId = nameof(NetworkManager.LocalClientId); @@ -157,160 +319,243 @@ public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly) private const string k_ServerRpcParams_Receive = nameof(ServerRpcParams.Receive); private const string k_ServerRpcReceiveParams_SenderClientId = nameof(ServerRpcReceiveParams.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"; + private bool ImportReferences(ModuleDefinition moduleDefinition) { - var debugType = typeof(Debug); - foreach (var methodInfo in debugType.GetMethods()) + TypeDefinition debugTypeDef = null; + foreach (var unityTypeDef in m_UnityModule.GetAllTypes()) + { + if (debugTypeDef == null && unityTypeDef.FullName == typeof(Debug).FullName) + { + debugTypeDef = unityTypeDef; + continue; + } + } + + TypeDefinition networkManagerTypeDef = null; + TypeDefinition networkBehaviourTypeDef = null; + TypeDefinition networkHandlerDelegateTypeDef = null; + TypeDefinition rpcParamsTypeDef = null; + TypeDefinition serverRpcParamsTypeDef = null; + TypeDefinition clientRpcParamsTypeDef = null; + TypeDefinition fastBufferWriterTypeDef = null; + TypeDefinition fastBufferReaderTypeDef = null; + TypeDefinition networkVariableSerializationTypesTypeDef = null; + foreach (var netcodeTypeDef in m_NetcodeModule.GetAllTypes()) + { + if (networkManagerTypeDef == null && netcodeTypeDef.Name == nameof(NetworkManager)) + { + networkManagerTypeDef = netcodeTypeDef; + continue; + } + + if (networkBehaviourTypeDef == null && netcodeTypeDef.Name == nameof(NetworkBehaviour)) + { + networkBehaviourTypeDef = netcodeTypeDef; + continue; + } + + if (networkHandlerDelegateTypeDef == null && netcodeTypeDef.Name == nameof(NetworkManager.RpcReceiveHandler)) + { + networkHandlerDelegateTypeDef = netcodeTypeDef; + continue; + } + + if (rpcParamsTypeDef == null && netcodeTypeDef.Name == nameof(__RpcParams)) + { + rpcParamsTypeDef = netcodeTypeDef; + continue; + } + + if (serverRpcParamsTypeDef == null && netcodeTypeDef.Name == nameof(ServerRpcParams)) + { + serverRpcParamsTypeDef = netcodeTypeDef; + continue; + } + + if (clientRpcParamsTypeDef == null && netcodeTypeDef.Name == nameof(ClientRpcParams)) + { + clientRpcParamsTypeDef = netcodeTypeDef; + continue; + } + + if (fastBufferWriterTypeDef == null && netcodeTypeDef.Name == nameof(FastBufferWriter)) + { + fastBufferWriterTypeDef = netcodeTypeDef; + continue; + } + + if (fastBufferReaderTypeDef == null && netcodeTypeDef.Name == nameof(FastBufferReader)) + { + fastBufferReaderTypeDef = netcodeTypeDef; + continue; + } + + if (networkVariableSerializationTypesTypeDef == null && netcodeTypeDef.Name == nameof(NetworkVariableSerializationTypes)) + { + networkVariableSerializationTypesTypeDef = netcodeTypeDef; + continue; + } + } + + foreach (var methodDef in debugTypeDef.Methods) { - switch (methodInfo.Name) + switch (methodDef.Name) { case k_Debug_LogError: - if (methodInfo.GetParameters().Length == 1) + if (methodDef.Parameters.Count == 1) { - m_Debug_LogError_MethodRef = moduleDefinition.ImportReference(methodInfo); + m_Debug_LogError_MethodRef = moduleDefinition.ImportReference(methodDef); } break; } } - var networkManagerType = typeof(NetworkManager); - m_NetworkManager_TypeRef = moduleDefinition.ImportReference(networkManagerType); - foreach (var propertyInfo in networkManagerType.GetProperties()) + m_NetworkManager_TypeRef = moduleDefinition.ImportReference(networkManagerTypeDef); + foreach (var propertyDef in networkManagerTypeDef.Properties) { - switch (propertyInfo.Name) + switch (propertyDef.Name) { case k_NetworkManager_LocalClientId: - m_NetworkManager_getLocalClientId_MethodRef = moduleDefinition.ImportReference(propertyInfo.GetMethod); + m_NetworkManager_getLocalClientId_MethodRef = moduleDefinition.ImportReference(propertyDef.GetMethod); break; case k_NetworkManager_IsListening: - m_NetworkManager_getIsListening_MethodRef = moduleDefinition.ImportReference(propertyInfo.GetMethod); + m_NetworkManager_getIsListening_MethodRef = moduleDefinition.ImportReference(propertyDef.GetMethod); break; case k_NetworkManager_IsHost: - m_NetworkManager_getIsHost_MethodRef = moduleDefinition.ImportReference(propertyInfo.GetMethod); + m_NetworkManager_getIsHost_MethodRef = moduleDefinition.ImportReference(propertyDef.GetMethod); break; case k_NetworkManager_IsServer: - m_NetworkManager_getIsServer_MethodRef = moduleDefinition.ImportReference(propertyInfo.GetMethod); + m_NetworkManager_getIsServer_MethodRef = moduleDefinition.ImportReference(propertyDef.GetMethod); break; case k_NetworkManager_IsClient: - m_NetworkManager_getIsClient_MethodRef = moduleDefinition.ImportReference(propertyInfo.GetMethod); + m_NetworkManager_getIsClient_MethodRef = moduleDefinition.ImportReference(propertyDef.GetMethod); break; } } - foreach (var fieldInfo in networkManagerType.GetFields(BindingFlags.Static | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)) + foreach (var fieldDef in networkManagerTypeDef.Fields) { - switch (fieldInfo.Name) + switch (fieldDef.Name) { case k_NetworkManager_LogLevel: - m_NetworkManager_LogLevel_FieldRef = moduleDefinition.ImportReference(fieldInfo); + m_NetworkManager_LogLevel_FieldRef = moduleDefinition.ImportReference(fieldDef); break; case k_NetworkManager_rpc_func_table: - m_NetworkManager_rpc_func_table_FieldRef = moduleDefinition.ImportReference(fieldInfo); - m_NetworkManager_rpc_func_table_Add_MethodRef = moduleDefinition.ImportReference(fieldInfo.FieldType.GetMethod("Add")); + m_NetworkManager_rpc_func_table_FieldRef = moduleDefinition.ImportReference(fieldDef); + + m_NetworkManager_rpc_func_table_Add_MethodRef = fieldDef.FieldType.Resolve().Methods.First(m => m.Name == "Add"); + m_NetworkManager_rpc_func_table_Add_MethodRef.DeclaringType = fieldDef.FieldType; + m_NetworkManager_rpc_func_table_Add_MethodRef = moduleDefinition.ImportReference(m_NetworkManager_rpc_func_table_Add_MethodRef); break; case k_NetworkManager_rpc_name_table: - m_NetworkManager_rpc_name_table_FieldRef = moduleDefinition.ImportReference(fieldInfo); - m_NetworkManager_rpc_name_table_Add_MethodRef = moduleDefinition.ImportReference(fieldInfo.FieldType.GetMethod("Add")); + m_NetworkManager_rpc_name_table_FieldRef = moduleDefinition.ImportReference(fieldDef); + + m_NetworkManager_rpc_name_table_Add_MethodRef = fieldDef.FieldType.Resolve().Methods.First(m => m.Name == "Add"); + m_NetworkManager_rpc_name_table_Add_MethodRef.DeclaringType = fieldDef.FieldType; + m_NetworkManager_rpc_name_table_Add_MethodRef = moduleDefinition.ImportReference(m_NetworkManager_rpc_name_table_Add_MethodRef); break; } } - var networkBehaviourType = typeof(NetworkBehaviour); - m_NetworkBehaviour_TypeRef = moduleDefinition.ImportReference(networkBehaviourType); - foreach (var propertyInfo in networkBehaviourType.GetProperties()) + m_NetworkBehaviour_TypeRef = moduleDefinition.ImportReference(networkBehaviourTypeDef); + foreach (var propertyDef in networkBehaviourTypeDef.Properties) { - switch (propertyInfo.Name) + switch (propertyDef.Name) { case k_NetworkBehaviour_NetworkManager: - m_NetworkBehaviour_getNetworkManager_MethodRef = moduleDefinition.ImportReference(propertyInfo.GetMethod); + m_NetworkBehaviour_getNetworkManager_MethodRef = moduleDefinition.ImportReference(propertyDef.GetMethod); break; case k_NetworkBehaviour_OwnerClientId: - m_NetworkBehaviour_getOwnerClientId_MethodRef = moduleDefinition.ImportReference(propertyInfo.GetMethod); + m_NetworkBehaviour_getOwnerClientId_MethodRef = moduleDefinition.ImportReference(propertyDef.GetMethod); break; } } - foreach (var methodInfo in networkBehaviourType.GetMethods(BindingFlags.Static | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)) + foreach (var methodDef in networkBehaviourTypeDef.Methods) { - switch (methodInfo.Name) + switch (methodDef.Name) { case k_NetworkBehaviour_beginSendServerRpc: - m_NetworkBehaviour_beginSendServerRpc_MethodRef = moduleDefinition.ImportReference(methodInfo); + m_NetworkBehaviour_beginSendServerRpc_MethodRef = moduleDefinition.ImportReference(methodDef); break; case k_NetworkBehaviour_endSendServerRpc: - m_NetworkBehaviour_endSendServerRpc_MethodRef = moduleDefinition.ImportReference(methodInfo); + m_NetworkBehaviour_endSendServerRpc_MethodRef = moduleDefinition.ImportReference(methodDef); break; case k_NetworkBehaviour_beginSendClientRpc: - m_NetworkBehaviour_beginSendClientRpc_MethodRef = moduleDefinition.ImportReference(methodInfo); + m_NetworkBehaviour_beginSendClientRpc_MethodRef = moduleDefinition.ImportReference(methodDef); break; case k_NetworkBehaviour_endSendClientRpc: - m_NetworkBehaviour_endSendClientRpc_MethodRef = moduleDefinition.ImportReference(methodInfo); + m_NetworkBehaviour_endSendClientRpc_MethodRef = moduleDefinition.ImportReference(methodDef); break; } } - foreach (var fieldInfo in networkBehaviourType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)) + foreach (var fieldDef in networkBehaviourTypeDef.Fields) { - switch (fieldInfo.Name) + switch (fieldDef.Name) { case k_NetworkBehaviour_rpc_exec_stage: - m_NetworkBehaviour_rpc_exec_stage_FieldRef = moduleDefinition.ImportReference(fieldInfo); + m_NetworkBehaviour_rpc_exec_stage_FieldRef = moduleDefinition.ImportReference(fieldDef); break; } } - var networkHandlerDelegateType = typeof(NetworkManager.RpcReceiveHandler); - m_NetworkHandlerDelegateCtor_MethodRef = moduleDefinition.ImportReference(networkHandlerDelegateType.GetConstructor(new[] { typeof(object), typeof(IntPtr) })); + foreach (var ctor in networkHandlerDelegateTypeDef.Resolve().GetConstructors()) + { + if (ctor.HasParameters && + ctor.Parameters.Count == 2 && + ctor.Parameters[0].ParameterType.Name == nameof(System.Object) && + ctor.Parameters[1].ParameterType.Name == nameof(IntPtr)) + { + m_NetworkHandlerDelegateCtor_MethodRef = moduleDefinition.ImportReference(ctor); + break; + } + } - var rpcParamsType = typeof(__RpcParams); - m_RpcParams_TypeRef = moduleDefinition.ImportReference(rpcParamsType); - foreach (var fieldInfo in rpcParamsType.GetFields()) + m_RpcParams_TypeRef = moduleDefinition.ImportReference(rpcParamsTypeDef); + foreach (var fieldDef in rpcParamsTypeDef.Fields) { - switch (fieldInfo.Name) + switch (fieldDef.Name) { case k_RpcParams_Server: - m_RpcParams_Server_FieldRef = moduleDefinition.ImportReference(fieldInfo); + m_RpcParams_Server_FieldRef = moduleDefinition.ImportReference(fieldDef); break; case k_RpcParams_Client: - m_RpcParams_Client_FieldRef = moduleDefinition.ImportReference(fieldInfo); + m_RpcParams_Client_FieldRef = moduleDefinition.ImportReference(fieldDef); break; } } - var serverRpcParamsType = typeof(ServerRpcParams); - m_ServerRpcParams_TypeRef = moduleDefinition.ImportReference(serverRpcParamsType); - foreach (var fieldInfo in serverRpcParamsType.GetFields()) + m_ServerRpcParams_TypeRef = moduleDefinition.ImportReference(serverRpcParamsTypeDef); + foreach (var fieldDef in serverRpcParamsTypeDef.Fields) { - switch (fieldInfo.Name) + switch (fieldDef.Name) { case k_ServerRpcParams_Receive: - foreach (var recvFieldInfo in fieldInfo.FieldType.GetFields()) + foreach (var recvFieldDef in fieldDef.FieldType.Resolve().Fields) { - switch (recvFieldInfo.Name) + switch (recvFieldDef.Name) { case k_ServerRpcReceiveParams_SenderClientId: - m_ServerRpcParams_Receive_SenderClientId_FieldRef = moduleDefinition.ImportReference(recvFieldInfo); + m_ServerRpcParams_Receive_SenderClientId_FieldRef = moduleDefinition.ImportReference(recvFieldDef); break; } } - m_ServerRpcParams_Receive_FieldRef = moduleDefinition.ImportReference(fieldInfo); + m_ServerRpcParams_Receive_FieldRef = moduleDefinition.ImportReference(fieldDef); break; } } - var clientRpcParamsType = typeof(ClientRpcParams); - m_ClientRpcParams_TypeRef = moduleDefinition.ImportReference(clientRpcParamsType); - - var fastBufferWriterType = typeof(FastBufferWriter); - m_FastBufferWriter_TypeRef = moduleDefinition.ImportReference(fastBufferWriterType); + m_ClientRpcParams_TypeRef = moduleDefinition.ImportReference(clientRpcParamsTypeDef); + m_FastBufferWriter_TypeRef = moduleDefinition.ImportReference(fastBufferWriterTypeDef); + m_FastBufferReader_TypeRef = moduleDefinition.ImportReference(fastBufferReaderTypeDef); - var fastBufferReaderType = typeof(FastBufferReader); - m_FastBufferReader_TypeRef = moduleDefinition.ImportReference(fastBufferReaderType); - - // Find all extension methods for FastBufferReader and FastBufferWriter to enable user-implemented - // methods to be called. + // Find all extension methods for FastBufferReader and FastBufferWriter to enable user-implemented methods to be called var assemblies = new List { m_MainModule.Assembly }; foreach (var reference in m_MainModule.AssemblyReferences) { @@ -371,9 +616,94 @@ private bool ImportReferences(ModuleDefinition moduleDefinition) } } + foreach (var method in networkVariableSerializationTypesTypeDef.Methods) + { + if (!method.IsStatic) + { + continue; + } + + switch (method.Name) + { + case nameof(NetworkVariableSerializationTypes.InitializeSerializer_UnmanagedByMemcpy): + m_NetworkVariableSerializationTypes_InitializeSerializer_UnmanagedByMemcpy_MethodRef = method; + break; + case nameof(NetworkVariableSerializationTypes.InitializeSerializer_UnmanagedINetworkSerializable): + m_NetworkVariableSerializationTypes_InitializeSerializer_UnmanagedINetworkSerializable_MethodRef = method; + break; + case nameof(NetworkVariableSerializationTypes.InitializeSerializer_ManagedINetworkSerializable): + m_NetworkVariableSerializationTypes_InitializeSerializer_ManagedINetworkSerializable_MethodRef = method; + break; + case nameof(NetworkVariableSerializationTypes.InitializeSerializer_FixedString): + m_NetworkVariableSerializationTypes_InitializeSerializer_FixedString_MethodRef = method; + break; + case nameof(NetworkVariableSerializationTypes.InitializeEqualityChecker_ManagedIEquatable): + m_NetworkVariableSerializationTypes_InitializeEqualityChecker_ManagedIEquatable_MethodRef = method; + break; + case nameof(NetworkVariableSerializationTypes.InitializeEqualityChecker_UnmanagedIEquatable): + m_NetworkVariableSerializationTypes_InitializeEqualityChecker_UnmanagedIEquatable_MethodRef = method; + break; + case nameof(NetworkVariableSerializationTypes.InitializeEqualityChecker_UnmanagedValueEquals): + m_NetworkVariableSerializationTypes_InitializeEqualityChecker_UnmanagedValueEquals_MethodRef = method; + break; + case nameof(NetworkVariableSerializationTypes.InitializeEqualityChecker_ManagedClassEquals): + m_NetworkVariableSerializationTypes_InitializeEqualityChecker_ManagedClassEquals_MethodRef = method; + break; + } + } + return true; } + // This gets all fields from this type as well as any parent types, up to (but not including) the base NetworkBehaviour class + // Importantly... this also resolves any generics, so if the base class is Foo and contains a field of NetworkVariable, + // and this class is Bar : Foo, it will properly resolve NetworkVariable to NetworkVariable. + private void GetAllFieldsAndResolveGenerics(TypeDefinition type, ref List fieldTypes, Dictionary genericParameters = null) + { + foreach (var field in type.Fields) + { + if (field.FieldType.IsGenericInstance) + { + var genericType = (GenericInstanceType)field.FieldType; + var newGenericType = new GenericInstanceType(field.FieldType.Resolve()); + for (var i = 0; i < genericType.GenericArguments.Count; ++i) + { + var argument = genericType.GenericArguments[i]; + + if (genericParameters != null && genericParameters.ContainsKey(argument.Name)) + { + newGenericType.GenericArguments.Add(genericParameters[argument.Name]); + } + else + { + newGenericType.GenericArguments.Add(argument); + } + } + fieldTypes.Add(newGenericType); + } + else + { + fieldTypes.Add(field.FieldType); + } + } + + if (type.BaseType == null || type.BaseType.Name == nameof(NetworkBehaviour)) + { + return; + } + var genericParams = new Dictionary(); + var resolved = type.BaseType.Resolve(); + if (type.BaseType.IsGenericInstance) + { + var genericType = (GenericInstanceType)type.BaseType; + for (var i = 0; i < genericType.GenericArguments.Count; ++i) + { + genericParams[resolved.GenericParameters[i].Name] = genericType.GenericArguments[i]; + } + } + GetAllFieldsAndResolveGenerics(resolved, ref fieldTypes, genericParams); + } + private void ProcessNetworkBehaviour(TypeDefinition typeDefinition, string[] assemblyDefines) { var rpcHandlers = new List<(uint RpcMethodId, MethodDefinition RpcHandler)>(); @@ -416,6 +746,28 @@ private void ProcessNetworkBehaviour(TypeDefinition typeDefinition, string[] ass } } + if (!typeDefinition.HasGenericParameters && !typeDefinition.IsGenericInstance) + { + var fieldTypes = new List(); + GetAllFieldsAndResolveGenerics(typeDefinition, ref fieldTypes); + foreach (var type in fieldTypes) + { + //var type = field.FieldType; + if (type.IsGenericInstance) + { + if (type.Resolve().Name == typeof(NetworkVariable<>).Name || type.Resolve().Name == typeof(NetworkList<>).Name) + { + var genericInstanceType = (GenericInstanceType)type; + var wrappedType = genericInstanceType.GenericArguments[0]; + if (!m_WrappedNetworkVariableTypes.Contains(wrappedType)) + { + m_WrappedNetworkVariableTypes.Add(wrappedType); + } + } + } + } + } + if (rpcHandlers.Count > 0 || rpcNames.Count > 0) { var staticCtorMethodDef = typeDefinition.GetStaticConstructor(); @@ -669,7 +1021,7 @@ private bool GetWriteMethodForParameter(TypeReference paramType, out MethodRefer { if (parameters[1].IsIn) { - if (parameters[1].ParameterType.Resolve() == paramType.MakeByReferenceType().Resolve() && + if (((ByReferenceType)parameters[1].ParameterType).ElementType.FullName == paramType.FullName && ((ByReferenceType)parameters[1].ParameterType).ElementType.IsArray == paramType.IsArray) { methodRef = method; @@ -679,8 +1031,7 @@ private bool GetWriteMethodForParameter(TypeReference paramType, out MethodRefer } else { - - if (parameters[1].ParameterType.Resolve() == paramType.Resolve() && + if (parameters[1].ParameterType.FullName == paramType.FullName && parameters[1].ParameterType.IsArray == paramType.IsArray) { methodRef = method; @@ -813,7 +1164,7 @@ private bool GetReadMethodForParameter(TypeReference paramType, out MethodRefere var parameters = method.Resolve().Parameters; if (method.Name == k_ReadValueMethodName && parameters[1].IsOut && - parameters[1].ParameterType.Resolve() == paramType.MakeByReferenceType().Resolve() && + ((ByReferenceType)parameters[1].ParameterType).ElementType.FullName == paramType.FullName && ((ByReferenceType)parameters[1].ParameterType).ElementType.IsArray == paramType.IsArray) { methodRef = method; diff --git a/Editor/HiddenScriptEditor.cs b/Editor/HiddenScriptEditor.cs new file mode 100644 index 0000000..592580e --- /dev/null +++ b/Editor/HiddenScriptEditor.cs @@ -0,0 +1,81 @@ +using Unity.Netcode.Components; +#if UNITY_UNET_PRESENT +using Unity.Netcode.Transports.UNET; +#endif +using Unity.Netcode.Transports.UTP; +using UnityEditor; + +namespace Unity.Netcode.Editor +{ + /// + /// Internal use. Hides the script field for the given component. + /// + public class HiddenScriptEditor : UnityEditor.Editor + { + private static readonly string[] k_HiddenFields = { "m_Script" }; + + /// + /// Draws inspector properties without the script field. + /// + public override void OnInspectorGUI() + { + EditorGUI.BeginChangeCheck(); + serializedObject.UpdateIfRequiredOrScript(); + DrawPropertiesExcluding(serializedObject, k_HiddenFields); + serializedObject.ApplyModifiedProperties(); + EditorGUI.EndChangeCheck(); + } + } +#if UNITY_UNET_PRESENT + /// + /// Internal use. Hides the script field for UNetTransport. + /// + [CustomEditor(typeof(UNetTransport), true)] + public class UNetTransportEditor : HiddenScriptEditor + { + + } +#endif + + /// + /// Internal use. Hides the script field for UnityTransport. + /// + [CustomEditor(typeof(UnityTransport), true)] + public class UnityTransportEditor : HiddenScriptEditor + { + + } + +#if COM_UNITY_MODULES_ANIMATION + /// + /// Internal use. Hides the script field for NetworkAnimator. + /// + [CustomEditor(typeof(NetworkAnimator), true)] + public class NetworkAnimatorEditor : HiddenScriptEditor + { + + } +#endif + +#if COM_UNITY_MODULES_PHYSICS + /// + /// Internal use. Hides the script field for NetworkRigidbody. + /// + [CustomEditor(typeof(NetworkRigidbody), true)] + public class NetworkRigidbodyEditor : HiddenScriptEditor + { + + } +#endif + +#if COM_UNITY_MODULES_PHYSICS2D + /// + /// Internal use. Hides the script field for NetworkRigidbody2D. + /// + [CustomEditor(typeof(NetworkRigidbody2D), true)] + public class NetworkRigidbody2DEditor : HiddenScriptEditor + { + + } +#endif +} diff --git a/Editor/HiddenScriptEditor.cs.meta b/Editor/HiddenScriptEditor.cs.meta new file mode 100644 index 0000000..f8dd8da --- /dev/null +++ b/Editor/HiddenScriptEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ebf622cc80e94f488e59caf8b7419f50 +timeCreated: 1661959406 \ No newline at end of file diff --git a/Editor/NetworkBehaviourEditor.cs b/Editor/NetworkBehaviourEditor.cs index 7da70ea..95e353a 100644 --- a/Editor/NetworkBehaviourEditor.cs +++ b/Editor/NetworkBehaviourEditor.cs @@ -263,8 +263,7 @@ public static void CheckForNetworkObject(GameObject gameObject, bool networkObje // Now get the root parent transform to the current GameObject (or itself) var rootTransform = GetRootParentTransform(gameObject.transform); - var networkManager = rootTransform.GetComponent(); - if (networkManager == null) + if (!rootTransform.TryGetComponent(out var networkManager)) { networkManager = rootTransform.GetComponentInChildren(); } @@ -299,8 +298,7 @@ public static void CheckForNetworkObject(GameObject gameObject, bool networkObje // Otherwise, check to see if there is any NetworkObject from the root GameObject down to all children. // If not, notify the user that NetworkBehaviours require that the relative GameObject has a NetworkObject component. - var networkObject = rootTransform.GetComponent(); - if (networkObject == null) + if (!rootTransform.TryGetComponent(out var networkObject)) { networkObject = rootTransform.GetComponentInChildren(); diff --git a/Editor/NetworkManagerEditor.cs b/Editor/NetworkManagerEditor.cs index a788cb8..debaca2 100644 --- a/Editor/NetworkManagerEditor.cs +++ b/Editor/NetworkManagerEditor.cs @@ -214,18 +214,6 @@ public override void OnInspectorGUI() DrawInstallMultiplayerToolsTip(); #endif - { - var iterator = serializedObject.GetIterator(); - - for (bool enterChildren = true; iterator.NextVisible(enterChildren); enterChildren = false) - { - using (new EditorGUI.DisabledScope("m_Script" == iterator.propertyPath)) - { - EditorGUILayout.PropertyField(iterator, false); - } - } - } - if (!m_NetworkManager.IsServer && !m_NetworkManager.IsClient) { serializedObject.Update(); diff --git a/Editor/NetworkManagerHelper.cs b/Editor/NetworkManagerHelper.cs index 5430404..4c4aabd 100644 --- a/Editor/NetworkManagerHelper.cs +++ b/Editor/NetworkManagerHelper.cs @@ -64,7 +64,7 @@ private static void ScenesInBuildActiveSceneCheck() { var scenesList = EditorBuildSettings.scenes.ToList(); var activeScene = SceneManager.GetActiveScene(); - var isSceneInBuildSettings = scenesList.Where((c) => c.path == activeScene.path).Count() == 1; + var isSceneInBuildSettings = scenesList.Count((c) => c.path == activeScene.path) == 1; var networkManager = Object.FindObjectOfType(); if (!isSceneInBuildSettings && networkManager != null) { @@ -109,9 +109,8 @@ private static void EditorApplication_hierarchyChanged() public void CheckAndNotifyUserNetworkObjectRemoved(NetworkManager networkManager, bool editorTest = false) { // Check for any NetworkObject at the same gameObject relative layer - var networkObject = networkManager.gameObject.GetComponent(); - if (networkObject == null) + if (!networkManager.gameObject.TryGetComponent(out var networkObject)) { // if none is found, check to see if any children have a NetworkObject networkObject = networkManager.gameObject.GetComponentInChildren(); diff --git a/Editor/NetworkObjectEditor.cs b/Editor/NetworkObjectEditor.cs index 5111e92..c63f9fc 100644 --- a/Editor/NetworkObjectEditor.cs +++ b/Editor/NetworkObjectEditor.cs @@ -15,6 +15,8 @@ public class NetworkObjectEditor : UnityEditor.Editor private NetworkObject m_NetworkObject; private bool m_ShowObservers; + private static readonly string[] k_HiddenFields = { "m_Script" }; + private void Initialize() { if (m_Initialized) @@ -95,7 +97,11 @@ public override void OnInspectorGUI() } else { - base.OnInspectorGUI(); + EditorGUI.BeginChangeCheck(); + serializedObject.UpdateIfRequiredOrScript(); + DrawPropertiesExcluding(serializedObject, k_HiddenFields); + serializedObject.ApplyModifiedProperties(); + EditorGUI.EndChangeCheck(); var guiEnabled = GUI.enabled; GUI.enabled = false; diff --git a/Editor/com.unity.netcode.editor.asmdef b/Editor/com.unity.netcode.editor.asmdef index ab1d271..29b759d 100644 --- a/Editor/com.unity.netcode.editor.asmdef +++ b/Editor/com.unity.netcode.editor.asmdef @@ -13,6 +13,26 @@ "name": "com.unity.multiplayer.tools", "expression": "", "define": "MULTIPLAYER_TOOLS" + }, + { + "name": "Unity", + "expression": "(0,2022.2.0a5)", + "define": "UNITY_UNET_PRESENT" + }, + { + "name": "com.unity.modules.animation", + "expression": "", + "define": "COM_UNITY_MODULES_ANIMATION" + }, + { + "name": "com.unity.modules.physics", + "expression": "", + "define": "COM_UNITY_MODULES_PHYSICS" + }, + { + "name": "com.unity.modules.physics2d", + "expression": "", + "define": "COM_UNITY_MODULES_PHYSICS2D" } ] -} \ No newline at end of file +} diff --git a/README.md b/README.md index a264b60..5aac046 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Netcode for GameObjects is a Unity package that provides networking capabilities Visit the [Multiplayer Docs Site](https://docs-multiplayer.unity3d.com/) for package & API documentation, as well as information about several samples which leverage the Netcode for GameObjects package. -You can also jump right into our [Hello World](https://docs-multiplayer.unity3d.com/netcode/current/tutorials/helloworld/helloworldintro) guide for a taste of how to use the framework for basic networked tasks. +You can also jump right into our [Hello World](https://docs-multiplayer.unity3d.com/netcode/current/tutorials/helloworld) guide for a taste of how to use the framework for basic networked tasks. ### Community and Feedback diff --git a/Runtime/AssemblyInfo.cs b/Runtime/AssemblyInfo.cs index 0dddbab..935d273 100644 --- a/Runtime/AssemblyInfo.cs +++ b/Runtime/AssemblyInfo.cs @@ -12,3 +12,4 @@ [assembly: InternalsVisibleTo("Unity.Netcode.RuntimeTests")] [assembly: InternalsVisibleTo("Unity.Netcode.TestHelpers.Runtime")] [assembly: InternalsVisibleTo("Unity.Netcode.Adapter.UTP")] +[assembly: InternalsVisibleTo("Unity.Multiplayer.Tools.Adapters.Ngo1WithUtp2")] diff --git a/Runtime/Core/NetworkBehaviour.cs b/Runtime/Core/NetworkBehaviour.cs index 6f96f03..7f95289 100644 --- a/Runtime/Core/NetworkBehaviour.cs +++ b/Runtime/Core/NetworkBehaviour.cs @@ -235,14 +235,42 @@ internal void __endSendClientRpc(ref FastBufferWriter bufferWriter, uint rpcMeth #if DEVELOPMENT_BUILD || UNITY_EDITOR if (NetworkManager.__rpc_name_table.TryGetValue(rpcMethodId, out var rpcMethodName)) { - foreach (var client in NetworkManager.ConnectedClients) + if (clientRpcParams.Send.TargetClientIds != null) { - NetworkManager.NetworkMetrics.TrackRpcSent( - client.Key, - NetworkObject, - rpcMethodName, - __getTypeName(), - rpcWriteSize); + foreach (var targetClientId in clientRpcParams.Send.TargetClientIds) + { + NetworkManager.NetworkMetrics.TrackRpcSent( + targetClientId, + NetworkObject, + rpcMethodName, + __getTypeName(), + rpcWriteSize); + } + } + else if (clientRpcParams.Send.TargetClientIdsNativeArray != null) + { + foreach (var targetClientId in clientRpcParams.Send.TargetClientIdsNativeArray) + { + NetworkManager.NetworkMetrics.TrackRpcSent( + targetClientId, + NetworkObject, + rpcMethodName, + __getTypeName(), + rpcWriteSize); + } + } + else + { + var observerEnumerator = NetworkObject.Observers.GetEnumerator(); + while (observerEnumerator.MoveNext()) + { + NetworkManager.NetworkMetrics.TrackRpcSent( + observerEnumerator.Current, + NetworkObject, + rpcMethodName, + __getTypeName(), + rpcWriteSize); + } } } #endif @@ -436,14 +464,41 @@ internal void InternalOnNetworkSpawn() IsSpawned = true; InitializeVariables(); UpdateNetworkProperties(); - OnNetworkSpawn(); + } + + internal void VisibleOnNetworkSpawn() + { + try + { + OnNetworkSpawn(); + } + catch (Exception e) + { + Debug.LogException(e); + } + + InitializeVariables(); + if (IsServer) + { + // Since we just spawned the object and since user code might have modified their NetworkVariable, esp. + // NetworkList, we need to mark the object as free of updates. + // This should happen for all objects on the machine triggering the spawn. + PostNetworkVariableWrite(true); + } } internal void InternalOnNetworkDespawn() { IsSpawned = false; UpdateNetworkProperties(); - OnNetworkDespawn(); + try + { + OnNetworkDespawn(); + } + catch (Exception e) + { + Debug.LogException(e); + } } /// @@ -576,12 +631,24 @@ internal void PreNetworkVariableWrite() NetworkVariableIndexesToResetSet.Clear(); } - internal void PostNetworkVariableWrite() + internal void PostNetworkVariableWrite(bool forced = false) { - // mark any variables we wrote as no longer dirty - for (int i = 0; i < NetworkVariableIndexesToReset.Count; i++) + if (forced) { - NetworkVariableFields[NetworkVariableIndexesToReset[i]].ResetDirty(); + // Mark every variable as no longer dirty. We just spawned the object and whatever the game code did + // during OnNetworkSpawn has been sent and needs to be cleared + for (int i = 0; i < NetworkVariableFields.Count; i++) + { + NetworkVariableFields[i].ResetDirty(); + } + } + else + { + // mark any variables we wrote as no longer dirty + for (int i = 0; i < NetworkVariableIndexesToReset.Count; i++) + { + NetworkVariableFields[NetworkVariableIndexesToReset[i]].ResetDirty(); + } } MarkVariablesDirty(false); diff --git a/Runtime/Core/NetworkBehaviourUpdater.cs b/Runtime/Core/NetworkBehaviourUpdater.cs index 7e9176e..e20fe90 100644 --- a/Runtime/Core/NetworkBehaviourUpdater.cs +++ b/Runtime/Core/NetworkBehaviourUpdater.cs @@ -26,6 +26,10 @@ internal void NetworkBehaviourUpdate(NetworkManager networkManager) #endif try { + // NetworkObject references can become null, when hidden or despawned. Once NUll, there is no point + // trying to process them, even if they were previously marked as dirty. + m_DirtyNetworkObjects.RemoveWhere((sobj) => sobj == null); + if (networkManager.IsServer) { foreach (var dirtyObj in m_DirtyNetworkObjects) @@ -38,10 +42,6 @@ internal void NetworkBehaviourUpdate(NetworkManager networkManager) for (int i = 0; i < networkManager.ConnectedClientsList.Count; i++) { var client = networkManager.ConnectedClientsList[i]; - if (networkManager.IsHost && client.ClientId == networkManager.LocalClientId) - { - continue; - } if (dirtyObj.IsNetworkVisibleTo(client.ClientId)) { @@ -75,10 +75,7 @@ internal void NetworkBehaviourUpdate(NetworkManager networkManager) // Now, reset all the no-longer-dirty variables foreach (var dirtyobj in m_DirtyNetworkObjects) { - for (int k = 0; k < dirtyobj.ChildNetworkBehaviours.Count; k++) - { - dirtyobj.ChildNetworkBehaviours[k].PostNetworkVariableWrite(); - } + dirtyobj.PostNetworkVariableWrite(); } m_DirtyNetworkObjects.Clear(); } diff --git a/Runtime/Core/NetworkManager.cs b/Runtime/Core/NetworkManager.cs index 21c7082..20794b4 100644 --- a/Runtime/Core/NetworkManager.cs +++ b/Runtime/Core/NetworkManager.cs @@ -20,7 +20,7 @@ namespace Unity.Netcode /// /// The main component of the library /// - [AddComponentMenu("Netcode/" + nameof(NetworkManager), -100)] + [AddComponentMenu("Netcode/Network Manager", -100)] public class NetworkManager : MonoBehaviour, INetworkUpdateSystem { #pragma warning disable IDE1006 // disable naming rule violation check @@ -51,7 +51,7 @@ public class NetworkManager : MonoBehaviour, INetworkUpdateSystem internal static string PrefabDebugHelper(NetworkPrefab networkPrefab) { - return $"{nameof(NetworkPrefab)} \"{networkPrefab.Prefab.gameObject.name}\""; + return $"{nameof(NetworkPrefab)} \"{networkPrefab.Prefab.name}\""; } internal NetworkBehaviourUpdater BehaviourUpdater { get; set; } @@ -187,8 +187,7 @@ public void Send(ulong clientId, NetworkDelivery delivery, FastBufferWriter batc /// a that is either the override or if no overrides exist it returns the same as the one passed in as a parameter public GameObject GetNetworkPrefabOverride(GameObject gameObject) { - var networkObject = gameObject.GetComponent(); - if (networkObject != null) + if (gameObject.TryGetComponent(out var networkObject)) { if (NetworkConfig.NetworkPrefabOverrideLinks.ContainsKey(networkObject.GlobalObjectIdHash)) { @@ -367,10 +366,22 @@ public IReadOnlyList ConnectedClientsIds public bool IsListening { get; internal set; } /// - /// Gets if we are connected as a client + /// When true, the client is connected, approved, and synchronized with + /// the server. /// public bool IsConnectedClient { get; internal set; } + /// + /// Is true when the client has been approved. + /// + /// + /// This only reflects the client's approved status and does not mean the client + /// has finished the connection and synchronization process. The server-host will + /// always be approved upon being starting the + /// + /// + public bool IsApproved { get; internal set; } + /// /// Can be used to determine if the is currently shutting itself down /// @@ -519,8 +530,7 @@ internal void OnValidate() var networkPrefabGo = networkPrefab?.Prefab; if (networkPrefabGo != null) { - var networkObject = networkPrefabGo.GetComponent(); - if (networkObject == null) + if (!networkPrefabGo.TryGetComponent(out var networkObject)) { if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) { @@ -695,8 +705,7 @@ private bool ShouldAddPrefab(NetworkPrefab networkPrefab, out uint sourcePrefabG } else if (networkPrefab.Override == NetworkPrefabOverride.None) { - networkObject = networkPrefab.Prefab.GetComponent(); - if (networkObject == null) + if (!networkPrefab.Prefab.TryGetComponent(out networkObject)) { if (NetworkLog.CurrentLogLevel <= LogLevel.Error) { @@ -742,8 +751,7 @@ private bool ShouldAddPrefab(NetworkPrefab networkPrefab, out uint sourcePrefabG } else { - networkObject = networkPrefab.SourcePrefabToOverride.GetComponent(); - if (networkObject == null) + if (!networkPrefab.SourcePrefabToOverride.TryGetComponent(out networkObject)) { if (NetworkLog.CurrentLogLevel <= LogLevel.Error) { @@ -881,6 +889,8 @@ private void Initialize(bool server) return; } + IsApproved = false; + ComponentFactory.SetDefaults(); if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) @@ -968,8 +978,7 @@ private void Initialize(bool server) // If we have a player prefab, then we need to verify it is in the list of NetworkPrefabOverrideLinks for client side spawning. if (NetworkConfig.PlayerPrefab != null) { - var playerPrefabNetworkObject = NetworkConfig.PlayerPrefab.GetComponent(); - if (playerPrefabNetworkObject != null) + if (NetworkConfig.PlayerPrefab.TryGetComponent(out var playerPrefabNetworkObject)) { //In the event there is no NetworkPrefab entry (i.e. no override for default player prefab) if (!NetworkConfig.NetworkPrefabOverrideLinks.ContainsKey(playerPrefabNetworkObject @@ -1023,24 +1032,38 @@ public bool StartServer() } Initialize(true); + IsServer = true; + IsClient = false; + IsListening = true; - // If we failed to start then shutdown and notify user that the transport failed to start - if (NetworkConfig.NetworkTransport.StartServer()) + try { - IsServer = true; - IsClient = false; - IsListening = true; + // If we failed to start then shutdown and notify user that the transport failed to start + if (NetworkConfig.NetworkTransport.StartServer()) + { + SpawnManager.ServerSpawnSceneObjectsOnStartSweep(); - SpawnManager.ServerSpawnSceneObjectsOnStartSweep(); + OnServerStarted?.Invoke(); + IsApproved = true; + return true; + } + else + { + IsServer = false; + IsClient = false; + IsListening = false; - OnServerStarted?.Invoke(); - return true; + Debug.LogError($"Server is shutting down due to network transport start failure of {NetworkConfig.NetworkTransport.GetType().Name}!"); + OnTransportFailure?.Invoke(); + Shutdown(); + } } - else + catch (Exception) { - Debug.LogError($"Server is shutting down due to network transport start failure of {NetworkConfig.NetworkTransport.GetType().Name}!"); - OnTransportFailure?.Invoke(); - Shutdown(); + IsServer = false; + IsClient = false; + IsListening = false; + throw; } return false; @@ -1098,23 +1121,38 @@ public bool StartHost() Initialize(true); - // If we failed to start then shutdown and notify user that the transport failed to start - if (!NetworkConfig.NetworkTransport.StartServer()) + IsServer = true; + IsClient = true; + IsListening = true; + + try { - Debug.LogError($"Server is shutting down due to network transport start failure of {NetworkConfig.NetworkTransport.GetType().Name}!"); - OnTransportFailure?.Invoke(); - Shutdown(); - return false; + // If we failed to start then shutdown and notify user that the transport failed to start + if (!NetworkConfig.NetworkTransport.StartServer()) + { + Debug.LogError($"Server is shutting down due to network transport start failure of {NetworkConfig.NetworkTransport.GetType().Name}!"); + OnTransportFailure?.Invoke(); + Shutdown(); + + IsServer = false; + IsClient = false; + IsListening = false; + + return false; + } + } + catch (Exception) + { + IsServer = false; + IsClient = false; + IsListening = false; + throw; } MessagingSystem.ClientConnected(ServerClientId); LocalClientId = ServerClientId; NetworkMetrics.SetConnectionId(LocalClientId); - IsServer = true; - IsClient = true; - IsListening = true; - if (NetworkConfig.ConnectionApproval && ConnectionApprovalCallback != null) { var response = new ConnectionApprovalResponse(); @@ -1128,6 +1166,7 @@ public bool StartHost() } response.Approved = true; + IsApproved = true; HandleConnectionApproval(ServerClientId, response); } else @@ -1389,6 +1428,7 @@ internal void ShutdownInternal() } IsConnectedClient = false; + IsApproved = false; // We need to clean up NetworkObjects before we reset the IsServer // and IsClient properties. This provides consistency of these two @@ -1560,7 +1600,7 @@ private void OnNetworkPreUpdate() } // Only update RTT here, server time is updated by time sync messages - var reset = NetworkTimeSystem.Advance(Time.deltaTime); + var reset = NetworkTimeSystem.Advance(Time.unscaledDeltaTime); if (reset) { NetworkTickSystem.Reset(NetworkTimeSystem.LocalTime, NetworkTimeSystem.ServerTime); @@ -1569,7 +1609,7 @@ private void OnNetworkPreUpdate() if (IsServer == false) { - NetworkTimeSystem.Sync(NetworkTimeSystem.LastSyncedServerTimeSec + Time.deltaTime, NetworkConfig.NetworkTransport.GetCurrentRtt(ServerClientId) / 1000d); + NetworkTimeSystem.Sync(NetworkTimeSystem.LastSyncedServerTimeSec + Time.unscaledDeltaTime, NetworkConfig.NetworkTransport.GetCurrentRtt(ServerClientId) / 1000d); } } @@ -1614,7 +1654,9 @@ private void SendConnectionRequest() { var message = new ConnectionRequestMessage { - ConfigHash = NetworkConfig.GetConfig(), + // Since only a remote client will send a connection request, + // we should always force the rebuilding of the NetworkConfig hash value + ConfigHash = NetworkConfig.GetConfig(false), ShouldSendConnectionData = NetworkConfig.ConnectionApproval, ConnectionData = NetworkConfig.ConnectionData }; @@ -1623,23 +1665,73 @@ private void SendConnectionRequest() private IEnumerator ApprovalTimeout(ulong clientId) { - NetworkTime timeStarted = LocalTime; + var timeStarted = IsServer ? LocalTime.TimeAsFloat : Time.realtimeSinceStartup; + var timedOut = false; + var connectionApproved = false; + var connectionNotApproved = false; + var timeoutMarker = timeStarted + NetworkConfig.ClientConnectionBufferTimeout; - //We yield every frame incase a pending client disconnects and someone else gets its connection id - while ((LocalTime - timeStarted).Time < NetworkConfig.ClientConnectionBufferTimeout && PendingClients.ContainsKey(clientId)) + while (IsListening && !ShutdownInProgress && !timedOut && !connectionApproved) { yield return null; + // Check if we timed out + timedOut = timeoutMarker < (IsServer ? LocalTime.TimeAsFloat : Time.realtimeSinceStartup); + + if (IsServer) + { + // When the client is no longer in the pending clients list and is in the connected clients list + // it has been approved + connectionApproved = !PendingClients.ContainsKey(clientId) && ConnectedClients.ContainsKey(clientId); + + // For the server side, if the client is in neither list then it was declined or the client disconnected + connectionNotApproved = !PendingClients.ContainsKey(clientId) && !ConnectedClients.ContainsKey(clientId); + } + else + { + connectionApproved = IsApproved; + } + } + + // Exit coroutine if we are no longer listening or a shutdown is in progress (client or server) + if (!IsListening || ShutdownInProgress) + { + yield break; } - if (PendingClients.ContainsKey(clientId) && !ConnectedClients.ContainsKey(clientId)) + // If the client timed out or was not approved + if (timedOut || connectionNotApproved) { // Timeout if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) { - NetworkLog.LogInfo($"Client {clientId} Handshake Timed Out"); + if (timedOut) + { + if (IsServer) + { + // Log a warning that the transport detected a connection but then did not receive a follow up connection request message. + // (hacking or something happened to the server's network connection) + NetworkLog.LogWarning($"Server detected a transport connection from Client-{clientId}, but timed out waiting for the connection request message."); + } + else + { + // We only provide informational logging for the client side + NetworkLog.LogInfo("Timed out waiting for the server to approve the connection request."); + } + } + else if (connectionNotApproved) + { + NetworkLog.LogInfo($"Client-{clientId} was either denied approval or disconnected while being approved."); + } } - DisconnectClient(clientId); + if (IsServer) + { + DisconnectClient(clientId); + } + else + { + Shutdown(true); + } } } @@ -1916,13 +2008,20 @@ private void OnClientDisconnectFromServer(ulong clientId) var playerObject = networkClient.PlayerObject; if (playerObject != null) { - if (PrefabHandler.ContainsHandler(ConnectedClients[clientId].PlayerObject.GlobalObjectIdHash)) + if (!playerObject.DontDestroyWithOwner) { - PrefabHandler.HandleNetworkPrefabDestroy(ConnectedClients[clientId].PlayerObject); + if (PrefabHandler.ContainsHandler(ConnectedClients[clientId].PlayerObject.GlobalObjectIdHash)) + { + PrefabHandler.HandleNetworkPrefabDestroy(ConnectedClients[clientId].PlayerObject); + } + else + { + Destroy(playerObject.gameObject); + } } else { - Destroy(playerObject.gameObject); + playerObject.RemoveOwnership(); } } @@ -2033,14 +2132,31 @@ internal void HandleConnectionApproval(ulong ownerClientId, ConnectionApprovalRe if (response.CreatePlayerObject) { - var networkObject = SpawnManager.CreateLocalNetworkObject( - isSceneObject: false, - response.PlayerPrefabHash ?? NetworkConfig.PlayerPrefab.GetComponent().GlobalObjectIdHash, - ownerClientId, - parentNetworkId: null, - networkSceneHandle: null, - response.Position, - response.Rotation); + var playerPrefabHash = response.PlayerPrefabHash ?? NetworkConfig.PlayerPrefab.GetComponent().GlobalObjectIdHash; + + // Generate a SceneObject for the player object to spawn + var sceneObject = new NetworkObject.SceneObject + { + Header = new NetworkObject.SceneObject.HeaderData + { + IsPlayerObject = true, + OwnerClientId = ownerClientId, + IsSceneObject = false, + HasTransform = true, + Hash = playerPrefabHash, + }, + TargetClientId = ownerClientId, + Transform = new NetworkObject.SceneObject.TransformData + { + Position = response.Position.GetValueOrDefault(), + Rotation = response.Rotation.GetValueOrDefault() + } + }; + + // Create the player NetworkObject locally + var networkObject = SpawnManager.CreateLocalNetworkObject(sceneObject); + + // Spawn the player NetworkObject locally SpawnManager.SpawnNetworkObjectLocally( networkObject, SpawnManager.GetNetworkObjectId(), diff --git a/Runtime/Core/NetworkObject.cs b/Runtime/Core/NetworkObject.cs index 0d889cb..9d956f7 100644 --- a/Runtime/Core/NetworkObject.cs +++ b/Runtime/Core/NetworkObject.cs @@ -9,7 +9,7 @@ namespace Unity.Netcode /// /// A component used to identify that a GameObject in the network /// - [AddComponentMenu("Netcode/" + nameof(NetworkObject), -99)] + [AddComponentMenu("Netcode/Network Object", -99)] [DisallowMultipleComponent] public sealed class NetworkObject : MonoBehaviour { @@ -129,7 +129,7 @@ internal void GenerateGlobalObjectIdHash() /// /// Whether or not to destroy this object if it's owner is destroyed. - /// If false, the objects ownership will be given to the server. + /// If true, the objects ownership will be given to the server. /// public bool DontDestroyWithOwner; @@ -435,7 +435,7 @@ public static void NetworkHide(List networkObjects, ulong clientI private void OnDestroy() { if (NetworkManager != null && NetworkManager.IsListening && NetworkManager.IsServer == false && IsSpawned && - (IsSceneObject == null || (IsSceneObject != null && IsSceneObject.Value != true))) + (IsSceneObject == null || (IsSceneObject.Value != true))) { throw new NotServerException($"Destroy a spawned {nameof(NetworkObject)} on a non-host client is not valid. Call {nameof(Destroy)} or {nameof(Despawn)} on the server/host instead."); } @@ -509,6 +509,7 @@ public void SpawnAsPlayerObject(ulong clientId, bool destroyWithScene = false) /// (true) the will be destroyed (false) the will persist after being despawned public void Despawn(bool destroy = true) { + MarkVariablesDirty(false); NetworkManager.SpawnManager.DespawnObject(this, destroy); } @@ -572,21 +573,21 @@ internal void InvokeBehaviourOnNetworkObjectParentChanged(NetworkObject parentNe } } - private bool m_IsReparented; // Did initial parent (came from the scene hierarchy) change at runtime? private ulong? m_LatestParent; // What is our last set parent NetworkObject's ID? private Transform m_CachedParent; // What is our last set parent Transform reference? + private bool m_CachedWorldPositionStays = true; // Used to preserve the world position stays parameter passed in TrySetParent internal void SetCachedParent(Transform parentTransform) { m_CachedParent = parentTransform; } - internal (bool IsReparented, ulong? LatestParent) GetNetworkParenting() => (m_IsReparented, m_LatestParent); + internal ulong? GetNetworkParenting() => m_LatestParent; - internal void SetNetworkParenting(bool isReparented, ulong? latestParent) + internal void SetNetworkParenting(ulong? latestParent, bool worldPositionStays) { - m_IsReparented = isReparented; m_LatestParent = latestParent; + m_CachedWorldPositionStays = worldPositionStays; } /// @@ -597,7 +598,10 @@ internal void SetNetworkParenting(bool isReparented, ulong? latestParent) /// Whether or not reparenting was successful. public bool TrySetParent(Transform parent, bool worldPositionStays = true) { - return TrySetParent(parent.GetComponent(), worldPositionStays); + var networkObject = parent.GetComponent(); + + // If the parent doesn't have a NetworkObjet then return false, otherwise continue trying to parent + return networkObject == null ? false : TrySetParent(networkObject, worldPositionStays); } /// @@ -608,7 +612,37 @@ public bool TrySetParent(Transform parent, bool worldPositionStays = true) /// Whether or not reparenting was successful. public bool TrySetParent(GameObject parent, bool worldPositionStays = true) { - return TrySetParent(parent.GetComponent(), worldPositionStays); + // If we are removing ourself from a parent + if (parent == null) + { + return TrySetParent((NetworkObject)null, worldPositionStays); + } + + var networkObject = parent.GetComponent(); + + // If the parent doesn't have a NetworkObjet then return false, otherwise continue trying to parent + return networkObject == null ? false : TrySetParent(networkObject, worldPositionStays); + } + + /// + /// Used when despawning the parent, we want to preserve the cached WorldPositionStays value + /// + internal bool TryRemoveParentCachedWorldPositionStays() + { + return TrySetParent((NetworkObject)null, m_CachedWorldPositionStays); + } + + /// + /// Removes the parent of the NetworkObject's transform + /// + /// + /// This is a more convenient way to remove the parent without having to cast the null value to either or + /// + /// If true, the parent-relative position, scale and rotation are modified such that the object keeps the same world space position, rotation and scale as before. + /// + public bool TryRemoveParent(bool worldPositionStays = true) + { + return TrySetParent((NetworkObject)null, worldPositionStays); } /// @@ -639,17 +673,21 @@ public bool TrySetParent(NetworkObject parent, bool worldPositionStays = true) return false; } - if (parent == null) + if (parent != null && !parent.IsSpawned) { return false; } + m_CachedWorldPositionStays = worldPositionStays; - if (!parent.IsSpawned) + if (parent == null) { - return false; + transform.SetParent(null, worldPositionStays); + } + else + { + transform.SetParent(parent.transform, worldPositionStays); } - transform.SetParent(parent.transform, worldPositionStays); return true; } @@ -685,12 +723,11 @@ private void OnTransformParentChanged() Debug.LogException(new SpawnStateException($"{nameof(NetworkObject)} can only be reparented after being spawned")); return; } - + var removeParent = false; var parentTransform = transform.parent; if (parentTransform != null) { - var parentObject = transform.parent.GetComponent(); - if (parentObject == null) + if (!transform.parent.TryGetComponent(out var parentObject)) { transform.parent = m_CachedParent; Debug.LogException(new InvalidParentException($"Invalid parenting, {nameof(NetworkObject)} moved under a non-{nameof(NetworkObject)} parent")); @@ -709,19 +746,31 @@ private void OnTransformParentChanged() else { m_LatestParent = null; + removeParent = m_CachedParent != null; } - m_IsReparented = true; - ApplyNetworkParenting(); + ApplyNetworkParenting(removeParent); var message = new ParentSyncMessage { NetworkObjectId = NetworkObjectId, - IsReparented = m_IsReparented, IsLatestParentSet = m_LatestParent != null && m_LatestParent.HasValue, - LatestParent = m_LatestParent + LatestParent = m_LatestParent, + RemoveParent = removeParent, + WorldPositionStays = m_CachedWorldPositionStays, + Position = m_CachedWorldPositionStays ? transform.position : transform.localPosition, + Rotation = m_CachedWorldPositionStays ? transform.rotation : transform.localRotation, + Scale = transform.localScale, }; + // We need to preserve the m_CachedWorldPositionStays value until after we create the message + // in order to assure any local space values changed/reset get applied properly. If our + // parent is null then go ahead and reset the m_CachedWorldPositionStays the default value. + if (parentTransform == null) + { + m_CachedWorldPositionStays = true; + } + unsafe { var maxCount = NetworkManager.ConnectedClientsIds.Count; @@ -749,42 +798,90 @@ private void OnTransformParentChanged() // we call CheckOrphanChildren() method and quickly iterate over OrphanChildren set and see if we can reparent/adopt one. internal static HashSet OrphanChildren = new HashSet(); - internal bool ApplyNetworkParenting() + internal bool ApplyNetworkParenting(bool removeParent = false, bool ignoreNotSpawned = false) { if (!AutoObjectParentSync) { return false; } - if (!IsSpawned) + // SPECIAL CASE: + // The ignoreNotSpawned is a special case scenario where a late joining client has joined + // and loaded one or more scenes that contain nested in-scene placed NetworkObject children + // yet the server's synchronization information does not indicate the NetworkObject in question + // has a parent. Under this scenario, we want to remove the parent before spawning and setting + // the transform values. This is the only scenario where the ignoreNotSpawned parameter is used. + if (!IsSpawned && !ignoreNotSpawned) { return false; } - if (!m_IsReparented) + // Handle the first in-scene placed NetworkObject parenting scenarios. Once the m_LatestParent + // has been set, this will not be entered into again (i.e. the later code will be invoked and + // users will get notifications when the parent changes). + var isInScenePlaced = IsSceneObject.HasValue && IsSceneObject.Value; + if (transform.parent != null && !removeParent && !m_LatestParent.HasValue && isInScenePlaced) { - return true; + var parentNetworkObject = transform.parent.GetComponent(); + + // If parentNetworkObject is null then the parent is a GameObject without a NetworkObject component + // attached. Under this case, we preserve the hierarchy but we don't keep track of the parenting. + // Note: We only start tracking parenting if the user removes the child from the standard GameObject + // parent and then re-parents the child under a GameObject with a NetworkObject component attached. + if (parentNetworkObject == null) + { + return true; + } + else // If the parent still isn't spawned add this to the orphaned children and return false + if (!parentNetworkObject.IsSpawned) + { + OrphanChildren.Add(this); + return false; + } + else + { + // If we made it this far, go ahead and set the network parenting values + // with the default WorldPoisitonSays value + SetNetworkParenting(parentNetworkObject.NetworkObjectId, true); + + // Set the cached parent + m_CachedParent = parentNetworkObject.transform; + + return true; + } } - if (m_LatestParent == null || !m_LatestParent.HasValue) + // If we are removing the parent or our latest parent is not set, then remove the parent + // removeParent is only set when: + // - The server-side NetworkObject.OnTransformParentChanged is invoked and the parent is being removed + // - The client-side when handling a ParentSyncMessage + // When clients are synchronizing only the m_LatestParent.HasValue will not have a value if there is no parent + // or a parent was removed prior to the client connecting (i.e. in-scene placed NetworkObjects) + if (removeParent || !m_LatestParent.HasValue) { m_CachedParent = null; - transform.parent = null; - + // We must use Transform.SetParent when taking WorldPositionStays into + // consideration, otherwise just setting transform.parent = null defaults + // to WorldPositionStays which can cause scaling issues if the parent's + // scale is not the default (Vetctor3.one) value. + transform.SetParent(null, m_CachedWorldPositionStays); InvokeBehaviourOnNetworkObjectParentChanged(null); return true; } - if (!NetworkManager.SpawnManager.SpawnedObjects.ContainsKey(m_LatestParent.Value)) + // If we have a latest parent id but it hasn't been spawned yet, then add this instance to the orphanChildren + // HashSet and return false (i.e. parenting not applied yet) + if (m_LatestParent.HasValue && !NetworkManager.SpawnManager.SpawnedObjects.ContainsKey(m_LatestParent.Value)) { OrphanChildren.Add(this); return false; } + // If we made it here, then parent this instance under the parentObject var parentObject = NetworkManager.SpawnManager.SpawnedObjects[m_LatestParent.Value]; m_CachedParent = parentObject.transform; - transform.parent = parentObject.transform; + transform.SetParent(parentObject.transform, m_CachedWorldPositionStays); InvokeBehaviourOnNetworkObjectParentChanged(parentObject); return true; @@ -821,6 +918,13 @@ internal void InvokeBehaviourNetworkSpawn() Debug.LogWarning($"{ChildNetworkBehaviours[i].gameObject.name} is disabled! Netcode for GameObjects does not support spawning disabled NetworkBehaviours! The {ChildNetworkBehaviours[i].GetType().Name} component was skipped during spawn!"); } } + for (int i = 0; i < ChildNetworkBehaviours.Count; i++) + { + if (ChildNetworkBehaviours[i].gameObject.activeInHierarchy) + { + ChildNetworkBehaviours[i].VisibleOnNetworkSpawn(); + } + } } internal void InvokeBehaviourNetworkDespawn() @@ -962,7 +1066,6 @@ public struct HeaderData : INetworkSerializeByMemcpy public bool HasParent; public bool IsSceneObject; public bool HasTransform; - public bool IsReparented; } public HeaderData Header; @@ -975,6 +1078,7 @@ public struct TransformData : INetworkSerializeByMemcpy { public Vector3 Position; public Quaternion Rotation; + public Vector3 Scale; } public TransformData Transform; @@ -990,12 +1094,19 @@ public struct TransformData : INetworkSerializeByMemcpy public int NetworkSceneHandle; + public bool WorldPositionStays; + public unsafe void Serialize(FastBufferWriter writer) { var writeSize = sizeof(HeaderData); - writeSize += Header.HasParent ? FastBufferWriter.GetWriteSize(ParentObjectId) : 0; - writeSize += Header.HasTransform ? FastBufferWriter.GetWriteSize(Transform) : 0; - writeSize += Header.IsReparented ? FastBufferWriter.GetWriteSize(IsLatestParentSet) + (IsLatestParentSet ? FastBufferWriter.GetWriteSize() : 0) : 0; + if (Header.HasParent) + { + writeSize += FastBufferWriter.GetWriteSize(ParentObjectId); + writeSize += FastBufferWriter.GetWriteSize(WorldPositionStays); + writeSize += FastBufferWriter.GetWriteSize(IsLatestParentSet); + writeSize += IsLatestParentSet ? FastBufferWriter.GetWriteSize() : 0; + } + writeSize += Header.HasTransform ? FastBufferWriter.GetWriteSize() : 0; writeSize += Header.IsSceneObject ? FastBufferWriter.GetWriteSize() : 0; if (!writer.TryBeginWrite(writeSize)) @@ -1008,6 +1119,12 @@ public unsafe void Serialize(FastBufferWriter writer) if (Header.HasParent) { writer.WriteValue(ParentObjectId); + writer.WriteValue(WorldPositionStays); + writer.WriteValue(IsLatestParentSet); + if (IsLatestParentSet) + { + writer.WriteValue(LatestParent.Value); + } } if (Header.HasTransform) @@ -1015,15 +1132,6 @@ public unsafe void Serialize(FastBufferWriter writer) writer.WriteValue(Transform); } - if (Header.IsReparented) - { - writer.WriteValue(IsLatestParentSet); - if (IsLatestParentSet) - { - writer.WriteValue((ulong)LatestParent); - } - } - // In-Scene NetworkObjects are uniquely identified NetworkPrefabs defined by their // NetworkSceneHandle and GlobalObjectIdHash. Since each loaded scene has a unique // handle, it provides us with a unique and persistent "scene prefab asset" instance. @@ -1044,19 +1152,41 @@ public unsafe void Deserialize(FastBufferReader reader) throw new OverflowException("Could not deserialize SceneObject: Out of buffer space."); } reader.ReadValue(out Header); - var readSize = Header.HasParent ? FastBufferWriter.GetWriteSize(ParentObjectId) : 0; - readSize += Header.HasTransform ? FastBufferWriter.GetWriteSize(Transform) : 0; - readSize += Header.IsReparented ? FastBufferWriter.GetWriteSize(IsLatestParentSet) + (IsLatestParentSet ? FastBufferWriter.GetWriteSize() : 0) : 0; + var readSize = 0; + if (Header.HasParent) + { + readSize += FastBufferWriter.GetWriteSize(ParentObjectId); + readSize += FastBufferWriter.GetWriteSize(WorldPositionStays); + readSize += FastBufferWriter.GetWriteSize(IsLatestParentSet); + // We need to read at this point in order to get the IsLatestParentSet value + if (!reader.TryBeginRead(readSize)) + { + throw new OverflowException("Could not deserialize SceneObject: Out of buffer space."); + } + + // Read the initial parenting related properties + reader.ReadValue(out ParentObjectId); + reader.ReadValue(out WorldPositionStays); + reader.ReadValue(out IsLatestParentSet); + + // Now calculate the remaining bytes to read + readSize = 0; + readSize += IsLatestParentSet ? FastBufferWriter.GetWriteSize() : 0; + } + + readSize += Header.HasTransform ? FastBufferWriter.GetWriteSize() : 0; readSize += Header.IsSceneObject ? FastBufferWriter.GetWriteSize() : 0; + // Try to begin reading the remaining bytes if (!reader.TryBeginRead(readSize)) { throw new OverflowException("Could not deserialize SceneObject: Out of buffer space."); } - if (Header.HasParent) + if (IsLatestParentSet) { - reader.ReadValue(out ParentObjectId); + reader.ReadValueSafe(out ulong latestParent); + LatestParent = latestParent; } if (Header.HasTransform) @@ -1064,16 +1194,6 @@ public unsafe void Deserialize(FastBufferReader reader) reader.ReadValue(out Transform); } - if (Header.IsReparented) - { - reader.ReadValue(out IsLatestParentSet); - if (IsLatestParentSet) - { - reader.ReadValueSafe(out ulong latestParent); - LatestParent = latestParent; - } - } - // In-Scene NetworkObjects are uniquely identified NetworkPrefabs defined by their // NetworkSceneHandle and GlobalObjectIdHash. Since each loaded scene has a unique // handle, it provides us with a unique and persistent "scene prefab asset" instance. @@ -1086,6 +1206,14 @@ public unsafe void Deserialize(FastBufferReader reader) } } + internal void PostNetworkVariableWrite() + { + for (int k = 0; k < ChildNetworkBehaviours.Count; k++) + { + ChildNetworkBehaviours[k].PostNetworkVariableWrite(); + } + } + internal SceneObject GetMessageSceneObject(ulong targetClientId) { var obj = new SceneObject @@ -1109,33 +1237,39 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId) parentNetworkObject = transform.parent.GetComponent(); } - if (parentNetworkObject) + if (parentNetworkObject != null) { obj.Header.HasParent = true; obj.ParentObjectId = parentNetworkObject.NetworkObjectId; + obj.WorldPositionStays = m_CachedWorldPositionStays; + var latestParent = GetNetworkParenting(); + var isLatestParentSet = latestParent != null && latestParent.HasValue; + obj.IsLatestParentSet = isLatestParentSet; + if (isLatestParentSet) + { + obj.LatestParent = latestParent.Value; + } } + if (IncludeTransformWhenSpawning == null || IncludeTransformWhenSpawning(OwnerClientId)) { obj.Header.HasTransform = true; obj.Transform = new SceneObject.TransformData { - Position = transform.position, - Rotation = transform.rotation + // If we are parented and we have the m_CachedWorldPositionStays disabled, then use local space + // values as opposed world space values. + Position = parentNetworkObject && !m_CachedWorldPositionStays ? transform.localPosition : transform.position, + Rotation = parentNetworkObject && !m_CachedWorldPositionStays ? transform.localRotation : transform.rotation, + + // We only use the lossyScale if the NetworkObject has a parent. Multi-generation nested children scales can + // impact the final scale of the child NetworkObject in question. The solution is to use the lossy scale + // which can be thought of as "world space scale". + // More information: + // https://docs.unity3d.com/ScriptReference/Transform-lossyScale.html + Scale = parentNetworkObject && !m_CachedWorldPositionStays ? transform.localScale : transform.lossyScale, }; } - var (isReparented, latestParent) = GetNetworkParenting(); - obj.Header.IsReparented = isReparented; - if (isReparented) - { - var isLatestParentSet = latestParent != null && latestParent.HasValue; - obj.IsLatestParentSet = isLatestParentSet; - if (isLatestParentSet) - { - obj.LatestParent = latestParent.Value; - } - } - return obj; } @@ -1149,33 +1283,8 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId) /// optional to use NetworkObject deserialized internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBufferReader variableData, NetworkManager networkManager) { - Vector3? position = null; - Quaternion? rotation = null; - ulong? parentNetworkId = null; - int? networkSceneHandle = null; - - if (sceneObject.Header.HasTransform) - { - position = sceneObject.Transform.Position; - rotation = sceneObject.Transform.Rotation; - } - - if (sceneObject.Header.HasParent) - { - parentNetworkId = sceneObject.ParentObjectId; - } - - if (sceneObject.Header.IsSceneObject) - { - networkSceneHandle = sceneObject.NetworkSceneHandle; - } - //Attempt to create a local NetworkObject - var networkObject = networkManager.SpawnManager.CreateLocalNetworkObject( - sceneObject.Header.IsSceneObject, sceneObject.Header.Hash, - sceneObject.Header.OwnerClientId, parentNetworkId, networkSceneHandle, position, rotation, sceneObject.Header.IsReparented); - - networkObject?.SetNetworkParenting(sceneObject.Header.IsReparented, sceneObject.LatestParent); + var networkObject = networkManager.SpawnManager.CreateLocalNetworkObject(sceneObject); if (networkObject == null) { @@ -1190,7 +1299,7 @@ internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBuf return null; } - // Spawn the NetworkObject( + // Spawn the NetworkObject networkManager.SpawnManager.SpawnNetworkObjectLocally(networkObject, sceneObject, variableData, false); return networkObject; diff --git a/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs b/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs index 14c4ac3..0a685a7 100644 --- a/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs +++ b/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs @@ -79,6 +79,7 @@ public void Handle(ref NetworkContext context) networkManager.NetworkTickSystem.Reset(networkManager.NetworkTimeSystem.LocalTime, networkManager.NetworkTimeSystem.ServerTime); networkManager.LocalClient = new NetworkClient() { ClientId = networkManager.LocalClientId }; + networkManager.IsApproved = true; // Only if scene management is disabled do we handle NetworkObject synchronization at this point if (!networkManager.NetworkConfig.EnableSceneManagement) diff --git a/Runtime/Messaging/Messages/ParentSyncMessage.cs b/Runtime/Messaging/Messages/ParentSyncMessage.cs index 95d20ea..0023b1a 100644 --- a/Runtime/Messaging/Messages/ParentSyncMessage.cs +++ b/Runtime/Messaging/Messages/ParentSyncMessage.cs @@ -1,10 +1,12 @@ +using UnityEngine; + namespace Unity.Netcode { internal struct ParentSyncMessage : INetworkMessage { public ulong NetworkObjectId; - public bool IsReparented; + public bool WorldPositionStays; //If(Metadata.IsReparented) public bool IsLatestParentSet; @@ -12,18 +14,36 @@ internal struct ParentSyncMessage : INetworkMessage //If(IsLatestParentSet) public ulong? LatestParent; + // Is set when the parent should be removed (similar to IsReparented functionality but only for removing the parent) + public bool RemoveParent; + + // These additional properties are used to synchronize clients with the current position, + // rotation, and scale after parenting/de-parenting (world/local space relative). This + // allows users to control the final child's transform values without having to have a + // NetworkTransform component on the child. (i.e. picking something up) + public Vector3 Position; + public Quaternion Rotation; + public Vector3 Scale; + public void Serialize(FastBufferWriter writer) { - writer.WriteValueSafe(NetworkObjectId); - writer.WriteValueSafe(IsReparented); - if (IsReparented) + BytePacker.WriteValuePacked(writer, NetworkObjectId); + writer.WriteValueSafe(RemoveParent); + writer.WriteValueSafe(WorldPositionStays); + if (!RemoveParent) { writer.WriteValueSafe(IsLatestParentSet); + if (IsLatestParentSet) { - writer.WriteValueSafe((ulong)LatestParent); + BytePacker.WriteValueBitPacked(writer, (ulong)LatestParent); } } + + // Whether parenting or removing a parent, we always update the position, rotation, and scale + writer.WriteValueSafe(Position); + writer.WriteValueSafe(Rotation); + writer.WriteValueSafe(Scale); } public bool Deserialize(FastBufferReader reader, ref NetworkContext context) @@ -34,24 +54,30 @@ public bool Deserialize(FastBufferReader reader, ref NetworkContext context) return false; } - reader.ReadValueSafe(out NetworkObjectId); - reader.ReadValueSafe(out IsReparented); - if (IsReparented) + ByteUnpacker.ReadValuePacked(reader, out NetworkObjectId); + reader.ReadValueSafe(out RemoveParent); + reader.ReadValueSafe(out WorldPositionStays); + if (!RemoveParent) { reader.ReadValueSafe(out IsLatestParentSet); + if (IsLatestParentSet) { - reader.ReadValueSafe(out ulong latestParent); + ByteUnpacker.ReadValueBitPacked(reader, out ulong latestParent); LatestParent = latestParent; } } + // Whether parenting or removing a parent, we always update the position, rotation, and scale + reader.ReadValueSafe(out Position); + reader.ReadValueSafe(out Rotation); + reader.ReadValueSafe(out Scale); + if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(NetworkObjectId)) { networkManager.DeferredMessageManager.DeferMessage(IDeferredMessageManager.TriggerType.OnSpawn, NetworkObjectId, reader, ref context); return false; } - return true; } @@ -59,8 +85,22 @@ public void Handle(ref NetworkContext context) { var networkManager = (NetworkManager)context.SystemOwner; var networkObject = networkManager.SpawnManager.SpawnedObjects[NetworkObjectId]; - networkObject.SetNetworkParenting(IsReparented, LatestParent); - networkObject.ApplyNetworkParenting(); + networkObject.SetNetworkParenting(LatestParent, WorldPositionStays); + networkObject.ApplyNetworkParenting(RemoveParent); + + // We set all of the transform values after parenting as they are + // the values of the server-side post-parenting transform values + if (!WorldPositionStays) + { + networkObject.transform.localPosition = Position; + networkObject.transform.localRotation = Rotation; + } + else + { + networkObject.transform.position = Position; + networkObject.transform.rotation = Rotation; + } + networkObject.transform.localScale = Scale; } } } diff --git a/Runtime/Metrics/NetworkMetrics.cs b/Runtime/Metrics/NetworkMetrics.cs index fb92716..b171932 100644 --- a/Runtime/Metrics/NetworkMetrics.cs +++ b/Runtime/Metrics/NetworkMetrics.cs @@ -5,17 +5,14 @@ using Unity.Multiplayer.Tools.MetricTypes; using Unity.Multiplayer.Tools.NetStats; using Unity.Profiling; -using UnityEngine; namespace Unity.Netcode { internal class NetworkMetrics : INetworkMetrics { - const ulong k_MaxMetricsPerFrame = 1000L; - - static Dictionary s_SceneEventTypeNames; - - static ProfilerMarker s_FrameDispatch = new ProfilerMarker($"{nameof(NetworkMetrics)}.DispatchFrame"); + private const ulong k_MaxMetricsPerFrame = 1000L; + private static Dictionary s_SceneEventTypeNames; + private static ProfilerMarker s_FrameDispatch = new ProfilerMarker($"{nameof(NetworkMetrics)}.DispatchFrame"); static NetworkMetrics() { @@ -531,7 +528,7 @@ private static NetworkObjectIdentifier GetObjectIdentifier(NetworkObject network } } - internal class NetcodeObserver + internal class NetcodeObserver { public static IMetricObserver Observer { get; } = MetricObserverFactory.Construct(); } diff --git a/Runtime/Metrics/NetworkObjectProvider.cs b/Runtime/Metrics/NetworkObjectProvider.cs index e1f6f01..7d2ad6e 100644 --- a/Runtime/Metrics/NetworkObjectProvider.cs +++ b/Runtime/Metrics/NetworkObjectProvider.cs @@ -4,7 +4,7 @@ namespace Unity.Netcode { - class NetworkObjectProvider : INetworkObjectProvider + internal class NetworkObjectProvider : INetworkObjectProvider { private readonly NetworkManager m_NetworkManager; @@ -15,7 +15,7 @@ public NetworkObjectProvider(NetworkManager networkManager) public Object GetNetworkObject(ulong networkObjectId) { - if(m_NetworkManager.SpawnManager.SpawnedObjects.TryGetValue(networkObjectId, out NetworkObject value)) + if (m_NetworkManager.SpawnManager.SpawnedObjects.TryGetValue(networkObjectId, out NetworkObject value)) { return value; } diff --git a/Runtime/NetworkVariable/Collections/NetworkList.cs b/Runtime/NetworkVariable/Collections/NetworkList.cs index 8f69b77..e893db7 100644 --- a/Runtime/NetworkVariable/Collections/NetworkList.cs +++ b/Runtime/NetworkVariable/Collections/NetworkList.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Unity.Collections; +using UnityEngine; namespace Unity.Netcode { @@ -11,7 +12,6 @@ namespace Unity.Netcode public class NetworkList : NetworkVariableBase where T : unmanaged, IEquatable { private NativeList m_List = new NativeList(64, Allocator.Persistent); - private NativeList m_ListAtLastReset = new NativeList(64, Allocator.Persistent); private NativeList> m_DirtyEvents = new NativeList>(64, Allocator.Persistent); /// @@ -39,9 +39,13 @@ public NetworkList(IEnumerable values = default, NetworkVariableWritePermission writePerm = DefaultWritePerm) : base(readPerm, writePerm) { - foreach (var value in values) + // allow null IEnumerable to mean "no values" + if (values != null) { - m_List.Add(value); + foreach (var value in values) + { + m_List.Add(value); + } } } @@ -52,7 +56,6 @@ public override void ResetDirty() if (m_DirtyEvents.Length > 0) { m_DirtyEvents.Clear(); - m_ListAtLastReset.CopyFrom(m_List); } } @@ -65,6 +68,13 @@ public override bool IsDirty() internal void MarkNetworkObjectDirty() { + if (m_NetworkBehaviour == null) + { + Debug.LogWarning($"NetworkList is written to, but doesn't know its NetworkBehaviour yet. " + + "Are you modifying a NetworkList before the NetworkObject is spawned?"); + return; + } + m_NetworkBehaviour.NetworkManager.MarkNetworkObjectDirty(m_NetworkBehaviour.NetworkObject); } @@ -127,26 +137,10 @@ public override void WriteDelta(FastBufferWriter writer) /// public override void WriteField(FastBufferWriter writer) { - // The listAtLastReset mechanism was put in place to deal with duplicate adds - // upon initial spawn. However, it causes issues with in-scene placed objects - // due to difference in spawn order. In order to address this, we pick the right - // list based on the type of object. - bool isSceneObject = m_NetworkBehaviour.NetworkObject.IsSceneObject != false; - if (isSceneObject) - { - writer.WriteValueSafe((ushort)m_ListAtLastReset.Length); - for (int i = 0; i < m_ListAtLastReset.Length; i++) - { - NetworkVariableSerialization.Write(writer, ref m_ListAtLastReset.ElementAt(i)); - } - } - else + writer.WriteValueSafe((ushort)m_List.Length); + for (int i = 0; i < m_List.Length; i++) { - writer.WriteValueSafe((ushort)m_List.Length); - for (int i = 0; i < m_List.Length; i++) - { - NetworkVariableSerialization.Write(writer, ref m_List.ElementAt(i)); - } + NetworkVariableSerialization.Write(writer, ref m_List.ElementAt(i)); } } @@ -157,7 +151,8 @@ public override void ReadField(FastBufferReader reader) reader.ReadValueSafe(out ushort count); for (int i = 0; i < count; i++) { - NetworkVariableSerialization.Read(reader, out T value); + var value = new T(); + NetworkVariableSerialization.Read(reader, ref value); m_List.Add(value); } } @@ -173,7 +168,8 @@ public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) { case NetworkListEvent.EventType.Add: { - NetworkVariableSerialization.Read(reader, out T value); + var value = new T(); + NetworkVariableSerialization.Read(reader, ref value); m_List.Add(value); if (OnListChanged != null) @@ -201,7 +197,8 @@ public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) case NetworkListEvent.EventType.Insert: { reader.ReadValueSafe(out int index); - NetworkVariableSerialization.Read(reader, out T value); + var value = new T(); + NetworkVariableSerialization.Read(reader, ref value); if (index < m_List.Length) { @@ -237,7 +234,8 @@ public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) break; case NetworkListEvent.EventType.Remove: { - NetworkVariableSerialization.Read(reader, out T value); + var value = new T(); + NetworkVariableSerialization.Read(reader, ref value); int index = m_List.IndexOf(value); if (index == -1) { @@ -299,7 +297,8 @@ public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) case NetworkListEvent.EventType.Value: { reader.ReadValueSafe(out int index); - NetworkVariableSerialization.Read(reader, out T value); + var value = new T(); + NetworkVariableSerialization.Read(reader, ref value); if (index >= m_List.Length) { throw new Exception("Shouldn't be here, index is higher than list length"); @@ -374,6 +373,12 @@ public IEnumerator GetEnumerator() /// public void Add(T item) { + // check write permissions + if (!CanClientWrite(m_NetworkBehaviour.NetworkManager.LocalClientId)) + { + throw new InvalidOperationException("Client is not allowed to write to this NetworkList"); + } + m_List.Add(item); var listEvent = new NetworkListEvent() @@ -389,6 +394,12 @@ public void Add(T item) /// public void Clear() { + // check write permissions + if (!CanClientWrite(m_NetworkBehaviour.NetworkManager.LocalClientId)) + { + throw new InvalidOperationException("Client is not allowed to write to this NetworkList"); + } + m_List.Clear(); var listEvent = new NetworkListEvent() @@ -409,6 +420,12 @@ public bool Contains(T item) /// public bool Remove(T item) { + // check write permissions + if (!CanClientWrite(m_NetworkBehaviour.NetworkManager.LocalClientId)) + { + throw new InvalidOperationException("Client is not allowed to write to this NetworkList"); + } + int index = NativeArrayExtensions.IndexOf(m_List, item); if (index == -1) { @@ -438,6 +455,12 @@ public int IndexOf(T item) /// public void Insert(int index, T item) { + // check write permissions + if (!CanClientWrite(m_NetworkBehaviour.NetworkManager.LocalClientId)) + { + throw new InvalidOperationException("Client is not allowed to write to this NetworkList"); + } + if (index < m_List.Length) { m_List.InsertRangeWithBeginEnd(index, index + 1); @@ -461,6 +484,12 @@ public void Insert(int index, T item) /// public void RemoveAt(int index) { + // check write permissions + if (!CanClientWrite(m_NetworkBehaviour.NetworkManager.LocalClientId)) + { + throw new InvalidOperationException("Client is not allowed to write to this NetworkList"); + } + m_List.RemoveAt(index); var listEvent = new NetworkListEvent() @@ -478,6 +507,12 @@ public T this[int index] get => m_List[index]; set { + // check write permissions + if (!CanClientWrite(m_NetworkBehaviour.NetworkManager.LocalClientId)) + { + throw new InvalidOperationException("Client is not allowed to write to this NetworkList"); + } + var previousValue = m_List[index]; m_List[index] = value; @@ -520,7 +555,6 @@ public int LastModifiedTick public override void Dispose() { m_List.Dispose(); - m_ListAtLastReset.Dispose(); m_DirtyEvents.Dispose(); } } diff --git a/Runtime/NetworkVariable/NetworkVariable.cs b/Runtime/NetworkVariable/NetworkVariable.cs index 698fe01..5f62721 100644 --- a/Runtime/NetworkVariable/NetworkVariable.cs +++ b/Runtime/NetworkVariable/NetworkVariable.cs @@ -1,7 +1,5 @@ using UnityEngine; using System; -using System.Runtime.CompilerServices; -using Unity.Collections.LowLevel.Unsafe; namespace Unity.Netcode { @@ -10,7 +8,7 @@ namespace Unity.Netcode /// /// the unmanaged type for [Serializable] - public class NetworkVariable : NetworkVariableBase where T : unmanaged + public class NetworkVariable : NetworkVariableBase { /// /// Delegate type for value changed event @@ -52,7 +50,7 @@ public virtual T Value set { // Compare bitwise - if (ValueEquals(ref m_InternalValue, ref value)) + if (NetworkVariableSerialization.AreEqual(ref m_InternalValue, ref value)) { return; } @@ -66,20 +64,6 @@ public virtual T Value } } - // Compares two values of the same unmanaged type by underlying memory - // Ignoring any overridden value checks - // Size is fixed - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static unsafe bool ValueEquals(ref T a, ref T b) - { - // get unmanaged pointers - var aptr = UnsafeUtility.AddressOf(ref a); - var bptr = UnsafeUtility.AddressOf(ref b); - - // compare addresses - return UnsafeUtility.MemCmp(aptr, bptr, sizeof(T)) == 0; - } - /// /// Sets the , marks the dirty, and invokes the callback /// if there are subscribers to that event. @@ -115,7 +99,7 @@ public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) // would be stored in different fields T previousValue = m_InternalValue; - NetworkVariableSerialization.Read(reader, out m_InternalValue); + NetworkVariableSerialization.Read(reader, ref m_InternalValue); if (keepDirtyDelta) { @@ -128,7 +112,7 @@ public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) /// public override void ReadField(FastBufferReader reader) { - NetworkVariableSerialization.Read(reader, out m_InternalValue); + NetworkVariableSerialization.Read(reader, ref m_InternalValue); } /// diff --git a/Runtime/NetworkVariable/NetworkVariableBase.cs b/Runtime/NetworkVariable/NetworkVariableBase.cs index 4b8ea84..d3e4c0e 100644 --- a/Runtime/NetworkVariable/NetworkVariableBase.cs +++ b/Runtime/NetworkVariable/NetworkVariableBase.cs @@ -1,4 +1,5 @@ using System; +using UnityEngine; namespace Unity.Netcode { @@ -79,8 +80,15 @@ protected NetworkVariableBase( public virtual void SetDirty(bool isDirty) { m_IsDirty = isDirty; - if (m_IsDirty && m_NetworkBehaviour != null) + + if (m_IsDirty) { + if (m_NetworkBehaviour == null) + { + Debug.LogWarning($"NetworkVariable is written to, but doesn't know its NetworkBehaviour yet. " + + "Are you modifying a NetworkVariable before the NetworkObject is spawned?"); + return; + } m_NetworkBehaviour.NetworkManager.MarkNetworkObjectDirty(m_NetworkBehaviour.NetworkObject); } } diff --git a/Runtime/NetworkVariable/NetworkVariableSerialization.cs b/Runtime/NetworkVariable/NetworkVariableSerialization.cs index cb3633f..83c4de2 100644 --- a/Runtime/NetworkVariable/NetworkVariableSerialization.cs +++ b/Runtime/NetworkVariable/NetworkVariableSerialization.cs @@ -1,7 +1,6 @@ using System; -using System.Collections.Generic; using Unity.Collections; -using UnityEngine; +using Unity.Collections.LowLevel.Unsafe; namespace Unity.Netcode { @@ -18,7 +17,7 @@ internal interface INetworkVariableSerializer // Taking T as an in parameter like we do in other places would require making a copy // of it to pass it as a ref parameter. public void Write(FastBufferWriter writer, ref T value); - public void Read(FastBufferReader reader, out T value); + public void Read(FastBufferReader reader, ref T value); } /// @@ -35,79 +34,80 @@ public void Write(FastBufferWriter writer, ref T value) { writer.WriteUnmanagedSafe(value); } - public void Read(FastBufferReader reader, out T value) + public void Read(FastBufferReader reader, ref T value) { reader.ReadUnmanagedSafe(out value); } } /// - /// Serializer for FixedStrings, which does the same thing FastBufferWriter/FastBufferReader do, - /// but is implemented to get the data it needs using open instance delegates that are passed in - /// via reflection. This prevents needing T to meet any interface requirements (which isn't achievable - /// without incurring GC allocs on every call to Write or Read - reflection + Open Instance Delegates - /// circumvent that.) - /// - /// Tests show that calling these delegates doesn't cause any GC allocations even though they're - /// obtained via reflection and Delegate.CreateDelegate() and called on types that, at compile time, - /// aren't known to actually contain those methods. + /// Serializer for FixedStrings /// /// - internal class FixedStringSerializer : INetworkVariableSerializer where T : unmanaged + internal class FixedStringSerializer : INetworkVariableSerializer where T : unmanaged, INativeList, IUTF8Bytes { - internal delegate int GetLengthDelegate(ref T value); - internal delegate void SetLengthDelegate(ref T value, int length); - internal unsafe delegate byte* GetUnsafePtrDelegate(ref T value); - - internal GetLengthDelegate GetLength; - internal SetLengthDelegate SetLength; - internal GetUnsafePtrDelegate GetUnsafePtr; - - public unsafe void Write(FastBufferWriter writer, ref T value) + public void Write(FastBufferWriter writer, ref T value) { - int length = GetLength(ref value); - byte* data = GetUnsafePtr(ref value); - writer.WriteUnmanagedSafe(length); - writer.WriteBytesSafe(data, length); + writer.WriteValueSafe(value); } - public unsafe void Read(FastBufferReader reader, out T value) + public void Read(FastBufferReader reader, ref T value) { - value = new T(); - reader.ReadValueSafe(out int length); - SetLength(ref value, length); - reader.ReadBytesSafe(GetUnsafePtr(ref value), length); + reader.ReadValueSafeInPlace(ref value); } } /// - /// Serializer for INetworkSerializable types, which does the same thing - /// FastBufferWriter/FastBufferReader do, but is implemented to call the NetworkSerialize() method - /// via open instance delegates passed in via reflection. This prevents needing T to meet any interface - /// requirements (which isn't achievable without incurring GC allocs on every call to Write or Read - - /// reflection + Open Instance Delegates circumvent that.) - /// - /// Tests show that calling these delegates doesn't cause any GC allocations even though they're - /// obtained via reflection and Delegate.CreateDelegate() and called on types that, at compile time, - /// aren't known to actually contain those methods. + /// Serializer for unmanaged INetworkSerializable types /// /// - internal class NetworkSerializableSerializer : INetworkVariableSerializer where T : unmanaged + internal class UnmanagedNetworkSerializableSerializer : INetworkVariableSerializer where T : unmanaged, INetworkSerializable { - internal delegate void WriteValueDelegate(ref T value, BufferSerializer serializer); - internal delegate void ReadValueDelegate(ref T value, BufferSerializer serializer); + public void Write(FastBufferWriter writer, ref T value) + { + var bufferSerializer = new BufferSerializer(new BufferSerializerWriter(writer)); + value.NetworkSerialize(bufferSerializer); + } + public void Read(FastBufferReader reader, ref T value) + { + var bufferSerializer = new BufferSerializer(new BufferSerializerReader(reader)); + value.NetworkSerialize(bufferSerializer); - internal WriteValueDelegate WriteValue; - internal ReadValueDelegate ReadValue; + } + } + + /// + /// Serializer for managed INetworkSerializable types, which differs from the unmanaged implementation in that it + /// has to be null-aware + /// + internal class ManagedNetworkSerializableSerializer : INetworkVariableSerializer where T : class, INetworkSerializable, new() + { public void Write(FastBufferWriter writer, ref T value) { var bufferSerializer = new BufferSerializer(new BufferSerializerWriter(writer)); - WriteValue(ref value, bufferSerializer); + bool isNull = (value == null); + bufferSerializer.SerializeValue(ref isNull); + if (!isNull) + { + value.NetworkSerialize(bufferSerializer); + } } - public void Read(FastBufferReader reader, out T value) + public void Read(FastBufferReader reader, ref T value) { - value = new T(); var bufferSerializer = new BufferSerializer(new BufferSerializerReader(reader)); - ReadValue(ref value, bufferSerializer); + bool isNull = false; + bufferSerializer.SerializeValue(ref isNull); + if (isNull) + { + value = null; + } + else + { + if (value == null) + { + value = new T(); + } + value.NetworkSerialize(bufferSerializer); + } } } @@ -164,7 +164,7 @@ public void Write(FastBufferWriter writer, ref T value) } UserNetworkVariableSerialization.WriteValue(writer, value); } - public void Read(FastBufferReader reader, out T value) + public void Read(FastBufferReader reader, ref T value) { if (UserNetworkVariableSerialization.ReadValue == null || UserNetworkVariableSerialization.WriteValue == null) { @@ -174,34 +174,95 @@ public void Read(FastBufferReader reader, out T value) } } - internal static class NetworkVariableSerializationTypes + /// + /// This class contains initialization functions for various different types used in NetworkVariables. + /// Generally speaking, these methods are called by a module initializer created by codegen (NetworkBehaviourILPP) + /// and do not need to be called manually. + /// + /// There are two types of initializers: Serializers and EqualityCheckers. Every type must have an EqualityChecker + /// registered to it in order to be used in NetworkVariable; however, not all types need a Serializer. Types without + /// a serializer registered will fall back to using the delegates in . + /// If no such delegate has been registered, a type without a serializer will throw an exception on the first attempt + /// to serialize or deserialize it. (Again, however, codegen handles this automatically and this registration doesn't + /// typically need to be performed manually.) + /// + public static class NetworkVariableSerializationTypes { - internal static readonly HashSet BaseSupportedTypes = new HashSet + /// + /// Registeres an unmanaged type that will be serialized by a direct memcpy into a buffer + /// + /// + public static void InitializeSerializer_UnmanagedByMemcpy() where T : unmanaged + { + NetworkVariableSerialization.Serializer = new UnmanagedTypeSerializer(); + } + + /// + /// Registers an unmanaged type that implements INetworkSerializable and will be serialized through a call to + /// NetworkSerialize + /// + /// + public static void InitializeSerializer_UnmanagedINetworkSerializable() where T : unmanaged, INetworkSerializable + { + NetworkVariableSerialization.Serializer = new UnmanagedNetworkSerializableSerializer(); + } + + /// + /// Registers a managed type that implements INetworkSerializable and will be serialized through a call to + /// NetworkSerialize + /// + /// + public static void InitializeSerializer_ManagedINetworkSerializable() where T : class, INetworkSerializable, new() + { + NetworkVariableSerialization.Serializer = new ManagedNetworkSerializableSerializer(); + } + + /// + /// Registers a FixedString type that will be serialized through FastBufferReader/FastBufferWriter's FixedString + /// serializers + /// + /// + public static void InitializeSerializer_FixedString() where T : unmanaged, INativeList, IUTF8Bytes + { + NetworkVariableSerialization.Serializer = new FixedStringSerializer(); + } + + /// + /// Registers a managed type that will be checked for equality using T.Equals() + /// + /// + public static void InitializeEqualityChecker_ManagedIEquatable() where T : class, IEquatable + { + NetworkVariableSerialization.AreEqual = NetworkVariableSerialization.EqualityEqualsObject; + } + + /// + /// Registers an unmanaged type that will be checked for equality using T.Equals() + /// + /// + public static void InitializeEqualityChecker_UnmanagedIEquatable() where T : unmanaged, IEquatable + { + NetworkVariableSerialization.AreEqual = NetworkVariableSerialization.EqualityEquals; + } + + /// + /// Registers an unmanaged type that will be checked for equality using memcmp and only considered + /// equal if they are bitwise equivalent in memory + /// + /// + public static void InitializeEqualityChecker_UnmanagedValueEquals() where T : unmanaged + { + NetworkVariableSerialization.AreEqual = NetworkVariableSerialization.ValueEquals; + } + + /// + /// Registers a managed type that will be checked for equality using the == operator + /// + /// + public static void InitializeEqualityChecker_ManagedClassEquals() where T : class { - typeof(bool), - typeof(byte), - typeof(sbyte), - typeof(char), - typeof(decimal), - typeof(double), - typeof(float), - typeof(int), - typeof(uint), - typeof(long), - typeof(ulong), - typeof(short), - typeof(ushort), - typeof(Vector2), - typeof(Vector3), - typeof(Vector2Int), - typeof(Vector3Int), - typeof(Vector4), - typeof(Quaternion), - typeof(Color), - typeof(Color32), - typeof(Ray), - typeof(Ray2D) - }; + NetworkVariableSerialization.AreEqual = NetworkVariableSerialization.ClassEquals; + } } /// @@ -212,56 +273,59 @@ internal static class NetworkVariableSerializationTypes /// /// The type the associated NetworkVariable is templated on [Serializable] - public static class NetworkVariableSerialization where T : unmanaged + public static class NetworkVariableSerialization { - private static INetworkVariableSerializer s_Serializer = GetSerializer(); + internal static INetworkVariableSerializer Serializer = new FallbackSerializer(); + + internal delegate bool EqualsDelegate(ref T a, ref T b); + internal static EqualsDelegate AreEqual; - private static INetworkVariableSerializer GetSerializer() + // Compares two values of the same unmanaged type by underlying memory + // Ignoring any overridden value checks + // Size is fixed + internal static unsafe bool ValueEquals(ref TValueType a, ref TValueType b) where TValueType : unmanaged { - if (NetworkVariableSerializationTypes.BaseSupportedTypes.Contains(typeof(T))) - { - return new UnmanagedTypeSerializer(); - } - if (typeof(INetworkSerializeByMemcpy).IsAssignableFrom(typeof(T))) - { - return new UnmanagedTypeSerializer(); - } - if (typeof(Enum).IsAssignableFrom(typeof(T))) - { - return new UnmanagedTypeSerializer(); - } + // get unmanaged pointers + var aptr = UnsafeUtility.AddressOf(ref a); + var bptr = UnsafeUtility.AddressOf(ref b); + + // compare addresses + return UnsafeUtility.MemCmp(aptr, bptr, sizeof(TValueType)) == 0; + } - if (typeof(INetworkSerializable).IsAssignableFrom(typeof(T))) + internal static bool EqualityEqualsObject(ref TValueType a, ref TValueType b) where TValueType : class, IEquatable + { + if (a == null) { - // Obtains "Open Instance Delegates" for the type's NetworkSerialize() methods - - // one for an instance of the generic method taking BufferSerializerWriter as T, - // one for an instance of the generic method taking BufferSerializerReader as T - var writeMethod = (NetworkSerializableSerializer.WriteValueDelegate)Delegate.CreateDelegate(typeof(NetworkSerializableSerializer.WriteValueDelegate), null, typeof(T).GetMethod(nameof(INetworkSerializable.NetworkSerialize)).MakeGenericMethod(typeof(BufferSerializerWriter))); - var readMethod = (NetworkSerializableSerializer.ReadValueDelegate)Delegate.CreateDelegate(typeof(NetworkSerializableSerializer.ReadValueDelegate), null, typeof(T).GetMethod(nameof(INetworkSerializable.NetworkSerialize)).MakeGenericMethod(typeof(BufferSerializerReader))); - return new NetworkSerializableSerializer { WriteValue = writeMethod, ReadValue = readMethod }; + return b == null; } - if (typeof(IUTF8Bytes).IsAssignableFrom(typeof(T)) && typeof(INativeList).IsAssignableFrom(typeof(T))) + if (b == null) { - // Get "OpenInstanceDelegates" for the Length property (get and set, which are prefixed - // with "get_" and "set_" under the hood and emitted as methods) and GetUnsafePtr() - var getLength = (FixedStringSerializer.GetLengthDelegate)Delegate.CreateDelegate(typeof(FixedStringSerializer.GetLengthDelegate), null, typeof(T).GetMethod("get_" + nameof(INativeList.Length))); - var setLength = (FixedStringSerializer.SetLengthDelegate)Delegate.CreateDelegate(typeof(FixedStringSerializer.SetLengthDelegate), null, typeof(T).GetMethod("set_" + nameof(INativeList.Length))); - var getUnsafePtr = (FixedStringSerializer.GetUnsafePtrDelegate)Delegate.CreateDelegate(typeof(FixedStringSerializer.GetUnsafePtrDelegate), null, typeof(T).GetMethod(nameof(IUTF8Bytes.GetUnsafePtr))); - return new FixedStringSerializer { GetLength = getLength, SetLength = setLength, GetUnsafePtr = getUnsafePtr }; + return false; } - return new FallbackSerializer(); + return a.Equals(b); + } + + internal static bool EqualityEquals(ref TValueType a, ref TValueType b) where TValueType : unmanaged, IEquatable + { + return a.Equals(b); + } + + internal static bool ClassEquals(ref TValueType a, ref TValueType b) where TValueType : class + { + return a == b; } internal static void Write(FastBufferWriter writer, ref T value) { - s_Serializer.Write(writer, ref value); + Serializer.Write(writer, ref value); } - internal static void Read(FastBufferReader reader, out T value) + internal static void Read(FastBufferReader reader, ref T value) { - s_Serializer.Read(reader, out value); + Serializer.Read(reader, ref value); } } } diff --git a/Runtime/SceneManagement/ISceneManagerHandler.cs b/Runtime/SceneManagement/ISceneManagerHandler.cs index 92976f3..8b5b7e7 100644 --- a/Runtime/SceneManagement/ISceneManagerHandler.cs +++ b/Runtime/SceneManagement/ISceneManagerHandler.cs @@ -1,4 +1,3 @@ -using System; using UnityEngine; using UnityEngine.SceneManagement; @@ -10,25 +9,8 @@ namespace Unity.Netcode /// internal interface ISceneManagerHandler { - // Generic action to call when a scene is finished loading/unloading - struct SceneEventAction - { - internal uint SceneEventId; - internal Action EventAction; - /// - /// Used server-side for integration testing in order to - /// invoke the SceneEventProgress once done loading - /// - internal Action Completed; - internal void Invoke() - { - Completed?.Invoke(); - EventAction.Invoke(SceneEventId); - } - } + AsyncOperation LoadSceneAsync(string sceneName, LoadSceneMode loadSceneMode, SceneEventProgress sceneEventProgress); - AsyncOperation LoadSceneAsync(string sceneName, LoadSceneMode loadSceneMode, SceneEventAction sceneEventAction); - - AsyncOperation UnloadSceneAsync(Scene scene, SceneEventAction sceneEventAction); + AsyncOperation UnloadSceneAsync(Scene scene, SceneEventProgress sceneEventProgress); } } diff --git a/Runtime/SceneManagement/NetworkSceneManager.cs b/Runtime/SceneManagement/NetworkSceneManager.cs index 7f7c9ff..c91cee2 100644 --- a/Runtime/SceneManagement/NetworkSceneManager.cs +++ b/Runtime/SceneManagement/NetworkSceneManager.cs @@ -338,17 +338,17 @@ public class NetworkSceneManager : IDisposable /// private class DefaultSceneManagerHandler : ISceneManagerHandler { - public AsyncOperation LoadSceneAsync(string sceneName, LoadSceneMode loadSceneMode, ISceneManagerHandler.SceneEventAction sceneEventAction) + public AsyncOperation LoadSceneAsync(string sceneName, LoadSceneMode loadSceneMode, SceneEventProgress sceneEventProgress) { var operation = SceneManager.LoadSceneAsync(sceneName, loadSceneMode); - operation.completed += new Action(asyncOp2 => { sceneEventAction.Invoke(); }); + sceneEventProgress.SetAsyncOperation(operation); return operation; } - public AsyncOperation UnloadSceneAsync(Scene scene, ISceneManagerHandler.SceneEventAction sceneEventAction) + public AsyncOperation UnloadSceneAsync(Scene scene, SceneEventProgress sceneEventProgress) { var operation = SceneManager.UnloadSceneAsync(scene); - operation.completed += new Action(asyncOp2 => { sceneEventAction.Invoke(); }); + sceneEventProgress.SetAsyncOperation(operation); return operation; } } @@ -887,12 +887,14 @@ private SceneEventProgress ValidateSceneEvent(string sceneName, bool isUnloading private bool OnSceneEventProgressCompleted(SceneEventProgress sceneEventProgress) { var sceneEventData = BeginSceneEvent(); + var clientsThatCompleted = sceneEventProgress.GetClientsWithStatus(true); + var clientsThatTimedOut = sceneEventProgress.GetClientsWithStatus(false); sceneEventData.SceneEventProgressId = sceneEventProgress.Guid; sceneEventData.SceneHash = sceneEventProgress.SceneHash; sceneEventData.SceneEventType = sceneEventProgress.SceneEventType; - sceneEventData.ClientsCompleted = sceneEventProgress.DoneClients; + sceneEventData.ClientsCompleted = clientsThatCompleted; sceneEventData.LoadSceneMode = sceneEventProgress.LoadSceneMode; - sceneEventData.ClientsTimedOut = sceneEventProgress.ClientsThatStartedSceneEvent.Except(sceneEventProgress.DoneClients).ToList(); + sceneEventData.ClientsTimedOut = clientsThatTimedOut; var message = new SceneEventMessage { @@ -913,8 +915,8 @@ private bool OnSceneEventProgressCompleted(SceneEventProgress sceneEventProgress SceneName = SceneNameFromHash(sceneEventProgress.SceneHash), ClientId = NetworkManager.ServerClientId, LoadSceneMode = sceneEventProgress.LoadSceneMode, - ClientsThatCompleted = sceneEventProgress.DoneClients, - ClientsThatTimedOut = m_NetworkManager.ConnectedClients.Keys.Except(sceneEventProgress.DoneClients).ToList(), + ClientsThatCompleted = clientsThatCompleted, + ClientsThatTimedOut = clientsThatTimedOut, }); if (sceneEventData.SceneEventType == SceneEventType.LoadEventCompleted) @@ -969,18 +971,9 @@ public SceneEventProgressStatus UnloadScene(Scene scene) sceneEventProgress.SceneEventType = SceneEventType.UnloadEventCompleted; ScenesLoaded.Remove(scene.handle); - var sceneEventAction = new ISceneManagerHandler.SceneEventAction() { SceneEventId = sceneEventData.SceneEventId, EventAction = OnSceneUnloaded }; - var sceneUnload = SceneManagerHandler.UnloadSceneAsync(scene, sceneEventAction); - - // If integration testing, IntegrationTestSceneHandler returns null - if (sceneUnload == null) - { - sceneEventProgress.SetSceneLoadOperation(sceneEventAction); - } - else - { - sceneEventProgress.SetSceneLoadOperation(sceneUnload); - } + sceneEventProgress.SceneEventId = sceneEventData.SceneEventId; + sceneEventProgress.OnSceneEventCompleted = OnSceneUnloaded; + var sceneUnload = SceneManagerHandler.UnloadSceneAsync(scene, sceneEventProgress); // Notify local server that a scene is going to be unloaded OnSceneEvent?.Invoke(new SceneEvent() @@ -1024,9 +1017,10 @@ private void OnClientUnloadScene(uint sceneEventId) $"because the client scene handle {sceneHandle} was not found in ScenesLoaded!"); } m_IsSceneEventActive = true; - - var sceneUnload = SceneManagerHandler.UnloadSceneAsync(ScenesLoaded[sceneHandle], - new ISceneManagerHandler.SceneEventAction() { SceneEventId = sceneEventData.SceneEventId, EventAction = OnSceneUnloaded }); + var sceneEventProgress = new SceneEventProgress(m_NetworkManager); + sceneEventProgress.SceneEventId = sceneEventData.SceneEventId; + sceneEventProgress.OnSceneEventCompleted = OnSceneUnloaded; + var sceneUnload = SceneManagerHandler.UnloadSceneAsync(ScenesLoaded[sceneHandle], sceneEventProgress); ScenesLoaded.Remove(sceneHandle); @@ -1070,7 +1064,7 @@ private void OnSceneUnloaded(uint sceneEventId) //Only if we are a host do we want register having loaded for the associated SceneEventProgress if (SceneEventProgressTracking.ContainsKey(sceneEventData.SceneEventProgressId) && m_NetworkManager.IsHost) { - SceneEventProgressTracking[sceneEventData.SceneEventProgressId].AddClientAsDone(NetworkManager.ServerClientId); + SceneEventProgressTracking[sceneEventData.SceneEventProgressId].ClientFinishedSceneEvent(NetworkManager.ServerClientId); } } @@ -1119,8 +1113,10 @@ internal void UnloadAdditivelyLoadedScenes(uint sceneEventId) // Validate the scene as well as ignore the DDOL (which will have a negative buildIndex) if (currentActiveScene.name != keyHandleEntry.Value.name && keyHandleEntry.Value.buildIndex >= 0) { - var sceneUnload = SceneManagerHandler.UnloadSceneAsync(keyHandleEntry.Value, - new ISceneManagerHandler.SceneEventAction() { SceneEventId = sceneEventId, EventAction = EmptySceneUnloadedOperation }); + var sceneEventProgress = new SceneEventProgress(m_NetworkManager); + sceneEventProgress.SceneEventId = sceneEventId; + sceneEventProgress.OnSceneEventCompleted = EmptySceneUnloadedOperation; + var sceneUnload = SceneManagerHandler.UnloadSceneAsync(keyHandleEntry.Value, sceneEventProgress); SceneUnloadEventHandler.RegisterScene(this, keyHandleEntry.Value, LoadSceneMode.Additive, sceneUnload); } } @@ -1180,18 +1176,9 @@ public SceneEventProgressStatus LoadScene(string sceneName, LoadSceneMode loadSc } // Now start loading the scene - var sceneEventAction = new ISceneManagerHandler.SceneEventAction() { SceneEventId = sceneEventId, EventAction = OnSceneLoaded }; - var sceneLoad = SceneManagerHandler.LoadSceneAsync(sceneName, loadSceneMode, sceneEventAction); - // If integration testing, IntegrationTestSceneHandler returns null - if (sceneLoad == null) - { - sceneEventProgress.SetSceneLoadOperation(sceneEventAction); - } - else - { - sceneEventProgress.SetSceneLoadOperation(sceneLoad); - } - + sceneEventProgress.SceneEventId = sceneEventId; + sceneEventProgress.OnSceneEventCompleted = OnSceneLoaded; + var sceneLoad = SceneManagerHandler.LoadSceneAsync(sceneName, loadSceneMode, sceneEventProgress); // Notify the local server that a scene loading event has begun OnSceneEvent?.Invoke(new SceneEvent() { @@ -1355,9 +1342,10 @@ private void OnClientSceneLoadingEvent(uint sceneEventId) SceneUnloadEventHandler.RegisterScene(this, SceneManager.GetActiveScene(), LoadSceneMode.Single); } - - var sceneLoad = SceneManagerHandler.LoadSceneAsync(sceneName, sceneEventData.LoadSceneMode, - new ISceneManagerHandler.SceneEventAction() { SceneEventId = sceneEventId, EventAction = OnSceneLoaded }); + var sceneEventProgress = new SceneEventProgress(m_NetworkManager); + sceneEventProgress.SceneEventId = sceneEventId; + sceneEventProgress.OnSceneEventCompleted = OnSceneLoaded; + var sceneLoad = SceneManagerHandler.LoadSceneAsync(sceneName, sceneEventData.LoadSceneMode, sceneEventProgress); OnSceneEvent?.Invoke(new SceneEvent() { @@ -1453,6 +1441,9 @@ private void OnServerLoadedScene(uint sceneEventId, Scene scene) } } + // Add any despawned when spawned in-scene placed NetworkObjects to the scene event data + sceneEventData.AddDespawnedInSceneNetworkObjects(); + // Set the server's scene's handle so the client can build a look up table sceneEventData.SceneHandle = scene.handle; @@ -1488,7 +1479,7 @@ private void OnServerLoadedScene(uint sceneEventId, Scene scene) //Second, only if we are a host do we want register having loaded for the associated SceneEventProgress if (SceneEventProgressTracking.ContainsKey(sceneEventData.SceneEventProgressId) && m_NetworkManager.IsHost) { - SceneEventProgressTracking[sceneEventData.SceneEventProgressId].AddClientAsDone(NetworkManager.ServerClientId); + SceneEventProgressTracking[sceneEventData.SceneEventProgressId].ClientFinishedSceneEvent(NetworkManager.ServerClientId); } EndSceneEvent(sceneEventId); } @@ -1662,8 +1653,10 @@ private void OnClientBeginSync(uint sceneEventId) if (!shouldPassThrough) { // If not, then load the scene - sceneLoad = SceneManagerHandler.LoadSceneAsync(sceneName, loadSceneMode, - new ISceneManagerHandler.SceneEventAction() { SceneEventId = sceneEventId, EventAction = ClientLoadedSynchronization }); + var sceneEventProgress = new SceneEventProgress(m_NetworkManager); + sceneEventProgress.SceneEventId = sceneEventId; + sceneEventProgress.OnSceneEventCompleted = ClientLoadedSynchronization; + sceneLoad = SceneManagerHandler.LoadSceneAsync(sceneName, loadSceneMode, sceneEventProgress); // Notify local client that a scene load has begun OnSceneEvent?.Invoke(new SceneEvent() @@ -1880,7 +1873,7 @@ private void HandleServerSceneEvent(uint sceneEventId, ulong clientId) if (SceneEventProgressTracking.ContainsKey(sceneEventData.SceneEventProgressId)) { - SceneEventProgressTracking[sceneEventData.SceneEventProgressId].AddClientAsDone(clientId); + SceneEventProgressTracking[sceneEventData.SceneEventProgressId].ClientFinishedSceneEvent(clientId); } EndSceneEvent(sceneEventId); break; @@ -1889,7 +1882,7 @@ private void HandleServerSceneEvent(uint sceneEventId, ulong clientId) { if (SceneEventProgressTracking.ContainsKey(sceneEventData.SceneEventProgressId)) { - SceneEventProgressTracking[sceneEventData.SceneEventProgressId].AddClientAsDone(clientId); + SceneEventProgressTracking[sceneEventData.SceneEventProgressId].ClientFinishedSceneEvent(clientId); } // Notify the local server that the client has finished unloading a scene OnSceneEvent?.Invoke(new SceneEvent() diff --git a/Runtime/SceneManagement/SceneEventData.cs b/Runtime/SceneManagement/SceneEventData.cs index 197f38f..222e437 100644 --- a/Runtime/SceneManagement/SceneEventData.cs +++ b/Runtime/SceneManagement/SceneEventData.cs @@ -243,13 +243,33 @@ internal void AddSpawnedNetworkObjects() m_NetworkObjectsSync.Add(sobj); } } + + // Sort by parents before children + m_NetworkObjectsSync.Sort(SortParentedNetworkObjects); + + // Sort by INetworkPrefabInstanceHandler implementation before the + // NetworkObjects spawned by the implementation m_NetworkObjectsSync.Sort(SortNetworkObjects); + + // This is useful to know what NetworkObjects a client is going to be synchronized with + // as well as the order in which they will be deserialized + if (m_NetworkManager.LogLevel == LogLevel.Developer) + { + var messageBuilder = new System.Text.StringBuilder(0xFFFF); + messageBuilder.Append("[Server-Side Client-Synchronization] NetworkObject serialization order:"); + foreach (var networkObject in m_NetworkObjectsSync) + { + messageBuilder.Append($"{networkObject.name}"); + } + NetworkLog.LogInfo(messageBuilder.ToString()); + } } internal void AddDespawnedInSceneNetworkObjects() { m_DespawnedInSceneObjectsSync.Clear(); - var inSceneNetworkObjects = UnityEngine.Object.FindObjectsOfType().Where((c) => c.NetworkManager == m_NetworkManager); + // Find all active and non-active in-scene placed NetworkObjects + var inSceneNetworkObjects = UnityEngine.Object.FindObjectsOfType(includeInactive: true).Where((c) => c.NetworkManager == m_NetworkManager); foreach (var sobj in inSceneNetworkObjects) { if (sobj.IsSceneObject.HasValue && sobj.IsSceneObject.Value && !sobj.IsSpawned) @@ -323,6 +343,32 @@ private int SortNetworkObjects(NetworkObject first, NetworkObject second) return 0; } + /// + /// Sorts the synchronization order of the NetworkObjects to be serialized + /// by parents before children. + /// + /// + /// This only handles late joining players. Spawning and nesting several children + /// dynamically is still handled by the orphaned child list when deserialized out of + /// hierarchical order (i.e. Spawn parent and child dynamically, parent message is + /// dropped and re-sent but child object is received and processed) + /// + private int SortParentedNetworkObjects(NetworkObject first, NetworkObject second) + { + // If the first has a parent, move the first down + if (first.transform.parent != null) + { + return 1; + } + else // If the second has a parent and the first does not, then move the first up + if (second.transform.parent != null) + { + return -1; + } + return 0; + } + + /// /// Client and Server Side: /// Serializes data based on the SceneEvent type () @@ -398,9 +444,9 @@ internal void WriteSceneSynchronizationData(FastBufferWriter writer) int totalBytes = 0; // Write the number of NetworkObjects we are serializing - BytePacker.WriteValuePacked(writer, m_NetworkObjectsSync.Count()); + BytePacker.WriteValuePacked(writer, m_NetworkObjectsSync.Count); // Serialize all NetworkObjects that are spawned - for (var i = 0; i < m_NetworkObjectsSync.Count(); ++i) + for (var i = 0; i < m_NetworkObjectsSync.Count; ++i) { var noStart = writer.Position; var sceneObject = m_NetworkObjectsSync[i].GetMessageSceneObject(TargetClientId); @@ -411,12 +457,11 @@ internal void WriteSceneSynchronizationData(FastBufferWriter writer) } // Write the number of despawned in-scene placed NetworkObjects - writer.WriteValueSafe(m_DespawnedInSceneObjectsSync.Count()); + writer.WriteValueSafe(m_DespawnedInSceneObjectsSync.Count); // Write the scene handle and GlobalObjectIdHash value - for (var i = 0; i < m_DespawnedInSceneObjectsSync.Count(); ++i) + for (var i = 0; i < m_DespawnedInSceneObjectsSync.Count; ++i) { var noStart = writer.Position; - var sceneObject = m_DespawnedInSceneObjectsSync[i].GetMessageSceneObject(TargetClientId); BytePacker.WriteValuePacked(writer, m_DespawnedInSceneObjectsSync[i].GetSceneOriginHandle()); BytePacker.WriteValuePacked(writer, m_DespawnedInSceneObjectsSync[i].GlobalObjectIdHash); var noStop = writer.Position; @@ -462,6 +507,15 @@ internal void SerializeScenePlacedObjects(FastBufferWriter writer) } } + // Write the number of despawned in-scene placed NetworkObjects + writer.WriteValueSafe(m_DespawnedInSceneObjectsSync.Count); + // Write the scene handle and GlobalObjectIdHash value + for (var i = 0; i < m_DespawnedInSceneObjectsSync.Count; ++i) + { + BytePacker.WriteValuePacked(writer, m_DespawnedInSceneObjectsSync[i].GetSceneOriginHandle()); + BytePacker.WriteValuePacked(writer, m_DespawnedInSceneObjectsSync[i].GlobalObjectIdHash); + } + var tailPosition = writer.Position; // Reposition to our count position to the head before we wrote our object count writer.Seek(headPosition); @@ -579,6 +633,8 @@ internal void DeserializeScenePlacedObjects() sceneObject.Deserialize(InternalBuffer); NetworkObject.AddSceneObject(sceneObject, InternalBuffer, m_NetworkManager); } + // Now deserialize the despawned in-scene placed NetworkObjects list (if any) + DeserializeDespawnedInScenePlacedNetworkObjects(); } finally { @@ -701,6 +757,84 @@ internal void WriteClientSynchronizationResults(FastBufferWriter writer) } } + /// + /// For synchronizing any despawned in-scene placed NetworkObjects that were + /// despawned by the server during synchronization or scene loading + /// + private void DeserializeDespawnedInScenePlacedNetworkObjects() + { + // Process all de-spawned in-scene NetworkObjects for this network session + m_DespawnedInSceneObjects.Clear(); + InternalBuffer.ReadValueSafe(out int despawnedObjectsCount); + var sceneCache = new Dictionary>(); + + for (int i = 0; i < despawnedObjectsCount; i++) + { + // We just need to get the scene + ByteUnpacker.ReadValuePacked(InternalBuffer, out int networkSceneHandle); + ByteUnpacker.ReadValuePacked(InternalBuffer, out uint globalObjectIdHash); + var sceneRelativeNetworkObjects = new Dictionary(); + if (!sceneCache.ContainsKey(networkSceneHandle)) + { + if (m_NetworkManager.SceneManager.ServerSceneHandleToClientSceneHandle.ContainsKey(networkSceneHandle)) + { + var localSceneHandle = m_NetworkManager.SceneManager.ServerSceneHandleToClientSceneHandle[networkSceneHandle]; + if (m_NetworkManager.SceneManager.ScenesLoaded.ContainsKey(localSceneHandle)) + { + var objectRelativeScene = m_NetworkManager.SceneManager.ScenesLoaded[localSceneHandle]; + + // Find all active and non-active in-scene placed NetworkObjects + var inSceneNetworkObjects = UnityEngine.Object.FindObjectsOfType(includeInactive: true).Where((c) => + c.GetSceneOriginHandle() == localSceneHandle && (c.IsSceneObject != false)).ToList(); + + foreach (var inSceneObject in inSceneNetworkObjects) + { + if (!sceneRelativeNetworkObjects.ContainsKey(inSceneObject.GlobalObjectIdHash)) + { + sceneRelativeNetworkObjects.Add(inSceneObject.GlobalObjectIdHash, inSceneObject); + } + } + // Add this to a cache so we don't have to run this potentially multiple times (nothing will spawn or despawn during this time + sceneCache.Add(networkSceneHandle, sceneRelativeNetworkObjects); + } + else + { + UnityEngine.Debug.LogError($"In-Scene NetworkObject GlobalObjectIdHash ({globalObjectIdHash}) cannot find its relative local scene handle {localSceneHandle}!"); + } + } + else + { + UnityEngine.Debug.LogError($"In-Scene NetworkObject GlobalObjectIdHash ({globalObjectIdHash}) cannot find its relative NetworkSceneHandle {networkSceneHandle}!"); + } + } + else // Use the cached NetworkObjects if they exist + { + sceneRelativeNetworkObjects = sceneCache[networkSceneHandle]; + } + + // Now find the in-scene NetworkObject with the current GlobalObjectIdHash we are looking for + if (sceneRelativeNetworkObjects.ContainsKey(globalObjectIdHash)) + { + // Since this is a NetworkObject that was never spawned, we just need to send a notification + // out that it was despawned so users can make adjustments + sceneRelativeNetworkObjects[globalObjectIdHash].InvokeBehaviourNetworkDespawn(); + if (!m_NetworkManager.SceneManager.ScenePlacedObjects.ContainsKey(globalObjectIdHash)) + { + m_NetworkManager.SceneManager.ScenePlacedObjects.Add(globalObjectIdHash, new Dictionary()); + } + + if (!m_NetworkManager.SceneManager.ScenePlacedObjects[globalObjectIdHash].ContainsKey(sceneRelativeNetworkObjects[globalObjectIdHash].GetSceneOriginHandle())) + { + m_NetworkManager.SceneManager.ScenePlacedObjects[globalObjectIdHash].Add(sceneRelativeNetworkObjects[globalObjectIdHash].GetSceneOriginHandle(), sceneRelativeNetworkObjects[globalObjectIdHash]); + } + } + else + { + UnityEngine.Debug.LogError($"In-Scene NetworkObject GlobalObjectIdHash ({globalObjectIdHash}) could not be found!"); + } + } + } + /// /// Client Side: /// During the processing of a server sent Event_Sync, this method will be called for each scene once @@ -734,72 +868,9 @@ internal void SynchronizeSceneNetworkObjects(NetworkManager networkManager) } } - // Process all de-spawned in-scene NetworkObjects for this network session - m_DespawnedInSceneObjects.Clear(); - InternalBuffer.ReadValueSafe(out int despawnedObjectsCount); - var sceneCache = new Dictionary>(); - - for (int i = 0; i < despawnedObjectsCount; i++) - { - // We just need to get the scene - ByteUnpacker.ReadValuePacked(InternalBuffer, out int networkSceneHandle); - ByteUnpacker.ReadValuePacked(InternalBuffer, out uint globalObjectIdHash); - var sceneRelativeNetworkObjects = new Dictionary(); - if (!sceneCache.ContainsKey(networkSceneHandle)) - { - if (m_NetworkManager.SceneManager.ServerSceneHandleToClientSceneHandle.ContainsKey(networkSceneHandle)) - { - var localSceneHandle = m_NetworkManager.SceneManager.ServerSceneHandleToClientSceneHandle[networkSceneHandle]; - if (m_NetworkManager.SceneManager.ScenesLoaded.ContainsKey(localSceneHandle)) - { - var objectRelativeScene = m_NetworkManager.SceneManager.ScenesLoaded[localSceneHandle]; - var inSceneNetworkObjects = UnityEngine.Object.FindObjectsOfType().Where((c) => - c.GetSceneOriginHandle() == localSceneHandle && (c.IsSceneObject != false)).ToList(); + // Now deserialize the despawned in-scene placed NetworkObjects list (if any) + DeserializeDespawnedInScenePlacedNetworkObjects(); - foreach (var inSceneObject in inSceneNetworkObjects) - { - sceneRelativeNetworkObjects.Add(inSceneObject.GlobalObjectIdHash, inSceneObject); - } - // Add this to a cache so we don't have to run this potentially multiple times (nothing will spawn or despawn during this time - sceneCache.Add(networkSceneHandle, sceneRelativeNetworkObjects); - } - else - { - UnityEngine.Debug.LogError($"In-Scene NetworkObject GlobalObjectIdHash ({globalObjectIdHash}) cannot find its relative local scene handle {localSceneHandle}!"); - } - } - else - { - UnityEngine.Debug.LogError($"In-Scene NetworkObject GlobalObjectIdHash ({globalObjectIdHash}) cannot find its relative NetworkSceneHandle {networkSceneHandle}!"); - } - } - else // Use the cached NetworkObjects if they exist - { - sceneRelativeNetworkObjects = sceneCache[networkSceneHandle]; - } - - // Now find the in-scene NetworkObject with the current GlobalObjectIdHash we are looking for - if (sceneRelativeNetworkObjects.ContainsKey(globalObjectIdHash)) - { - // Since this is a NetworkObject that was never spawned, we just need to send a notification - // out that it was despawned so users can make adjustments - sceneRelativeNetworkObjects[globalObjectIdHash].InvokeBehaviourNetworkDespawn(); - if (!m_NetworkManager.SceneManager.ScenePlacedObjects.ContainsKey(globalObjectIdHash)) - { - m_NetworkManager.SceneManager.ScenePlacedObjects.Add(globalObjectIdHash, new Dictionary()); - } - - if (!m_NetworkManager.SceneManager.ScenePlacedObjects[globalObjectIdHash].ContainsKey(sceneRelativeNetworkObjects[globalObjectIdHash].GetSceneOriginHandle())) - { - m_NetworkManager.SceneManager.ScenePlacedObjects[globalObjectIdHash].Add(sceneRelativeNetworkObjects[globalObjectIdHash].GetSceneOriginHandle(), sceneRelativeNetworkObjects[globalObjectIdHash]); - } - - } - else - { - UnityEngine.Debug.LogError($"In-Scene NetworkObject GlobalObjectIdHash ({globalObjectIdHash}) could not be found!"); - } - } } finally { diff --git a/Runtime/SceneManagement/SceneEventProgress.cs b/Runtime/SceneManagement/SceneEventProgress.cs index 838bbfc..bce5389 100644 --- a/Runtime/SceneManagement/SceneEventProgress.cs +++ b/Runtime/SceneManagement/SceneEventProgress.cs @@ -58,12 +58,13 @@ internal class SceneEventProgress /// /// List of clientIds of those clients that is done loading the scene. /// - internal List DoneClients { get; } = new List(); + internal Dictionary ClientsProcessingSceneEvent { get; } = new Dictionary(); + internal List ClientsThatDisconnected = new List(); /// - /// The local time when the scene event was "roughly started" + /// This is when the current scene event will have timed out /// - internal float TimeAtInitiation { get; } + internal float WhenSceneEventHasTimedOut; /// /// Delegate type for when the switch scene progress is completed. Either by all clients done loading the scene or by time out. @@ -75,17 +76,15 @@ internal class SceneEventProgress /// internal OnCompletedDelegate OnComplete; - /// - /// Is this scene switch progresses completed, all clients are done loading the scene or a timeout has occurred. - /// - internal bool IsCompleted { get; private set; } - - internal bool TimedOut { get; private set; } + internal Action OnSceneEventCompleted; /// - /// If all clients are done loading the scene, at the moment of completed. + /// This will make sure that we only have timed out if we never completed /// - internal bool AreAllClientsDoneLoading { get; private set; } + internal bool HasTimedOut() + { + return WhenSceneEventHasTimedOut <= Time.realtimeSinceStartup; + } /// /// The hash value generated from the full scene path @@ -93,9 +92,10 @@ internal class SceneEventProgress internal uint SceneHash { get; set; } internal Guid Guid { get; } = Guid.NewGuid(); + internal uint SceneEventId; private Coroutine m_TimeOutCoroutine; - private AsyncOperation m_SceneLoadOperation; + private AsyncOperation m_AsyncOperation; private NetworkManager m_NetworkManager { get; } @@ -105,21 +105,62 @@ internal class SceneEventProgress internal LoadSceneMode LoadSceneMode; - internal List ClientsThatStartedSceneEvent; + internal List GetClientsWithStatus(bool completedSceneEvent) + { + var clients = new List(); + foreach (var clientStatus in ClientsProcessingSceneEvent) + { + if (clientStatus.Value == completedSceneEvent) + { + clients.Add(clientStatus.Key); + } + } + + // If we are getting the list of clients that have not completed the + // scene event, then add any clients that disconnected during this + // scene event. + if (!completedSceneEvent) + { + clients.AddRange(ClientsThatDisconnected); + } + return clients; + } internal SceneEventProgress(NetworkManager networkManager, SceneEventProgressStatus status = SceneEventProgressStatus.Started) { if (status == SceneEventProgressStatus.Started) { - // Track the clients that were connected when we started this event - ClientsThatStartedSceneEvent = new List(networkManager.ConnectedClientsIds); m_NetworkManager = networkManager; - m_TimeOutCoroutine = m_NetworkManager.StartCoroutine(TimeOutSceneEventProgress()); - TimeAtInitiation = Time.realtimeSinceStartup; + + if (networkManager.IsServer) + { + m_NetworkManager.OnClientDisconnectCallback += OnClientDisconnectCallback; + // Track the clients that were connected when we started this event + foreach (var connectedClientId in networkManager.ConnectedClientsIds) + { + ClientsProcessingSceneEvent.Add(connectedClientId, false); + } + + WhenSceneEventHasTimedOut = Time.realtimeSinceStartup + networkManager.NetworkConfig.LoadSceneTimeOut; + m_TimeOutCoroutine = m_NetworkManager.StartCoroutine(TimeOutSceneEventProgress()); + } } Status = status; } + /// + /// Remove the client from the clients processing the current scene event + /// Add this client to the clients that disconnected list + /// + private void OnClientDisconnectCallback(ulong clientId) + { + if (ClientsProcessingSceneEvent.ContainsKey(clientId)) + { + ClientsThatDisconnected.Add(clientId); + ClientsProcessingSceneEvent.Remove(clientId); + } + } + /// /// Coroutine that checks to see if the scene event is complete every network tick period. /// This will handle completing the scene event when one or more client(s) disconnect(s) @@ -129,79 +170,104 @@ internal SceneEventProgress(NetworkManager networkManager, SceneEventProgressSta internal IEnumerator TimeOutSceneEventProgress() { var waitForNetworkTick = new WaitForSeconds(1.0f / m_NetworkManager.NetworkConfig.TickRate); - while (!TimedOut && !IsCompleted) + while (!HasTimedOut()) { yield return waitForNetworkTick; - CheckCompletion(); - if (!IsCompleted) - { - TimedOut = TimeAtInitiation - Time.realtimeSinceStartup >= m_NetworkManager.NetworkConfig.LoadSceneTimeOut; - } + TryFinishingSceneEventProgress(); } } - internal void AddClientAsDone(ulong clientId) + /// + /// Sets the client's scene event progress to finished/true + /// + internal void ClientFinishedSceneEvent(ulong clientId) { - DoneClients.Add(clientId); - CheckCompletion(); + if (ClientsProcessingSceneEvent.ContainsKey(clientId)) + { + ClientsProcessingSceneEvent[clientId] = true; + TryFinishingSceneEventProgress(); + } } - internal void RemoveClientAsDone(ulong clientId) + /// + /// Determines if the scene event has finished for both + /// client(s) and server. + /// + /// + /// The server checks if all known clients processing this scene event + /// have finished and then it returns its local AsyncOperation status. + /// Clients finish when their AsyncOperation finishes. + /// + private bool HasFinished() { - DoneClients.Remove(clientId); - CheckCompletion(); - } + // If the network session is terminated/terminating then finish tracking + // this scene event + if (!IsNetworkSessionActive()) + { + return true; + } - internal void SetSceneLoadOperation(AsyncOperation sceneLoadOperation) - { - m_SceneLoadOperation = sceneLoadOperation; - m_SceneLoadOperation.completed += operation => CheckCompletion(); + // Clients skip over this + foreach (var clientStatus in ClientsProcessingSceneEvent) + { + if (!clientStatus.Value) + { + return false; + } + } + + // Return the local scene event's AsyncOperation status + return m_AsyncOperation.isDone; } /// - /// Called only on the server-side during integration test (NetcodeIntegrationTest specific) - /// scene loading and unloading. - /// - /// Note: During integration testing we must queue all scene loading and unloading requests for - /// both the server and all clients so they can be processed in a FIFO/linear fashion to avoid - /// conflicts when the and - /// events are triggered. The Completed action simulates the event. - /// (See: Unity.Netcode.TestHelpers.Runtime.IntegrationTestSceneHandler) + /// Sets the AsyncOperation for the scene load/unload event /// - internal void SetSceneLoadOperation(ISceneManagerHandler.SceneEventAction sceneEventAction) + internal void SetAsyncOperation(AsyncOperation asyncOperation) { - sceneEventAction.Completed = SetComplete; + m_AsyncOperation = asyncOperation; + m_AsyncOperation.completed += new Action(asyncOp2 => + { + // Don't invoke the callback if the network session is disconnected + // during a SceneEventProgress + if (IsNetworkSessionActive()) + { + OnSceneEventCompleted?.Invoke(SceneEventId); + } + + // Go ahead and try finishing even if the network session is terminated/terminating + // as we might need to stop the coroutine + TryFinishingSceneEventProgress(); + }); } - /// - /// Finalizes the SceneEventProgress - /// - internal void SetComplete() - { - IsCompleted = true; - AreAllClientsDoneLoading = true; - // If OnComplete is not registered or it is and returns true then remove this from the progress tracking - if (OnComplete == null || (OnComplete != null && OnComplete.Invoke(this))) - { - m_NetworkManager.SceneManager.SceneEventProgressTracking.Remove(Guid); - } - m_NetworkManager.StopCoroutine(m_TimeOutCoroutine); + internal bool IsNetworkSessionActive() + { + return m_NetworkManager != null && m_NetworkManager.IsListening && !m_NetworkManager.ShutdownInProgress; } - internal void CheckCompletion() + /// + /// Will try to finish the current scene event in progress as long as + /// all conditions are met. + /// + internal void TryFinishingSceneEventProgress() { - try + if (HasFinished() || HasTimedOut()) { - if ((!IsCompleted && DoneClients.Count == m_NetworkManager.ConnectedClientsList.Count && (m_SceneLoadOperation == null || m_SceneLoadOperation.isDone)) || (!IsCompleted && TimedOut)) + // Don't attempt to finalize this scene event if we are no longer listening or a shutdown is in progress + if (IsNetworkSessionActive()) { - SetComplete(); + OnComplete?.Invoke(this); + m_NetworkManager.SceneManager.SceneEventProgressTracking.Remove(Guid); + m_NetworkManager.OnClientDisconnectCallback -= OnClientDisconnectCallback; + } + + if (m_TimeOutCoroutine != null) + { + m_NetworkManager.StopCoroutine(m_TimeOutCoroutine); } - } - catch (Exception ex) - { - Debug.LogException(ex); } } } diff --git a/Runtime/Serialization/BufferSerializerReader.cs b/Runtime/Serialization/BufferSerializerReader.cs index 5d43fca..271f357 100644 --- a/Runtime/Serialization/BufferSerializerReader.cs +++ b/Runtime/Serialization/BufferSerializerReader.cs @@ -34,7 +34,7 @@ public FastBufferWriter GetFastBufferWriter() public void SerializeValue(ref T[] value, FastBufferWriter.ForEnums unused = default) where T : unmanaged, Enum => m_Reader.ReadValueSafe(out value); public void SerializeValue(ref T value, FastBufferWriter.ForStructs unused = default) where T : unmanaged, INetworkSerializeByMemcpy => m_Reader.ReadValueSafe(out value); public void SerializeValue(ref T[] value, FastBufferWriter.ForStructs unused = default) where T : unmanaged, INetworkSerializeByMemcpy => m_Reader.ReadValueSafe(out value); - public void SerializeValue(ref T value, FastBufferWriter.ForNetworkSerializable unused = default) where T : INetworkSerializable, new() => m_Reader.ReadValue(out value); + public void SerializeValue(ref T value, FastBufferWriter.ForNetworkSerializable unused = default) where T : INetworkSerializable, new() => m_Reader.ReadNetworkSerializableInPlace(ref value); public void SerializeValue(ref T[] value, FastBufferWriter.ForNetworkSerializable unused = default) where T : INetworkSerializable, new() => m_Reader.ReadValue(out value); public void SerializeValue(ref T value, FastBufferWriter.ForFixedStrings unused = default) diff --git a/Runtime/Serialization/FastBufferReader.cs b/Runtime/Serialization/FastBufferReader.cs index 90ecd4a..e052c95 100644 --- a/Runtime/Serialization/FastBufferReader.cs +++ b/Runtime/Serialization/FastBufferReader.cs @@ -461,6 +461,19 @@ public unsafe byte[] ToArray() } } + /// + /// Read an INetworkSerializable in-place, without constructing a new one + /// Note that this will NOT check for null before calling NetworkSerialize + /// + /// + /// INetworkSerializable instance + /// + public void ReadNetworkSerializableInPlace(ref T value) where T : INetworkSerializable + { + var bufferSerializer = new BufferSerializer(new BufferSerializerReader(this)); + value.NetworkSerialize(bufferSerializer); + } + /// /// Reads a string /// NOTE: ALLOCATES @@ -1310,5 +1323,24 @@ public unsafe void ReadValueSafe(out T value, FastBufferWriter.ForFixedString value.Length = length; ReadBytesSafe(value.GetUnsafePtr(), length); } + + + /// + /// Read a FixedString value. + /// + /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking + /// for multiple reads at once by calling TryBeginRead. + /// + /// the value to read + /// An unused parameter used for enabling overload resolution based on generic constraints + /// The type being serialized + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void ReadValueSafeInPlace(ref T value, FastBufferWriter.ForFixedStrings unused = default) + where T : unmanaged, INativeList, IUTF8Bytes + { + ReadUnmanagedSafe(out int length); + value.Length = length; + ReadBytesSafe(value.GetUnsafePtr(), length); + } } } diff --git a/Runtime/Serialization/NetworkObjectReference.cs b/Runtime/Serialization/NetworkObjectReference.cs index 11a2ab2..2634035 100644 --- a/Runtime/Serialization/NetworkObjectReference.cs +++ b/Runtime/Serialization/NetworkObjectReference.cs @@ -139,7 +139,16 @@ public void NetworkSerialize(BufferSerializer serializer) where T : IReade /// /// The to convert from. /// This returns the that the is attached to and is referenced by the passed in as a parameter - public static implicit operator GameObject(NetworkObjectReference networkObjectRef) => Resolve(networkObjectRef).gameObject; + public static implicit operator GameObject(NetworkObjectReference networkObjectRef) + { + var networkObject = Resolve(networkObjectRef); + if (networkObject != null) + { + return networkObject.gameObject; + } + + return null; + } /// /// Implicitly convert to . diff --git a/Runtime/Spawning/NetworkPrefabHandler.cs b/Runtime/Spawning/NetworkPrefabHandler.cs index ec8ca8d..a123dc2 100644 --- a/Runtime/Spawning/NetworkPrefabHandler.cs +++ b/Runtime/Spawning/NetworkPrefabHandler.cs @@ -119,8 +119,7 @@ public void RegisterHostGlobalObjectIdHashValues(GameObject sourceNetworkPrefab, // Now we register all foreach (var gameObject in networkPrefabOverrides) { - var targetNetworkObject = gameObject.GetComponent(); - if (targetNetworkObject != null) + if (gameObject.TryGetComponent(out var targetNetworkObject)) { if (!m_PrefabInstanceToPrefabAsset.ContainsKey(targetNetworkObject.GlobalObjectIdHash)) { diff --git a/Runtime/Spawning/NetworkSpawnManager.cs b/Runtime/Spawning/NetworkSpawnManager.cs index 3300af9..45c190b 100644 --- a/Runtime/Spawning/NetworkSpawnManager.cs +++ b/Runtime/Spawning/NetworkSpawnManager.cs @@ -213,11 +213,11 @@ internal void RemoveOwnership(NetworkObject networkObject) return; } + networkObject.OwnerClientId = NetworkManager.ServerClientId; + // Server removes the entry and takes over ownership before notifying UpdateOwnershipTable(networkObject, NetworkManager.ServerClientId, true); - networkObject.OwnerClientId = NetworkManager.ServerClientId; - var message = new ChangeOwnershipMessage { NetworkObjectId = networkObject.NetworkObjectId, @@ -278,11 +278,14 @@ internal void ChangeOwnership(NetworkObject networkObject, ulong clientId) NetworkObjectId = networkObject.NetworkObjectId, OwnerClientId = networkObject.OwnerClientId }; - var size = NetworkManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, NetworkManager.ConnectedClientsIds); foreach (var client in NetworkManager.ConnectedClients) { - NetworkManager.NetworkMetrics.TrackOwnershipChangeSent(client.Key, networkObject, size); + if (networkObject.IsNetworkVisibleTo(client.Value.ClientId)) + { + var size = NetworkManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, client.Value.ClientId); + NetworkManager.NetworkMetrics.TrackOwnershipChangeSent(client.Key, networkObject, size); + } } } @@ -314,48 +317,33 @@ internal bool HasPrefab(NetworkObject.SceneObject sceneObject) } /// - /// Should only run on the client + /// Creates a local NetowrkObject to be spawned. /// - internal NetworkObject CreateLocalNetworkObject(bool isSceneObject, uint globalObjectIdHash, ulong ownerClientId, ulong? parentNetworkId, int? networkSceneHandle, Vector3? position, Quaternion? rotation, bool isReparented = false) + /// + /// For most cases this is client-side only, with the exception of when the server + /// is spawning a player. + /// + internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneObject) { - NetworkObject parentNetworkObject = null; - - if (parentNetworkId != null && !isReparented) - { - if (SpawnedObjects.TryGetValue(parentNetworkId.Value, out NetworkObject networkObject)) - { - parentNetworkObject = networkObject; - } - else - { - if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) - { - NetworkLog.LogWarning("Cannot find parent. Parent objects always have to be spawned and replicated BEFORE the child"); - } - } - } - - if (!NetworkManager.NetworkConfig.EnableSceneManagement || !isSceneObject) + NetworkObject networkObject = null; + var globalObjectIdHash = sceneObject.Header.Hash; + var position = sceneObject.Header.HasTransform ? sceneObject.Transform.Position : default; + var rotation = sceneObject.Header.HasTransform ? sceneObject.Transform.Rotation : default; + var scale = sceneObject.Header.HasTransform ? sceneObject.Transform.Scale : default; + var parentNetworkId = sceneObject.Header.HasParent ? sceneObject.ParentObjectId : default; + var worldPositionStays = sceneObject.Header.HasParent ? sceneObject.WorldPositionStays : true; + var isSpawnedByPrefabHandler = false; + + // If scene management is disabled or the NetworkObject was dynamically spawned + if (!NetworkManager.NetworkConfig.EnableSceneManagement || !sceneObject.Header.IsSceneObject) { // If the prefab hash has a registered INetworkPrefabInstanceHandler derived class if (NetworkManager.PrefabHandler.ContainsHandler(globalObjectIdHash)) { // Let the handler spawn the NetworkObject - var networkObject = NetworkManager.PrefabHandler.HandleNetworkPrefabSpawn(globalObjectIdHash, ownerClientId, position.GetValueOrDefault(Vector3.zero), rotation.GetValueOrDefault(Quaternion.identity)); - + networkObject = NetworkManager.PrefabHandler.HandleNetworkPrefabSpawn(globalObjectIdHash, sceneObject.Header.OwnerClientId, position, rotation); networkObject.NetworkManagerOwner = NetworkManager; - - if (parentNetworkObject != null) - { - networkObject.transform.SetParent(parentNetworkObject.transform, true); - } - - if (NetworkSceneManager.IsSpawnedObjectsPendingInDontDestroyOnLoad) - { - UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject); - } - - return networkObject; + isSpawnedByPrefabHandler = true; } else { @@ -383,49 +371,97 @@ internal NetworkObject CreateLocalNetworkObject(bool isSceneObject, uint globalO { NetworkLog.LogError($"Failed to create object locally. [{nameof(globalObjectIdHash)}={globalObjectIdHash}]. {nameof(NetworkPrefab)} could not be found. Is the prefab registered with {nameof(NetworkManager)}?"); } - - return null; } - - // Otherwise, instantiate an instance of the NetworkPrefab linked to the prefabHash - var networkObject = ((position == null && rotation == null) ? UnityEngine.Object.Instantiate(networkPrefabReference) : UnityEngine.Object.Instantiate(networkPrefabReference, position.GetValueOrDefault(Vector3.zero), rotation.GetValueOrDefault(Quaternion.identity))).GetComponent(); - - networkObject.NetworkManagerOwner = NetworkManager; - - if (parentNetworkObject != null) + else { - networkObject.transform.SetParent(parentNetworkObject.transform, true); + // Create prefab instance + networkObject = UnityEngine.Object.Instantiate(networkPrefabReference).GetComponent(); + networkObject.NetworkManagerOwner = NetworkManager; } + } + } + else // Get the in-scene placed NetworkObject + { + networkObject = NetworkManager.SceneManager.GetSceneRelativeInSceneNetworkObject(globalObjectIdHash, sceneObject.NetworkSceneHandle); - if (NetworkSceneManager.IsSpawnedObjectsPendingInDontDestroyOnLoad) + if (networkObject == null) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Error) { - UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject); + NetworkLog.LogError($"{nameof(NetworkPrefab)} hash was not found! In-Scene placed {nameof(NetworkObject)} soft synchronization failure for Hash: {globalObjectIdHash}!"); } + } - return networkObject; + // Since this NetworkObject is an in-scene placed NetworkObject, if it is disabled then enable it so + // NetworkBehaviours will have their OnNetworkSpawn method invoked + if (networkObject != null && !networkObject.gameObject.activeInHierarchy) + { + networkObject.gameObject.SetActive(true); } } - else + + if (networkObject != null) { - var networkObject = NetworkManager.SceneManager.GetSceneRelativeInSceneNetworkObject(globalObjectIdHash, networkSceneHandle); + // SPECIAL CASE: + // This is a special case scenario where a late joining client has joined and loaded one or + // more scenes that contain nested in-scene placed NetworkObject children yet the server's + // synchronization information does not indicate the NetworkObject in question has a parent. + // Under this scenario, we want to remove the parent before spawning and setting the transform values. + if (sceneObject.Header.IsSceneObject && !sceneObject.Header.HasParent && networkObject.transform.parent != null) + { + // if the in-scene placed NetworkObject has a parent NetworkObject but the synchronization information does not + // include parenting, then we need to force the removal of that parent + if (networkObject.transform.parent.GetComponent() != null) + { + // remove the parent + networkObject.ApplyNetworkParenting(true, true); + } + } - if (networkObject == null) + // Set the transform unless we were spawned by a prefab handler + // Note: prefab handlers are provided the position and rotation + // but it is up to the user to set those values + if (sceneObject.Header.HasTransform && !isSpawnedByPrefabHandler) { - if (NetworkLog.CurrentLogLevel <= LogLevel.Error) + if (worldPositionStays) { - NetworkLog.LogError($"{nameof(NetworkPrefab)} hash was not found! In-Scene placed {nameof(NetworkObject)} soft synchronization failure for Hash: {globalObjectIdHash}!"); + networkObject.transform.position = position; + networkObject.transform.rotation = rotation; + } + else + { + networkObject.transform.localPosition = position; + networkObject.transform.localRotation = rotation; } - return null; + // SPECIAL CASE: + // Since players are created uniquely we don't apply scale because + // the ConnectionApprovalResponse does not currently provide the + // ability to specify scale. So, we just use the default scale of + // the network prefab used to represent the player. + // Note: not doing this would set the player's scale to zero since + // that is the default value of Vector3. + if (!sceneObject.Header.IsPlayerObject) + { + networkObject.transform.localScale = scale; + } } - if (parentNetworkObject != null) + if (sceneObject.Header.HasParent) { - networkObject.transform.SetParent(parentNetworkObject.transform, true); + // Go ahead and set network parenting properties + networkObject.SetNetworkParenting(parentNetworkId, worldPositionStays); } - return networkObject; + + // Dynamically spawned NetworkObjects that occur during a LoadSceneMode.Single load scene event are migrated into the DDOL + // until the scene is loaded. They are then migrated back into the newly loaded and currently active scene. + if (!sceneObject.Header.IsSceneObject && NetworkSceneManager.IsSpawnedObjectsPendingInDontDestroyOnLoad) + { + UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject); + } } + return networkObject; } // Ran on both server and client @@ -545,7 +581,6 @@ private void SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong } } - networkObject.SetCachedParent(networkObject.transform.parent); networkObject.ApplyNetworkParenting(); NetworkObject.CheckOrphanChildren(); @@ -575,8 +610,6 @@ internal void SendSpawnCallForObject(ulong clientId, NetworkObject networkObject }; var size = NetworkManager.SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, clientId); NetworkManager.NetworkMetrics.TrackObjectSpawnSent(clientId, networkObject, size); - - networkObject.MarkVariablesDirty(true); } internal ulong? GetSpawnParentId(NetworkObject networkObject) @@ -745,15 +778,26 @@ internal void OnDespawnObject(NetworkObject networkObject, bool destroyGameObjec } // If we are shutting down the NetworkManager, then ignore resetting the parent - if (!NetworkManager.ShutdownInProgress) + // and only attempt to remove the child's parent on the server-side + if (!NetworkManager.ShutdownInProgress && NetworkManager.IsServer) { // Move child NetworkObjects to the root when parent NetworkObject is destroyed foreach (var spawnedNetObj in SpawnedObjectsList) { - var (isReparented, latestParent) = spawnedNetObj.GetNetworkParenting(); - if (isReparented && latestParent == networkObject.NetworkObjectId) + var latestParent = spawnedNetObj.GetNetworkParenting(); + if (latestParent.HasValue && latestParent.Value == networkObject.NetworkObjectId) { - spawnedNetObj.gameObject.transform.parent = null; + // Try to remove the parent using the cached WorldPositioNStays value + // Note: WorldPositionStays will still default to true if this was an + // in-scene placed NetworkObject and parenting was predefined in the + // scene via the editor. + if (!spawnedNetObj.TryRemoveParentCachedWorldPositionStays()) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogError($"{nameof(NetworkObject)} #{spawnedNetObj.NetworkObjectId} could not be moved to the root when its parent {nameof(NetworkObject)} #{networkObject.NetworkObjectId} was being destroyed"); + } + } if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) { diff --git a/Runtime/Timing/NetworkTime.cs b/Runtime/Timing/NetworkTime.cs index 9f86de9..7af210b 100644 --- a/Runtime/Timing/NetworkTime.cs +++ b/Runtime/Timing/NetworkTime.cs @@ -25,7 +25,7 @@ public struct NetworkTime public double TickOffset => m_CachedTickOffset; /// - /// Gets the current time. This is a non fixed time value and similar to + /// Gets the current time. This is a non fixed time value and similar to . /// public double Time => m_TimeSec; @@ -35,13 +35,13 @@ public struct NetworkTime public float TimeAsFloat => (float)m_TimeSec; /// - /// Gets he current fixed network time. This is the time value of the last network tick. Similar to + /// Gets he current fixed network time. This is the time value of the last network tick. Similar to . /// public double FixedTime => m_CachedTick * m_TickInterval; /// /// Gets the fixed delta time. This value is based on the and stays constant. - /// Similar to There is no equivalent to + /// Similar to There is no equivalent to . /// public float FixedDeltaTime => (float)m_TickInterval; diff --git a/Runtime/Timing/NetworkTimeSystem.cs b/Runtime/Timing/NetworkTimeSystem.cs index e9de73a..f083133 100644 --- a/Runtime/Timing/NetworkTimeSystem.cs +++ b/Runtime/Timing/NetworkTimeSystem.cs @@ -76,7 +76,7 @@ public static NetworkTimeSystem ServerTimeSystem() } /// - /// Advances the time system by a certain amount of time. Should be called once per frame with Time.deltaTime or similar. + /// Advances the time system by a certain amount of time. Should be called once per frame with Time.unscaledDeltaTime or similar. /// /// The amount of time to advance. The delta time which passed since Advance was last called. /// diff --git a/Runtime/Transports/UNET/UNetTransport.cs b/Runtime/Transports/UNET/UNetTransport.cs index 992a791..7afb7b3 100644 --- a/Runtime/Transports/UNET/UNetTransport.cs +++ b/Runtime/Transports/UNET/UNetTransport.cs @@ -7,6 +7,7 @@ namespace Unity.Netcode.Transports.UNET { + [AddComponentMenu("Netcode/UNet Transport")] public class UNetTransport : NetworkTransport { public enum SendMode diff --git a/Runtime/Transports/UTP/BatchedReceiveQueue.cs b/Runtime/Transports/UTP/BatchedReceiveQueue.cs index c6a3f55..e0cdeeb 100644 --- a/Runtime/Transports/UTP/BatchedReceiveQueue.cs +++ b/Runtime/Transports/UTP/BatchedReceiveQueue.cs @@ -1,5 +1,9 @@ using System; using Unity.Networking.Transport; +#if UTP_TRANSPORT_2_0_ABOVE +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +#endif namespace Unity.Netcode.Transports.UTP { @@ -25,7 +29,11 @@ public BatchedReceiveQueue(DataStreamReader reader) { fixed (byte* dataPtr = m_Data) { +#if UTP_TRANSPORT_2_0_ABOVE + reader.ReadBytesUnsafe(dataPtr, reader.Length); +#else reader.ReadBytes(dataPtr, reader.Length); +#endif } } @@ -62,7 +70,11 @@ public void PushReader(DataStreamReader reader) { fixed (byte* dataPtr = m_Data) { +#if UTP_TRANSPORT_2_0_ABOVE + reader.ReadBytesUnsafe(dataPtr + m_Offset + m_Length, reader.Length); +#else reader.ReadBytes(dataPtr + m_Offset + m_Length, reader.Length); +#endif } } diff --git a/Runtime/Transports/UTP/BatchedSendQueue.cs b/Runtime/Transports/UTP/BatchedSendQueue.cs index e217e37..c305884 100644 --- a/Runtime/Transports/UTP/BatchedSendQueue.cs +++ b/Runtime/Transports/UTP/BatchedSendQueue.cs @@ -8,22 +8,30 @@ namespace Unity.Netcode.Transports.UTP /// Queue for batched messages meant to be sent through UTP. /// /// Messages should be pushed on the queue with . To send batched - /// messages, call with the obtained from - /// . This will fill the writer with as many messages as - /// possible. If the send is successful, call to remove the data from the - /// queue. + /// messages, call or + /// with the obtained from . + /// This will fill the writer with as many messages/bytes as possible. If the send is + /// successful, call to remove the data from the queue. /// /// This is meant as a companion to , which should be used to /// read messages sent with this queue. /// internal struct BatchedSendQueue : IDisposable { - private NativeArray m_Data; + // Note that we're using NativeList basically like a growable NativeArray, where the length + // of the list is the capacity of our array. (We can't use the capacity of the list as our + // queue capacity because NativeList may elect to set it higher than what we'd set it to + // with SetCapacity, which breaks the logic of our code.) + private NativeList m_Data; private NativeArray m_HeadTailIndices; + private int m_MaximumCapacity; + private int m_MinimumCapacity; /// Overhead that is added to each message in the queue. public const int PerMessageOverhead = sizeof(int); + internal const int MinimumMinimumCapacity = 4096; + // Indices into m_HeadTailIndicies. private const int k_HeadInternalIndex = 0; private const int k_TailInternalIndex = 1; @@ -43,18 +51,33 @@ private int TailIndex } public int Length => TailIndex - HeadIndex; - + public int Capacity => m_Data.Length; public bool IsEmpty => HeadIndex == TailIndex; - public bool IsCreated => m_Data.IsCreated; /// Construct a new empty send queue. /// Maximum capacity of the send queue. public BatchedSendQueue(int capacity) { - m_Data = new NativeArray(capacity, Allocator.Persistent); + // Make sure the maximum capacity will be even. + m_MaximumCapacity = capacity + (capacity & 1); + + // We pick the minimum capacity such that if we keep doubling it, we'll eventually hit + // the maximum capacity exactly. The alternative would be to use capacities that are + // powers of 2, but this can lead to over-allocating quite a bit of memory (especially + // since we expect maximum capacities to be in the megabytes range). The approach taken + // here avoids this issue, at the cost of not having allocations of nice round sizes. + m_MinimumCapacity = m_MaximumCapacity; + while (m_MinimumCapacity / 2 >= MinimumMinimumCapacity) + { + m_MinimumCapacity /= 2; + } + + m_Data = new NativeList(m_MinimumCapacity, Allocator.Persistent); m_HeadTailIndices = new NativeArray(2, Allocator.Persistent); + m_Data.ResizeUninitialized(m_MinimumCapacity); + HeadIndex = 0; TailIndex = 0; } @@ -68,18 +91,28 @@ public void Dispose() } } + /// Write a raw buffer to a DataStreamWriter. + private unsafe void WriteBytes(ref DataStreamWriter writer, byte* data, int length) + { +#if UTP_TRANSPORT_2_0_ABOVE + writer.WriteBytesUnsafe(data, length); +#else + writer.WriteBytes(data, length); +#endif + } + /// Append data at the tail of the queue. No safety checks. private void AppendDataAtTail(ArraySegment data) { unsafe { - var writer = new DataStreamWriter((byte*)m_Data.GetUnsafePtr() + TailIndex, m_Data.Length - TailIndex); + var writer = new DataStreamWriter((byte*)m_Data.GetUnsafePtr() + TailIndex, Capacity - TailIndex); writer.WriteInt(data.Count); fixed (byte* dataPtr = data.Array) { - writer.WriteBytes(dataPtr + data.Offset, data.Count); + WriteBytes(ref writer, dataPtr + data.Offset, data.Count); } } @@ -100,16 +133,16 @@ public bool PushMessage(ArraySegment message) } // Check if there's enough room after the current tail index. - if (m_Data.Length - TailIndex >= sizeof(int) + message.Count) + if (Capacity - TailIndex >= sizeof(int) + message.Count) { AppendDataAtTail(message); return true; } - // Check if there would be enough room if we moved data at the beginning of m_Data. - if (m_Data.Length - TailIndex + HeadIndex >= sizeof(int) + message.Count) + // Move the data at the beginning of of m_Data. Either it will leave enough space for + // the message, or we'll grow m_Data and will want the data at the beginning anyway. + if (HeadIndex > 0 && Length > 0) { - // Move the data back at the beginning of m_Data. unsafe { UnsafeUtility.MemMove(m_Data.GetUnsafePtr(), (byte*)m_Data.GetUnsafePtr() + HeadIndex, Length); @@ -117,12 +150,38 @@ public bool PushMessage(ArraySegment message) TailIndex = Length; HeadIndex = 0; + } + // If there's enough space left at the end for the message, now is a good time to trim + // the capacity of m_Data if it got very large. We define "very large" here as having + // more than 75% of m_Data unused after adding the new message. + if (Capacity - TailIndex >= sizeof(int) + message.Count) + { AppendDataAtTail(message); + + while (TailIndex < Capacity / 4 && Capacity > m_MinimumCapacity) + { + m_Data.ResizeUninitialized(Capacity / 2); + } + return true; } - return false; + // If we get here we need to grow m_Data until the data fits (or it's too large). + while (Capacity - TailIndex < sizeof(int) + message.Count) + { + // Can't grow m_Data anymore. Message simply won't fit. + if (Capacity * 2 > m_MaximumCapacity) + { + return false; + } + + m_Data.ResizeUninitialized(Capacity * 2); + } + + // If we get here we know there's now enough room for the message. + AppendDataAtTail(message); + return true; } /// @@ -149,12 +208,12 @@ public int FillWriterWithMessages(ref DataStreamWriter writer) unsafe { - var reader = new DataStreamReader((byte*)m_Data.GetUnsafePtr() + HeadIndex, Length); + var reader = new DataStreamReader(m_Data.AsArray()); var writerAvailable = writer.Capacity; - var readerOffset = 0; + var readerOffset = HeadIndex; - while (readerOffset < Length) + while (readerOffset < TailIndex) { reader.SeekSet(readerOffset); var messageLength = reader.ReadInt(); @@ -168,7 +227,7 @@ public int FillWriterWithMessages(ref DataStreamWriter writer) writer.WriteInt(messageLength); var messageOffset = HeadIndex + reader.GetBytesRead(); - writer.WriteBytes((byte*)m_Data.GetUnsafePtr() + messageOffset, messageLength); + WriteBytes(ref writer, (byte*)m_Data.GetUnsafePtr() + messageOffset, messageLength); writerAvailable -= sizeof(int) + messageLength; readerOffset += sizeof(int) + messageLength; @@ -205,7 +264,7 @@ public int FillWriterWithBytes(ref DataStreamWriter writer) unsafe { - writer.WriteBytes((byte*)m_Data.GetUnsafePtr() + HeadIndex, copyLength); + WriteBytes(ref writer, (byte*)m_Data.GetUnsafePtr() + HeadIndex, copyLength); } return copyLength; @@ -219,10 +278,14 @@ public int FillWriterWithBytes(ref DataStreamWriter writer) /// Number of bytes to consume from the queue. public void Consume(int size) { + // Adjust the head/tail indices such that we consume the given size. if (size >= Length) { HeadIndex = 0; TailIndex = 0; + + // This is a no-op if m_Data is already at minimum capacity. + m_Data.ResizeUninitialized(m_MinimumCapacity); } else { diff --git a/Runtime/Transports/UTP/NetworkMetricsPipelineStage.cs b/Runtime/Transports/UTP/NetworkMetricsPipelineStage.cs index d74fe94..30c7dd2 100644 --- a/Runtime/Transports/UTP/NetworkMetricsPipelineStage.cs +++ b/Runtime/Transports/UTP/NetworkMetricsPipelineStage.cs @@ -4,25 +4,24 @@ using Unity.Burst; using Unity.Collections.LowLevel.Unsafe; using Unity.Networking.Transport; -using UnityEngine; namespace Unity.Netcode.Transports.UTP { [BurstCompile] internal unsafe struct NetworkMetricsPipelineStage : INetworkPipelineStage { - static TransportFunctionPointer ReceiveFunction = new TransportFunctionPointer(Receive); - static TransportFunctionPointer SendFunction = new TransportFunctionPointer(Send); - static TransportFunctionPointer InitializeConnectionFunction = new TransportFunctionPointer(InitializeConnection); + private static TransportFunctionPointer s_ReceiveFunction = new TransportFunctionPointer(Receive); + private static TransportFunctionPointer s_SendFunction = new TransportFunctionPointer(Send); + private static TransportFunctionPointer s_InitializeConnectionFunction = new TransportFunctionPointer(InitializeConnection); public NetworkPipelineStage StaticInitialize(byte* staticInstanceBuffer, int staticInstanceBufferLength, NetworkSettings settings) { return new NetworkPipelineStage( - ReceiveFunction, - SendFunction, - InitializeConnectionFunction, + s_ReceiveFunction, + s_SendFunction, + s_InitializeConnectionFunction, ReceiveCapacity: 0, SendCapacity: 0, HeaderCapacity: 0, diff --git a/Runtime/Transports/UTP/SecretsLoaderHelper.cs b/Runtime/Transports/UTP/SecretsLoaderHelper.cs new file mode 100644 index 0000000..40b8f2b --- /dev/null +++ b/Runtime/Transports/UTP/SecretsLoaderHelper.cs @@ -0,0 +1,188 @@ +using System; +using System.IO; +using UnityEngine; + +namespace Unity.Netcode.Transports.UTP +{ + /// + /// Component to add to a NetworkManager if you want the certificates to be loaded from files. + /// Mostly helpful to ease development and testing, especially with self-signed certificates + /// + /// Shipping code should make the calls to + /// - SetServerSecrets + /// - SetClientSecrets + /// directly, instead of relying on this. + /// + public class SecretsLoaderHelper : MonoBehaviour + { + internal struct ServerSecrets + { + public string ServerPrivate; + public string ServerCertificate; + }; + + internal struct ClientSecrets + { + public string ServerCommonName; + public string ClientCertificate; + }; + + private void Awake() + { + var serverSecrets = new ServerSecrets(); + + try + { + serverSecrets.ServerCertificate = ServerCertificate; + } + catch (Exception exception) + { + Debug.Log(exception); + } + + try + { + serverSecrets.ServerPrivate = ServerPrivate; + } + catch (Exception exception) + { + Debug.Log(exception); + } + + var clientSecrets = new ClientSecrets(); + try + { + clientSecrets.ClientCertificate = ClientCA; + } + catch (Exception exception) + { + Debug.Log(exception); + } + + try + { + clientSecrets.ServerCommonName = ServerCommonName; + } + catch (Exception exception) + { + Debug.Log(exception); + } + + var unityTransportComponent = GetComponent(); + + if (unityTransportComponent == null) + { + Debug.LogError($"You need to select the UnityTransport protocol, in the NetworkManager, in order for the SecretsLoaderHelper component to be useful."); + return; + } + + unityTransportComponent.SetServerSecrets(serverSecrets.ServerCertificate, serverSecrets.ServerPrivate); + unityTransportComponent.SetClientSecrets(clientSecrets.ServerCommonName, clientSecrets.ClientCertificate); + } + + [Tooltip("Hostname")] + [SerializeField] + private string m_ServerCommonName = "localhost"; + + /// Common name of the server (typically its hostname). + public string ServerCommonName + { + get => m_ServerCommonName; + set => m_ServerCommonName = value; + } + + [Tooltip("Client CA filepath. Useful with self-signed certificates")] + [SerializeField] + private string m_ClientCAFilePath = ""; // "Assets/Secure/myGameClientCA.pem" + + /// Client CA filepath. Useful with self-signed certificates + public string ClientCAFilePath + { + get => m_ClientCAFilePath; + set => m_ClientCAFilePath = value; + } + + [Tooltip("Client CA Override. Only useful for development with self-signed certificates. Certificate content, for platforms that lack file access (WebGL)")] + [SerializeField] + private string m_ClientCAOverride = ""; + + /// + /// Client CA Override. Only useful for development with self-signed certificates. + /// Certificate content, for platforms that lack file access (WebGL) + /// + public string ClientCAOverride + { + get => m_ClientCAOverride; + set => m_ClientCAOverride = value; + } + + [Tooltip("Server Certificate filepath")] + [SerializeField] + private string m_ServerCertificateFilePath = ""; // "Assets/Secure/myGameServerCertificate.pem" + + /// Server Certificate filepath + public string ServerCertificateFilePath + { + get => m_ServerCertificateFilePath; + set => m_ServerCertificateFilePath = value; + } + + [Tooltip("Server Private Key filepath")] + [SerializeField] + private string m_ServerPrivateFilePath = ""; // "Assets/Secure/myGameServerPrivate.pem" + + /// Server Private Key filepath + public string ServerPrivateFilePath + { + get => m_ServerPrivateFilePath; + set => m_ServerPrivate = value; + } + + private string m_ClientCA; + + /// CA certificate used by the client. + public string ClientCA + { + get + { + if (m_ClientCAOverride != "") + { + return m_ClientCAOverride; + } + return ReadFile(m_ClientCAFilePath, "Client Certificate"); + } + set => m_ClientCA = value; + } + + private string m_ServerCertificate; + + /// Certificate used by the server. + public string ServerCertificate + { + get => ReadFile(m_ServerCertificateFilePath, "Server Certificate"); + set => m_ServerCertificate = value; + } + + private string m_ServerPrivate; + + /// Private key used by the server. + public string ServerPrivate + { + get => ReadFile(m_ServerPrivateFilePath, "Server Key"); + set => m_ServerPrivate = value; + } + + private static string ReadFile(string path, string label) + { + if (path == null || path == "") + { + return ""; + } + + var reader = new StreamReader(path); + string fileContent = reader.ReadToEnd(); + Debug.Log((fileContent.Length > 1) ? ("Successfully loaded " + fileContent.Length + " byte(s) from " + label) : ("Could not read " + label + " file")); + return fileContent; + } + } +} diff --git a/Runtime/Transports/UTP/SecretsLoaderHelper.cs.meta b/Runtime/Transports/UTP/SecretsLoaderHelper.cs.meta new file mode 100644 index 0000000..7ceb562 --- /dev/null +++ b/Runtime/Transports/UTP/SecretsLoaderHelper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dc1e7a8dc597cf24c95e4acf92c0edf5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Transports/UTP/UnityTransport.cs b/Runtime/Transports/UTP/UnityTransport.cs index 5a06191..f616b1d 100644 --- a/Runtime/Transports/UTP/UnityTransport.cs +++ b/Runtime/Transports/UTP/UnityTransport.cs @@ -1,3 +1,10 @@ +// NetSim Implementation compilation boilerplate +// All references to UNITY_MP_TOOLS_NETSIM_IMPLEMENTATION_ENABLED should be defined in the same way, +// as any discrepancies are likely to result in build failures +#if UNITY_EDITOR || (DEVELOPMENT_BUILD && !UNITY_MP_TOOLS_NETSIM_DISABLED_IN_DEVELOP) || (!DEVELOPMENT_BUILD && UNITY_MP_TOOLS_NETSIM_ENABLED_IN_RELEASE) +#define UNITY_MP_TOOLS_NETSIM_IMPLEMENTATION_ENABLED +#endif + using System; using System.Collections.Generic; using UnityEngine; @@ -8,6 +15,13 @@ using Unity.Networking.Transport; using Unity.Networking.Transport.Relay; using Unity.Networking.Transport.Utilities; +#if UTP_TRANSPORT_2_0_ABOVE +using Unity.Networking.Transport.TLS; +#endif + +#if !UTP_TRANSPORT_2_0_ABOVE +using NetworkEndpoint = Unity.Networking.Transport.NetworkEndPoint; +#endif namespace Unity.Netcode.Transports.UTP { @@ -88,6 +102,7 @@ public static string ErrorToString(Networking.Transport.Error.StatusCode error, /// The Netcode for GameObjects NetworkTransport for UnityTransport. /// Note: This is highly recommended to use over UNet. /// + [AddComponentMenu("Netcode/Unity Transport")] public partial class UnityTransport : NetworkTransport, INetworkStreamDriverConstructor { /// @@ -125,8 +140,13 @@ private enum State /// /// The default maximum send queue size /// + [Obsolete("MaxSendQueueSize is now determined dynamically (can still be set programmatically using the MaxSendQueueSize property). This initial value is not used anymore.", false)] public const int InitialMaxSendQueueSize = 16 * InitialMaxPayloadSize; + // Maximum reliable throughput, assuming the full reliable window can be sent on every + // frame at 60 FPS. This will be a large over-estimation in any realistic scenario. + private const int k_MaxReliableThroughput = (NetworkParameterConstants.MTU * 32 * 60) / 1000; // bytes per millisecond + private static ConnectionAddressData s_DefaultConnectionAddressData = new ConnectionAddressData { Address = "127.0.0.1", Port = 7777, ServerListenAddress = string.Empty }; #pragma warning disable IDE1006 // Naming Styles @@ -146,6 +166,30 @@ private enum State [SerializeField] private ProtocolType m_ProtocolType; +#if UTP_TRANSPORT_2_0_ABOVE + [Tooltip("Per default the client/server will communicate over UDP. Set to true to communicate with WebSocket.")] + [SerializeField] + private bool m_UseWebSockets = false; + + public bool UseWebSockets + { + get => m_UseWebSockets; + set => m_UseWebSockets = value; + } + + /// + /// Per default the client/server communication will not be encrypted. Select true to enable DTLS for UDP and TLS for Websocket. + /// + [Tooltip("Per default the client/server communication will not be encrypted. Select true to enable DTLS for UDP and TLS for Websocket.")] + [SerializeField] + private bool m_UseEncryption = false; + public bool UseEncryption + { + get => m_UseEncryption; + set => m_UseEncryption = value; + } +#endif + [Tooltip("The maximum amount of packets that can be in the internal send/receive queues. Basically this is how many packets can be sent/received in a single update/frame.")] [SerializeField] private int m_MaxPacketQueueSize = InitialMaxPacketQueueSize; @@ -169,15 +213,17 @@ public int MaxPayloadSize set => m_MaxPayloadSize = value; } - [Tooltip("The maximum size in bytes of the transport send queue. The send queue accumulates messages for batching and stores messages when other internal send queues are full. If you routinely observe an error about too many in-flight packets, try increasing this.")] - [SerializeField] - private int m_MaxSendQueueSize = InitialMaxSendQueueSize; + private int m_MaxSendQueueSize = 0; /// The maximum size in bytes of the transport send queue. /// /// The send queue accumulates messages for batching and stores messages when other internal - /// send queues are full. If you routinely observe an error about too many in-flight packets, - /// try increasing this. + /// send queues are full. Note that there should not be any need to set this value manually + /// since the send queue size is dynamically sized based on need. + /// + /// This value should only be set if you have particular requirements (e.g. if you want to + /// limit the memory usage of the send queues). Note however that setting this value too low + /// can easily lead to disconnections under heavy traffic. /// public int MaxSendQueueSize { @@ -263,12 +309,14 @@ public struct ConnectionAddressData [SerializeField] public string ServerListenAddress; - private static NetworkEndPoint ParseNetworkEndpoint(string ip, ushort port) + private static NetworkEndpoint ParseNetworkEndpoint(string ip, ushort port) { - if (!NetworkEndPoint.TryParse(ip, port, out var endpoint)) + NetworkEndpoint endpoint = default; + + if (!NetworkEndpoint.TryParse(ip, port, out endpoint, NetworkFamily.Ipv4) && + !NetworkEndpoint.TryParse(ip, port, out endpoint, NetworkFamily.Ipv6)) { Debug.LogError($"Invalid network endpoint: {ip}:{port}."); - return default; } return endpoint; @@ -277,12 +325,12 @@ private static NetworkEndPoint ParseNetworkEndpoint(string ip, ushort port) /// /// Endpoint (IP address and port) clients will connect to. /// - public NetworkEndPoint ServerEndPoint => ParseNetworkEndpoint(Address, Port); + public NetworkEndpoint ServerEndPoint => ParseNetworkEndpoint(Address, Port); /// /// Endpoint (IP address and port) server will listen/bind on. /// - public NetworkEndPoint ListenEndPoint => ParseNetworkEndpoint((ServerListenAddress == string.Empty) ? Address : ServerListenAddress, Port); + public NetworkEndpoint ListenEndPoint => ParseNetworkEndpoint((ServerListenAddress?.Length == 0) ? Address : ServerListenAddress, Port); } /// @@ -326,6 +374,9 @@ public struct SimulatorParameters /// - packet jitter (variances in latency, see: https://en.wikipedia.org/wiki/Jitter) /// - packet drop rate (packet loss) /// +#if UTP_TRANSPORT_2_0_ABOVE + [Obsolete("DebugSimulator is no longer supported and has no effect. Use Network Simulator from the Multiplayer Tools package.", false)] +#endif public SimulatorParameters DebugSimulator = new SimulatorParameters { PacketDelayMS = 0, @@ -333,6 +384,8 @@ public struct SimulatorParameters PacketDropRate = 0 }; + internal uint? DebugSimulatorRandomSeed { get; set; } = null; + private struct PacketLossCache { public int PacketsReceived; @@ -340,6 +393,10 @@ private struct PacketLossCache public float PacketLoss; }; + internal static event Action TransportInitialized; + internal static event Action TransportDisposed; + internal NetworkDriver NetworkDriver => m_Driver; + private PacketLossCache m_PacketLossCache = new PacketLossCache(); private State m_State = State.Disconnected; @@ -383,6 +440,8 @@ private void InitDriver() out m_UnreliableFragmentedPipeline, out m_UnreliableSequencedFragmentedPipeline, out m_ReliableSequencedPipeline); + + TransportInitialized?.Invoke(GetInstanceID(), NetworkDriver); } private void DisposeInternals() @@ -400,6 +459,8 @@ private void DisposeInternals() } m_SendQueue.Clear(); + + TransportDisposed?.Invoke(GetInstanceID()); } private NetworkPipeline SelectSendPipeline(NetworkDelivery delivery) @@ -425,7 +486,7 @@ private NetworkPipeline SelectSendPipeline(NetworkDelivery delivery) private bool ClientBindAndConnect() { - var serverEndpoint = default(NetworkEndPoint); + var serverEndpoint = default(NetworkEndpoint); if (m_ProtocolType == ProtocolType.RelayUnityTransport) { @@ -438,6 +499,7 @@ private bool ClientBindAndConnect() } m_NetworkSettings.WithRelayParameters(ref m_RelayServerData, m_HeartbeatTimeoutMS); + serverEndpoint = m_RelayServerData.Endpoint; } else { @@ -446,7 +508,8 @@ private bool ClientBindAndConnect() InitDriver(); - int result = m_Driver.Bind(NetworkEndPoint.AnyIpv4); + var bindEndpoint = serverEndpoint.Family == NetworkFamily.Ipv6 ? NetworkEndpoint.AnyIpv6 : NetworkEndpoint.AnyIpv4; + int result = m_Driver.Bind(bindEndpoint); if (result != 0) { Debug.LogError("Client failed to bind"); @@ -459,7 +522,7 @@ private bool ClientBindAndConnect() return true; } - private bool ServerBindAndListen(NetworkEndPoint endPoint) + private bool ServerBindAndListen(NetworkEndpoint endPoint) { InitDriver(); @@ -481,51 +544,13 @@ private bool ServerBindAndListen(NetworkEndPoint endPoint) return true; } - private static RelayAllocationId ConvertFromAllocationIdBytes(byte[] allocationIdBytes) - { - unsafe - { - fixed (byte* ptr = allocationIdBytes) - { - return RelayAllocationId.FromBytePointer(ptr, allocationIdBytes.Length); - } - } - } - - private static RelayHMACKey ConvertFromHMAC(byte[] hmac) - { - unsafe - { - fixed (byte* ptr = hmac) - { - return RelayHMACKey.FromBytePointer(ptr, RelayHMACKey.k_Length); - } - } - } - - private static RelayConnectionData ConvertConnectionData(byte[] connectionData) - { - unsafe - { - fixed (byte* ptr = connectionData) - { - return RelayConnectionData.FromBytePointer(ptr, RelayConnectionData.k_Length); - } - } - } - - internal void SetMaxPayloadSize(int maxPayloadSize) - { - m_MaxPayloadSize = maxPayloadSize; - } - private void SetProtocol(ProtocolType inProtocol) { m_ProtocolType = inProtocol; } /// Set the relay server data for the server. - /// IP address of the relay server. + /// IP address or hostname of the relay server. /// UDP port of the relay server. /// Allocation ID as a byte array. /// Allocation key as a byte array. @@ -534,39 +559,21 @@ private void SetProtocol(ProtocolType inProtocol) /// Whether the connection is secure (uses DTLS). public void SetRelayServerData(string ipv4Address, ushort port, byte[] allocationIdBytes, byte[] keyBytes, byte[] connectionDataBytes, byte[] hostConnectionDataBytes = null, bool isSecure = false) { - RelayConnectionData hostConnectionData; - - if (!NetworkEndPoint.TryParse(ipv4Address, port, out var serverEndpoint)) - { - Debug.LogError($"Invalid address {ipv4Address}:{port}"); - - // We set this to default to cause other checks to fail to state you need to call this - // function again. - m_RelayServerData = default; - return; - } - - var allocationId = ConvertFromAllocationIdBytes(allocationIdBytes); - var key = ConvertFromHMAC(keyBytes); - var connectionData = ConvertConnectionData(connectionDataBytes); - - if (hostConnectionDataBytes != null) - { - hostConnectionData = ConvertConnectionData(hostConnectionDataBytes); - } - else - { - hostConnectionData = connectionData; - } - - m_RelayServerData = new RelayServerData(ref serverEndpoint, 0, ref allocationId, ref connectionData, ref hostConnectionData, ref key, isSecure); - m_RelayServerData.ComputeNewNonce(); + var hostConnectionData = hostConnectionDataBytes ?? connectionDataBytes; + m_RelayServerData = new RelayServerData(ipv4Address, port, allocationIdBytes, connectionDataBytes, hostConnectionData, keyBytes, isSecure); + SetProtocol(ProtocolType.RelayUnityTransport); + } + /// Set the relay server data (using the lower-level Unity Transport data structure). + /// Data for the Relay server to use. + public void SetRelayServerData(RelayServerData serverData) + { + m_RelayServerData = serverData; SetProtocol(ProtocolType.RelayUnityTransport); } /// Set the relay server data for the host. - /// IP address of the relay server. + /// IP address or hostname of the relay server. /// UDP port of the relay server. /// Allocation ID as a byte array. /// Allocation key as a byte array. @@ -578,7 +585,7 @@ public void SetHostRelayData(string ipAddress, ushort port, byte[] allocationId, } /// Set the relay server data for the host. - /// IP address of the relay server. + /// IP address or hostname of the relay server. /// UDP port of the relay server. /// Allocation ID as a byte array. /// Allocation key as a byte array. @@ -593,7 +600,7 @@ public void SetClientRelayData(string ipAddress, ushort port, byte[] allocationI /// /// Sets IP and Port information. This will be ignored if using the Unity Relay and you should call /// - /// The remote IP address + /// The remote IP address (despite the name, can be an IPv6 address) /// The remote port /// The local listen address public void SetConnectionData(string ipv4Address, ushort port, string listenAddress = null) @@ -613,7 +620,7 @@ public void SetConnectionData(string ipv4Address, ushort port, string listenAddr /// /// The remote end point /// The local listen endpoint - public void SetConnectionData(NetworkEndPoint endPoint, NetworkEndPoint listenEndPoint = default) + public void SetConnectionData(NetworkEndpoint endPoint, NetworkEndpoint listenEndPoint = default) { string serverAddress = endPoint.Address.Split(':')[0]; @@ -634,6 +641,9 @@ public void SetConnectionData(NetworkEndPoint endPoint, NetworkEndPoint listenEn /// Packet delay in milliseconds. /// Packet jitter in milliseconds. /// Packet drop percentage. +#if UTP_TRANSPORT_2_0_ABOVE + [Obsolete("SetDebugSimulatorParameters is no longer supported and has no effect. Use Network Simulator from the Multiplayer Tools package.", false)] +#endif public void SetDebugSimulatorParameters(int packetDelay, int packetJitter, int dropRate) { if (m_Driver.IsCreated) @@ -662,7 +672,7 @@ private bool StartRelayServer() else { m_NetworkSettings.WithRelayParameters(ref m_RelayServerData, m_HeartbeatTimeoutMS); - return ServerBindAndListen(NetworkEndPoint.AnyIpv4); + return ServerBindAndListen(NetworkEndpoint.AnyIpv4); } } @@ -891,7 +901,7 @@ private void ExtractNetworkMetrics() private void ExtractNetworkMetricsForClient(ulong transportClientId) { - var networkConnection = ParseClientId(transportClientId); + var networkConnection = ParseClientId(transportClientId); ExtractNetworkMetricsFromPipeline(m_UnreliableFragmentedPipeline, networkConnection); ExtractNetworkMetricsFromPipeline(m_UnreliableSequencedFragmentedPipeline, networkConnection); ExtractNetworkMetricsFromPipeline(m_ReliableSequencedPipeline, networkConnection); @@ -907,7 +917,11 @@ private void ExtractNetworkMetricsFromPipeline(NetworkPipeline pipeline, Network { //Don't need to dispose of the buffers, they are filled with data pointers. m_Driver.GetPipelineBuffers(pipeline, +#if UTP_TRANSPORT_2_0_ABOVE + NetworkPipelineStageId.Get(), +#else NetworkPipelineStageCollection.GetStageId(typeof(NetworkMetricsPipelineStage)), +#endif networkConnection, out _, out _, @@ -934,7 +948,11 @@ private int ExtractRtt(NetworkConnection networkConnection) } m_Driver.GetPipelineBuffers(m_ReliableSequencedPipeline, +#if UTP_TRANSPORT_2_0_ABOVE + NetworkPipelineStageId.Get(), +#else NetworkPipelineStageCollection.GetStageId(typeof(ReliableSequencedPipelineStage)), +#endif networkConnection, out _, out _, @@ -956,7 +974,11 @@ private float ExtractPacketLoss(NetworkConnection networkConnection) } m_Driver.GetPipelineBuffers(m_ReliableSequencedPipeline, +#if UTP_TRANSPORT_2_0_ABOVE + NetworkPipelineStageId.Get(), +#else NetworkPipelineStageCollection.GetStageId(typeof(ReliableSequencedPipelineStage)), +#endif networkConnection, out _, out _, @@ -1117,11 +1139,12 @@ public override void Initialize(NetworkManager networkManager = null) // account for the overhead of its length when we store it in the send queue. var fragmentationCapacity = m_MaxPayloadSize + BatchedSendQueue.PerMessageOverhead; - m_NetworkSettings - .WithFragmentationStageParameters(payloadCapacity: fragmentationCapacity) - .WithBaselibNetworkInterfaceParameters( - receiveQueueCapacity: m_MaxPacketQueueSize, - sendQueueCapacity: m_MaxPacketQueueSize); + m_NetworkSettings.WithFragmentationStageParameters(payloadCapacity: fragmentationCapacity); +#if !UTP_TRANSPORT_2_0_ABOVE + m_NetworkSettings.WithBaselibNetworkInterfaceParameters( + receiveQueueCapacity: m_MaxPacketQueueSize, + sendQueueCapacity: m_MaxPacketQueueSize); +#endif #endif } @@ -1159,7 +1182,23 @@ public override void Send(ulong clientId, ArraySegment payload, NetworkDel var sendTarget = new SendTarget(clientId, pipeline); if (!m_SendQueue.TryGetValue(sendTarget, out var queue)) { - queue = new BatchedSendQueue(Math.Max(m_MaxSendQueueSize, m_MaxPayloadSize)); + // The maximum size of a send queue is determined according to the disconnection + // timeout. The idea being that if the send queue contains enough reliable data that + // sending it all out would take longer than the disconnection timeout, then there's + // no point storing even more in the queue (it would be like having a ping higher + // than the disconnection timeout, which is far into the realm of unplayability). + // + // The throughput used to determine what consists the maximum send queue size is + // the maximum theoritical throughput of the reliable pipeline assuming we only send + // on each update at 60 FPS, which turns out to be around 2.688 MB/s. + // + // Note that we only care about reliable throughput for send queues because that's + // the only case where a full send queue causes a connection loss. Full unreliable + // send queues are dealt with by flushing it out to the network or simply dropping + // new messages if that fails. + var maxCapacity = m_MaxSendQueueSize > 0 ? m_MaxSendQueueSize : m_DisconnectTimeoutMS * k_MaxReliableThroughput; + + queue = new BatchedSendQueue(Math.Max(maxCapacity, m_MaxPayloadSize)); m_SendQueue.Add(sendTarget, queue); } @@ -1173,8 +1212,7 @@ public override void Send(ulong clientId, ArraySegment payload, NetworkDel var ngoClientId = NetworkManager?.TransportIdToClientId(clientId) ?? clientId; Debug.LogError($"Couldn't add payload of size {payload.Count} to reliable send queue. " + - $"Closing connection {ngoClientId} as reliability guarantees can't be maintained. " + - $"Perhaps 'Max Send Queue Size' ({m_MaxSendQueueSize}) is too small for workload."); + $"Closing connection {ngoClientId} as reliability guarantees can't be maintained."); if (clientId == m_ServerClientId) { @@ -1223,9 +1261,9 @@ public override bool StartClient() } var succeeded = ClientBindAndConnect(); - if (!succeeded) + if (!succeeded && m_Driver.IsCreated) { - Shutdown(); + m_Driver.Dispose(); } return succeeded; } @@ -1250,16 +1288,16 @@ public override bool StartServer() { case ProtocolType.UnityTransport: succeeded = ServerBindAndListen(ConnectionData.ListenEndPoint); - if (!succeeded) + if (!succeeded && m_Driver.IsCreated) { - Shutdown(); + m_Driver.Dispose(); } return succeeded; case ProtocolType.RelayUnityTransport: succeeded = StartRelayServer(); - if (!succeeded) + if (!succeeded && m_Driver.IsCreated) { - Shutdown(); + m_Driver.Dispose(); } return succeeded; default: @@ -1298,16 +1336,65 @@ public override void Shutdown() m_ServerClientId = 0; } - private void ConfigureSimulator() +#if UTP_TRANSPORT_2_0_ABOVE + private void ConfigureSimulatorForUtp2() + { + // As DebugSimulator is deprecated, the 'packetDelayMs', 'packetJitterMs' and 'packetDropPercentage' + // parameters are set to the default and are supposed to be changed using Network Simulator tool instead. + m_NetworkSettings.WithSimulatorStageParameters( + maxPacketCount: 300, // TODO Is there any way to compute a better value? + maxPacketSize: NetworkParameterConstants.MTU, + packetDelayMs: 0, + packetJitterMs: 0, + packetDropPercentage: 0, + randomSeed: DebugSimulatorRandomSeed ?? (uint)System.Diagnostics.Stopwatch.GetTimestamp() + , mode: ApplyMode.AllPackets + ); + + m_NetworkSettings.WithNetworkSimulatorParameters(); + } +#else + private void ConfigureSimulatorForUtp1() { m_NetworkSettings.WithSimulatorStageParameters( maxPacketCount: 300, // TODO Is there any way to compute a better value? maxPacketSize: NetworkParameterConstants.MTU, packetDelayMs: DebugSimulator.PacketDelayMS, packetJitterMs: DebugSimulator.PacketJitterMS, - packetDropPercentage: DebugSimulator.PacketDropRate + packetDropPercentage: DebugSimulator.PacketDropRate, + randomSeed: DebugSimulatorRandomSeed ?? (uint)System.Diagnostics.Stopwatch.GetTimestamp() ); } +#endif + + private string m_ServerPrivateKey; + private string m_ServerCertificate; + + private string m_ServerCommonName; + private string m_ClientCaCertificate; + + /// Set the server parameters for encryption. + /// Public certificate for the server (PEM format). + /// Private key for the server (PEM format). + public void SetServerSecrets(string serverCertificate, string serverPrivateKey) + { + m_ServerPrivateKey = serverPrivateKey; + m_ServerCertificate = serverCertificate; + } + + /// Set the client parameters for encryption. + /// + /// If the CA certificate is not provided, validation will be done against the OS/browser + /// certificate store. This is what you'd want if using certificates from a known provider. + /// For self-signed certificates, the CA certificate needs to be provided. + /// + /// Common name of the server (typically hostname). + /// CA certificate used to validate the server's authenticity. + public void SetClientSecrets(string serverCommonName, string caCertificate = null) + { + m_ServerCommonName = serverCommonName; + m_ClientCaCertificate = caCertificate; + } /// /// Creates the internal NetworkDriver @@ -1322,22 +1409,124 @@ public void CreateDriver(UnityTransport transport, out NetworkDriver driver, out NetworkPipeline unreliableSequencedFragmentedPipeline, out NetworkPipeline reliableSequencedPipeline) { -#if MULTIPLAYER_TOOLS_1_0_0_PRE_7 +#if MULTIPLAYER_TOOLS_1_0_0_PRE_7 && !UTP_TRANSPORT_2_0_ABOVE NetworkPipelineStageCollection.RegisterPipelineStage(new NetworkMetricsPipelineStage()); #endif -#if UNITY_EDITOR || DEVELOPMENT_BUILD - ConfigureSimulator(); +#if UTP_TRANSPORT_2_0_ABOVE && UNITY_MP_TOOLS_NETSIM_IMPLEMENTATION_ENABLED + ConfigureSimulatorForUtp2(); +#elif !UTP_TRANSPORT_2_0_ABOVE && (UNITY_EDITOR || DEVELOPMENT_BUILD) + ConfigureSimulatorForUtp1(); #endif m_NetworkSettings.WithNetworkConfigParameters( maxConnectAttempts: transport.m_MaxConnectAttempts, connectTimeoutMS: transport.m_ConnectTimeoutMS, disconnectTimeoutMS: transport.m_DisconnectTimeoutMS, +#if UTP_TRANSPORT_2_0_ABOVE + sendQueueCapacity: m_MaxPacketQueueSize, + receiveQueueCapacity: m_MaxPacketQueueSize, +#endif heartbeatTimeoutMS: transport.m_HeartbeatTimeoutMS); +#if UNITY_WEBGL && !UNITY_EDITOR + if (NetworkManager.IsServer) + { + throw new Exception("WebGL as a server is not supported by Unity Transport, outside the Editor."); + } +#endif + +#if UTP_TRANSPORT_2_0_ABOVE + if (m_UseEncryption) + { + if (m_ProtocolType == ProtocolType.RelayUnityTransport) + { + if (m_RelayServerData.IsSecure != 0) + { + // log an error because we have mismatched configuration + Debug.LogError("Mismatched security configuration, between Relay and local NetworkManager settings"); + } + + // No need to to anything else if using Relay because UTP will handle the + // configuration of the security parameters on its own. + } + else + { + try + { + if (NetworkManager.IsServer) + { + if (m_ServerCertificate.Length == 0 || m_ServerPrivateKey.Length == 0) + { + throw new Exception("In order to use encrypted communications, when hosting, you must set the server certificate and key."); + } + m_NetworkSettings.WithSecureServerParameters(m_ServerCertificate, m_ServerPrivateKey); + } + else + { + if (m_ServerCommonName.Length == 0) + { + throw new Exception("In order to use encrypted communications, clients must set the server common name."); + } + else if (m_ClientCaCertificate == null) + { + m_NetworkSettings.WithSecureClientParameters(m_ServerCommonName); + } + else + { + m_NetworkSettings.WithSecureClientParameters(m_ClientCaCertificate, m_ServerCommonName); + } + } + } + catch(Exception e) + { + Debug.LogException(e, this); + } + } + } +#endif + +#if UTP_TRANSPORT_2_0_ABOVE + if (m_UseWebSockets) + { + driver = NetworkDriver.Create(new WebSocketNetworkInterface(), m_NetworkSettings); + } + else + { +#if UNITY_WEBGL + Debug.LogWarning($"WebSockets were used even though they're not selected in NetworkManager. You should check {nameof(UseWebSockets)}', on the Unity Transport component, to silence this warning."); + driver = NetworkDriver.Create(new WebSocketNetworkInterface(), m_NetworkSettings); +#else + driver = NetworkDriver.Create(new UDPNetworkInterface(), m_NetworkSettings); +#endif + } +#else driver = NetworkDriver.Create(m_NetworkSettings); +#endif + +#if MULTIPLAYER_TOOLS_1_0_0_PRE_7 && UTP_TRANSPORT_2_0_ABOVE + driver.RegisterPipelineStage(new NetworkMetricsPipelineStage()); +#endif + +#if !UTP_TRANSPORT_2_0_ABOVE + SetupPipelinesForUtp1(driver, + out unreliableFragmentedPipeline, + out unreliableSequencedFragmentedPipeline, + out reliableSequencedPipeline); +#else + SetupPipelinesForUtp2(driver, + out unreliableFragmentedPipeline, + out unreliableSequencedFragmentedPipeline, + out reliableSequencedPipeline); +#endif + } +#if !UTP_TRANSPORT_2_0_ABOVE + private void SetupPipelinesForUtp1(NetworkDriver driver, + out NetworkPipeline unreliableFragmentedPipeline, + out NetworkPipeline unreliableSequencedFragmentedPipeline, + out NetworkPipeline reliableSequencedPipeline) + { #if UNITY_EDITOR || DEVELOPMENT_BUILD if (DebugSimulator.PacketDelayMS > 0 || DebugSimulator.PacketDropRate > 0) { @@ -1355,7 +1544,7 @@ public void CreateDriver(UnityTransport transport, out NetworkDriver driver, typeof(SimulatorPipelineStage), typeof(SimulatorPipelineStageInSend) #if MULTIPLAYER_TOOLS_1_0_0_PRE_7 - ,typeof(NetworkMetricsPipelineStage) + , typeof(NetworkMetricsPipelineStage) #endif ); reliableSequencedPipeline = driver.CreatePipeline( @@ -1363,7 +1552,7 @@ public void CreateDriver(UnityTransport transport, out NetworkDriver driver, typeof(SimulatorPipelineStage), typeof(SimulatorPipelineStageInSend) #if MULTIPLAYER_TOOLS_1_0_0_PRE_7 - ,typeof(NetworkMetricsPipelineStage) + , typeof(NetworkMetricsPipelineStage) #endif ); } @@ -1373,25 +1562,63 @@ public void CreateDriver(UnityTransport transport, out NetworkDriver driver, unreliableFragmentedPipeline = driver.CreatePipeline( typeof(FragmentationPipelineStage) #if MULTIPLAYER_TOOLS_1_0_0_PRE_7 - ,typeof(NetworkMetricsPipelineStage) + , typeof(NetworkMetricsPipelineStage) #endif ); unreliableSequencedFragmentedPipeline = driver.CreatePipeline( typeof(FragmentationPipelineStage), typeof(UnreliableSequencedPipelineStage) #if MULTIPLAYER_TOOLS_1_0_0_PRE_7 - ,typeof(NetworkMetricsPipelineStage) + , typeof(NetworkMetricsPipelineStage) #endif ); reliableSequencedPipeline = driver.CreatePipeline( typeof(ReliableSequencedPipelineStage) #if MULTIPLAYER_TOOLS_1_0_0_PRE_7 - ,typeof(NetworkMetricsPipelineStage) + , typeof(NetworkMetricsPipelineStage) #endif ); } } +#else + private void SetupPipelinesForUtp2(NetworkDriver driver, + out NetworkPipeline unreliableFragmentedPipeline, + out NetworkPipeline unreliableSequencedFragmentedPipeline, + out NetworkPipeline reliableSequencedPipeline) + { + + unreliableFragmentedPipeline = driver.CreatePipeline( + typeof(FragmentationPipelineStage) +#if UNITY_MP_TOOLS_NETSIM_IMPLEMENTATION_ENABLED + , typeof(SimulatorPipelineStage) +#endif +#if MULTIPLAYER_TOOLS_1_0_0_PRE_7 + , typeof(NetworkMetricsPipelineStage) +#endif + ); + + unreliableSequencedFragmentedPipeline = driver.CreatePipeline( + typeof(FragmentationPipelineStage), + typeof(UnreliableSequencedPipelineStage) +#if UNITY_MP_TOOLS_NETSIM_IMPLEMENTATION_ENABLED + , typeof(SimulatorPipelineStage) +#endif +#if MULTIPLAYER_TOOLS_1_0_0_PRE_7 + , typeof(NetworkMetricsPipelineStage) +#endif + ); + reliableSequencedPipeline = driver.CreatePipeline( + typeof(ReliableSequencedPipelineStage) +#if UNITY_MP_TOOLS_NETSIM_IMPLEMENTATION_ENABLED + , typeof(SimulatorPipelineStage) +#endif +#if MULTIPLAYER_TOOLS_1_0_0_PRE_7 + , typeof(NetworkMetricsPipelineStage) +#endif + ); + } +#endif // -------------- Utility Types ------------------------------------------------------------------------------- diff --git a/Runtime/com.unity.netcode.runtime.asmdef b/Runtime/com.unity.netcode.runtime.asmdef index 24a10f6..334966a 100644 --- a/Runtime/com.unity.netcode.runtime.asmdef +++ b/Runtime/com.unity.netcode.runtime.asmdef @@ -30,6 +30,11 @@ "name": "com.unity.multiplayer.tools", "expression": "1.0.0-pre.7", "define": "MULTIPLAYER_TOOLS_1_0_0_PRE_7" + }, + { + "name": "com.unity.transport", + "expression": "2.0.0-exp", + "define": "UTP_TRANSPORT_2_0_ABOVE" } ] } diff --git a/TestHelpers/Runtime/IntegrationTestSceneHandler.cs b/TestHelpers/Runtime/IntegrationTestSceneHandler.cs index cee1b8b..15b6bc1 100644 --- a/TestHelpers/Runtime/IntegrationTestSceneHandler.cs +++ b/TestHelpers/Runtime/IntegrationTestSceneHandler.cs @@ -48,7 +48,7 @@ public enum JobTypes public JobTypes JobType; public string SceneName; public Scene Scene; - public ISceneManagerHandler.SceneEventAction SceneAction; + public SceneEventProgress SceneEventProgress; public IntegrationTestSceneHandler IntegrationTestSceneHandler; } @@ -106,7 +106,8 @@ static internal IEnumerator ProcessLoadingSceneJob(QueuedSceneJob queuedSceneJob SceneManager.sceneLoaded += SceneManager_sceneLoaded; // We always load additively for all scenes during integration tests - SceneManager.LoadSceneAsync(queuedSceneJob.SceneName, LoadSceneMode.Additive); + var asyncOperation = SceneManager.LoadSceneAsync(queuedSceneJob.SceneName, LoadSceneMode.Additive); + queuedSceneJob.SceneEventProgress.SetAsyncOperation(asyncOperation); // Wait for it to finish while (queuedSceneJob.JobType != QueuedSceneJob.JobTypes.Completed) @@ -114,7 +115,6 @@ static internal IEnumerator ProcessLoadingSceneJob(QueuedSceneJob queuedSceneJob yield return s_WaitForSeconds; } yield return s_WaitForSeconds; - CurrentQueuedSceneJob.SceneAction.Invoke(); } /// @@ -176,7 +176,8 @@ static internal IEnumerator ProcessUnloadingSceneJob(QueuedSceneJob queuedSceneJ SceneManager.sceneUnloaded += SceneManager_sceneUnloaded; if (queuedSceneJob.Scene.IsValid() && queuedSceneJob.Scene.isLoaded && !queuedSceneJob.Scene.name.Contains(NetcodeIntegrationTestHelpers.FirstPartOfTestRunnerSceneName)) { - SceneManager.UnloadSceneAsync(queuedSceneJob.Scene); + var asyncOperation = SceneManager.UnloadSceneAsync(queuedSceneJob.Scene); + queuedSceneJob.SceneEventProgress.SetAsyncOperation(asyncOperation); } else { @@ -188,7 +189,6 @@ static internal IEnumerator ProcessUnloadingSceneJob(QueuedSceneJob queuedSceneJ { yield return s_WaitForSeconds; } - CurrentQueuedSceneJob.SceneAction.Invoke(); } /// @@ -246,7 +246,7 @@ private void AddJobToQueue(QueuedSceneJob queuedSceneJob) /// /// Server always loads like it normally would /// - public AsyncOperation GenericLoadSceneAsync(string sceneName, LoadSceneMode loadSceneMode, ISceneManagerHandler.SceneEventAction sceneEventAction) + public AsyncOperation GenericLoadSceneAsync(string sceneName, LoadSceneMode loadSceneMode, SceneEventProgress sceneEventProgress) { m_ServerSceneBeingLoaded = sceneName; if (NetcodeIntegrationTest.IsRunning) @@ -254,8 +254,7 @@ public AsyncOperation GenericLoadSceneAsync(string sceneName, LoadSceneMode load SceneManager.sceneLoaded += Sever_SceneLoaded; } var operation = SceneManager.LoadSceneAsync(sceneName, loadSceneMode); - - operation.completed += new Action(asyncOp2 => { sceneEventAction.Invoke(); }); + sceneEventProgress.SetAsyncOperation(operation); return operation; } @@ -271,39 +270,39 @@ private void Sever_SceneLoaded(Scene scene, LoadSceneMode arg1) /// /// Server always unloads like it normally would /// - public AsyncOperation GenericUnloadSceneAsync(Scene scene, ISceneManagerHandler.SceneEventAction sceneEventAction) + public AsyncOperation GenericUnloadSceneAsync(Scene scene, SceneEventProgress sceneEventProgress) { var operation = SceneManager.UnloadSceneAsync(scene); - operation.completed += new Action(asyncOp2 => { sceneEventAction.Invoke(); }); + sceneEventProgress.SetAsyncOperation(operation); return operation; } - public AsyncOperation LoadSceneAsync(string sceneName, LoadSceneMode loadSceneMode, ISceneManagerHandler.SceneEventAction sceneEventAction) + public AsyncOperation LoadSceneAsync(string sceneName, LoadSceneMode loadSceneMode, SceneEventProgress sceneEventProgress) { // Server and non NetcodeIntegrationTest tests use the generic load scene method if (!NetcodeIntegrationTest.IsRunning) { - return GenericLoadSceneAsync(sceneName, loadSceneMode, sceneEventAction); + return GenericLoadSceneAsync(sceneName, loadSceneMode, sceneEventProgress); } else // NetcodeIntegrationTest Clients always get added to the jobs queue { - AddJobToQueue(new QueuedSceneJob() { IntegrationTestSceneHandler = this, SceneName = sceneName, SceneAction = sceneEventAction, JobType = QueuedSceneJob.JobTypes.Loading }); + AddJobToQueue(new QueuedSceneJob() { IntegrationTestSceneHandler = this, SceneName = sceneName, SceneEventProgress = sceneEventProgress, JobType = QueuedSceneJob.JobTypes.Loading }); } return null; } - public AsyncOperation UnloadSceneAsync(Scene scene, ISceneManagerHandler.SceneEventAction sceneEventAction) + public AsyncOperation UnloadSceneAsync(Scene scene, SceneEventProgress sceneEventProgress) { // Server and non NetcodeIntegrationTest tests use the generic unload scene method if (!NetcodeIntegrationTest.IsRunning) { - return GenericUnloadSceneAsync(scene, sceneEventAction); + return GenericUnloadSceneAsync(scene, sceneEventProgress); } else // NetcodeIntegrationTest Clients always get added to the jobs queue { - AddJobToQueue(new QueuedSceneJob() { IntegrationTestSceneHandler = this, Scene = scene, SceneAction = sceneEventAction, JobType = QueuedSceneJob.JobTypes.Unloading }); + AddJobToQueue(new QueuedSceneJob() { IntegrationTestSceneHandler = this, Scene = scene, SceneEventProgress = sceneEventProgress, JobType = QueuedSceneJob.JobTypes.Unloading }); } // This is OK to return a "nothing" AsyncOperation since we are simulating client loading return null; diff --git a/TestHelpers/Runtime/MessageHooks.cs b/TestHelpers/Runtime/MessageHooks.cs index dceceb3..23f5f6c 100644 --- a/TestHelpers/Runtime/MessageHooks.cs +++ b/TestHelpers/Runtime/MessageHooks.cs @@ -4,14 +4,22 @@ namespace Unity.Netcode.TestHelpers.Runtime { internal class MessageHooks : INetworkHooks { - public bool IsWaiting; - public delegate bool MessageReceiptCheck(object receivedMessage); + public bool IsWaiting = true; + public delegate bool MessageReceiptCheck(Type receivedMessageType); public MessageReceiptCheck ReceiptCheck; + public delegate bool MessageHandleCheck(object receivedMessage); + public MessageHandleCheck HandleCheck; - public static bool CheckForMessageOfType(object receivedMessage) where T : INetworkMessage + public static bool CurrentMessageHasTriggerdAHook = false; + + public static bool CheckForMessageOfTypeHandled(object receivedMessage) where T : INetworkMessage { return receivedMessage is T; } + public static bool CheckForMessageOfTypeReceived(Type receivedMessageType) where T : INetworkMessage + { + return receivedMessageType == typeof(T); + } public void OnBeforeSendMessage(ulong clientId, ref T message, NetworkDelivery delivery) where T : INetworkMessage { @@ -23,10 +31,24 @@ public void OnAfterSendMessage(ulong clientId, ref T message, NetworkDelivery public void OnBeforeReceiveMessage(ulong senderId, Type messageType, int messageSizeBytes) { + // The way the system works, it goes through all hooks and calls OnBeforeHandleMessage, then handles the message, + // then goes thorugh all hooks and calls OnAfterHandleMessage. + // This ensures each message only manages to activate a single message hook - because we know that only + // one message will ever be handled between OnBeforeHandleMessage and OnAfterHandleMessage, + // we can reset the flag here, and then in OnAfterHandleMessage, the moment the message matches a hook, + // it'll flip this flag back on, and then other hooks will stop checking that message. + // Without this flag, waiting for 10 messages of the same type isn't possible - all 10 hooks would get + // tripped by the first message. + CurrentMessageHasTriggerdAHook = false; } public void OnAfterReceiveMessage(ulong senderId, Type messageType, int messageSizeBytes) { + if (!CurrentMessageHasTriggerdAHook && IsWaiting && (HandleCheck == null || HandleCheck.Invoke(messageType))) + { + IsWaiting = false; + CurrentMessageHasTriggerdAHook = true; + } } public void OnBeforeSendBatch(ulong clientId, int messageCount, int batchSizeInBytes, NetworkDelivery delivery) @@ -57,13 +79,23 @@ public bool OnVerifyCanReceive(ulong senderId, Type messageType, FastBufferReade public void OnBeforeHandleMessage(ref T message, ref NetworkContext context) where T : INetworkMessage { + // The way the system works, it goes through all hooks and calls OnBeforeHandleMessage, then handles the message, + // then goes thorugh all hooks and calls OnAfterHandleMessage. + // This ensures each message only manages to activate a single message hook - because we know that only + // one message will ever be handled between OnBeforeHandleMessage and OnAfterHandleMessage, + // we can reset the flag here, and then in OnAfterHandleMessage, the moment the message matches a hook, + // it'll flip this flag back on, and then other hooks will stop checking that message. + // Without this flag, waiting for 10 messages of the same type isn't possible - all 10 hooks would get + // tripped by the first message. + CurrentMessageHasTriggerdAHook = false; } public void OnAfterHandleMessage(ref T message, ref NetworkContext context) where T : INetworkMessage { - if (IsWaiting && (ReceiptCheck == null || ReceiptCheck.Invoke(message))) + if (!CurrentMessageHasTriggerdAHook && IsWaiting && (HandleCheck == null || HandleCheck.Invoke(message))) { IsWaiting = false; + CurrentMessageHasTriggerdAHook = true; } } } diff --git a/TestHelpers/Runtime/MessageHooksConditional.cs b/TestHelpers/Runtime/MessageHooksConditional.cs index 71791d2..f16978e 100644 --- a/TestHelpers/Runtime/MessageHooksConditional.cs +++ b/TestHelpers/Runtime/MessageHooksConditional.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -63,18 +64,34 @@ public MessageHooksConditional(List messageHookEntries) } } + public enum ReceiptType + { + Received, + Handled + } + public class MessageHookEntry { internal MessageHooks MessageHooks; protected NetworkManager m_NetworkManager; private MessageHooks.MessageReceiptCheck m_MessageReceiptCheck; + private MessageHooks.MessageHandleCheck m_MessageHandleCheck; internal string MessageType; + private ReceiptType m_ReceiptType; public void Initialize() { - Assert.IsNotNull(m_MessageReceiptCheck, $"{nameof(m_MessageReceiptCheck)} is null, did you forget to initialize?"); MessageHooks = new MessageHooks(); - MessageHooks.ReceiptCheck = m_MessageReceiptCheck; + if (m_ReceiptType == ReceiptType.Handled) + { + Assert.IsNotNull(m_MessageHandleCheck, $"{nameof(m_MessageHandleCheck)} is null, did you forget to initialize?"); + MessageHooks.HandleCheck = m_MessageHandleCheck; + } + else + { + Assert.IsNotNull(m_MessageReceiptCheck, $"{nameof(m_MessageReceiptCheck)} is null, did you forget to initialize?"); + MessageHooks.ReceiptCheck = m_MessageReceiptCheck; + } Assert.IsNotNull(m_NetworkManager.MessagingSystem, $"{nameof(NetworkManager.MessagingSystem)} is null! Did you forget to start first?"); m_NetworkManager.MessagingSystem.Hook(MessageHooks); } @@ -82,14 +99,41 @@ public void Initialize() internal void AssignMessageType() where T : INetworkMessage { MessageType = typeof(T).Name; - m_MessageReceiptCheck = MessageHooks.CheckForMessageOfType; + if (m_ReceiptType == ReceiptType.Handled) + { + m_MessageHandleCheck = MessageHooks.CheckForMessageOfTypeHandled; + } + else + { + m_MessageReceiptCheck = MessageHooks.CheckForMessageOfTypeReceived; + } + Initialize(); + } + + internal void AssignMessageType(Type type) + { + MessageType = type.Name; + if (m_ReceiptType == ReceiptType.Handled) + { + m_MessageHandleCheck = (message) => + { + return message.GetType() == type; + }; + } + else + { + m_MessageReceiptCheck = (messageType) => + { + return messageType == type; + }; + } Initialize(); } - public MessageHookEntry(NetworkManager networkManager) + public MessageHookEntry(NetworkManager networkManager, ReceiptType type = ReceiptType.Handled) { m_NetworkManager = networkManager; + m_ReceiptType = type; } } } - diff --git a/TestHelpers/Runtime/Metrics/RpcTestComponent.cs b/TestHelpers/Runtime/Metrics/RpcTestComponent.cs index 354a801..158f5d3 100644 --- a/TestHelpers/Runtime/Metrics/RpcTestComponent.cs +++ b/TestHelpers/Runtime/Metrics/RpcTestComponent.cs @@ -14,7 +14,7 @@ public void MyServerRpc() } [ClientRpc] - public void MyClientRpc() + public void MyClientRpc(ClientRpcParams rpcParams = default) { OnClientRpcAction?.Invoke(); } diff --git a/TestHelpers/Runtime/Metrics/WaitForEventMetricValues.cs b/TestHelpers/Runtime/Metrics/WaitForEventMetricValues.cs index 8378f16..a9af594 100644 --- a/TestHelpers/Runtime/Metrics/WaitForEventMetricValues.cs +++ b/TestHelpers/Runtime/Metrics/WaitForEventMetricValues.cs @@ -1,10 +1,6 @@ #if MULTIPLAYER_TOOLS -using System; -using System.Collections; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; -using NUnit.Framework; using Unity.Multiplayer.Tools.MetricTypes; using Unity.Multiplayer.Tools.NetStats; @@ -12,10 +8,11 @@ namespace Unity.Netcode.TestHelpers.Runtime.Metrics { internal class WaitForEventMetricValues : WaitForMetricValues { - IReadOnlyCollection m_EventValues; + private IReadOnlyCollection m_EventValues; public delegate bool EventFilter(TMetric metric); - EventFilter m_EventFilterDelegate; + + private EventFilter m_EventFilterDelegate; public WaitForEventMetricValues(IMetricDispatcher dispatcher, DirectionalMetricInfo directionalMetricName) : base(dispatcher, directionalMetricName) diff --git a/TestHelpers/Runtime/NetcodeIntegrationTest.cs b/TestHelpers/Runtime/NetcodeIntegrationTest.cs index 8ab5617..c2f3dfe 100644 --- a/TestHelpers/Runtime/NetcodeIntegrationTest.cs +++ b/TestHelpers/Runtime/NetcodeIntegrationTest.cs @@ -7,7 +7,7 @@ using UnityEngine.SceneManagement; using UnityEngine.TestTools; using System.Runtime.CompilerServices; - +using Unity.Netcode.RuntimeTests; using Object = UnityEngine.Object; namespace Unity.Netcode.TestHelpers.Runtime @@ -23,7 +23,9 @@ public abstract class NetcodeIntegrationTest /// internal static bool IsRunning { get; private set; } protected static TimeoutHelper s_GlobalTimeoutHelper = new TimeoutHelper(8.0f); - protected static WaitForSeconds s_DefaultWaitForTick = new WaitForSeconds(1.0f / k_DefaultTickRate); + protected static WaitForSecondsRealtime s_DefaultWaitForTick = new WaitForSecondsRealtime(1.0f / k_DefaultTickRate); + + public NetcodeLogAssert NetcodeLogAssert; /// /// Registered list of all NetworkObjects spawned. @@ -129,6 +131,18 @@ public enum HostOrServer protected bool m_EnableVerboseDebug { get; set; } + /// + /// When set to true, this will bypass the entire + /// wait for clients to connect process. + /// + /// + /// CAUTION: + /// Setting this to true will bypass other helper + /// identification related code, so this should only + /// be used for connection failure oriented testing + /// + protected bool m_BypassConnectionTimeout { get; set; } + /// /// Used to display the various integration test /// stages and can be used to log verbose information @@ -207,6 +221,7 @@ public IEnumerator SetUp() { VerboseDebug($"Entering {nameof(SetUp)}"); + NetcodeLogAssert = new NetcodeLogAssert(); yield return OnSetup(); if (m_NetworkManagerInstatiationMode == NetworkManagerInstatiationMode.AllTests && m_ServerNetworkManager == null || m_NetworkManagerInstatiationMode == NetworkManagerInstatiationMode.PerTest) @@ -336,7 +351,7 @@ protected void CreateServerAndClients(int numberOfClients) if (m_ServerNetworkManager != null) { - s_DefaultWaitForTick = new WaitForSeconds(1.0f / m_ServerNetworkManager.NetworkConfig.TickRate); + s_DefaultWaitForTick = new WaitForSecondsRealtime(1.0f / m_ServerNetworkManager.NetworkConfig.TickRate); } // Set the player prefab for the server and clients @@ -452,31 +467,36 @@ protected IEnumerator StartServerAndClients() // Notification that the server and clients have been started yield return OnStartedServerAndClients(); - // Wait for all clients to connect - yield return WaitForClientsConnectedOrTimeOut(); - AssertOnTimeout($"{nameof(StartServerAndClients)} timed out waiting for all clients to be connected!"); - - if (m_UseHost || m_ServerNetworkManager.IsHost) + // When true, we skip everything else (most likely a connection oriented test) + if (!m_BypassConnectionTimeout) { - // Add the server player instance to all m_ClientSidePlayerNetworkObjects entries - var serverPlayerClones = Object.FindObjectsOfType().Where((c) => c.IsPlayerObject && c.OwnerClientId == m_ServerNetworkManager.LocalClientId); - foreach (var playerNetworkObject in serverPlayerClones) + // Wait for all clients to connect + yield return WaitForClientsConnectedOrTimeOut(); + + AssertOnTimeout($"{nameof(StartServerAndClients)} timed out waiting for all clients to be connected!"); + + if (m_UseHost || m_ServerNetworkManager.IsHost) { - if (!m_PlayerNetworkObjects.ContainsKey(playerNetworkObject.NetworkManager.LocalClientId)) + // Add the server player instance to all m_ClientSidePlayerNetworkObjects entries + var serverPlayerClones = Object.FindObjectsOfType().Where((c) => c.IsPlayerObject && c.OwnerClientId == m_ServerNetworkManager.LocalClientId); + foreach (var playerNetworkObject in serverPlayerClones) { - m_PlayerNetworkObjects.Add(playerNetworkObject.NetworkManager.LocalClientId, new Dictionary()); + if (!m_PlayerNetworkObjects.ContainsKey(playerNetworkObject.NetworkManager.LocalClientId)) + { + m_PlayerNetworkObjects.Add(playerNetworkObject.NetworkManager.LocalClientId, new Dictionary()); + } + m_PlayerNetworkObjects[playerNetworkObject.NetworkManager.LocalClientId].Add(m_ServerNetworkManager.LocalClientId, playerNetworkObject); } - m_PlayerNetworkObjects[playerNetworkObject.NetworkManager.LocalClientId].Add(m_ServerNetworkManager.LocalClientId, playerNetworkObject); } - } - ClientNetworkManagerPostStartInit(); + ClientNetworkManagerPostStartInit(); - // Notification that at this time the server and client(s) are instantiated, - // started, and connected on both sides. - yield return OnServerAndClientsConnected(); + // Notification that at this time the server and client(s) are instantiated, + // started, and connected on both sides. + yield return OnServerAndClientsConnected(); - VerboseDebug($"Exiting {nameof(StartServerAndClients)}"); + VerboseDebug($"Exiting {nameof(StartServerAndClients)}"); + } } } @@ -571,7 +591,7 @@ protected void ShutdownAndCleanUp() UnloadRemainingScenes(); // reset the m_ServerWaitForTick for the next test to initialize - s_DefaultWaitForTick = new WaitForSeconds(1.0f / k_DefaultTickRate); + s_DefaultWaitForTick = new WaitForSecondsRealtime(1.0f / k_DefaultTickRate); VerboseDebug($"Exiting {nameof(ShutdownAndCleanUp)}"); } @@ -596,6 +616,7 @@ public IEnumerator TearDown() } VerboseDebug($"Exiting {nameof(TearDown)}"); + NetcodeLogAssert.Dispose(); } /// @@ -758,6 +779,41 @@ protected IEnumerator WaitForClientsConnectedOrTimeOut() yield return WaitForClientsConnectedOrTimeOut(m_ClientNetworkManagers); } + internal IEnumerator WaitForMessageReceived(List wiatForReceivedBy, ReceiptType type = ReceiptType.Handled) where T : INetworkMessage + { + // Build our message hook entries tables so we can determine if all clients received spawn or ownership messages + var messageHookEntriesForSpawn = new List(); + foreach (var clientNetworkManager in wiatForReceivedBy) + { + var messageHook = new MessageHookEntry(clientNetworkManager, type); + messageHook.AssignMessageType(); + messageHookEntriesForSpawn.Add(messageHook); + } + // Used to determine if all clients received the CreateObjectMessage + var hooks = new MessageHooksConditional(messageHookEntriesForSpawn); + yield return WaitForConditionOrTimeOut(hooks); + Assert.False(s_GlobalTimeoutHelper.TimedOut); + } + + internal IEnumerator WaitForMessagesReceived(List messagesInOrder, List wiatForReceivedBy, ReceiptType type = ReceiptType.Handled) + { + // Build our message hook entries tables so we can determine if all clients received spawn or ownership messages + var messageHookEntriesForSpawn = new List(); + foreach (var clientNetworkManager in wiatForReceivedBy) + { + foreach (var message in messagesInOrder) + { + var messageHook = new MessageHookEntry(clientNetworkManager, type); + messageHook.AssignMessageType(message); + messageHookEntriesForSpawn.Add(messageHook); + } + } + // Used to determine if all clients received the CreateObjectMessage + var hooks = new MessageHooksConditional(messageHookEntriesForSpawn); + yield return WaitForConditionOrTimeOut(hooks); + Assert.False(s_GlobalTimeoutHelper.TimedOut); + } + /// /// Creates a basic NetworkObject test prefab, assigns it to a new /// NetworkPrefab entry, and then adds it to the server and client(s) diff --git a/TestHelpers/Runtime/NetcodeLogAssert.cs b/TestHelpers/Runtime/NetcodeLogAssert.cs new file mode 100644 index 0000000..d6e4136 --- /dev/null +++ b/TestHelpers/Runtime/NetcodeLogAssert.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using NUnit.Framework; +using UnityEngine; + +namespace Unity.Netcode.RuntimeTests +{ + public class NetcodeLogAssert + { + private struct LogData + { + public LogType LogType; + public string Message; + public string StackTrace; + } + + private readonly object m_Lock = new object(); + private bool m_Disposed; + + private List AllLogs { get; } + + public NetcodeLogAssert() + { + AllLogs = new List(); + Activate(); + } + + private void Activate() + { + Application.logMessageReceivedThreaded += AddLog; + } + + private void Deactivate() + { + Application.logMessageReceivedThreaded -= AddLog; + } + + public void AddLog(string message, string stacktrace, LogType type) + { + lock (m_Lock) + { + var log = new LogData + { + LogType = type, + Message = message, + StackTrace = stacktrace, + }; + + AllLogs.Add(log); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (m_Disposed) + { + return; + } + + m_Disposed = true; + + if (disposing) + { + Deactivate(); + } + } + + public void LogWasNotReceived(LogType type, string message) + { + lock (m_Lock) + { + foreach (var logEvent in AllLogs) + { + if (logEvent.LogType == type && message.Equals(logEvent.Message)) + { + Assert.Fail($"Unexpected log: [{logEvent.LogType}] {logEvent.Message}"); + } + } + } + } + + public void LogWasNotReceived(LogType type, Regex messageRegex) + { + lock (m_Lock) + { + foreach (var logEvent in AllLogs) + { + if (logEvent.LogType == type && messageRegex.IsMatch(logEvent.Message)) + { + Assert.Fail($"Unexpected log: [{logEvent.LogType}] {logEvent.Message}"); + } + } + } + } + + public void LogWasReceived(LogType type, string message) + { + lock (m_Lock) + { + var found = false; + foreach (var logEvent in AllLogs) + { + if (logEvent.LogType == type && message.Equals(logEvent.Message)) + { + found = true; + break; + } + } + + if (!found) + { + Assert.Fail($"Expected log was not received: [{type}] {message}"); + } + } + } + + public void LogWasReceived(LogType type, Regex messageRegex) + { + lock (m_Lock) + { + var found = false; + foreach (var logEvent in AllLogs) + { + if (logEvent.LogType == type && messageRegex.IsMatch(logEvent.Message)) + { + found = true; + } + } + + if (!found) + { + Assert.Fail($"Expected log was not received: [{type}] {messageRegex}"); + } + } + } + + public void Reset() + { + lock (m_Lock) + { + AllLogs.Clear(); + } + } + } +} diff --git a/TestHelpers/Runtime/NetcodeLogAssert.cs.meta b/TestHelpers/Runtime/NetcodeLogAssert.cs.meta new file mode 100644 index 0000000..af86928 --- /dev/null +++ b/TestHelpers/Runtime/NetcodeLogAssert.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 61774c54cd14423ca4de6d56c9fd0fe8 +timeCreated: 1661800793 \ No newline at end of file diff --git a/TestHelpers/Runtime/NetworkVariableHelper.cs b/TestHelpers/Runtime/NetworkVariableHelper.cs index 880c14f..343e0bb 100644 --- a/TestHelpers/Runtime/NetworkVariableHelper.cs +++ b/TestHelpers/Runtime/NetworkVariableHelper.cs @@ -13,7 +13,7 @@ namespace Unity.Netcode.TestHelpers.Runtime /// From both we can then at least determine if the value indeed changed /// /// - public class NetworkVariableHelper : NetworkVariableBaseHelper where T : unmanaged + public class NetworkVariableHelper : NetworkVariableBaseHelper { private readonly NetworkVariable m_NetworkVariable; public delegate void OnMyValueChangedDelegateHandler(T previous, T next); @@ -50,7 +50,7 @@ private void OnVariableChanged(T previous, T next) { if (previous is ValueType testValueType) { - CheckVariableChanged(previous, next); + CheckVariableChanged(previous as ValueType, next as ValueType); } else { diff --git a/Tests/Editor/Messaging/MessageRegistrationTests.cs b/Tests/Editor/Messaging/MessageRegistrationTests.cs index 1228712..38ca9b0 100644 --- a/Tests/Editor/Messaging/MessageRegistrationTests.cs +++ b/Tests/Editor/Messaging/MessageRegistrationTests.cs @@ -137,9 +137,9 @@ public void WhenCreatingMessageSystem_OnlyProvidedTypesAreRegistered() { var sender = new NopMessageSender(); - var systemOne = new MessagingSystem(sender, null, new TestMessageProviderOne()); - var systemTwo = new MessagingSystem(sender, null, new TestMessageProviderTwo()); - var systemThree = new MessagingSystem(sender, null, new TestMessageProviderThree()); + using var systemOne = new MessagingSystem(sender, null, new TestMessageProviderOne()); + using var systemTwo = new MessagingSystem(sender, null, new TestMessageProviderTwo()); + using var systemThree = new MessagingSystem(sender, null, new TestMessageProviderThree()); using (systemOne) using (systemTwo) @@ -161,9 +161,9 @@ public void WhenCreatingMessageSystem_BoundTypeMessageHandlersAreRegistered() { var sender = new NopMessageSender(); - var systemOne = new MessagingSystem(sender, null, new TestMessageProviderOne()); - var systemTwo = new MessagingSystem(sender, null, new TestMessageProviderTwo()); - var systemThree = new MessagingSystem(sender, null, new TestMessageProviderThree()); + using var systemOne = new MessagingSystem(sender, null, new TestMessageProviderOne()); + using var systemTwo = new MessagingSystem(sender, null, new TestMessageProviderTwo()); + using var systemThree = new MessagingSystem(sender, null, new TestMessageProviderThree()); using (systemOne) using (systemTwo) @@ -235,7 +235,7 @@ public void MessagesGetPrioritizedCorrectly() { var sender = new NopMessageSender(); var provider = new OrderingMessageProvider(); - var messagingSystem = new MessagingSystem(sender, null, provider); + using var messagingSystem = new MessagingSystem(sender, null, provider); // the 3 priority messages should appear first, in lexicographic order Assert.AreEqual(messagingSystem.MessageTypes[0], typeof(ConnectionApprovedMessage)); diff --git a/Tests/Editor/Metrics/NetworkMetricsRegistrationTests.cs b/Tests/Editor/Metrics/NetworkMetricsRegistrationTests.cs index 56fb132..e962c86 100644 --- a/Tests/Editor/Metrics/NetworkMetricsRegistrationTests.cs +++ b/Tests/Editor/Metrics/NetworkMetricsRegistrationTests.cs @@ -10,12 +10,13 @@ namespace Unity.Netcode.EditorTests.Metrics { public class NetworkMetricsRegistrationTests { - static Type[] s_MetricTypes = AppDomain.CurrentDomain.GetAssemblies() + private static Type[] s_MetricTypes = AppDomain.CurrentDomain.GetAssemblies() .SelectMany(x => x.GetTypes()) .Where(x => x.GetInterfaces().Contains(typeof(INetworkMetricEvent))) .ToArray(); - [TestCaseSource(nameof(s_MetricTypes))][Ignore("Disable test while we reevaluate the assumption that INetworkMetricEvent interfaces must be reported from MLAPI.")] + [TestCaseSource(nameof(s_MetricTypes))] + [Ignore("Disable test while we reevaluate the assumption that INetworkMetricEvent interfaces must be reported from MLAPI.")] public void ValidateThatAllMetricTypesAreRegistered(Type metricType) { var dispatcher = new NetworkMetrics().Dispatcher as MetricDispatcher; diff --git a/Tests/Editor/Transports/BatchedSendQueueTests.cs b/Tests/Editor/Transports/BatchedSendQueueTests.cs index 34a8a53..eac6b73 100644 --- a/Tests/Editor/Transports/BatchedSendQueueTests.cs +++ b/Tests/Editor/Transports/BatchedSendQueueTests.cs @@ -8,8 +8,9 @@ namespace Unity.Netcode.EditorTests { public class BatchedSendQueueTests { - private const int k_TestQueueCapacity = 1024; - private const int k_TestMessageSize = 42; + private const int k_TestQueueCapacity = 16 * 1024; + private const int k_TestMessageSize = 1020; + private const int k_NumMessagesToFillQueue = k_TestQueueCapacity / (k_TestMessageSize + BatchedSendQueue.PerMessageOverhead); private ArraySegment m_TestMessage; @@ -52,7 +53,14 @@ public void BatchedSendQueue_NotCreatedAfterDispose() } [Test] - public void BatchedSendQueue_PushMessageReturnValue() + public void BatchedSendQueue_InitialCapacityLessThanMaximum() + { + using var q = new BatchedSendQueue(k_TestQueueCapacity); + Assert.AreEqual(q.Capacity, BatchedSendQueue.MinimumMinimumCapacity); + } + + [Test] + public void BatchedSendQueue_PushMessage_ReturnValue() { // Will fit a single test message, but not two (with overhead included). var queueCapacity = (k_TestMessageSize * 2) + BatchedSendQueue.PerMessageOverhead; @@ -64,7 +72,7 @@ public void BatchedSendQueue_PushMessageReturnValue() } [Test] - public void BatchedSendQueue_LengthIncreasedAfterPush() + public void BatchedSendQueue_PushMessage_IncreasesLength() { using var q = new BatchedSendQueue(k_TestQueueCapacity); @@ -73,13 +81,12 @@ public void BatchedSendQueue_LengthIncreasedAfterPush() } [Test] - public void BatchedSendQueue_PushedMessageGeneratesCopy() + public void BatchedSendQueue_PushMessage_SucceedsAfterConsume() { var messageLength = k_TestMessageSize + BatchedSendQueue.PerMessageOverhead; var queueCapacity = messageLength * 2; using var q = new BatchedSendQueue(queueCapacity); - using var data = new NativeArray(k_TestQueueCapacity, Allocator.Temp); q.PushMessage(m_TestMessage); q.PushMessage(m_TestMessage); @@ -89,6 +96,60 @@ public void BatchedSendQueue_PushedMessageGeneratesCopy() Assert.AreEqual(queueCapacity, q.Length); } + [Test] + public void BatchedSendQueue_PushMessage_GrowsDataIfNeeded() + { + using var q = new BatchedSendQueue(k_TestQueueCapacity); + var messageLength = k_TestMessageSize + BatchedSendQueue.PerMessageOverhead; + + Assert.AreEqual(q.Capacity, BatchedSendQueue.MinimumMinimumCapacity); + + var numMessagesToFillMinimum = BatchedSendQueue.MinimumMinimumCapacity / messageLength; + for (int i = 0; i < numMessagesToFillMinimum; i++) + { + q.PushMessage(m_TestMessage); + } + + Assert.AreEqual(q.Capacity, BatchedSendQueue.MinimumMinimumCapacity); + + q.PushMessage(m_TestMessage); + + Assert.AreEqual(q.Capacity, BatchedSendQueue.MinimumMinimumCapacity * 2); + } + + [Test] + public void BatchedSendQueue_PushMessage_DoesNotGrowDataPastMaximum() + { + using var q = new BatchedSendQueue(k_TestQueueCapacity); + + for (int i = 0; i < k_NumMessagesToFillQueue; i++) + { + Assert.IsTrue(q.PushMessage(m_TestMessage)); + } + + Assert.AreEqual(q.Capacity, k_TestQueueCapacity); + Assert.IsFalse(q.PushMessage(m_TestMessage)); + Assert.AreEqual(q.Capacity, k_TestQueueCapacity); + } + + [Test] + public void BatchedSendQueue_PushMessage_TrimsDataAfterGrowing() + { + using var q = new BatchedSendQueue(k_TestQueueCapacity); + var messageLength = k_TestMessageSize + BatchedSendQueue.PerMessageOverhead; + + for (int i = 0; i < k_NumMessagesToFillQueue; i++) + { + Assert.IsTrue(q.PushMessage(m_TestMessage)); + } + + Assert.AreEqual(q.Capacity, k_TestQueueCapacity); + q.Consume(messageLength * (k_NumMessagesToFillQueue - 1)); + Assert.IsTrue(q.PushMessage(m_TestMessage)); + Assert.AreEqual(messageLength * 2, q.Length); + Assert.AreEqual(q.Capacity, BatchedSendQueue.MinimumMinimumCapacity * 2); + } + [Test] public void BatchedSendQueue_FillWriterWithMessages_ReturnValue() { @@ -227,7 +288,7 @@ public void BatchedSendQueue_FillWriterWithBytes_WriterCapacityEqualToLength() } [Test] - public void BatchedSendQueue_ConsumeLessThanLength() + public void BatchedSendQueue_Consume_LessThanLength() { using var q = new BatchedSendQueue(k_TestQueueCapacity); @@ -240,7 +301,7 @@ public void BatchedSendQueue_ConsumeLessThanLength() } [Test] - public void BatchedSendQueue_ConsumeExactLength() + public void BatchedSendQueue_Consume_ExactLength() { using var q = new BatchedSendQueue(k_TestQueueCapacity); @@ -252,7 +313,7 @@ public void BatchedSendQueue_ConsumeExactLength() } [Test] - public void BatchedSendQueue_ConsumeMoreThanLength() + public void BatchedSendQueue_Consume_MoreThanLength() { using var q = new BatchedSendQueue(k_TestQueueCapacity); @@ -262,5 +323,20 @@ public void BatchedSendQueue_ConsumeMoreThanLength() Assert.AreEqual(0, q.Length); Assert.True(q.IsEmpty); } + + [Test] + public void BatchedSendQueue_Consume_TrimsDataOnEmpty() + { + using var q = new BatchedSendQueue(k_TestQueueCapacity); + + for (int i = 0; i < k_NumMessagesToFillQueue; i++) + { + q.PushMessage(m_TestMessage); + } + + Assert.AreEqual(q.Capacity, k_TestQueueCapacity); + q.Consume(k_TestQueueCapacity); + Assert.AreEqual(q.Capacity, BatchedSendQueue.MinimumMinimumCapacity); + } } } diff --git a/Tests/Editor/Transports/UnityTransportTests.cs b/Tests/Editor/Transports/UnityTransportTests.cs index 20b83b0..fa53588 100644 --- a/Tests/Editor/Transports/UnityTransportTests.cs +++ b/Tests/Editor/Transports/UnityTransportTests.cs @@ -1,14 +1,15 @@ using NUnit.Framework; using Unity.Netcode.Transports.UTP; using UnityEngine; +using UnityEngine.TestTools; namespace Unity.Netcode.EditorTests { public class UnityTransportTests { - // Check that starting a server doesn't immediately result in faulted tasks. + // Check that starting an IPv4 server succeeds. [Test] - public void BasicInitServer() + public void UnityTransport_BasicInitServer_IPv4() { UnityTransport transport = new GameObject().AddComponent(); transport.Initialize(); @@ -18,9 +19,9 @@ public void BasicInitServer() transport.Shutdown(); } - // Check that starting a client doesn't immediately result in faulted tasks. + // Check that starting an IPv4 client succeeds. [Test] - public void BasicInitClient() + public void UnityTransport_BasicInitClient_IPv4() { UnityTransport transport = new GameObject().AddComponent(); transport.Initialize(); @@ -30,9 +31,35 @@ public void BasicInitClient() transport.Shutdown(); } + // Check that starting an IPv6 server succeeds. + [Test] + public void UnityTransport_BasicInitServer_IPv6() + { + UnityTransport transport = new GameObject().AddComponent(); + transport.Initialize(); + transport.SetConnectionData("::1", 7777); + + Assert.True(transport.StartServer()); + + transport.Shutdown(); + } + + // Check that starting an IPv6 client succeeds. + [Test] + public void UnityTransport_BasicInitClient_IPv6() + { + UnityTransport transport = new GameObject().AddComponent(); + transport.Initialize(); + transport.SetConnectionData("::1", 7777); + + Assert.True(transport.StartClient()); + + transport.Shutdown(); + } + // Check that we can't restart a server. [Test] - public void NoRestartServer() + public void UnityTransport_NoRestartServer() { UnityTransport transport = new GameObject().AddComponent(); transport.Initialize(); @@ -45,7 +72,7 @@ public void NoRestartServer() // Check that we can't restart a client. [Test] - public void NoRestartClient() + public void UnityTransport_NoRestartClient() { UnityTransport transport = new GameObject().AddComponent(); transport.Initialize(); @@ -58,7 +85,7 @@ public void NoRestartClient() // Check that we can't start both a server and client on the same transport. [Test] - public void NotBothServerAndClient() + public void UnityTransport_NotBothServerAndClient() { UnityTransport transport; @@ -80,5 +107,24 @@ public void NotBothServerAndClient() transport.Shutdown(); } + + // Check that restarting after failure succeeds. + [Test] + public void UnityTransport_RestartSucceedsAfterFailure() + { + UnityTransport transport = new GameObject().AddComponent(); + transport.Initialize(); + + transport.SetConnectionData("127.0.0.", 4242); + Assert.False(transport.StartServer()); + + LogAssert.Expect(LogType.Error, "Invalid network endpoint: 127.0.0.:4242."); + LogAssert.Expect(LogType.Error, "Server failed to bind"); + + transport.SetConnectionData("127.0.0.1", 4242); + Assert.True(transport.StartServer()); + + transport.Shutdown(); + } } } diff --git a/Tests/Runtime/AddNetworkPrefabTests.cs b/Tests/Runtime/AddNetworkPrefabTests.cs index fdf2cb4..5ed703b 100644 --- a/Tests/Runtime/AddNetworkPrefabTests.cs +++ b/Tests/Runtime/AddNetworkPrefabTests.cs @@ -22,17 +22,18 @@ protected override IEnumerator OnSetup() // Host is irrelevant, messages don't get sent to the host "client" m_UseHost = false; + yield return null; + } + + protected override void OnServerAndClientsCreated() + { m_Prefab = new GameObject("Object"); var networkObject = m_Prefab.AddComponent(); m_Prefab.AddComponent(); // Make it a prefab NetcodeIntegrationTestHelpers.MakeNetworkObjectTestPrefab(networkObject); - yield return null; - } - protected override void OnServerAndClientsCreated() - { m_ServerNetworkManager.NetworkConfig.SpawnTimeout = 0; m_ServerNetworkManager.NetworkConfig.ForceSamePrefabs = false; foreach (var client in m_ClientNetworkManagers) diff --git a/Tests/Runtime/Components/NetworkVariableTestComponent.cs b/Tests/Runtime/Components/NetworkVariableTestComponent.cs index 6b24f18..5f1788f 100644 --- a/Tests/Runtime/Components/NetworkVariableTestComponent.cs +++ b/Tests/Runtime/Components/NetworkVariableTestComponent.cs @@ -1,8 +1,161 @@ +using System; +using NUnit.Framework; +using Unity.Collections; using UnityEngine; using Unity.Netcode.TestHelpers.Runtime; namespace Unity.Netcode.RuntimeTests { + public class ManagedNetworkSerializableType : INetworkSerializable, IEquatable + { + public string Str = ""; + public int[] Ints = Array.Empty(); + public int InMemoryValue; + + public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter + { + serializer.SerializeValue(ref Str, true); + var length = Ints.Length; + serializer.SerializeValue(ref length); + if (serializer.IsReader) + { + Ints = new int[length]; + } + + for (var i = 0; i < length; ++i) + { + var val = Ints[i]; + serializer.SerializeValue(ref val); + Ints[i] = val; + } + } + + public bool Equals(ManagedNetworkSerializableType other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + if (Str != other.Str) + { + return false; + } + + if (Ints.Length != other.Ints.Length) + { + return false; + } + + for (var i = 0; i < Ints.Length; ++i) + { + if (Ints[i] != other.Ints[i]) + { + return false; + } + } + + return true; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((ManagedNetworkSerializableType)obj); + } + + public override int GetHashCode() + { + return 0; + } + } + public struct UnmanagedNetworkSerializableType : INetworkSerializable, IEquatable + { + public FixedString32Bytes Str; + public int Int; + public int InMemoryValue; + + public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter + { + serializer.SerializeValue(ref Str); + serializer.SerializeValue(ref Int); + } + + public bool Equals(UnmanagedNetworkSerializableType other) + { + return Str.Equals(other.Str) && Int == other.Int; + } + + public override bool Equals(object obj) + { + if (obj == null) + { + return false; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((ManagedNetworkSerializableType)obj); + } + + public override int GetHashCode() + { + return Str.GetHashCode() ^ Int.GetHashCode() ^ InMemoryValue.GetHashCode(); + } + } + + + public struct UnmanagedTemplateNetworkSerializableType : INetworkSerializable where T : unmanaged, INetworkSerializable + { + public T Value; + + public void NetworkSerialize(BufferSerializer serializer) where TReaderWriterType : IReaderWriter + { + serializer.SerializeValue(ref Value); + } + } + + public struct ManagedTemplateNetworkSerializableType : INetworkSerializable where T : class, INetworkSerializable, new() + { + public T Value; + + public void NetworkSerialize(BufferSerializer serializer) where TReaderWriterType : IReaderWriter + { + bool isNull = Value == null; + serializer.SerializeValue(ref isNull); + if (!isNull) + { + if (Value == null) + { + Value = new T(); + } + serializer.SerializeValue(ref Value); + } + } + } + /// /// This provides coverage for all of the predefined NetworkVariable types /// The initial goal is for generalized full coverage of NetworkVariables: @@ -30,6 +183,12 @@ internal class NetworkVariableTestComponent : NetworkBehaviour private NetworkVariable m_NetworkVariableULong = new NetworkVariable(); private NetworkVariable m_NetworkVariableUInt = new NetworkVariable(); private NetworkVariable m_NetworkVariableUShort = new NetworkVariable(); + private NetworkVariable m_NetworkVariableFixedString32 = new NetworkVariable(); + private NetworkVariable m_NetworkVariableFixedString64 = new NetworkVariable(); + private NetworkVariable m_NetworkVariableFixedString128 = new NetworkVariable(); + private NetworkVariable m_NetworkVariableFixedString512 = new NetworkVariable(); + private NetworkVariable m_NetworkVariableFixedString4096 = new NetworkVariable(); + private NetworkVariable m_NetworkVariableManaged = new NetworkVariable(); public NetworkVariableHelper Bool_Var; @@ -50,6 +209,12 @@ internal class NetworkVariableTestComponent : NetworkBehaviour public NetworkVariableHelper Ulong_Var; public NetworkVariableHelper Uint_Var; public NetworkVariableHelper Ushort_Var; + public NetworkVariableHelper FixedString32_Var; + public NetworkVariableHelper FixedString64_Var; + public NetworkVariableHelper FixedString128_Var; + public NetworkVariableHelper FixedString512_Var; + public NetworkVariableHelper FixedString4096_Var; + public NetworkVariableHelper Managed_Var; public bool EnableTesting; @@ -80,6 +245,12 @@ private void InitializeTest() m_NetworkVariableULong = new NetworkVariable(); m_NetworkVariableUInt = new NetworkVariable(); m_NetworkVariableUShort = new NetworkVariable(); + m_NetworkVariableFixedString32 = new NetworkVariable(); + m_NetworkVariableFixedString64 = new NetworkVariable(); + m_NetworkVariableFixedString128 = new NetworkVariable(); + m_NetworkVariableFixedString512 = new NetworkVariable(); + m_NetworkVariableFixedString4096 = new NetworkVariable(); + m_NetworkVariableManaged = new NetworkVariable(); // NetworkVariable Value Type Constructor Test Coverage @@ -101,6 +272,16 @@ private void InitializeTest() m_NetworkVariableULong = new NetworkVariable(1); m_NetworkVariableUInt = new NetworkVariable(1); m_NetworkVariableUShort = new NetworkVariable(1); + m_NetworkVariableFixedString32 = new NetworkVariable("1234567890"); + m_NetworkVariableFixedString64 = new NetworkVariable("1234567890"); + m_NetworkVariableFixedString128 = new NetworkVariable("1234567890"); + m_NetworkVariableFixedString512 = new NetworkVariable("1234567890"); + m_NetworkVariableFixedString4096 = new NetworkVariable("1234567890"); + m_NetworkVariableManaged = new NetworkVariable(new ManagedNetworkSerializableType + { + Str = "1234567890", + Ints = new[] { 1, 2, 3, 4, 5 } + }); // Use this nifty class: NetworkVariableHelper // Tracks if NetworkVariable changed invokes the OnValueChanged callback for the given instance type @@ -122,6 +303,12 @@ private void InitializeTest() Ulong_Var = new NetworkVariableHelper(m_NetworkVariableULong); Uint_Var = new NetworkVariableHelper(m_NetworkVariableUInt); Ushort_Var = new NetworkVariableHelper(m_NetworkVariableUShort); + FixedString32_Var = new NetworkVariableHelper(m_NetworkVariableFixedString32); + FixedString64_Var = new NetworkVariableHelper(m_NetworkVariableFixedString64); + FixedString128_Var = new NetworkVariableHelper(m_NetworkVariableFixedString128); + FixedString512_Var = new NetworkVariableHelper(m_NetworkVariableFixedString512); + FixedString4096_Var = new NetworkVariableHelper(m_NetworkVariableFixedString4096); + Managed_Var = new NetworkVariableHelper(m_NetworkVariableManaged); } /// @@ -152,6 +339,57 @@ public void Awake() InitializeTest(); } + public void AssertAllValuesAreCorrect() + { + Assert.AreEqual(false, m_NetworkVariableBool.Value); + Assert.AreEqual(255, m_NetworkVariableByte.Value); + Assert.AreEqual(100, m_NetworkVariableColor.Value.r); + Assert.AreEqual(100, m_NetworkVariableColor.Value.g); + Assert.AreEqual(100, m_NetworkVariableColor.Value.b); + Assert.AreEqual(100, m_NetworkVariableColor32.Value.r); + Assert.AreEqual(100, m_NetworkVariableColor32.Value.g); + Assert.AreEqual(100, m_NetworkVariableColor32.Value.b); + Assert.AreEqual(100, m_NetworkVariableColor32.Value.a); + Assert.AreEqual(1000, m_NetworkVariableDouble.Value); + Assert.AreEqual(1000.0f, m_NetworkVariableFloat.Value); + Assert.AreEqual(1000, m_NetworkVariableInt.Value); + Assert.AreEqual(100000, m_NetworkVariableLong.Value); + Assert.AreEqual(-127, m_NetworkVariableSByte.Value); + Assert.AreEqual(100, m_NetworkVariableQuaternion.Value.w); + Assert.AreEqual(100, m_NetworkVariableQuaternion.Value.x); + Assert.AreEqual(100, m_NetworkVariableQuaternion.Value.y); + Assert.AreEqual(100, m_NetworkVariableQuaternion.Value.z); + Assert.AreEqual(short.MaxValue, m_NetworkVariableShort.Value); + Assert.AreEqual(1000, m_NetworkVariableVector4.Value.w); + Assert.AreEqual(1000, m_NetworkVariableVector4.Value.x); + Assert.AreEqual(1000, m_NetworkVariableVector4.Value.y); + Assert.AreEqual(1000, m_NetworkVariableVector4.Value.z); + Assert.AreEqual(1000, m_NetworkVariableVector3.Value.x); + Assert.AreEqual(1000, m_NetworkVariableVector3.Value.y); + Assert.AreEqual(1000, m_NetworkVariableVector3.Value.z); + Assert.AreEqual(1000, m_NetworkVariableVector2.Value.x); + Assert.AreEqual(1000, m_NetworkVariableVector2.Value.y); + Assert.AreEqual(Vector3.one.x, m_NetworkVariableRay.Value.origin.x); + Assert.AreEqual(Vector3.one.y, m_NetworkVariableRay.Value.origin.y); + Assert.AreEqual(Vector3.one.z, m_NetworkVariableRay.Value.origin.z); + Assert.AreEqual(Vector3.right.x, m_NetworkVariableRay.Value.direction.x); + Assert.AreEqual(Vector3.right.y, m_NetworkVariableRay.Value.direction.y); + Assert.AreEqual(Vector3.right.z, m_NetworkVariableRay.Value.direction.z); + Assert.AreEqual(ulong.MaxValue, m_NetworkVariableULong.Value); + Assert.AreEqual(uint.MaxValue, m_NetworkVariableUInt.Value); + Assert.AreEqual(ushort.MaxValue, m_NetworkVariableUShort.Value); + Assert.IsTrue(m_NetworkVariableFixedString32.Value.Equals("FixedString32Bytes")); + Assert.IsTrue(m_NetworkVariableFixedString64.Value.Equals("FixedString64Bytes")); + Assert.IsTrue(m_NetworkVariableFixedString128.Value.Equals("FixedString128Bytes")); + Assert.IsTrue(m_NetworkVariableFixedString512.Value.Equals("FixedString512Bytes")); + Assert.IsTrue(m_NetworkVariableFixedString4096.Value.Equals("FixedString4096Bytes")); + Assert.IsTrue(m_NetworkVariableManaged.Value.Equals(new ManagedNetworkSerializableType + { + Str = "ManagedNetworkSerializableType", + Ints = new[] { 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000 } + })); + } + // Update is called once per frame private void Update() { @@ -187,6 +425,16 @@ private void Update() m_NetworkVariableULong.Value = ulong.MaxValue; m_NetworkVariableUInt.Value = uint.MaxValue; m_NetworkVariableUShort.Value = ushort.MaxValue; + m_NetworkVariableFixedString32.Value = new FixedString32Bytes("FixedString32Bytes"); + m_NetworkVariableFixedString64.Value = new FixedString64Bytes("FixedString64Bytes"); + m_NetworkVariableFixedString128.Value = new FixedString128Bytes("FixedString128Bytes"); + m_NetworkVariableFixedString512.Value = new FixedString512Bytes("FixedString512Bytes"); + m_NetworkVariableFixedString4096.Value = new FixedString4096Bytes("FixedString4096Bytes"); + m_NetworkVariableManaged.Value = new ManagedNetworkSerializableType + { + Str = "ManagedNetworkSerializableType", + Ints = new[] { 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000 } + }; //Set the timeout (i.e. how long we will wait for all NetworkVariables to have registered their changes) m_WaitForChangesTimeout = Time.realtimeSinceStartup + 0.50f; diff --git a/Tests/Runtime/ConnectionApproval.cs b/Tests/Runtime/ConnectionApproval.cs index 8c8f6e5..4e7ec95 100644 --- a/Tests/Runtime/ConnectionApproval.cs +++ b/Tests/Runtime/ConnectionApproval.cs @@ -62,11 +62,27 @@ private void NetworkManagerObject_ConnectionApprovalCallback(NetworkManager.Conn response.PlayerPrefabHash = null; } + + [Test] + public void VerifyUniqueNetworkConfigPerRequest() + { + var networkConfig = new NetworkConfig(); + networkConfig.EnableSceneManagement = true; + networkConfig.TickRate = 30; + var currentHash = networkConfig.GetConfig(); + networkConfig.EnableSceneManagement = false; + networkConfig.TickRate = 60; + var newHash = networkConfig.GetConfig(false); + + Assert.True(currentHash != newHash, $"Hashed {nameof(NetworkConfig)} values {currentHash} and {newHash} should not be the same!"); + } + [TearDown] public void TearDown() { // Stop, shutdown, and destroy NetworkManagerHelper.ShutdownNetworkManager(); } + } } diff --git a/Tests/Runtime/ConnectionApprovalTimeoutTests.cs b/Tests/Runtime/ConnectionApprovalTimeoutTests.cs new file mode 100644 index 0000000..9f58c59 --- /dev/null +++ b/Tests/Runtime/ConnectionApprovalTimeoutTests.cs @@ -0,0 +1,101 @@ +using System.Collections; +using System.Text.RegularExpressions; +using NUnit.Framework; +using Unity.Netcode.TestHelpers.Runtime; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests +{ + [TestFixture(ApprovalTimedOutTypes.ServerDoesNotRespond)] + [TestFixture(ApprovalTimedOutTypes.ClientDoesNotRequest)] + public class ConnectionApprovalTimeoutTests : NetcodeIntegrationTest + { + protected override int NumberOfClients => 1; + + public enum ApprovalTimedOutTypes + { + ClientDoesNotRequest, + ServerDoesNotRespond + } + + private ApprovalTimedOutTypes m_ApprovalFailureType; + + public ConnectionApprovalTimeoutTests(ApprovalTimedOutTypes approvalFailureType) + { + m_ApprovalFailureType = approvalFailureType; + } + + // Must be >= 2 since this is an int value and the test waits for timeout - 1 to try to verify it doesn't + // time out early + private const int k_TestTimeoutPeriod = 1; + + private Regex m_ExpectedLogMessage; + private LogType m_LogType; + + + protected override IEnumerator OnSetup() + { + m_BypassConnectionTimeout = true; + return base.OnSetup(); + } + + protected override IEnumerator OnTearDown() + { + m_BypassConnectionTimeout = false; + return base.OnTearDown(); + } + + protected override void OnServerAndClientsCreated() + { + m_ServerNetworkManager.NetworkConfig.ClientConnectionBufferTimeout = k_TestTimeoutPeriod; + m_ServerNetworkManager.LogLevel = LogLevel.Developer; + m_ClientNetworkManagers[0].NetworkConfig.ClientConnectionBufferTimeout = k_TestTimeoutPeriod; + m_ClientNetworkManagers[0].LogLevel = LogLevel.Developer; + base.OnServerAndClientsCreated(); + } + + protected override IEnumerator OnStartedServerAndClients() + { + if (m_ApprovalFailureType == ApprovalTimedOutTypes.ServerDoesNotRespond) + { + // We catch (don't process) the incoming approval message to simulate the server not sending the approved message in time + m_ClientNetworkManagers[0].MessagingSystem.Hook(new MessageCatcher(m_ClientNetworkManagers[0])); + m_ExpectedLogMessage = new Regex("Timed out waiting for the server to approve the connection request."); + m_LogType = LogType.Log; + } + else + { + // We catch (don't process) the incoming connection request message to simulate a transport connection but the client never + // sends (or takes too long to send) the connection request. + m_ServerNetworkManager.MessagingSystem.Hook(new MessageCatcher(m_ServerNetworkManager)); + + // For this test, we know the timed out client will be Client-1 + m_ExpectedLogMessage = new Regex("Server detected a transport connection from Client-1, but timed out waiting for the connection request message."); + m_LogType = LogType.Warning; + } + yield return null; + } + + [UnityTest] + public IEnumerator ValidateApprovalTimeout() + { + // Delay for half of the wait period + yield return new WaitForSeconds(k_TestTimeoutPeriod * 0.5f); + + // Verify we haven't received the time out message yet + NetcodeLogAssert.LogWasNotReceived(LogType.Log, m_ExpectedLogMessage); + + // Wait for 3/4s of the time out period to pass (totaling 1.25x the wait period) + yield return new WaitForSeconds(k_TestTimeoutPeriod * 0.75f); + + // We should have the test relative log message by this time. + NetcodeLogAssert.LogWasReceived(m_LogType, m_ExpectedLogMessage); + + // It should only have the host client connected + Assert.AreEqual(1, m_ServerNetworkManager.ConnectedClients.Count, $"Expected only one client when there were {m_ServerNetworkManager.ConnectedClients.Count} clients connected!"); + Assert.AreEqual(0, m_ServerNetworkManager.PendingClients.Count, $"Expected no pending clients when there were {m_ServerNetworkManager.PendingClients.Count} pending clients!"); + Assert.True(!m_ClientNetworkManagers[0].IsApproved, $"Expected the client to not have been approved, but it was!"); + } + } +} diff --git a/Tests/Runtime/ConnectionApprovalTimeoutTests.cs.meta b/Tests/Runtime/ConnectionApprovalTimeoutTests.cs.meta new file mode 100644 index 0000000..34b8e91 --- /dev/null +++ b/Tests/Runtime/ConnectionApprovalTimeoutTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b0c4159ea234415fa9497860e6ef4fc2 +timeCreated: 1661796642 \ No newline at end of file diff --git a/Tests/Runtime/DeferredMessagingTests.cs b/Tests/Runtime/DeferredMessagingTests.cs index 28ed379..6c63358 100644 --- a/Tests/Runtime/DeferredMessagingTests.cs +++ b/Tests/Runtime/DeferredMessagingTests.cs @@ -1,8 +1,8 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Linq; using NUnit.Framework; -using Unity.Collections; using UnityEngine; using UnityEngine.TestTools; using Unity.Netcode.TestHelpers.Runtime; @@ -113,147 +113,96 @@ public override void CleanupStaleTriggers() } } - internal class SpawnCatcher : INetworkHooks + public class DeferredMessageTestRpcComponent : NetworkBehaviour { - public struct TriggerData - { - public FastBufferReader Reader; - public MessageHeader Header; - public ulong SenderId; - public float Timestamp; - public int SerializedHeaderSize; - } - public readonly List CaughtMessages = new List(); - - public void OnBeforeSendMessage(ulong clientId, ref T message, NetworkDelivery delivery) where T : INetworkMessage - { - } - - public void OnAfterSendMessage(ulong clientId, ref T message, NetworkDelivery delivery, int messageSizeBytes) where T : INetworkMessage - { - } - - public void OnBeforeReceiveMessage(ulong senderId, Type messageType, int messageSizeBytes) - { - } - - public void OnAfterReceiveMessage(ulong senderId, Type messageType, int messageSizeBytes) - { - } + public bool ClientRpcCalled; - public void OnBeforeSendBatch(ulong clientId, int messageCount, int batchSizeInBytes, NetworkDelivery delivery) + [ClientRpc] + public void SendTestClientRpc() { + ClientRpcCalled = true; } - public void OnAfterSendBatch(ulong clientId, int messageCount, int batchSizeInBytes, NetworkDelivery delivery) + public static readonly List ClientInstances = new List(); + public override void OnNetworkSpawn() { + if (!IsServer) + { + ClientInstances.Add(NetworkManager.LocalClientId); + } + base.OnNetworkSpawn(); } + } - public void OnBeforeReceiveBatch(ulong senderId, int messageCount, int batchSizeInBytes) - { - } + public class DeferredMessageTestNetworkVariableComponent : NetworkBehaviour + { + public static readonly List ClientInstances = new List(); - public void OnAfterReceiveBatch(ulong senderId, int messageCount, int batchSizeInBytes) - { - } + public NetworkVariable TestNetworkVariable; - public bool OnVerifyCanSend(ulong destinationId, Type messageType, NetworkDelivery delivery) + public void Awake() { - return true; + TestNetworkVariable = new NetworkVariable(default, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); } - public bool OnVerifyCanReceive(ulong senderId, Type messageType, FastBufferReader messageContent, ref NetworkContext context) + public override void OnNetworkSpawn() { - if (messageType == typeof(CreateObjectMessage)) + if (!IsServer) { - CaughtMessages.Add(new TriggerData - { - Reader = new FastBufferReader(messageContent, Allocator.Persistent), - Header = context.Header, - Timestamp = context.Timestamp, - SenderId = context.SenderId, - SerializedHeaderSize = context.SerializedHeaderSize - }); - return false; + ClientInstances.Add(NetworkManager.LocalClientId); } - - return true; - } - - public void OnBeforeHandleMessage(ref T message, ref NetworkContext context) where T : INetworkMessage - { - } - - public void OnAfterHandleMessage(ref T message, ref NetworkContext context) where T : INetworkMessage - { + base.OnNetworkSpawn(); } } - public class DeferredMessageTestRpcComponent : NetworkBehaviour + public class DeferredMessageTestRpcAndNetworkVariableComponent : NetworkBehaviour { + public static readonly List ClientInstances = new List(); public bool ClientRpcCalled; + public NetworkVariable TestNetworkVariable; - [ClientRpc] - public void SendTestClientRpc() + public void Awake() { - ClientRpcCalled = true; + TestNetworkVariable = new NetworkVariable(default, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); } - } - - public class DeferredMessageTestNetworkVariableComponent : NetworkBehaviour - { - public NetworkVariable TestNetworkVariable = new NetworkVariable(); - } - public class DeferredMessageTestRpcAndNetworkVariableComponent : NetworkBehaviour - { - public bool ClientRpcCalled; + public override void OnNetworkSpawn() + { + if (!IsServer) + { + ClientInstances.Add(NetworkManager.LocalClientId); + } + base.OnNetworkSpawn(); + } [ClientRpc] public void SendTestClientRpc() { ClientRpcCalled = true; } - - public NetworkVariable TestNetworkVariable = new NetworkVariable(); } public class DeferredMessagingTest : NetcodeIntegrationTest { - protected override int NumberOfClients => 2; + protected override int NumberOfClients => 0; - private List m_ClientSpawnCatchers = new List(); + private List> m_ClientSpawnCatchers = new List>(); private GameObject m_RpcPrefab; private GameObject m_NetworkVariablePrefab; private GameObject m_RpcAndNetworkVariablePrefab; + private int m_NumberOfClientsToLateJoin = 2; + protected override IEnumerator OnSetup() { + DeferredMessageTestRpcAndNetworkVariableComponent.ClientInstances.Clear(); + DeferredMessageTestRpcComponent.ClientInstances.Clear(); + DeferredMessageTestNetworkVariableComponent.ClientInstances.Clear(); + m_SkipAddingPrefabsToClient = false; // Host is irrelevant, messages don't get sent to the host "client" m_UseHost = false; - m_RpcPrefab = new GameObject("Object With RPC"); - var networkObject = m_RpcPrefab.AddComponent(); - m_RpcPrefab.AddComponent(); - - // Make it a prefab - NetcodeIntegrationTestHelpers.MakeNetworkObjectTestPrefab(networkObject); - - m_NetworkVariablePrefab = new GameObject("Object With NetworkVariable"); - networkObject = m_NetworkVariablePrefab.AddComponent(); - m_NetworkVariablePrefab.AddComponent(); - - // Make it a prefab - NetcodeIntegrationTestHelpers.MakeNetworkObjectTestPrefab(networkObject); - - m_RpcAndNetworkVariablePrefab = new GameObject("Object With NetworkVariable And RPC"); - networkObject = m_RpcAndNetworkVariablePrefab.AddComponent(); - m_RpcAndNetworkVariablePrefab.AddComponent(); - - // Make it a prefab - NetcodeIntegrationTestHelpers.MakeNetworkObjectTestPrefab(networkObject); - // Replace the IDeferredMessageManager component with our test one in the component factory ComponentFactory.Register(networkManager => new TestDeferredMessageManager(networkManager)); yield return null; @@ -269,13 +218,54 @@ protected override IEnumerator OnTearDown() protected override void OnServerAndClientsCreated() { - m_ServerNetworkManager.AddNetworkPrefab(m_RpcPrefab); - m_ServerNetworkManager.AddNetworkPrefab(m_NetworkVariablePrefab); - m_ServerNetworkManager.AddNetworkPrefab(m_RpcAndNetworkVariablePrefab); + // Note: This is where prefabs should be created + m_RpcPrefab = CreateNetworkObjectPrefab("Object With RPC"); + var networkObject = m_RpcPrefab.GetComponent(); + m_RpcPrefab.AddComponent(); + + m_NetworkVariablePrefab = CreateNetworkObjectPrefab("Object With NetworkVariable"); + networkObject = m_NetworkVariablePrefab.GetComponent(); + m_NetworkVariablePrefab.AddComponent(); + + m_RpcAndNetworkVariablePrefab = CreateNetworkObjectPrefab("Object With NetworkVariable And RPC"); + networkObject = m_RpcAndNetworkVariablePrefab.GetComponent(); + m_RpcAndNetworkVariablePrefab.AddComponent(); + m_ServerNetworkManager.NetworkConfig.ForceSamePrefabs = false; - foreach (var client in m_ClientNetworkManagers) + + } + + private bool m_SkipAddingPrefabsToClient = false; + + private void AddPrefabsToClient(NetworkManager networkManager) + { + networkManager.AddNetworkPrefab(m_RpcPrefab); + networkManager.AddNetworkPrefab(m_NetworkVariablePrefab); + networkManager.AddNetworkPrefab(m_RpcAndNetworkVariablePrefab); + } + + protected override void OnNewClientCreated(NetworkManager networkManager) + { + networkManager.NetworkConfig.ForceSamePrefabs = false; + if (!m_SkipAddingPrefabsToClient) { - client.NetworkConfig.ForceSamePrefabs = false; + AddPrefabsToClient(networkManager); + } + + base.OnNewClientCreated(networkManager); + } + + private IEnumerator SpawnClients(bool clearTestDeferredMessageManagerCallFlags = true) + { + for (int i = 0; i < m_NumberOfClientsToLateJoin; i++) + { + // Create and join client + yield return CreateAndStartNewClient(); + } + + if (clearTestDeferredMessageManagerCallFlags) + { + ClearTestDeferredMessageManagerCallFlags(); } } @@ -296,38 +286,19 @@ private void CatchSpawns() { foreach (var client in m_ClientNetworkManagers) { - var catcher = new SpawnCatcher(); + var catcher = new MessageCatcher(client); m_ClientSpawnCatchers.Add(catcher); client.MessagingSystem.Hook(catcher); } } - private void RegisterClientPrefabs(bool clearTestDeferredMessageManagerCallFlags = true) - { - foreach (var client in m_ClientNetworkManagers) - { - client.AddNetworkPrefab(m_RpcPrefab); - client.AddNetworkPrefab(m_NetworkVariablePrefab); - client.AddNetworkPrefab(m_RpcAndNetworkVariablePrefab); - } - - if (clearTestDeferredMessageManagerCallFlags) - { - ClearTestDeferredMessageManagerCallFlags(); - } - } - private void ReleaseSpawns() { for (var i = 0; i < m_ClientNetworkManagers.Length; ++i) { // Unhook first so the spawn catcher stops catching spawns m_ClientNetworkManagers[i].MessagingSystem.Unhook(m_ClientSpawnCatchers[i]); - foreach (var caughtSpawn in m_ClientSpawnCatchers[i].CaughtMessages) - { - // Reader will be disposed within HandleMessage - m_ClientNetworkManagers[i].MessagingSystem.HandleMessage(caughtSpawn.Header, caughtSpawn.Reader, caughtSpawn.SenderId, caughtSpawn.Timestamp, caughtSpawn.SerializedHeaderSize); - } + m_ClientSpawnCatchers[i].ReleaseMessages(); } m_ClientSpawnCatchers.Clear(); } @@ -345,7 +316,7 @@ private IEnumerator WaitForClientsToCatchSpawns(int count = 1) { foreach (var catcher in m_ClientSpawnCatchers) { - if (catcher.CaughtMessages.Count != count) + if (catcher.CaughtMessageCount != count) { return false; } @@ -372,89 +343,54 @@ private void AssertSpawnTriggerCountForObject(TestDeferredMessageManager manager Assert.AreEqual(0, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnAddPrefab)); } - private static CoroutineRunner s_CoroutineRunner; - - private Coroutine Run(IEnumerator enumerator) - { - if (s_CoroutineRunner == null) - { - s_CoroutineRunner = new GameObject(nameof(CoroutineRunner)).AddComponent(); - } - - return s_CoroutineRunner.StartCoroutine(enumerator); - } - - private IEnumerator RunMultiple(List waitFor) - { - yield return WaitMultiple(StartMultiple(waitFor)); - } - - private List StartMultiple(List waitFor) + private IEnumerator WaitForAllClientsToReceive() where T : INetworkMessage { - var runningCoroutines = new List(); - foreach (var enumerator in waitFor) - { - runningCoroutines.Add(Run(enumerator)); - } - - return runningCoroutines; + yield return WaitForMessageReceived(m_ClientNetworkManagers.ToList(), ReceiptType.Received); } - private IEnumerator WaitMultiple(List runningCoroutines) - { - foreach (var coroutine in runningCoroutines) - { - yield return coroutine; - } - } - - private List WaitForAllClientsToReceive() where T : INetworkMessage + private IEnumerator WaitForAllClientsToReceive() + where TFirstMessage : INetworkMessage + where TSecondMessage : INetworkMessage { - var waiters = new List(); - foreach (var client in m_ClientNetworkManagers) + yield return WaitForMessagesReceived(new List { - waiters.Add(NetcodeIntegrationTestHelpers.WaitForMessageOfTypeReceived(client)); - } - - return waiters; + typeof(TFirstMessage), + typeof(TSecondMessage) + }, m_ClientNetworkManagers.ToList(), ReceiptType.Received); } - private List WaitForAllClientsToReceive() - where TFirstMessage : INetworkMessage - where TSecondMessage : INetworkMessage + private IEnumerator WaitForAllClientsToReceive() + where TFirstMessage : INetworkMessage + where TSecondMessage : INetworkMessage + where TThirdMessage : INetworkMessage { - var waiters = new List(); - foreach (var client in m_ClientNetworkManagers) + yield return WaitForMessagesReceived(new List { - waiters.Add(NetcodeIntegrationTestHelpers.WaitForMessageOfTypeReceived(client)); - waiters.Add(NetcodeIntegrationTestHelpers.WaitForMessageOfTypeReceived(client)); - } - - return waiters; + typeof(TFirstMessage), + typeof(TSecondMessage), + typeof(TThirdMessage), + }, m_ClientNetworkManagers.ToList(), ReceiptType.Received); } - private List WaitForAllClientsToReceive() + private IEnumerator WaitForAllClientsToReceive() where TFirstMessage : INetworkMessage where TSecondMessage : INetworkMessage where TThirdMessage : INetworkMessage where TFourthMessage : INetworkMessage { - var waiters = new List(); - foreach (var client in m_ClientNetworkManagers) + yield return WaitForMessagesReceived(new List { - waiters.Add(NetcodeIntegrationTestHelpers.WaitForMessageOfTypeReceived(client)); - waiters.Add(NetcodeIntegrationTestHelpers.WaitForMessageOfTypeReceived(client)); - waiters.Add(NetcodeIntegrationTestHelpers.WaitForMessageOfTypeReceived(client)); - waiters.Add(NetcodeIntegrationTestHelpers.WaitForMessageOfTypeReceived(client)); - } - - return waiters; + typeof(TFirstMessage), + typeof(TSecondMessage), + typeof(TThirdMessage), + typeof(TFourthMessage), + }, m_ClientNetworkManagers.ToList(), ReceiptType.Received); } [UnityTest] public IEnumerator WhenAnRpcArrivesBeforeASpawnArrives_ItIsDeferred() { - RegisterClientPrefabs(); + yield return SpawnClients(); CatchSpawns(); var serverObject = Object.Instantiate(m_RpcPrefab); serverObject.GetComponent().NetworkManagerOwner = m_ServerNetworkManager; @@ -463,7 +399,7 @@ public IEnumerator WhenAnRpcArrivesBeforeASpawnArrives_ItIsDeferred() serverObject.GetComponent().SendTestClientRpc(); - yield return RunMultiple(WaitForAllClientsToReceive()); + yield return WaitForAllClientsToReceive(); foreach (var client in m_ClientNetworkManagers) { @@ -477,7 +413,7 @@ public IEnumerator WhenAnRpcArrivesBeforeASpawnArrives_ItIsDeferred() [UnityTest] public IEnumerator WhenADespawnArrivesBeforeASpawnArrives_ItIsDeferred() { - RegisterClientPrefabs(); + yield return SpawnClients(); CatchSpawns(); var serverObject = Object.Instantiate(m_RpcPrefab); serverObject.GetComponent().NetworkManagerOwner = m_ServerNetworkManager; @@ -486,7 +422,7 @@ public IEnumerator WhenADespawnArrivesBeforeASpawnArrives_ItIsDeferred() serverObject.GetComponent().Despawn(false); - yield return RunMultiple(WaitForAllClientsToReceive()); + yield return WaitForAllClientsToReceive(); foreach (var client in m_ClientNetworkManagers) { @@ -500,7 +436,7 @@ public IEnumerator WhenADespawnArrivesBeforeASpawnArrives_ItIsDeferred() [UnityTest] public IEnumerator WhenAChangeOwnershipMessageArrivesBeforeASpawnArrives_ItIsDeferred() { - RegisterClientPrefabs(); + yield return SpawnClients(); CatchSpawns(); var serverObject = Object.Instantiate(m_RpcPrefab); serverObject.GetComponent().NetworkManagerOwner = m_ServerNetworkManager; @@ -508,7 +444,7 @@ public IEnumerator WhenAChangeOwnershipMessageArrivesBeforeASpawnArrives_ItIsDef yield return WaitForClientsToCatchSpawns(); serverObject.GetComponent().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); - yield return RunMultiple(WaitForAllClientsToReceive()); + yield return WaitForAllClientsToReceive(); foreach (var client in m_ClientNetworkManagers) { var manager = (TestDeferredMessageManager)client.DeferredMessageManager; @@ -521,13 +457,10 @@ public IEnumerator WhenAChangeOwnershipMessageArrivesBeforeASpawnArrives_ItIsDef [UnityTest] public IEnumerator WhenANetworkVariableDeltaMessageArrivesBeforeASpawnArrives_ItIsDeferred() { - RegisterClientPrefabs(); + m_SkipAddingPrefabsToClient = true; + yield return SpawnClients(); CatchSpawns(); - // Have to start these before spawning here because spawning sends a NetworkVariableDeltaMessage, too - // Depending on timing, if we start this after spawning, we may end up missing the first one. - var waiters = WaitForAllClientsToReceive(); - var coroutines = StartMultiple(waiters); var serverObject = Object.Instantiate(m_NetworkVariablePrefab); serverObject.GetComponent().NetworkManagerOwner = m_ServerNetworkManager; @@ -536,7 +469,7 @@ public IEnumerator WhenANetworkVariableDeltaMessageArrivesBeforeASpawnArrives_It serverObject.GetComponent().TestNetworkVariable.Value = 1; - yield return WaitMultiple(coroutines); + yield return WaitForAllClientsToReceive(); foreach (var client in m_ClientNetworkManagers) { @@ -545,18 +478,21 @@ public IEnumerator WhenANetworkVariableDeltaMessageArrivesBeforeASpawnArrives_It Assert.IsFalse(manager.ProcessTriggersCalled); // TODO: Network Variables generate an extra message immediately at spawn for some reason... // Seems like a bug since the network variable data is in the spawn message already. - AssertSpawnTriggerCountForObject(manager, serverObject, 2); + AssertSpawnTriggerCountForObject(manager, serverObject, 1); } } [UnityTest] + //[Ignore("Disabling this temporarily until it is migrated into new integration test.")] public IEnumerator WhenASpawnMessageArrivesBeforeThePrefabIsAvailable_ItIsDeferred() { + m_SkipAddingPrefabsToClient = true; + yield return SpawnClients(); var serverObject = Object.Instantiate(m_RpcPrefab); serverObject.GetComponent().NetworkManagerOwner = m_ServerNetworkManager; serverObject.GetComponent().Spawn(); - yield return RunMultiple(WaitForAllClientsToReceive()); + yield return WaitForAllClientsToReceive(); foreach (var client in m_ClientNetworkManagers) { @@ -631,8 +567,28 @@ public IEnumerator WhenAChangeOwnershipMessageIsDeferred_ItIsProcessedOnSpawn() public IEnumerator WhenANetworkVariableDeltaMessageIsDeferred_ItIsProcessedOnSpawn() { yield return WhenANetworkVariableDeltaMessageArrivesBeforeASpawnArrives_ItIsDeferred(); + + foreach (var client in m_ClientNetworkManagers) + { + AddPrefabsToClient(client); + } + ReleaseSpawns(); + // Wait for the clients to spawn the NetworkObjects + bool HaveAllClientsSpawned() + { + foreach (var client in m_ClientNetworkManagers) + { + if (!DeferredMessageTestNetworkVariableComponent.ClientInstances.Contains(client.LocalClientId)) + { + return false; + } + } + return true; + } + yield return WaitForConditionOrTimeOut(HaveAllClientsSpawned); + foreach (var client in m_ClientNetworkManagers) { var manager = (TestDeferredMessageManager)client.DeferredMessageManager; @@ -647,9 +603,31 @@ public IEnumerator WhenANetworkVariableDeltaMessageIsDeferred_ItIsProcessedOnSpa [UnityTest] public IEnumerator WhenASpawnMessageIsDeferred_ItIsProcessedOnAddPrefab() { + // This will prevent spawned clients from adding prefabs + m_SkipAddingPrefabsToClient = true; yield return WhenASpawnMessageArrivesBeforeThePrefabIsAvailable_ItIsDeferred(); - RegisterClientPrefabs(false); + // Now add the prefabs + foreach (var client in m_ClientNetworkManagers) + { + AddPrefabsToClient(client); + } + + // Wait for the clients to spawn the NetworkObjects + bool HaveAllClientsSpawned() + { + foreach (var client in m_ClientNetworkManagers) + { + if (!DeferredMessageTestRpcComponent.ClientInstances.Contains(client.LocalClientId)) + { + return false; + } + } + return true; + } + yield return WaitForConditionOrTimeOut(HaveAllClientsSpawned); + + // Validate this test foreach (var client in m_ClientNetworkManagers) { var manager = (TestDeferredMessageManager)client.DeferredMessageManager; @@ -664,14 +642,10 @@ public IEnumerator WhenASpawnMessageIsDeferred_ItIsProcessedOnAddPrefab() [UnityTest] public IEnumerator WhenMultipleSpawnTriggeredMessagesAreDeferred_TheyAreAllProcessedOnSpawn() { - RegisterClientPrefabs(); + m_SkipAddingPrefabsToClient = true; + yield return SpawnClients(); CatchSpawns(); - // Have to start these before spawning here because spawning sends a NetworkVariableDeltaMessage, too - // Depending on timing, if we start this after spawning, we may end up missing the first one. - var waiters = WaitForAllClientsToReceive(); - var coroutines = StartMultiple(waiters); - var serverObject = Object.Instantiate(m_RpcAndNetworkVariablePrefab); serverObject.GetComponent().NetworkManagerOwner = m_ServerNetworkManager; serverObject.GetComponent().Spawn(); @@ -679,10 +653,12 @@ public IEnumerator WhenMultipleSpawnTriggeredMessagesAreDeferred_TheyAreAllProce serverObject.GetComponent().SendTestClientRpc(); serverObject.GetComponent().TestNetworkVariable.Value = 1; + + yield return WaitForAllClientsToReceive(); + serverObject.GetComponent().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); - // Should be received in order so we'll wait for the last one. - yield return WaitMultiple(coroutines); + yield return WaitForAllClientsToReceive(); foreach (var client in m_ClientNetworkManagers) { @@ -690,29 +666,49 @@ public IEnumerator WhenMultipleSpawnTriggeredMessagesAreDeferred_TheyAreAllProce Assert.IsTrue(manager.DeferMessageCalled); Assert.IsFalse(manager.ProcessTriggersCalled); - Assert.AreEqual(4, manager.DeferredMessageCountTotal()); - Assert.AreEqual(4, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn)); - Assert.AreEqual(4, manager.DeferredMessageCountForKey(IDeferredMessageManager.TriggerType.OnSpawn, serverObject.GetComponent().NetworkObjectId)); + Assert.AreEqual(3, manager.DeferredMessageCountTotal()); + Assert.AreEqual(3, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn)); + Assert.AreEqual(3, manager.DeferredMessageCountForKey(IDeferredMessageManager.TriggerType.OnSpawn, serverObject.GetComponent().NetworkObjectId)); Assert.AreEqual(0, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnAddPrefab)); + AddPrefabsToClient(client); } + ReleaseSpawns(); + // Wait for the clients to spawn the NetworkObjects + bool HaveAllClientsSpawned() + { + foreach (var client in m_ClientNetworkManagers) + { + if (!DeferredMessageTestRpcAndNetworkVariableComponent.ClientInstances.Contains(client.LocalClientId)) + { + return false; + } + } + return true; + } + yield return WaitForConditionOrTimeOut(HaveAllClientsSpawned); + yield return new WaitForSeconds(0.1f); + + // Validate the spawned objects foreach (var client in m_ClientNetworkManagers) { var manager = (TestDeferredMessageManager)client.DeferredMessageManager; - Assert.IsTrue(manager.ProcessTriggersCalled); - Assert.AreEqual(0, manager.DeferredMessageCountTotal()); + Assert.IsTrue(manager.ProcessTriggersCalled, "Process triggers were not called!"); + Assert.AreEqual(0, manager.DeferredMessageCountTotal(), $"Deferred message count ({manager.DeferredMessageCountTotal()}) is not zero!"); var component = GetComponentForClient(client.LocalClientId); - Assert.IsTrue(component.ClientRpcCalled); - Assert.AreEqual(1, component.TestNetworkVariable.Value); - Assert.AreEqual(m_ClientNetworkManagers[0].LocalClientId, component.OwnerClientId); + Assert.IsTrue(component.ClientRpcCalled, "Client RPC was not called!"); + Assert.AreEqual(1, component.TestNetworkVariable.Value, $"Test {nameof(NetworkVariable)} ({component.TestNetworkVariable.Value}) does not equal 1!"); + Assert.AreEqual(m_ClientNetworkManagers[0].LocalClientId, component.OwnerClientId, $"{component.name} owner id ({component.OwnerClientId}) does not equal first client id ({m_ClientNetworkManagers[0].LocalClientId})"); } } [UnityTest] public IEnumerator WhenMultipleAddPrefabTriggeredMessagesAreDeferred_TheyAreAllProcessedOnAddNetworkPrefab() { + m_SkipAddingPrefabsToClient = true; + yield return SpawnClients(); var serverObject = Object.Instantiate(m_RpcPrefab); serverObject.GetComponent().NetworkManagerOwner = m_ServerNetworkManager; serverObject.GetComponent().Spawn(); @@ -721,7 +717,7 @@ public IEnumerator WhenMultipleAddPrefabTriggeredMessagesAreDeferred_TheyAreAllP serverObject2.GetComponent().NetworkManagerOwner = m_ServerNetworkManager; serverObject2.GetComponent().Spawn(); - yield return RunMultiple(WaitForAllClientsToReceive()); + yield return WaitForAllClientsToReceive(); foreach (var client in m_ClientNetworkManagers) { @@ -733,9 +729,23 @@ public IEnumerator WhenMultipleAddPrefabTriggeredMessagesAreDeferred_TheyAreAllP Assert.AreEqual(0, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn)); Assert.AreEqual(2, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnAddPrefab)); Assert.AreEqual(2, manager.DeferredMessageCountForKey(IDeferredMessageManager.TriggerType.OnAddPrefab, serverObject.GetComponent().GlobalObjectIdHash)); + AddPrefabsToClient(client); } - RegisterClientPrefabs(false); + // Wait for the clients to spawn the NetworkObjects + bool HaveAllClientsSpawned() + { + foreach (var client in m_ClientNetworkManagers) + { + if (!DeferredMessageTestRpcComponent.ClientInstances.Contains(client.LocalClientId)) + { + return false; + } + } + return true; + } + yield return WaitForConditionOrTimeOut(HaveAllClientsSpawned); + foreach (var client in m_ClientNetworkManagers) { @@ -769,10 +779,8 @@ public IEnumerator WhenMultipleAddPrefabTriggeredMessagesAreDeferred_TheyAreAllP [UnityTest] public IEnumerator WhenSpawnTriggeredMessagesAreDeferredBeforeThePrefabIsAdded_AddingThePrefabCausesThemToBeProcessed() { - // Because we're not waiting for the client to receive the spawn before we change the network variable value, - // there's only one NetworkVariableDeltaMessage this time. - var waiters = WaitForAllClientsToReceive(); - var coroutines = StartMultiple(waiters); + m_SkipAddingPrefabsToClient = true; + yield return SpawnClients(); var serverObject = Object.Instantiate(m_RpcAndNetworkVariablePrefab); serverObject.GetComponent().NetworkManagerOwner = m_ServerNetworkManager; @@ -780,10 +788,15 @@ public IEnumerator WhenSpawnTriggeredMessagesAreDeferredBeforeThePrefabIsAdded_A serverObject.GetComponent().SendTestClientRpc(); serverObject.GetComponent().TestNetworkVariable.Value = 1; + // TODO: Remove this if we figure out how to work around the NetworkVariableDeltaMessage.Serialized issue at line 59 + // Otherwise, we have to wait for at least 1 tick for the NetworkVariableDeltaMessage to be generated before changing ownership + yield return WaitForAllClientsToReceive(); + serverObject.GetComponent().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); - yield return WaitMultiple(coroutines); + yield return WaitForAllClientsToReceive(); + // Validate messages are deferred and pending foreach (var client in m_ClientNetworkManagers) { var manager = (TestDeferredMessageManager)client.DeferredMessageManager; @@ -794,10 +807,26 @@ public IEnumerator WhenSpawnTriggeredMessagesAreDeferredBeforeThePrefabIsAdded_A Assert.AreEqual(3, manager.DeferredMessageCountForKey(IDeferredMessageManager.TriggerType.OnSpawn, serverObject.GetComponent().NetworkObjectId)); Assert.AreEqual(1, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnAddPrefab)); Assert.AreEqual(1, manager.DeferredMessageCountForKey(IDeferredMessageManager.TriggerType.OnAddPrefab, serverObject.GetComponent().GlobalObjectIdHash)); + AddPrefabsToClient(client); } - RegisterClientPrefabs(false); + // Wait for the clients to spawn the NetworkObjects + bool HaveAllClientsSpawned() + { + foreach (var client in m_ClientNetworkManagers) + { + if (!DeferredMessageTestRpcAndNetworkVariableComponent.ClientInstances.Contains(client.LocalClientId)) + { + return false; + } + } + return true; + } + yield return WaitForConditionOrTimeOut(HaveAllClientsSpawned); + yield return new WaitForSeconds(0.1f); + + // Validate the test foreach (var client in m_ClientNetworkManagers) { var manager = (TestDeferredMessageManager)client.DeferredMessageManager; @@ -813,10 +842,10 @@ public IEnumerator WhenSpawnTriggeredMessagesAreDeferredBeforeThePrefabIsAdded_A } [UnityTest] - [Ignore("This test is unstable (MTT-4146)")] public IEnumerator WhenAMessageIsDeferredForMoreThanTheConfiguredTime_ItIsRemoved([Values(1, 2, 3)] int timeout) { - RegisterClientPrefabs(); + m_SkipAddingPrefabsToClient = true; + yield return SpawnClients(); CatchSpawns(); foreach (var client in m_ClientNetworkManagers) { @@ -870,24 +899,29 @@ public IEnumerator WhenAMessageIsDeferredForMoreThanTheConfiguredTime_ItIsRemove yield return new WaitForSeconds(timeout + 0.1f); - Assert.AreEqual(NumberOfClients, purgeCount); - foreach (var client in m_ClientNetworkManagers) + bool HaveAllClientsPurged() { - var manager = (TestDeferredMessageManager)client.DeferredMessageManager; - Assert.AreEqual(0, manager.DeferredMessageCountTotal()); + foreach (var client in m_ClientNetworkManagers) + { + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + if (manager.DeferredMessageCountTotal() != 0) + { + return false; + } + } + return true; } + + yield return WaitForConditionOrTimeOut(HaveAllClientsPurged); + AssertOnTimeout("Timed out waiting for all clients to purge their deferred messages!"); } [UnityTest] - [Ignore("This test is unstable on standalones")] public IEnumerator WhenMultipleMessagesForTheSameObjectAreDeferredForMoreThanTheConfiguredTime_TheyAreAllRemoved([Values(1, 2, 3)] int timeout) { - RegisterClientPrefabs(); + m_SkipAddingPrefabsToClient = true; + yield return SpawnClients(); CatchSpawns(); - // Have to start these before spawning here because spawning sends a NetworkVariableDeltaMessage, too - // Depending on timing, if we start this after spawning, we may end up missing the first one. - var waiters = WaitForAllClientsToReceive(); - var coroutines = StartMultiple(waiters); foreach (var client in m_ClientNetworkManagers) { @@ -917,7 +951,9 @@ public IEnumerator WhenMultipleMessagesForTheSameObjectAreDeferredForMoreThanThe serverObject.GetComponent().TestNetworkVariable.Value = 1; serverObject.GetComponent().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); - yield return WaitMultiple(coroutines); + yield return WaitForMessagesReceived( + new List {typeof(ClientRpcMessage), typeof(NetworkVariableDeltaMessage), typeof(ChangeOwnershipMessage), + }, m_ClientNetworkManagers.ToList(), ReceiptType.Received); foreach (var unused in m_ClientNetworkManagers) { @@ -931,38 +967,40 @@ public IEnumerator WhenMultipleMessagesForTheSameObjectAreDeferredForMoreThanThe { ++purgeCount; var elapsed = Time.realtimeSinceStartup - start; - Assert.GreaterOrEqual(elapsed, timeout - 0.05f); - Assert.AreEqual(4, manager.DeferredMessageCountTotal()); - Assert.AreEqual(4, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn)); - Assert.AreEqual(4, manager.DeferredMessageCountForKey(IDeferredMessageManager.TriggerType.OnSpawn, key)); + Assert.GreaterOrEqual(elapsed, timeout - 0.25f); + Assert.AreEqual(3, manager.DeferredMessageCountTotal()); + Assert.AreEqual(3, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn)); + Assert.AreEqual(3, manager.DeferredMessageCountForKey(IDeferredMessageManager.TriggerType.OnSpawn, key)); Assert.AreEqual(serverObject.GetComponent().NetworkObjectId, key); }; var manager = (TestDeferredMessageManager)client.DeferredMessageManager; manager.OnBeforePurge = beforePurge; } - yield return new WaitForSeconds(timeout + 0.1f); - - Assert.AreEqual(NumberOfClients, purgeCount); - foreach (var client in m_ClientNetworkManagers) + bool HaveAllClientsPurged() { - var manager = (TestDeferredMessageManager)client.DeferredMessageManager; - Assert.AreEqual(0, manager.DeferredMessageCountTotal()); + foreach (var client in m_ClientNetworkManagers) + { + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + if (manager.DeferredMessageCountTotal() != 0) + { + return false; + } + } + return true; } + + yield return WaitForConditionOrTimeOut(HaveAllClientsPurged); + AssertOnTimeout("Timed out waiting for all clients to purge their deferred messages!"); } [UnityTest] public IEnumerator WhenMultipleMessagesForDifferentObjectsAreDeferredForMoreThanTheConfiguredTime_TheyAreAllRemoved([Values(1, 2, 3)] int timeout) { - RegisterClientPrefabs(); + m_SkipAddingPrefabsToClient = true; + yield return SpawnClients(); CatchSpawns(); - // Have to start these before spawning here because spawning sends a NetworkVariableDeltaMessage, too - // Depending on timing, if we start this after spawning, we may end up missing the first one. - var waiters = WaitForAllClientsToReceive(); - waiters.AddRange(WaitForAllClientsToReceive()); - var coroutines = StartMultiple(waiters); - foreach (var client in m_ClientNetworkManagers) { client.NetworkConfig.SpawnTimeout = timeout; @@ -1000,10 +1038,13 @@ public IEnumerator WhenMultipleMessagesForDifferentObjectsAreDeferredForMoreThan serverObject2.GetComponent().TestNetworkVariable.Value = 1; serverObject2.GetComponent().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); - yield return WaitMultiple(coroutines); + yield return WaitForMessagesReceived( + new List {typeof(ClientRpcMessage), typeof(NetworkVariableDeltaMessage), typeof(ChangeOwnershipMessage),typeof(ClientRpcMessage), typeof(NetworkVariableDeltaMessage), typeof(ChangeOwnershipMessage), + }, m_ClientNetworkManagers.ToList(), ReceiptType.Received); foreach (var unused in m_ClientNetworkManagers) { + LogAssert.Expect(LogType.Warning, $"[Netcode] Deferred messages were received for a trigger of type {IDeferredMessageManager.TriggerType.OnSpawn} with key {serverObject.GetComponent().NetworkObjectId}, but that trigger was not received within within {timeout} second(s)."); LogAssert.Expect(LogType.Warning, $"[Netcode] Deferred messages were received for a trigger of type {IDeferredMessageManager.TriggerType.OnSpawn} with key {serverObject2.GetComponent().NetworkObjectId}, but that trigger was not received within within {timeout} second(s)."); } @@ -1011,24 +1052,27 @@ public IEnumerator WhenMultipleMessagesForDifferentObjectsAreDeferredForMoreThan int purgeCount = 0; foreach (var client in m_ClientNetworkManagers) { - var remainingMessagesTotalThisClient = 8; + var remainingMessagesTotalThisClient = 6; TestDeferredMessageManager.BeforePurgeDelegate beforePurge = (manager, key) => { ++purgeCount; var elapsed = Time.realtimeSinceStartup - start; - Assert.GreaterOrEqual(elapsed, timeout - 0.05f); + Assert.GreaterOrEqual(elapsed, timeout - 0.25f); Assert.AreEqual(remainingMessagesTotalThisClient, manager.DeferredMessageCountTotal()); Assert.AreEqual(remainingMessagesTotalThisClient, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn)); - Assert.AreEqual(4, manager.DeferredMessageCountForKey(IDeferredMessageManager.TriggerType.OnSpawn, key)); - remainingMessagesTotalThisClient -= 4; + Assert.AreEqual(3, manager.DeferredMessageCountForKey(IDeferredMessageManager.TriggerType.OnSpawn, key)); + remainingMessagesTotalThisClient -= 3; }; var manager = (TestDeferredMessageManager)client.DeferredMessageManager; manager.OnBeforePurge = beforePurge; } yield return new WaitForSeconds(timeout + 0.1f); - - Assert.AreEqual(NumberOfClients * 2, purgeCount); + foreach (var client in m_ClientNetworkManagers) + { + AddPrefabsToClient(client); + } + Assert.AreEqual(m_NumberOfClientsToLateJoin * 2, purgeCount); foreach (var client in m_ClientNetworkManagers) { var manager = (TestDeferredMessageManager)client.DeferredMessageManager; @@ -1039,7 +1083,8 @@ public IEnumerator WhenMultipleMessagesForDifferentObjectsAreDeferredForMoreThan [UnityTest] public IEnumerator WhenADeferredMessageIsRemoved_OtherMessagesForSameObjectAreRemoved([Values(1, 2, 3)] int timeout) { - RegisterClientPrefabs(); + m_SkipAddingPrefabsToClient = true; + yield return SpawnClients(); CatchSpawns(); foreach (var client in m_ClientNetworkManagers) { @@ -1067,7 +1112,7 @@ public IEnumerator WhenADeferredMessageIsRemoved_OtherMessagesForSameObjectAreRe serverObject.GetComponent().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); - yield return RunMultiple(WaitForAllClientsToReceive()); + yield return WaitForAllClientsToReceive(); yield return new WaitForSeconds(timeout - 0.5f); @@ -1080,7 +1125,7 @@ public IEnumerator WhenADeferredMessageIsRemoved_OtherMessagesForSameObjectAreRe } serverObject.GetComponent().ChangeOwnership(m_ServerNetworkManager.LocalClientId); - yield return RunMultiple(WaitForAllClientsToReceive()); + yield return WaitForAllClientsToReceive(); foreach (var client in m_ClientNetworkManagers) { @@ -1112,9 +1157,14 @@ public IEnumerator WhenADeferredMessageIsRemoved_OtherMessagesForSameObjectAreRe manager.OnBeforePurge = beforePurge; } + foreach (var client in m_ClientNetworkManagers) + { + AddPrefabsToClient(client); + } + yield return new WaitForSeconds(0.6f); - Assert.AreEqual(NumberOfClients, purgeCount); + Assert.AreEqual(m_NumberOfClientsToLateJoin, purgeCount); foreach (var client in m_ClientNetworkManagers) { var manager = (TestDeferredMessageManager)client.DeferredMessageManager; @@ -1125,7 +1175,8 @@ public IEnumerator WhenADeferredMessageIsRemoved_OtherMessagesForSameObjectAreRe [UnityTest] public IEnumerator WhenADeferredMessageIsRemoved_OtherMessagesForDifferentObjectsAreNotRemoved([Values(1, 2, 3)] int timeout) { - RegisterClientPrefabs(); + m_SkipAddingPrefabsToClient = true; + yield return SpawnClients(); CatchSpawns(); foreach (var client in m_ClientNetworkManagers) { @@ -1156,7 +1207,7 @@ public IEnumerator WhenADeferredMessageIsRemoved_OtherMessagesForDifferentObject serverObject.GetComponent().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); - yield return RunMultiple(WaitForAllClientsToReceive()); + yield return WaitForAllClientsToReceive(); yield return new WaitForSeconds(timeout - 0.5f); @@ -1170,7 +1221,7 @@ public IEnumerator WhenADeferredMessageIsRemoved_OtherMessagesForDifferentObject } serverObject2.GetComponent().ChangeOwnership(m_ServerNetworkManager.LocalClientId); - yield return RunMultiple(WaitForAllClientsToReceive()); + yield return WaitForAllClientsToReceive(); foreach (var client in m_ClientNetworkManagers) { @@ -1206,9 +1257,14 @@ public IEnumerator WhenADeferredMessageIsRemoved_OtherMessagesForDifferentObject manager.OnBeforePurge = beforePurge; } + foreach (var client in m_ClientNetworkManagers) + { + AddPrefabsToClient(client); + } + yield return new WaitForSeconds(0.6f); - Assert.AreEqual(NumberOfClients, purgeCount); + Assert.AreEqual(m_NumberOfClientsToLateJoin, purgeCount); foreach (var client in m_ClientNetworkManagers) { var manager = (TestDeferredMessageManager)client.DeferredMessageManager; diff --git a/Tests/Runtime/DisconnectTests.cs b/Tests/Runtime/DisconnectTests.cs index 7590769..aae933b 100644 --- a/Tests/Runtime/DisconnectTests.cs +++ b/Tests/Runtime/DisconnectTests.cs @@ -60,5 +60,53 @@ private void OnClientDisconnectCallback(ulong obj) { m_ClientDisconnected = true; } + + [UnityTest] + public IEnumerator ClientDisconnectPlayerObjectCleanup() + { + // create server and client instances + NetcodeIntegrationTestHelpers.Create(1, out NetworkManager server, out NetworkManager[] clients); + + // create prefab + var gameObject = new GameObject("PlayerObject"); + var networkObject = gameObject.AddComponent(); + networkObject.DontDestroyWithOwner = true; + NetcodeIntegrationTestHelpers.MakeNetworkObjectTestPrefab(networkObject); + + server.NetworkConfig.PlayerPrefab = gameObject; + + for (int i = 0; i < clients.Length; i++) + { + clients[i].NetworkConfig.PlayerPrefab = gameObject; + } + + // start server and connect clients + NetcodeIntegrationTestHelpers.Start(false, server, clients); + + // wait for connection on client side + yield return NetcodeIntegrationTestHelpers.WaitForClientsConnected(clients); + + // wait for connection on server side + yield return NetcodeIntegrationTestHelpers.WaitForClientConnectedToServer(server); + + // disconnect the remote client + m_ClientDisconnected = false; + + server.OnClientDisconnectCallback += OnClientDisconnectCallback; + + var serverSideClientPlayer = server.ConnectedClients[clients[0].LocalClientId].PlayerObject; + + // Stopping the client is the same as the client disconnecting + NetcodeIntegrationTestHelpers.StopOneClient(clients[0]); + + var timeoutHelper = new TimeoutHelper(); + yield return NetcodeIntegrationTest.WaitForConditionOrTimeOut(() => m_ClientDisconnected, timeoutHelper); + + // ensure the object was destroyed + Assert.True(serverSideClientPlayer.IsOwnedByServer, $"The client's player object's ownership was not transferred back to the server!"); + + // cleanup + NetcodeIntegrationTestHelpers.Destroy(); + } } } diff --git a/Tests/Runtime/Metrics/ConnectionMetricsTests.cs b/Tests/Runtime/Metrics/ConnectionMetricsTests.cs index 9f678f1..35356d4 100644 --- a/Tests/Runtime/Metrics/ConnectionMetricsTests.cs +++ b/Tests/Runtime/Metrics/ConnectionMetricsTests.cs @@ -2,7 +2,6 @@ #if MULTIPLAYER_TOOLS_1_0_0_PRE_7 using System.Collections; -using System.Collections.Generic; using NUnit.Framework; using Unity.Multiplayer.Tools.MetricTypes; using Unity.Netcode.TestHelpers.Runtime; diff --git a/Tests/Runtime/Metrics/MetricsDispatchTests.cs b/Tests/Runtime/Metrics/MetricsDispatchTests.cs index 12adec0..519ddfa 100644 --- a/Tests/Runtime/Metrics/MetricsDispatchTests.cs +++ b/Tests/Runtime/Metrics/MetricsDispatchTests.cs @@ -5,7 +5,6 @@ using Unity.Multiplayer.Tools.NetStats; using UnityEngine.TestTools; using Unity.Netcode.TestHelpers.Runtime; -using Unity.Netcode.TestHelpers.Runtime.Metrics; namespace Unity.Netcode.RuntimeTests.Metrics { diff --git a/Tests/Runtime/Metrics/OwnershipChangeMetricsTests.cs b/Tests/Runtime/Metrics/OwnershipChangeMetricsTests.cs index f41b6ee..bfcfdc2 100644 --- a/Tests/Runtime/Metrics/OwnershipChangeMetricsTests.cs +++ b/Tests/Runtime/Metrics/OwnershipChangeMetricsTests.cs @@ -1,5 +1,4 @@ #if MULTIPLAYER_TOOLS -using System; using System.Collections; using System.Linq; using NUnit.Framework; @@ -36,7 +35,7 @@ protected override void OnServerAndClientsCreated() private NetworkObject SpawnNetworkObject() { // Spawn another network object so we can hide multiple. - var gameObject = UnityEngine.Object.Instantiate(m_NewNetworkPrefab); // new GameObject(NewNetworkObjectName); + var gameObject = Object.Instantiate(m_NewNetworkPrefab); // new GameObject(NewNetworkObjectName); var networkObject = gameObject.GetComponent(); networkObject.NetworkManagerOwner = Server; networkObject.Spawn(); @@ -62,6 +61,13 @@ public IEnumerator TrackOwnershipChangeSentMetric() var ownershipChangeSent = metricValues.First(); Assert.AreEqual(networkObject.NetworkObjectId, ownershipChangeSent.NetworkId.NetworkId); Assert.AreEqual(Server.LocalClientId, ownershipChangeSent.Connection.Id); + Assert.AreEqual(0, ownershipChangeSent.BytesCount); + + // The first metric is to the server(self), so its size is now correctly reported as 0. + // Let's check the last one instead, to have a valid value + ownershipChangeSent = metricValues.Last(); + Assert.AreEqual(networkObject.NetworkObjectId, ownershipChangeSent.NetworkId.NetworkId); + Assert.AreEqual(Client.LocalClientId, ownershipChangeSent.Connection.Id); Assert.AreEqual(FastBufferWriter.GetWriteSize() + k_MessageHeaderSize, ownershipChangeSent.BytesCount); } diff --git a/Tests/Runtime/Metrics/PacketLossMetricsTests.cs b/Tests/Runtime/Metrics/PacketLossMetricsTests.cs index d8c19c9..4a008cf 100644 --- a/Tests/Runtime/Metrics/PacketLossMetricsTests.cs +++ b/Tests/Runtime/Metrics/PacketLossMetricsTests.cs @@ -1,7 +1,6 @@ #if MULTIPLAYER_TOOLS #if MULTIPLAYER_TOOLS_1_0_0_PRE_7 -using System; using System.Collections; using NUnit.Framework; using Unity.Collections; @@ -9,7 +8,9 @@ using Unity.Netcode.TestHelpers.Runtime; using Unity.Netcode.TestHelpers.Runtime.Metrics; using Unity.Netcode.Transports.UTP; -using UnityEngine; +#if UTP_TRANSPORT_2_0_ABOVE +using Unity.Networking.Transport.Utilities; +#endif using UnityEngine.TestTools; namespace Unity.Netcode.RuntimeTests.Metrics @@ -18,16 +19,25 @@ public class PacketLossMetricsTests : NetcodeIntegrationTest { protected override int NumberOfClients => 1; private readonly int m_PacketLossRate = 25; - private readonly int m_PacketLossRangeDelta = 5; + private readonly int m_PacketLossRangeDelta = 3; + private readonly int m_MessageSize = 200; public PacketLossMetricsTests() : base(HostOrServer.Server) - {} + { } protected override void OnServerAndClientsCreated() { var clientTransport = (UnityTransport)m_ClientNetworkManagers[0].NetworkConfig.NetworkTransport; +#if !UTP_TRANSPORT_2_0_ABOVE clientTransport.SetDebugSimulatorParameters(0, 0, m_PacketLossRate); +#endif + + // Determined through trial and error. With both UTP 1.2 and 2.0, this random seed + // results in an effective packet loss percentage between 22% and 28%. Future UTP + // updates may change the RNG call patterns and cause this test to fail, in which + // case the value should be modified again. + clientTransport.DebugSimulatorRandomSeed = 4; base.OnServerAndClientsCreated(); } @@ -41,11 +51,9 @@ public IEnumerator TrackPacketLossAsServer() for (int i = 0; i < 1000; ++i) { - using (var writer = new FastBufferWriter(sizeof(byte), Allocator.Persistent)) - { - writer.WriteByteSafe(42); - m_ServerNetworkManager.CustomMessagingManager.SendNamedMessage("Test", m_ServerNetworkManager.ConnectedClientsIds, writer); - } + using var writer = new FastBufferWriter(m_MessageSize, Allocator.Persistent); + writer.WriteBytesSafe(new byte[m_MessageSize]); + m_ServerNetworkManager.CustomMessagingManager.SendNamedMessage("Test", m_ServerNetworkManager.ConnectedClientsIds, writer); } yield return waitForPacketLossMetric.WaitForMetricsReceived(); @@ -57,20 +65,26 @@ public IEnumerator TrackPacketLossAsServer() [UnityTest] public IEnumerator TrackPacketLossAsClient() { - double packetLossRateMinRange = (m_PacketLossRate-m_PacketLossRangeDelta) / 100d; + double packetLossRateMinRange = (m_PacketLossRate - m_PacketLossRangeDelta) / 100d; double packetLossRateMaxrange = (m_PacketLossRate + m_PacketLossRangeDelta) / 100d; var clientNetworkManager = m_ClientNetworkManagers[0]; + +#if UTP_TRANSPORT_2_0_ABOVE + var clientTransport = (UnityTransport)clientNetworkManager.NetworkConfig.NetworkTransport; + clientTransport.NetworkDriver.CurrentSettings.TryGet(out var parameters); + parameters.PacketDropPercentage = m_PacketLossRate; + clientTransport.NetworkDriver.ModifySimulatorStageParameters(parameters); +#endif + var waitForPacketLossMetric = new WaitForGaugeMetricValues((clientNetworkManager.NetworkMetrics as NetworkMetrics).Dispatcher, NetworkMetricTypes.PacketLoss, metric => packetLossRateMinRange <= metric && metric <= packetLossRateMaxrange); for (int i = 0; i < 1000; ++i) { - using (var writer = new FastBufferWriter(sizeof(byte), Allocator.Persistent)) - { - writer.WriteByteSafe(42); - m_ServerNetworkManager.CustomMessagingManager.SendNamedMessage("Test", m_ServerNetworkManager.ConnectedClientsIds, writer); - } + using var writer = new FastBufferWriter(m_MessageSize, Allocator.Persistent); + writer.WriteBytesSafe(new byte[m_MessageSize]); + m_ServerNetworkManager.CustomMessagingManager.SendNamedMessage("Test", m_ServerNetworkManager.ConnectedClientsIds, writer); } yield return waitForPacketLossMetric.WaitForMetricsReceived(); diff --git a/Tests/Runtime/Metrics/PacketMetricsTests.cs b/Tests/Runtime/Metrics/PacketMetricsTests.cs index a9e4cc0..90707de 100644 --- a/Tests/Runtime/Metrics/PacketMetricsTests.cs +++ b/Tests/Runtime/Metrics/PacketMetricsTests.cs @@ -5,7 +5,6 @@ using Unity.Collections; using Unity.Multiplayer.Tools.MetricTypes; using UnityEngine.TestTools; -using Unity.Netcode.TestHelpers.Runtime; using Unity.Netcode.TestHelpers.Runtime.Metrics; namespace Unity.Netcode.RuntimeTests.Metrics diff --git a/Tests/Runtime/Metrics/RpcMetricsTests.cs b/Tests/Runtime/Metrics/RpcMetricsTests.cs index 0d802ce..b2b186a 100644 --- a/Tests/Runtime/Metrics/RpcMetricsTests.cs +++ b/Tests/Runtime/Metrics/RpcMetricsTests.cs @@ -2,13 +2,14 @@ using System.Collections; using System.Linq; using NUnit.Framework; +using Unity.Collections; using Unity.Multiplayer.Tools.MetricTypes; using UnityEngine.TestTools; using Unity.Netcode.TestHelpers.Runtime.Metrics; namespace Unity.Netcode.RuntimeTests.Metrics { - internal class RpcMetricsTests : SingleClientMetricTestBase + internal class RpcMetricsTests : DualClientMetricTestBase { protected override void OnCreatePlayerPrefab() { @@ -17,30 +18,79 @@ protected override void OnCreatePlayerPrefab() } [UnityTest] - public IEnumerator TrackRpcSentMetricOnServer() + public IEnumerator TrackRpcSentMetricOnServerToOnlyOneClientWithArray() { var waitForMetricValues = new WaitForEventMetricValues(ServerMetrics.Dispatcher, NetworkMetricTypes.RpcSent); - m_PlayerNetworkObjects[m_ServerNetworkManager.LocalClientId][Client.LocalClientId].GetComponent().MyClientRpc(); + m_PlayerNetworkObjects[m_ServerNetworkManager.LocalClientId][FirstClient.LocalClientId].GetComponent().MyClientRpc(new ClientRpcParams + { + Send = new ClientRpcSendParams + { + TargetClientIds = new[] { FirstClient.LocalClientId } + } + }); yield return waitForMetricValues.WaitForMetricsReceived(); var serverRpcSentValues = waitForMetricValues.AssertMetricValuesHaveBeenFound(); - Assert.AreEqual(2, serverRpcSentValues.Count); // Server will receive this, since it's host + Assert.AreEqual(1, serverRpcSentValues.Count); + + Assert.That(serverRpcSentValues, Has.All.Matches(x => x.Name == nameof(RpcTestComponent.MyClientRpc))); + Assert.That(serverRpcSentValues, Has.All.Matches(x => x.NetworkBehaviourName == nameof(RpcTestComponent))); + Assert.That(serverRpcSentValues, Has.All.Matches(x => x.BytesCount != 0)); + Assert.AreEqual(FirstClient.LocalClientId, serverRpcSentValues.First().Connection.Id); + } + + [UnityTest] + public IEnumerator TrackRpcSentMetricOnServerToOnlyOneClientWithNativeArray() + { + var waitForMetricValues = new WaitForEventMetricValues(ServerMetrics.Dispatcher, NetworkMetricTypes.RpcSent); + + m_PlayerNetworkObjects[m_ServerNetworkManager.LocalClientId][FirstClient.LocalClientId].GetComponent().MyClientRpc(new ClientRpcParams + { + Send = new ClientRpcSendParams + { + TargetClientIdsNativeArray = new NativeArray(new[] { FirstClient.LocalClientId }, Allocator.Temp) + } + }); + + yield return waitForMetricValues.WaitForMetricsReceived(); + + var serverRpcSentValues = waitForMetricValues.AssertMetricValuesHaveBeenFound(); + Assert.AreEqual(1, serverRpcSentValues.Count); + + Assert.That(serverRpcSentValues, Has.All.Matches(x => x.Name == nameof(RpcTestComponent.MyClientRpc))); + Assert.That(serverRpcSentValues, Has.All.Matches(x => x.NetworkBehaviourName == nameof(RpcTestComponent))); + Assert.That(serverRpcSentValues, Has.All.Matches(x => x.BytesCount != 0)); + Assert.AreEqual(FirstClient.LocalClientId, serverRpcSentValues.First().Connection.Id); + } + + [UnityTest] + public IEnumerator TrackRpcSentMetricOnServerToAllClients() + { + var waitForMetricValues = new WaitForEventMetricValues(ServerMetrics.Dispatcher, NetworkMetricTypes.RpcSent); + + m_PlayerNetworkObjects[m_ServerNetworkManager.LocalClientId][FirstClient.LocalClientId].GetComponent().MyClientRpc(); + + yield return waitForMetricValues.WaitForMetricsReceived(); + + var serverRpcSentValues = waitForMetricValues.AssertMetricValuesHaveBeenFound(); + Assert.AreEqual(3, serverRpcSentValues.Count); // Server will receive this, since it's host Assert.That(serverRpcSentValues, Has.All.Matches(x => x.Name == nameof(RpcTestComponent.MyClientRpc))); Assert.That(serverRpcSentValues, Has.All.Matches(x => x.NetworkBehaviourName == nameof(RpcTestComponent))); Assert.That(serverRpcSentValues, Has.All.Matches(x => x.BytesCount != 0)); Assert.Contains(Server.LocalClientId, serverRpcSentValues.Select(x => x.Connection.Id).ToArray()); - Assert.Contains(Client.LocalClientId, serverRpcSentValues.Select(x => x.Connection.Id).ToArray()); + Assert.Contains(FirstClient.LocalClientId, serverRpcSentValues.Select(x => x.Connection.Id).ToArray()); + Assert.Contains(SecondClient.LocalClientId, serverRpcSentValues.Select(x => x.Connection.Id).ToArray()); } [UnityTest] public IEnumerator TrackRpcSentMetricOnClient() { - var waitForClientMetricsValues = new WaitForEventMetricValues(ClientMetrics.Dispatcher, NetworkMetricTypes.RpcSent); + var waitForClientMetricsValues = new WaitForEventMetricValues(FirstClientMetrics.Dispatcher, NetworkMetricTypes.RpcSent); - m_PlayerNetworkObjects[Client.LocalClientId][Client.LocalClientId].GetComponent().MyServerRpc(); + m_PlayerNetworkObjects[FirstClient.LocalClientId][FirstClient.LocalClientId].GetComponent().MyServerRpc(); yield return waitForClientMetricsValues.WaitForMetricsReceived(); @@ -58,7 +108,7 @@ public IEnumerator TrackRpcSentMetricOnClient() public IEnumerator TrackRpcReceivedMetricOnServer() { var waitForServerMetricsValues = new WaitForEventMetricValues(ServerMetrics.Dispatcher, NetworkMetricTypes.RpcReceived); - m_PlayerNetworkObjects[Client.LocalClientId][Client.LocalClientId].GetComponent().MyServerRpc(); + m_PlayerNetworkObjects[FirstClient.LocalClientId][FirstClient.LocalClientId].GetComponent().MyServerRpc(); yield return waitForServerMetricsValues.WaitForMetricsReceived(); @@ -66,7 +116,7 @@ public IEnumerator TrackRpcReceivedMetricOnServer() Assert.AreEqual(1, serverRpcReceivedValues.Count); var rpcReceived = serverRpcReceivedValues.First(); - Assert.AreEqual(Client.LocalClientId, rpcReceived.Connection.Id); + Assert.AreEqual(FirstClient.LocalClientId, rpcReceived.Connection.Id); Assert.AreEqual(nameof(RpcTestComponent.MyServerRpc), rpcReceived.Name); Assert.AreEqual(nameof(RpcTestComponent), rpcReceived.NetworkBehaviourName); Assert.AreNotEqual(0, rpcReceived.BytesCount); @@ -75,9 +125,9 @@ public IEnumerator TrackRpcReceivedMetricOnServer() [UnityTest] public IEnumerator TrackRpcReceivedMetricOnClient() { - var waitForClientMetricsValues = new WaitForEventMetricValues(ClientMetrics.Dispatcher, NetworkMetricTypes.RpcReceived); + var waitForClientMetricsValues = new WaitForEventMetricValues(FirstClientMetrics.Dispatcher, NetworkMetricTypes.RpcReceived); - m_PlayerNetworkObjects[m_ServerNetworkManager.LocalClientId][Client.LocalClientId].GetComponent().MyClientRpc(); + m_PlayerNetworkObjects[m_ServerNetworkManager.LocalClientId][FirstClient.LocalClientId].GetComponent().MyClientRpc(); yield return waitForClientMetricsValues.WaitForMetricsReceived(); diff --git a/Tests/Runtime/Metrics/TransportBytesMetricsTests.cs b/Tests/Runtime/Metrics/TransportBytesMetricsTests.cs index a6ded69..f1891bb 100644 --- a/Tests/Runtime/Metrics/TransportBytesMetricsTests.cs +++ b/Tests/Runtime/Metrics/TransportBytesMetricsTests.cs @@ -1,7 +1,6 @@ #if MULTIPLAYER_TOOLS using System; using System.Collections; -using System.IO; using NUnit.Framework; using Unity.Collections; using Unity.Multiplayer.Tools.MetricTypes; @@ -15,7 +14,7 @@ internal class TransportBytesMetricsTests : SingleClientMetricTestBase { // Header is dynamically sized due to packing, will be 2 bytes for all test messages. private const int k_MessageHeaderSize = 2; - static readonly long MessageOverhead = 8 + FastBufferWriter.GetWriteSize() + k_MessageHeaderSize; + private static readonly long k_MessageOverhead = 8 + FastBufferWriter.GetWriteSize() + k_MessageHeaderSize; [UnityTest] public IEnumerator TrackTotalNumberOfBytesSent() @@ -42,7 +41,7 @@ public IEnumerator TrackTotalNumberOfBytesSent() } Assert.True(observer.Found); - Assert.AreEqual(FastBufferWriter.GetWriteSize(messageName) + MessageOverhead, observer.Value); + Assert.AreEqual(FastBufferWriter.GetWriteSize(messageName) + k_MessageOverhead, observer.Value); } [UnityTest] @@ -72,7 +71,7 @@ public IEnumerator TrackTotalNumberOfBytesReceived() } Assert.True(observer.Found); - Assert.AreEqual(FastBufferWriter.GetWriteSize(messageName) + MessageOverhead, observer.Value); + Assert.AreEqual(FastBufferWriter.GetWriteSize(messageName) + k_MessageOverhead, observer.Value); } private class TotalBytesObserver : IMetricObserver diff --git a/Tests/Runtime/NetworkAnimator.meta b/Tests/Runtime/NetworkAnimator.meta deleted file mode 100644 index c08562e..0000000 --- a/Tests/Runtime/NetworkAnimator.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 3acde7838205d4b09ae3a035554c51c5 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Tests/Runtime/NetworkObject/NetworkObjectNetworkClientOwnedObjectsTests.cs b/Tests/Runtime/NetworkObject/NetworkObjectNetworkClientOwnedObjectsTests.cs index fcdbfd8..bcdd613 100644 --- a/Tests/Runtime/NetworkObject/NetworkObjectNetworkClientOwnedObjectsTests.cs +++ b/Tests/Runtime/NetworkObject/NetworkObjectNetworkClientOwnedObjectsTests.cs @@ -9,6 +9,11 @@ namespace Unity.Netcode.RuntimeTests { public class NetworkObjectNetworkClientOwnedObjectsTests : NetcodeIntegrationTest { + private class DummyNetworkBehaviour : NetworkBehaviour + { + + } + protected override int NumberOfClients => 1; private NetworkPrefab m_NetworkPrefab; protected override void OnServerAndClientsCreated() @@ -16,6 +21,7 @@ protected override void OnServerAndClientsCreated() // create prefab var gameObject = new GameObject("ClientOwnedObject"); var networkObject = gameObject.AddComponent(); + gameObject.AddComponent(); NetcodeIntegrationTestHelpers.MakeNetworkObjectTestPrefab(networkObject); m_NetworkPrefab = (new NetworkPrefab() @@ -39,7 +45,7 @@ public IEnumerator ChangeOwnershipOwnedObjectsAddTest() serverObject.Spawn(); // Provide enough time for the client to receive and process the spawned message. - yield return s_DefaultWaitForTick; + yield return WaitForMessageReceived(m_ClientNetworkManagers.ToList()); // The object is owned by server Assert.False(m_ServerNetworkManager.SpawnManager.GetClientOwnedObjects(m_ClientNetworkManagers[0].LocalClientId).Any(x => x.NetworkObjectId == serverObject.NetworkObjectId)); @@ -48,13 +54,71 @@ public IEnumerator ChangeOwnershipOwnedObjectsAddTest() serverObject.ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); // Provide enough time for the client to receive and process the change in ownership message. - yield return s_DefaultWaitForTick; + yield return WaitForMessageReceived(m_ClientNetworkManagers.ToList()); // Ensure it's now added to the list - yield return WaitForConditionOrTimeOut(() => m_ClientNetworkManagers[0].SpawnManager.GetClientOwnedObjects(m_ClientNetworkManagers[0].LocalClientId).Any(x => x.NetworkObjectId == serverObject.NetworkObjectId)); - Assert.False(s_GlobalTimeoutHelper.TimedOut, $"Timed out waiting for client to gain ownership!"); Assert.True(m_ClientNetworkManagers[0].SpawnManager.GetClientOwnedObjects(m_ClientNetworkManagers[0].LocalClientId).Any(x => x.NetworkObjectId == serverObject.NetworkObjectId)); Assert.True(m_ServerNetworkManager.SpawnManager.GetClientOwnedObjects(m_ClientNetworkManagers[0].LocalClientId).Any(x => x.NetworkObjectId == serverObject.NetworkObjectId)); } + + [UnityTest] + public IEnumerator WhenOwnershipIsChanged_OwnershipValuesUpdateCorrectly() + { + NetworkObject serverObject = Object.Instantiate(m_NetworkPrefab.Prefab).GetComponent(); + serverObject.NetworkManagerOwner = m_ServerNetworkManager; + serverObject.Spawn(); + + // Provide enough time for the client to receive and process the spawned message. + yield return WaitForMessageReceived(m_ClientNetworkManagers.ToList()); + + // The object is owned by server + Assert.False(m_ServerNetworkManager.SpawnManager.GetClientOwnedObjects(m_ClientNetworkManagers[0].LocalClientId).Any(x => x.NetworkObjectId == serverObject.NetworkObjectId)); + + // Change the ownership + serverObject.ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); + + // Provide enough time for the client to receive and process the change in ownership message. + yield return WaitForMessageReceived(m_ClientNetworkManagers.ToList()); + + Assert.IsFalse(serverObject.IsOwner); + Assert.IsFalse(serverObject.IsOwnedByServer); + Assert.AreEqual(m_ClientNetworkManagers[0].LocalClientId, serverObject.OwnerClientId); + + var serverBehaviour = serverObject.GetComponent(); + Assert.IsFalse(serverBehaviour.IsOwner); + Assert.IsFalse(serverBehaviour.IsOwnedByServer); + Assert.AreEqual(m_ClientNetworkManagers[0].LocalClientId, serverBehaviour.OwnerClientId); + + var clientObject = Object.FindObjectsOfType().Where((obj) => obj.NetworkManagerOwner == m_ClientNetworkManagers[0]).FirstOrDefault(); + + Assert.IsNotNull(clientObject); + Assert.IsTrue(clientObject.IsOwner); + Assert.IsFalse(clientObject.IsOwnedByServer); + Assert.AreEqual(m_ClientNetworkManagers[0].LocalClientId, clientObject.OwnerClientId); + + var clientBehaviour = clientObject.GetComponent(); + Assert.IsTrue(clientBehaviour.IsOwner); + Assert.IsFalse(clientBehaviour.IsOwnedByServer); + Assert.AreEqual(m_ClientNetworkManagers[0].LocalClientId, clientBehaviour.OwnerClientId); + + serverObject.RemoveOwnership(); + + // Provide enough time for the client to receive and process the change in ownership message. + yield return WaitForMessageReceived(m_ClientNetworkManagers.ToList()); + + Assert.IsTrue(serverObject.IsOwner); + Assert.IsTrue(serverObject.IsOwnedByServer); + Assert.AreEqual(NetworkManager.ServerClientId, serverObject.OwnerClientId); + Assert.IsTrue(serverBehaviour.IsOwner); + Assert.IsTrue(serverBehaviour.IsOwnedByServer); + Assert.AreEqual(NetworkManager.ServerClientId, serverBehaviour.OwnerClientId); + + Assert.IsFalse(clientObject.IsOwner); + Assert.IsTrue(clientObject.IsOwnedByServer); + Assert.AreEqual(NetworkManager.ServerClientId, clientObject.OwnerClientId); + Assert.IsFalse(clientBehaviour.IsOwner); + Assert.IsTrue(clientBehaviour.IsOwnedByServer); + Assert.AreEqual(NetworkManager.ServerClientId, clientBehaviour.OwnerClientId); + } } } diff --git a/Tests/Runtime/NetworkShowHideTests.cs b/Tests/Runtime/NetworkShowHideTests.cs index bd95fa4..2f43484 100644 --- a/Tests/Runtime/NetworkShowHideTests.cs +++ b/Tests/Runtime/NetworkShowHideTests.cs @@ -17,6 +17,7 @@ public class ShowHideObject : NetworkBehaviour { public static List ClientTargetedNetworkObjects = new List(); public static ulong ClientIdToTarget; + public static bool Silent; public static NetworkObject GetNetworkObjectById(ulong networkObjectId) { @@ -36,6 +37,17 @@ public override void OnNetworkSpawn() { ClientTargetedNetworkObjects.Add(this); } + + if (IsServer) + { + MyListSetOnSpawn.Add(45); + } + else + { + Debug.Assert(MyListSetOnSpawn.Count == 1); + Debug.Assert(MyListSetOnSpawn[0] == 45); + } + base.OnNetworkSpawn(); } @@ -49,16 +61,22 @@ public override void OnNetworkDespawn() } public NetworkVariable MyNetworkVariable; + public NetworkList MyListSetOnSpawn; private void Awake() { MyNetworkVariable = new NetworkVariable(); MyNetworkVariable.OnValueChanged += Changed; + + MyListSetOnSpawn = new NetworkList(); } public void Changed(int before, int after) { - Debug.Log($"Value changed from {before} to {after}"); + if (!Silent) + { + Debug.Log($"Value changed from {before} to {after}"); + } } } @@ -264,5 +282,61 @@ public IEnumerator NetworkShowHideQuickTest() yield return CheckVisible(true); } } + + [UnityTest] + public IEnumerator NetworkHideDespawnTest() + { + m_ClientId0 = m_ClientNetworkManagers[0].LocalClientId; + ShowHideObject.ClientTargetedNetworkObjects.Clear(); + ShowHideObject.ClientIdToTarget = m_ClientId0; + ShowHideObject.Silent = true; + + var spawnedObject1 = SpawnObject(m_PrefabToSpawn, m_ServerNetworkManager); + var spawnedObject2 = SpawnObject(m_PrefabToSpawn, m_ServerNetworkManager); + var spawnedObject3 = SpawnObject(m_PrefabToSpawn, m_ServerNetworkManager); + m_NetSpawnedObject1 = spawnedObject1.GetComponent(); + m_NetSpawnedObject2 = spawnedObject2.GetComponent(); + m_NetSpawnedObject3 = spawnedObject3.GetComponent(); + + m_NetSpawnedObject1.GetComponent().MyNetworkVariable.Value++; + m_NetSpawnedObject1.NetworkHide(m_ClientId0); + m_NetSpawnedObject1.Despawn(); + + yield return NetcodeIntegrationTestHelpers.WaitForTicks(m_ServerNetworkManager, 5); + + LogAssert.NoUnexpectedReceived(); + } + + [UnityTest] + public IEnumerator NetworkHideChangeOwnership() + { + ShowHideObject.ClientTargetedNetworkObjects.Clear(); + ShowHideObject.ClientIdToTarget = m_ClientNetworkManagers[1].LocalClientId; + ShowHideObject.Silent = true; + + var spawnedObject1 = SpawnObject(m_PrefabToSpawn, m_ServerNetworkManager); + m_NetSpawnedObject1 = spawnedObject1.GetComponent(); + + m_NetSpawnedObject1.GetComponent().MyNetworkVariable.Value++; + // Hide an object to a client + m_NetSpawnedObject1.NetworkHide(m_ClientNetworkManagers[1].LocalClientId); + + yield return WaitForConditionOrTimeOut(() => ShowHideObject.ClientTargetedNetworkObjects.Count == 0); + + // Change ownership while the object is hidden to some + m_NetSpawnedObject1.ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); + + // The two-second wait is actually needed as there's a potential warning of unhandled message after 1 second + yield return new WaitForSeconds(1.25f); + + LogAssert.NoUnexpectedReceived(); + + // Show the object again to check nothing unexpected happens + m_NetSpawnedObject1.NetworkShow(m_ClientNetworkManagers[1].LocalClientId); + + yield return WaitForConditionOrTimeOut(() => ShowHideObject.ClientTargetedNetworkObjects.Count == 1); + + Assert.True(ShowHideObject.ClientTargetedNetworkObjects[0].OwnerClientId == m_ClientNetworkManagers[0].LocalClientId); + } } } diff --git a/Tests/Runtime/NetworkTransform/NetworkTransformStateTests.cs b/Tests/Runtime/NetworkTransform/NetworkTransformStateTests.cs index 572b04a..2c56188 100644 --- a/Tests/Runtime/NetworkTransform/NetworkTransformStateTests.cs +++ b/Tests/Runtime/NetworkTransform/NetworkTransformStateTests.cs @@ -4,15 +4,87 @@ namespace Unity.Netcode.RuntimeTests { + + [TestFixture(TransformSpace.World)] + [TestFixture(TransformSpace.Local)] public class NetworkTransformStateTests { + public enum SyncAxis + { + SyncPosX, + SyncPosY, + SyncPosZ, + SyncPosXY, + SyncPosXZ, + SyncPosYZ, + SyncPosXYZ, + SyncRotX, + SyncRotY, + SyncRotZ, + SyncRotXY, + SyncRotXZ, + SyncRotYZ, + SyncRotXYZ, + SyncScaleX, + SyncScaleY, + SyncScaleZ, + SyncScaleXY, + SyncScaleXZ, + SyncScaleYZ, + SyncScaleXYZ, + SyncAllX, + SyncAllY, + SyncAllZ, + SyncAllXY, + SyncAllXZ, + SyncAllYZ, + SyncAllXYZ + } + + public enum TransformSpace + { + World, + Local + } + + public enum SynchronizationType + { + Delta, + Teleport + } + + private TransformSpace m_TransformSpace; + + public NetworkTransformStateTests(TransformSpace transformSpace) + { + m_TransformSpace = transformSpace; + } + + private bool WillAnAxisBeSynchronized(ref NetworkTransform networkTransform) + { + return networkTransform.SyncScaleX || networkTransform.SyncScaleY || networkTransform.SyncScaleZ || + networkTransform.SyncRotAngleX || networkTransform.SyncRotAngleY || networkTransform.SyncRotAngleZ || + networkTransform.SyncPositionX || networkTransform.SyncPositionY || networkTransform.SyncPositionZ; + } + [Test] - public void TestSyncAxes( - [Values] bool inLocalSpace, - [Values] bool syncPosX, [Values] bool syncPosY, [Values] bool syncPosZ, - [Values] bool syncRotX, [Values] bool syncRotY, [Values] bool syncRotZ, - [Values] bool syncScaX, [Values] bool syncScaY, [Values] bool syncScaZ) + public void TestSyncAxes([Values] SynchronizationType synchronizationType, [Values] SyncAxis syncAxis) + { + bool inLocalSpace = m_TransformSpace == TransformSpace.Local; + bool isTeleporting = synchronizationType == SynchronizationType.Teleport; + bool syncPosX = syncAxis == SyncAxis.SyncPosX || syncAxis == SyncAxis.SyncPosXY || syncAxis == SyncAxis.SyncPosXZ || syncAxis == SyncAxis.SyncPosXYZ || syncAxis == SyncAxis.SyncAllX || syncAxis == SyncAxis.SyncAllXY || syncAxis == SyncAxis.SyncAllXZ || syncAxis == SyncAxis.SyncAllXYZ; + bool syncPosY = syncAxis == SyncAxis.SyncPosY || syncAxis == SyncAxis.SyncPosXY || syncAxis == SyncAxis.SyncPosYZ || syncAxis == SyncAxis.SyncPosXYZ || syncAxis == SyncAxis.SyncAllY || syncAxis == SyncAxis.SyncAllXY || syncAxis == SyncAxis.SyncAllYZ || syncAxis == SyncAxis.SyncAllXYZ; + bool syncPosZ = syncAxis == SyncAxis.SyncPosZ || syncAxis == SyncAxis.SyncPosXZ || syncAxis == SyncAxis.SyncPosYZ || syncAxis == SyncAxis.SyncPosXYZ || syncAxis == SyncAxis.SyncAllZ || syncAxis == SyncAxis.SyncAllXZ || syncAxis == SyncAxis.SyncAllYZ || syncAxis == SyncAxis.SyncAllXYZ; + + bool syncRotX = syncAxis == SyncAxis.SyncRotX || syncAxis == SyncAxis.SyncRotXY || syncAxis == SyncAxis.SyncRotXZ || syncAxis == SyncAxis.SyncRotXYZ || syncAxis == SyncAxis.SyncRotX || syncAxis == SyncAxis.SyncAllXY || syncAxis == SyncAxis.SyncAllXZ || syncAxis == SyncAxis.SyncAllXYZ; + bool syncRotY = syncAxis == SyncAxis.SyncRotY || syncAxis == SyncAxis.SyncRotXY || syncAxis == SyncAxis.SyncRotYZ || syncAxis == SyncAxis.SyncRotXYZ || syncAxis == SyncAxis.SyncRotY || syncAxis == SyncAxis.SyncAllXY || syncAxis == SyncAxis.SyncAllYZ || syncAxis == SyncAxis.SyncAllXYZ; + bool syncRotZ = syncAxis == SyncAxis.SyncRotZ || syncAxis == SyncAxis.SyncRotXZ || syncAxis == SyncAxis.SyncRotYZ || syncAxis == SyncAxis.SyncRotXYZ || syncAxis == SyncAxis.SyncRotZ || syncAxis == SyncAxis.SyncAllXZ || syncAxis == SyncAxis.SyncAllYZ || syncAxis == SyncAxis.SyncAllXYZ; + + bool syncScaX = syncAxis == SyncAxis.SyncScaleX || syncAxis == SyncAxis.SyncScaleXY || syncAxis == SyncAxis.SyncScaleXZ || syncAxis == SyncAxis.SyncScaleXYZ || syncAxis == SyncAxis.SyncAllX || syncAxis == SyncAxis.SyncAllXY || syncAxis == SyncAxis.SyncAllXZ || syncAxis == SyncAxis.SyncAllXYZ; + bool syncScaY = syncAxis == SyncAxis.SyncScaleY || syncAxis == SyncAxis.SyncScaleXY || syncAxis == SyncAxis.SyncScaleYZ || syncAxis == SyncAxis.SyncScaleXYZ || syncAxis == SyncAxis.SyncAllY || syncAxis == SyncAxis.SyncAllXY || syncAxis == SyncAxis.SyncAllYZ || syncAxis == SyncAxis.SyncAllXYZ; + bool syncScaZ = syncAxis == SyncAxis.SyncScaleZ || syncAxis == SyncAxis.SyncScaleXZ || syncAxis == SyncAxis.SyncScaleYZ || syncAxis == SyncAxis.SyncScaleXYZ || syncAxis == SyncAxis.SyncAllZ || syncAxis == SyncAxis.SyncAllXZ || syncAxis == SyncAxis.SyncAllYZ || syncAxis == SyncAxis.SyncAllXYZ; + var gameObject = new GameObject($"Test-{nameof(NetworkTransformStateTests)}.{nameof(TestSyncAxes)}"); var networkObject = gameObject.AddComponent(); var networkTransform = gameObject.AddComponent(); @@ -36,27 +108,13 @@ public void TestSyncAxes( networkTransform.SyncScaleZ = syncScaZ; networkTransform.InLocalSpace = inLocalSpace; + // We want a relatively clean networkTransform state before we try to apply the transform to it + // We only preserve InLocalSpace and IsTeleportingNextFrame properties as they are the only things + // needed when applying a transform to a NetworkTransformState var networkTransformState = new NetworkTransform.NetworkTransformState { - PositionX = initialPosition.x, - PositionY = initialPosition.y, - PositionZ = initialPosition.z, - RotAngleX = initialRotAngles.x, - RotAngleY = initialRotAngles.y, - RotAngleZ = initialRotAngles.z, - ScaleX = initialScale.x, - ScaleY = initialScale.y, - ScaleZ = initialScale.z, - HasPositionX = syncPosX, - HasPositionY = syncPosY, - HasPositionZ = syncPosZ, - HasRotAngleX = syncRotX, - HasRotAngleY = syncRotY, - HasRotAngleZ = syncRotZ, - HasScaleX = syncScaX, - HasScaleY = syncScaY, - HasScaleZ = syncScaZ, - InLocalSpace = inLocalSpace + InLocalSpace = inLocalSpace, + IsTeleportingNextFrame = isTeleporting, }; // Step 1: change properties, expect state to be dirty @@ -71,95 +129,382 @@ public void TestSyncAxes( } } - // Step 2: disable a particular sync flag, expect state to be not dirty + // We want to start with a fresh NetworkTransformState since it could have other state + // information from the last time we applied the transform + networkTransformState = new NetworkTransform.NetworkTransformState + { + InLocalSpace = inLocalSpace, + IsTeleportingNextFrame = isTeleporting, + }; + var position = networkTransform.transform.position; + var rotAngles = networkTransform.transform.eulerAngles; + var scale = networkTransform.transform.localScale; + + // Step 2: Verify the state changes in a tick are additive + // TODO: This will need to change if we update NetworkTransform to send all of the + // axis deltas that happened over a tick as a collection instead of collapsing them + // as the changes are detected. + { + networkTransformState = new NetworkTransform.NetworkTransformState + { + InLocalSpace = inLocalSpace, + IsTeleportingNextFrame = isTeleporting, + }; + + // SyncPositionX + if (syncPosX) + { + position.x++; + networkTransform.transform.position = position; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + Assert.IsTrue(networkTransformState.HasPositionX); + } + + // SyncPositionY + if (syncPosY) + { + position = networkTransform.transform.position; + position.y++; + networkTransform.transform.position = position; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + Assert.IsTrue(networkTransformState.HasPositionX || !syncPosX); + Assert.IsTrue(networkTransformState.HasPositionY); + } + + // SyncPositionZ + if (syncPosZ) + { + position = networkTransform.transform.position; + position.z++; + networkTransform.transform.position = position; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + Assert.IsTrue(networkTransformState.HasPositionX || !syncPosX); + Assert.IsTrue(networkTransformState.HasPositionY || !syncPosY); + Assert.IsTrue(networkTransformState.HasPositionZ); + } + + // SyncRotAngleX + if (syncRotX) + { + rotAngles = networkTransform.transform.eulerAngles; + rotAngles.x++; + networkTransform.transform.eulerAngles = rotAngles; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + Assert.IsTrue(networkTransformState.HasPositionX || !syncPosX); + Assert.IsTrue(networkTransformState.HasPositionY || !syncPosY); + Assert.IsTrue(networkTransformState.HasPositionZ || !syncPosZ); + Assert.IsTrue(networkTransformState.HasRotAngleX); + } + + // SyncRotAngleY + if (syncRotY) + { + rotAngles = networkTransform.transform.eulerAngles; + rotAngles.y++; + networkTransform.transform.eulerAngles = rotAngles; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + Assert.IsTrue(networkTransformState.HasPositionX || !syncPosX); + Assert.IsTrue(networkTransformState.HasPositionY || !syncPosY); + Assert.IsTrue(networkTransformState.HasPositionZ || !syncPosZ); + Assert.IsTrue(networkTransformState.HasRotAngleX || !syncRotX); + Assert.IsTrue(networkTransformState.HasRotAngleY); + } + // SyncRotAngleZ + if (syncRotZ) + { + rotAngles = networkTransform.transform.eulerAngles; + rotAngles.z++; + networkTransform.transform.eulerAngles = rotAngles; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + Assert.IsTrue(networkTransformState.HasPositionX || !syncPosX); + Assert.IsTrue(networkTransformState.HasPositionY || !syncPosY); + Assert.IsTrue(networkTransformState.HasPositionZ || !syncPosZ); + Assert.IsTrue(networkTransformState.HasRotAngleX || !syncRotX); + Assert.IsTrue(networkTransformState.HasRotAngleY || !syncRotY); + Assert.IsTrue(networkTransformState.HasRotAngleZ); + } + + // SyncScaleX + if (syncScaX) + { + scale = networkTransform.transform.localScale; + scale.x++; + networkTransform.transform.localScale = scale; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + Assert.IsTrue(networkTransformState.HasPositionX || !syncPosX); + Assert.IsTrue(networkTransformState.HasPositionY || !syncPosY); + Assert.IsTrue(networkTransformState.HasPositionZ || !syncPosZ); + Assert.IsTrue(networkTransformState.HasRotAngleX || !syncRotX); + Assert.IsTrue(networkTransformState.HasRotAngleY || !syncRotY); + Assert.IsTrue(networkTransformState.HasRotAngleZ || !syncRotZ); + Assert.IsTrue(networkTransformState.HasScaleX); + } + // SyncScaleY + if (syncScaY) + { + scale = networkTransform.transform.localScale; + scale.y++; + networkTransform.transform.localScale = scale; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + Assert.IsTrue(networkTransformState.HasPositionX || !syncPosX); + Assert.IsTrue(networkTransformState.HasPositionY || !syncPosY); + Assert.IsTrue(networkTransformState.HasPositionZ || !syncPosZ); + Assert.IsTrue(networkTransformState.HasRotAngleX || !syncRotX); + Assert.IsTrue(networkTransformState.HasRotAngleY || !syncRotY); + Assert.IsTrue(networkTransformState.HasRotAngleZ || !syncRotZ); + Assert.IsTrue(networkTransformState.HasScaleX || !syncScaX); + Assert.IsTrue(networkTransformState.HasScaleY); + } + // SyncScaleZ + if (syncScaZ) + { + scale = networkTransform.transform.localScale; + scale.z++; + networkTransform.transform.localScale = scale; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + Assert.IsTrue(networkTransformState.HasPositionX || !syncPosX); + Assert.IsTrue(networkTransformState.HasPositionY || !syncPosY); + Assert.IsTrue(networkTransformState.HasPositionZ || !syncPosZ); + Assert.IsTrue(networkTransformState.HasRotAngleX || !syncRotX); + Assert.IsTrue(networkTransformState.HasRotAngleY || !syncRotY); + Assert.IsTrue(networkTransformState.HasRotAngleZ || !syncRotZ); + Assert.IsTrue(networkTransformState.HasScaleX || !syncScaX); + Assert.IsTrue(networkTransformState.HasScaleY || !syncScaY); + Assert.IsTrue(networkTransformState.HasScaleZ); + } + } + + // Step 3: disable a particular sync flag, expect state to be not dirty + // We do this last because it changes which axis will be synchronized. { - var position = networkTransform.transform.position; - var rotAngles = networkTransform.transform.eulerAngles; - var scale = networkTransform.transform.localScale; + networkTransformState = new NetworkTransform.NetworkTransformState + { + InLocalSpace = inLocalSpace, + IsTeleportingNextFrame = isTeleporting, + }; + + position = networkTransform.transform.position; + rotAngles = networkTransform.transform.eulerAngles; + scale = networkTransform.transform.localScale; // SyncPositionX + if (syncPosX) { networkTransform.SyncPositionX = false; position.x++; networkTransform.transform.position = position; - Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + // If we are synchronizing more than 1 axis (teleporting impacts this too) + if (syncAxis != SyncAxis.SyncPosX && WillAnAxisBeSynchronized(ref networkTransform)) + { + // For the x axis position value We should expect the state to still be considered dirty (more than one axis is being synchronized and we are teleporting) + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + // However, we expect it to not have applied the position x delta + Assert.IsFalse(networkTransformState.HasPositionX); + } + else + { + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } } + // SyncPositionY + if (syncPosY) { networkTransform.SyncPositionY = false; position.y++; networkTransform.transform.position = position; - - Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + if (syncAxis != SyncAxis.SyncPosY && WillAnAxisBeSynchronized(ref networkTransform)) + { + // We want to start with a fresh NetworkTransformState since it could have other state + // information from the last time we applied the transform + networkTransformState = new NetworkTransform.NetworkTransformState + { + InLocalSpace = inLocalSpace, + IsTeleportingNextFrame = isTeleporting, + }; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + Assert.IsFalse(networkTransformState.HasPositionY); + } + else + { + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } } + // SyncPositionZ + if (syncPosZ) { networkTransform.SyncPositionZ = false; position.z++; networkTransform.transform.position = position; - - Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + if (syncAxis != SyncAxis.SyncPosZ && WillAnAxisBeSynchronized(ref networkTransform)) + { + // We want to start with a fresh NetworkTransformState since it could have other state + // information from the last time we applied the transform + networkTransformState = new NetworkTransform.NetworkTransformState + { + InLocalSpace = inLocalSpace, + IsTeleportingNextFrame = isTeleporting, + }; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + Assert.IsFalse(networkTransformState.HasPositionZ); + } + else + { + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } } // SyncRotAngleX + if (syncRotX) { networkTransform.SyncRotAngleX = false; rotAngles.x++; networkTransform.transform.eulerAngles = rotAngles; - - Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + if (syncAxis != SyncAxis.SyncRotX && WillAnAxisBeSynchronized(ref networkTransform)) + { + // We want to start with a fresh NetworkTransformState since it could have other state + // information from the last time we applied the transform + networkTransformState = new NetworkTransform.NetworkTransformState + { + InLocalSpace = inLocalSpace, + IsTeleportingNextFrame = isTeleporting, + }; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + Assert.IsFalse(networkTransformState.HasRotAngleX); + } + else + { + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } } // SyncRotAngleY + if (syncRotY) { networkTransform.SyncRotAngleY = false; rotAngles.y++; networkTransform.transform.eulerAngles = rotAngles; - - Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + if (syncAxis != SyncAxis.SyncRotY && WillAnAxisBeSynchronized(ref networkTransform)) + { + // We want to start with a fresh NetworkTransformState since it could have other state + // information from the last time we applied the transform + networkTransformState = new NetworkTransform.NetworkTransformState + { + InLocalSpace = inLocalSpace, + IsTeleportingNextFrame = isTeleporting, + }; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + Assert.IsFalse(networkTransformState.HasRotAngleY); + } + else + { + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } } // SyncRotAngleZ + if (syncRotZ) { networkTransform.SyncRotAngleZ = false; rotAngles.z++; networkTransform.transform.eulerAngles = rotAngles; - - Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + if (syncAxis != SyncAxis.SyncRotZ && WillAnAxisBeSynchronized(ref networkTransform)) + { + // We want to start with a fresh NetworkTransformState since it could have other state + // information from the last time we applied the transform + networkTransformState = new NetworkTransform.NetworkTransformState + { + InLocalSpace = inLocalSpace, + IsTeleportingNextFrame = isTeleporting, + }; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + Assert.IsFalse(networkTransformState.HasRotAngleZ); + } + else + { + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } } // SyncScaleX + if (syncScaX) { networkTransform.SyncScaleX = false; scale.x++; networkTransform.transform.localScale = scale; - - Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + if (syncAxis != SyncAxis.SyncScaleX && WillAnAxisBeSynchronized(ref networkTransform)) + { + // We want to start with a fresh NetworkTransformState since it could have other state + // information from the last time we applied the transform + networkTransformState = new NetworkTransform.NetworkTransformState + { + InLocalSpace = inLocalSpace, + IsTeleportingNextFrame = isTeleporting, + }; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + Assert.IsFalse(networkTransformState.HasScaleX); + } + else + { + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } } // SyncScaleY + if (syncScaY) { networkTransform.SyncScaleY = false; scale.y++; networkTransform.transform.localScale = scale; - - Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + if (syncAxis != SyncAxis.SyncScaleY && WillAnAxisBeSynchronized(ref networkTransform)) + { + // We want to start with a fresh NetworkTransformState since it could have other state + // information from the last time we applied the transform + networkTransformState = new NetworkTransform.NetworkTransformState + { + InLocalSpace = inLocalSpace, + IsTeleportingNextFrame = isTeleporting, + }; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + Assert.IsFalse(networkTransformState.HasScaleY); + } + else + { + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } } // SyncScaleZ + if (syncScaZ) { networkTransform.SyncScaleZ = false; scale.z++; networkTransform.transform.localScale = scale; - - Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + if (syncAxis != SyncAxis.SyncScaleZ && WillAnAxisBeSynchronized(ref networkTransform)) + { + // We want to start with a fresh NetworkTransformState since it could have other state + // information from the last time we applied the transform + networkTransformState = new NetworkTransform.NetworkTransformState + { + InLocalSpace = inLocalSpace, + IsTeleportingNextFrame = isTeleporting, + }; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + Assert.IsFalse(networkTransformState.HasScaleZ); + } + else + { + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } } + } Object.DestroyImmediate(gameObject); @@ -168,11 +513,11 @@ public void TestSyncAxes( [Test] public void TestThresholds( - [Values] bool inLocalSpace, [Values(NetworkTransform.PositionThresholdDefault, 1.0f)] float positionThreshold, [Values(NetworkTransform.RotAngleThresholdDefault, 1.0f)] float rotAngleThreshold, [Values(NetworkTransform.ScaleThresholdDefault, 0.5f)] float scaleThreshold) { + var inLocalSpace = m_TransformSpace == TransformSpace.Local; var gameObject = new GameObject($"Test-{nameof(NetworkTransformStateTests)}.{nameof(TestThresholds)}"); var networkTransform = gameObject.AddComponent(); networkTransform.enabled = false; // do not tick `FixedUpdate()` or `Update()` diff --git a/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs b/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs index d88c0ce..9486bdf 100644 --- a/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs +++ b/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs @@ -189,7 +189,8 @@ public enum TransformSpace public enum OverrideState { Update, - CommitToTransform + CommitToTransform, + SetState } /// @@ -299,9 +300,8 @@ private IEnumerator WaitForAllChildrenLocalTransformValuesToMatch() /// parented under another NetworkTransform /// [UnityTest] - public IEnumerator NetworkTransformParentedLocalSpaceTest([Values] Interpolation interpolation, [Values] OverrideState overideState) + public IEnumerator NetworkTransformParentedLocalSpaceTest([Values] Interpolation interpolation) { - var overrideUpdate = overideState == OverrideState.CommitToTransform; m_AuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; m_NonAuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; var authoritativeChildObject = SpawnObject(m_ChildObjectToBeParented.gameObject, m_AuthoritativeTransform.NetworkManager); @@ -334,15 +334,28 @@ public IEnumerator NetworkTransformParentedLocalSpaceTest([Values] Interpolation /// 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 IEnumerator MoveRotateAndScaleAuthority(Vector3 position, Vector3 rotation, Vector3 scale) + private IEnumerator MoveRotateAndScaleAuthority(Vector3 position, Vector3 rotation, Vector3 scale, OverrideState overrideState) { - m_AuthoritativeTransform.transform.position = position; - yield return null; - var authoritativeRotation = m_AuthoritativeTransform.transform.rotation; - authoritativeRotation.eulerAngles = rotation; - m_AuthoritativeTransform.transform.rotation = authoritativeRotation; - yield return null; - m_AuthoritativeTransform.transform.localScale = scale; + switch (overrideState) + { + case OverrideState.SetState: + { + m_AuthoritativeTransform.SetState(position, Quaternion.Euler(rotation), scale); + break; + } + case OverrideState.Update: + default: + { + m_AuthoritativeTransform.transform.position = position; + yield return null; + var authoritativeRotation = m_AuthoritativeTransform.transform.rotation; + authoritativeRotation.eulerAngles = rotation; + m_AuthoritativeTransform.transform.rotation = authoritativeRotation; + yield return null; + m_AuthoritativeTransform.transform.localScale = scale; + break; + } + } } /// @@ -400,7 +413,6 @@ protected override void OnNewClientCreated(NetworkManager networkManager) [UnityTest] public IEnumerator NetworkTransformMultipleChangesOverTime([Values] TransformSpace testLocalTransform, [Values] OverrideState overideState) { - var overrideUpdate = overideState == OverrideState.CommitToTransform; m_AuthoritativeTransform.InLocalSpace = testLocalTransform == TransformSpace.Local; var positionStart = new Vector3(1.0f, 0.5f, 2.0f); @@ -422,7 +434,7 @@ public IEnumerator NetworkTransformMultipleChangesOverTime([Values] TransformSpa yield return WaitForNextTick(); // Apply deltas - MoveRotateAndScaleAuthority(position, rotation, scale); + MoveRotateAndScaleAuthority(position, rotation, scale, overideState); // Wait for deltas to synchronize on non-authoritative side yield return WaitForPositionRotationAndScaleToMatch(4); @@ -455,7 +467,7 @@ public IEnumerator NetworkTransformMultipleChangesOverTime([Values] TransformSpa // to apply both deltas within the same tick period. yield return WaitForNextTick(); - MoveRotateAndScaleAuthority(position, rotation, scale); + MoveRotateAndScaleAuthority(position, rotation, scale, overideState); yield return WaitForPositionRotationAndScaleToMatch(4); } @@ -471,7 +483,7 @@ public IEnumerator NetworkTransformMultipleChangesOverTime([Values] TransformSpa rotation = rotationStart * i; scale = scaleStart * i; - MoveRotateAndScaleAuthority(position, rotation, scale); + MoveRotateAndScaleAuthority(position, rotation, scale, overideState); } yield return WaitForPositionRotationAndScaleToMatch(1); @@ -486,7 +498,7 @@ public IEnumerator NetworkTransformMultipleChangesOverTime([Values] TransformSpa position = positionStart * i; rotation = rotationStart * i; scale = scaleStart * i; - MoveRotateAndScaleAuthority(position, rotation, scale); + MoveRotateAndScaleAuthority(position, rotation, scale, overideState); } yield return WaitForPositionRotationAndScaleToMatch(1); } @@ -513,11 +525,16 @@ public IEnumerator TestAuthoritativeTransformChangeOneAtATime([Values] Transform Assert.AreEqual(Vector3.zero, m_NonAuthoritativeTransform.transform.position, "server side pos should be zero at first"); // sanity check - authPlayerTransform.position = new Vector3(10, 20, 30); - if (overrideUpdate) + var nextPosition = new Vector3(10, 20, 30); + if (overideState != OverrideState.SetState) { + authPlayerTransform.position = nextPosition; m_OwnerTransform.CommitToTransform(); } + else + { + m_OwnerTransform.SetState(nextPosition, null, null, m_AuthoritativeTransform.Interpolate); + } yield return WaitForConditionOrTimeOut(PositionsMatch); AssertOnTimeout($"Timed out waiting for positions to match"); @@ -525,20 +542,30 @@ public IEnumerator TestAuthoritativeTransformChangeOneAtATime([Values] Transform // test rotation Assert.AreEqual(Quaternion.identity, m_NonAuthoritativeTransform.transform.rotation, "wrong initial value for rotation"); // sanity check - authPlayerTransform.rotation = Quaternion.Euler(45, 40, 35); // using euler angles instead of quaternions directly to really see issues users might encounter - if (overrideUpdate) + var nextRotation = Quaternion.Euler(45, 40, 35); // using euler angles instead of quaternions directly to really see issues users might encounter + if (overideState != OverrideState.SetState) { + authPlayerTransform.rotation = nextRotation; m_OwnerTransform.CommitToTransform(); } + else + { + m_OwnerTransform.SetState(null, nextRotation, null, m_AuthoritativeTransform.Interpolate); + } yield return WaitForConditionOrTimeOut(RotationsMatch); AssertOnTimeout($"Timed out waiting for rotations to match"); - authPlayerTransform.localScale = new Vector3(2, 3, 4); + var nextScale = new Vector3(2, 3, 4); if (overrideUpdate) { + authPlayerTransform.localScale = nextScale; m_OwnerTransform.CommitToTransform(); } + else + { + m_OwnerTransform.SetState(null, null, nextScale, m_AuthoritativeTransform.Interpolate); + } yield return WaitForConditionOrTimeOut(ScaleValuesMatch); AssertOnTimeout($"Timed out waiting for scale values to match"); diff --git a/Tests/Runtime/NetworkVariableTests.cs b/Tests/Runtime/NetworkVariableTests.cs index f8b7339..d4f17b2 100644 --- a/Tests/Runtime/NetworkVariableTests.cs +++ b/Tests/Runtime/NetworkVariableTests.cs @@ -17,6 +17,57 @@ public class NetVarPermTestComp : NetworkBehaviour public NetworkVariable OwnerReadWrite_Position = new NetworkVariable(Vector3.one, NetworkVariableReadPermission.Owner, NetworkVariableWritePermission.Owner); } + // The ILPP code for NetworkVariables to determine how to serialize them relies on them existing as fields of a NetworkBehaviour to find them. + // Some of the tests below create NetworkVariables on the stack, so this class is here just to make sure the relevant types are all accounted for. + public class NetVarILPPClassForTests : NetworkBehaviour + { + public NetworkVariable UnmanagedNetworkSerializableTypeVar; + public NetworkVariable ManagedNetworkSerializableTypeVar; + public NetworkVariable StringVar; + public NetworkVariable GuidVar; + } + + public class TemplateNetworkBehaviourType : NetworkBehaviour + { + public NetworkVariable TheVar; + } + + public class ClassHavingNetworkBehaviour : TemplateNetworkBehaviourType + { + + } + + // Please do not reference TestClass2 anywhere other than here! + public class ClassHavingNetworkBehaviour2 : TemplateNetworkBehaviourType + { + + } + + public class StructHavingNetworkBehaviour : TemplateNetworkBehaviourType + { + + } + + public struct StructUsedOnlyInNetworkList : IEquatable, INetworkSerializeByMemcpy + { + public int Value; + + public bool Equals(StructUsedOnlyInNetworkList other) + { + return Value == other.Value; + } + + public override bool Equals(object obj) + { + return obj is StructUsedOnlyInNetworkList other && Equals(other); + } + + public override int GetHashCode() + { + return Value; + } + } + [TestFixtureSource(nameof(TestDataSource))] public class NetworkVariablePermissionTests : NetcodeIntegrationTest { @@ -344,6 +395,53 @@ public override int GetHashCode() } } + public class TestClass : INetworkSerializable, IEquatable + { + public uint SomeInt; + public bool SomeBool; + public static bool NetworkSerializeCalledOnWrite; + public static bool NetworkSerializeCalledOnRead; + + public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter + { + if (serializer.IsReader) + { + NetworkSerializeCalledOnRead = true; + } + else + { + NetworkSerializeCalledOnWrite = true; + } + serializer.SerializeValue(ref SomeInt); + serializer.SerializeValue(ref SomeBool); + } + + public bool Equals(TestClass other) + { + return SomeInt == other.SomeInt && SomeBool == other.SomeBool; + } + + public override bool Equals(object obj) + { + return obj is TestClass other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + return ((int)SomeInt * 397) ^ SomeBool.GetHashCode(); + } + } + } + + // Used just to create a NetworkVariable in the templated NetworkBehaviour type that isn't referenced anywhere else + // Please do not reference this class anywhere else! + public class TestClass_ReferencedOnlyByTemplateNetworkBehavourType : TestClass + { + + } + public class NetworkVariableTest : NetworkBehaviour { public enum SomeEnum @@ -355,6 +453,7 @@ public enum SomeEnum public readonly NetworkVariable TheScalar = new NetworkVariable(); public readonly NetworkVariable TheEnum = new NetworkVariable(); public readonly NetworkList TheList = new NetworkList(); + public readonly NetworkList TheStructList = new NetworkList(); public readonly NetworkList TheLargeList = new NetworkList(); public readonly NetworkVariable FixedString32 = new NetworkVariable(); @@ -370,7 +469,10 @@ public void Awake() } public readonly NetworkVariable TheStruct = new NetworkVariable(); - public readonly NetworkList TheListOfStructs = new NetworkList(); + public readonly NetworkVariable TheClass = new NetworkVariable(); + + public NetworkVariable> TheTemplateStruct = new NetworkVariable>(); + public NetworkVariable> TheTemplateClass = new NetworkVariable>(); public bool ListDelegateTriggered; @@ -433,6 +535,10 @@ private IEnumerator InitializeServerAndClients(bool useHost) s_ClientNetworkVariableTestInstances.Clear(); m_PlayerPrefab.AddComponent(); + m_PlayerPrefab.AddComponent(); + m_PlayerPrefab.AddComponent(); + m_PlayerPrefab.AddComponent(); + m_ServerNetworkManager.NetworkConfig.EnsureNetworkVariableLengthSafety = m_EnsureLengthSafety; m_ServerNetworkManager.NetworkConfig.PlayerPrefab = m_PlayerPrefab; foreach (var client in m_ClientNetworkManagers) @@ -517,6 +623,7 @@ public IEnumerator AllNetworkVariableTypes([Values(true, false)] bool useHost) networkVariableTestComponent.EnableTesting = false; Assert.IsTrue(networkVariableTestComponent.DidAllValuesChange()); + networkVariableTestComponent.AssertAllValuesAreCorrect(); // Disable this once we are done. networkVariableTestComponent.gameObject.SetActive(false); @@ -536,6 +643,23 @@ public IEnumerator ClientWritePermissionTest([Values(true, false)] bool useHost) Assert.Throws(() => m_Player1OnClient1.TheScalar.Value = k_TestVal1); } + /// + /// Runs tests that network variables sync on client whatever the local value of . + /// + [UnityTest] + public IEnumerator NetworkVariableSync_WithDifferentTimeScale([Values(true, false)] bool useHost, [Values(0.0f, 1.0f, 2.0f)] float timeScale) + { + Time.timeScale = timeScale; + + yield return InitializeServerAndClients(useHost); + + m_Player1OnServer.TheScalar.Value = k_TestVal1; + + // Now wait for the client side version to be updated to k_TestVal1 + yield return WaitForConditionOrTimeOut(() => m_Player1OnClient1.TheScalar.Value == k_TestVal1); + Assert.IsFalse(s_GlobalTimeoutHelper.TimedOut, "Timed out waiting for client-side NetworkVariable to update!"); + } + [UnityTest] public IEnumerator FixedString32Test([Values(true, false)] bool useHost) { @@ -672,6 +796,63 @@ public IEnumerator NetworkListClear([Values(true, false)] bool useHost) yield return WaitForConditionOrTimeOut(m_NetworkListPredicateHandler); } + [UnityTest] + public IEnumerator TestNetworkVariableClass([Values(true, false)] bool useHost) + { + yield return InitializeServerAndClients(useHost); + + bool VerifyClass() + { + return m_Player1OnClient1.TheClass.Value != null && + m_Player1OnClient1.TheClass.Value.SomeBool == m_Player1OnServer.TheClass.Value.SomeBool && + m_Player1OnClient1.TheClass.Value.SomeInt == m_Player1OnServer.TheClass.Value.SomeInt; + } + + m_Player1OnServer.TheClass.Value = new TestClass { SomeInt = k_TestUInt, SomeBool = false }; + m_Player1OnServer.TheClass.SetDirty(true); + + // Wait for the client-side to notify it is finished initializing and spawning. + yield return WaitForConditionOrTimeOut(VerifyClass); + } + + [UnityTest] + public IEnumerator TestNetworkVariableTemplateClass([Values(true, false)] bool useHost) + { + yield return InitializeServerAndClients(useHost); + + bool VerifyClass() + { + return m_Player1OnClient1.TheTemplateClass.Value.Value != null && m_Player1OnClient1.TheTemplateClass.Value.Value.SomeBool == m_Player1OnServer.TheTemplateClass.Value.Value.SomeBool && + m_Player1OnClient1.TheTemplateClass.Value.Value.SomeInt == m_Player1OnServer.TheTemplateClass.Value.Value.SomeInt; + } + + m_Player1OnServer.TheTemplateClass.Value = new ManagedTemplateNetworkSerializableType { Value = new TestClass { SomeInt = k_TestUInt, SomeBool = false } }; + m_Player1OnServer.TheTemplateClass.SetDirty(true); + + // Wait for the client-side to notify it is finished initializing and spawning. + yield return WaitForConditionOrTimeOut(VerifyClass); + } + + [UnityTest] + public IEnumerator TestNetworkListStruct([Values(true, false)] bool useHost) + { + yield return InitializeServerAndClients(useHost); + + bool VerifyList() + { + return m_Player1OnClient1.TheStructList.Count == m_Player1OnServer.TheStructList.Count && + m_Player1OnClient1.TheStructList[0].Value == m_Player1OnServer.TheStructList[0].Value && + m_Player1OnClient1.TheStructList[1].Value == m_Player1OnServer.TheStructList[1].Value; + } + + m_Player1OnServer.TheStructList.Add(new StructUsedOnlyInNetworkList { Value = 1 }); + m_Player1OnServer.TheStructList.Add(new StructUsedOnlyInNetworkList { Value = 2 }); + m_Player1OnServer.TheStructList.SetDirty(true); + + // Wait for the client-side to notify it is finished initializing and spawning. + yield return WaitForConditionOrTimeOut(VerifyList); + } + [UnityTest] public IEnumerator TestNetworkVariableStruct([Values(true, false)] bool useHost) { @@ -683,13 +864,85 @@ bool VerifyStructure() m_Player1OnClient1.TheStruct.Value.SomeInt == m_Player1OnServer.TheStruct.Value.SomeInt; } - m_Player1OnServer.TheStruct.Value = new TestStruct() { SomeInt = k_TestUInt, SomeBool = false }; + m_Player1OnServer.TheStruct.Value = new TestStruct { SomeInt = k_TestUInt, SomeBool = false }; m_Player1OnServer.TheStruct.SetDirty(true); // Wait for the client-side to notify it is finished initializing and spawning. yield return WaitForConditionOrTimeOut(VerifyStructure); } + [UnityTest] + public IEnumerator TestNetworkVariableTemplateStruct([Values(true, false)] bool useHost) + { + yield return InitializeServerAndClients(useHost); + + bool VerifyStructure() + { + return m_Player1OnClient1.TheTemplateStruct.Value.Value.SomeBool == m_Player1OnServer.TheTemplateStruct.Value.Value.SomeBool && + m_Player1OnClient1.TheTemplateStruct.Value.Value.SomeInt == m_Player1OnServer.TheTemplateStruct.Value.Value.SomeInt; + } + + m_Player1OnServer.TheTemplateStruct.Value = new UnmanagedTemplateNetworkSerializableType { Value = new TestStruct { SomeInt = k_TestUInt, SomeBool = false } }; + m_Player1OnServer.TheTemplateStruct.SetDirty(true); + + // Wait for the client-side to notify it is finished initializing and spawning. + yield return WaitForConditionOrTimeOut(VerifyStructure); + } + + [UnityTest] + public IEnumerator TestNetworkVariableTemplateBehaviourClass([Values(true, false)] bool useHost) + { + yield return InitializeServerAndClients(useHost); + + bool VerifyClass() + { + return m_Player1OnClient1.GetComponent().TheVar.Value != null && m_Player1OnClient1.GetComponent().TheVar.Value.SomeBool == m_Player1OnServer.GetComponent().TheVar.Value.SomeBool && + m_Player1OnClient1.GetComponent().TheVar.Value.SomeInt == m_Player1OnServer.GetComponent().TheVar.Value.SomeInt; + } + + m_Player1OnServer.GetComponent().TheVar.Value = new TestClass { SomeInt = k_TestUInt, SomeBool = false }; + m_Player1OnServer.GetComponent().TheVar.SetDirty(true); + + // Wait for the client-side to notify it is finished initializing and spawning. + yield return WaitForConditionOrTimeOut(VerifyClass); + } + + [UnityTest] + public IEnumerator TestNetworkVariableTemplateBehaviourClassNotReferencedElsewhere([Values(true, false)] bool useHost) + { + yield return InitializeServerAndClients(useHost); + + bool VerifyClass() + { + return m_Player1OnClient1.GetComponent().TheVar.Value != null && m_Player1OnClient1.GetComponent().TheVar.Value.SomeBool == m_Player1OnServer.GetComponent().TheVar.Value.SomeBool && + m_Player1OnClient1.GetComponent().TheVar.Value.SomeInt == m_Player1OnServer.GetComponent().TheVar.Value.SomeInt; + } + + m_Player1OnServer.GetComponent().TheVar.Value = new TestClass_ReferencedOnlyByTemplateNetworkBehavourType { SomeInt = k_TestUInt, SomeBool = false }; + m_Player1OnServer.GetComponent().TheVar.SetDirty(true); + + // Wait for the client-side to notify it is finished initializing and spawning. + yield return WaitForConditionOrTimeOut(VerifyClass); + } + + [UnityTest] + public IEnumerator TestNetworkVariableTemplateBehaviourStruct([Values(true, false)] bool useHost) + { + yield return InitializeServerAndClients(useHost); + + bool VerifyClass() + { + return m_Player1OnClient1.GetComponent().TheVar.Value.SomeBool == m_Player1OnServer.GetComponent().TheVar.Value.SomeBool && + m_Player1OnClient1.GetComponent().TheVar.Value.SomeInt == m_Player1OnServer.GetComponent().TheVar.Value.SomeInt; + } + + m_Player1OnServer.GetComponent().TheVar.Value = new TestStruct { SomeInt = k_TestUInt, SomeBool = false }; + m_Player1OnServer.GetComponent().TheVar.SetDirty(true); + + // Wait for the client-side to notify it is finished initializing and spawning. + yield return WaitForConditionOrTimeOut(VerifyClass); + } + [UnityTest] public IEnumerator TestNetworkVariableEnum([Values(true, false)] bool useHost) { @@ -708,7 +961,25 @@ bool VerifyStructure() } [UnityTest] - public IEnumerator TestINetworkSerializableCallsNetworkSerialize([Values(true, false)] bool useHost) + public IEnumerator TestINetworkSerializableClassCallsNetworkSerialize([Values(true, false)] bool useHost) + { + yield return InitializeServerAndClients(useHost); + TestClass.NetworkSerializeCalledOnWrite = false; + TestClass.NetworkSerializeCalledOnRead = false; + m_Player1OnServer.TheClass.Value = new TestClass + { + SomeBool = true, + SomeInt = 32 + }; + + static bool VerifyCallback() => TestClass.NetworkSerializeCalledOnWrite && TestClass.NetworkSerializeCalledOnRead; + + // Wait for the client-side to notify it is finished initializing and spawning. + yield return WaitForConditionOrTimeOut(VerifyCallback); + } + + [UnityTest] + public IEnumerator TestINetworkSerializableStructCallsNetworkSerialize([Values(true, false)] bool useHost) { yield return InitializeServerAndClients(useHost); TestStruct.NetworkSerializeCalledOnWrite = false; @@ -756,11 +1027,181 @@ public IEnumerator NetworkListIEnumerator([Values(true, false)] bool useHost) } } } + + [Test] + public void TestUnsupportedManagedTypesThrowExceptions() + { + var variable = new NetworkVariable(); + using var writer = new FastBufferWriter(1024, Allocator.Temp); + using var reader = new FastBufferReader(writer, Allocator.None); + // Just making sure these are null, just in case. + UserNetworkVariableSerialization.ReadValue = null; + UserNetworkVariableSerialization.WriteValue = null; + Assert.Throws(() => + { + variable.WriteField(writer); + }); + Assert.Throws(() => + { + variable.ReadField(reader); + }); + } + + [Test] + public void TestUnsupportedManagedTypesWithUserSerializationDoNotThrowExceptions() + { + var variable = new NetworkVariable(); + UserNetworkVariableSerialization.ReadValue = (FastBufferReader reader, out string value) => + { + reader.ReadValueSafe(out value); + }; + UserNetworkVariableSerialization.WriteValue = (FastBufferWriter writer, in string value) => + { + writer.WriteValueSafe(value); + }; + try + { + using var writer = new FastBufferWriter(1024, Allocator.Temp); + variable.Value = "012345"; + variable.WriteField(writer); + variable.Value = ""; + + using var reader = new FastBufferReader(writer, Allocator.None); + variable.ReadField(reader); + Assert.AreEqual("012345", variable.Value); + } + finally + { + UserNetworkVariableSerialization.ReadValue = null; + UserNetworkVariableSerialization.WriteValue = null; + } + } + + [Test] + public void TestUnsupportedUnmanagedTypesThrowExceptions() + { + var variable = new NetworkVariable(); + using var writer = new FastBufferWriter(1024, Allocator.Temp); + using var reader = new FastBufferReader(writer, Allocator.None); + // Just making sure these are null, just in case. + UserNetworkVariableSerialization.ReadValue = null; + UserNetworkVariableSerialization.WriteValue = null; + Assert.Throws(() => + { + variable.WriteField(writer); + }); + Assert.Throws(() => + { + variable.ReadField(reader); + }); + } + + [Test] + public void TestUnsupportedUnmanagedTypesWithUserSerializationDoNotThrowExceptions() + { + var variable = new NetworkVariable(); + UserNetworkVariableSerialization.ReadValue = (FastBufferReader reader, out Guid value) => + { + var tmpValue = new ForceNetworkSerializeByMemcpy(); + reader.ReadValueSafe(out tmpValue); + value = tmpValue.Value; + }; + UserNetworkVariableSerialization.WriteValue = (FastBufferWriter writer, in Guid value) => + { + var tmpValue = new ForceNetworkSerializeByMemcpy(value); + writer.WriteValueSafe(tmpValue); + }; + try + { + using var writer = new FastBufferWriter(1024, Allocator.Temp); + var guid = Guid.NewGuid(); + variable.Value = guid; + variable.WriteField(writer); + variable.Value = Guid.Empty; + + using var reader = new FastBufferReader(writer, Allocator.None); + variable.ReadField(reader); + Assert.AreEqual(guid, variable.Value); + } + finally + { + UserNetworkVariableSerialization.ReadValue = null; + UserNetworkVariableSerialization.WriteValue = null; + } + } + + [Test] + public void TestManagedINetworkSerializableNetworkVariablesDeserializeInPlace() + { + var variable = new NetworkVariable(); + variable.Value = new ManagedNetworkSerializableType + { + InMemoryValue = 1, + Ints = new[] { 2, 3, 4 }, + Str = "five" + }; + + using var writer = new FastBufferWriter(1024, Allocator.Temp); + variable.WriteField(writer); + Assert.AreEqual(1, variable.Value.InMemoryValue); + Assert.AreEqual(new[] { 2, 3, 4 }, variable.Value.Ints); + Assert.AreEqual("five", variable.Value.Str); + variable.Value = new ManagedNetworkSerializableType + { + InMemoryValue = 10, + Ints = new[] { 20, 30, 40, 50 }, + Str = "sixty" + }; + + using var reader = new FastBufferReader(writer, Allocator.None); + variable.ReadField(reader); + Assert.AreEqual(10, variable.Value.InMemoryValue, "In-memory value was not the same - in-place deserialization should not change this"); + Assert.AreEqual(new[] { 2, 3, 4 }, variable.Value.Ints, "Ints were not correctly deserialized"); + Assert.AreEqual("five", variable.Value.Str, "Str was not correctly deserialized"); + } + + [Test] + public void TestUnmnagedINetworkSerializableNetworkVariablesDeserializeInPlace() + { + var variable = new NetworkVariable(); + variable.Value = new UnmanagedNetworkSerializableType + { + InMemoryValue = 1, + Int = 2, + Str = "three" + }; + using var writer = new FastBufferWriter(1024, Allocator.Temp); + variable.WriteField(writer); + Assert.AreEqual(1, variable.Value.InMemoryValue); + Assert.AreEqual(2, variable.Value.Int); + Assert.AreEqual("three", variable.Value.Str); + variable.Value = new UnmanagedNetworkSerializableType + { + InMemoryValue = 10, + Int = 20, + Str = "thirty" + }; + + using var reader = new FastBufferReader(writer, Allocator.None); + variable.ReadField(reader); + Assert.AreEqual(10, variable.Value.InMemoryValue, "In-memory value was not the same - in-place deserialization should not change this"); + Assert.AreEqual(2, variable.Value.Int, "Int was not correctly deserialized"); + Assert.AreEqual("three", variable.Value.Str, "Str was not correctly deserialized"); + } #endregion + private float m_OriginalTimeScale = 1.0f; + + protected override IEnumerator OnSetup() + { + m_OriginalTimeScale = Time.timeScale; + yield return null; + } protected override IEnumerator OnTearDown() { + Time.timeScale = m_OriginalTimeScale; + m_NetworkListPredicateHandler = null; yield return base.OnTearDown(); } @@ -818,8 +1259,8 @@ protected override bool OnHasConditionBeenReached() /// private string ConditionFailedInfo() { - return $"{m_NetworkListTestState} condition test failed:\n Server List Count: { m_Player1OnServer.TheList.Count} vs Client List Count: { m_Player1OnClient1.TheList.Count}\n" + - $"Server List Count: { m_Player1OnServer.TheLargeList.Count} vs Client List Count: { m_Player1OnClient1.TheLargeList.Count}\n" + + return $"{m_NetworkListTestState} condition test failed:\n Server List Count: {m_Player1OnServer.TheList.Count} vs Client List Count: {m_Player1OnClient1.TheList.Count}\n" + + $"Server List Count: {m_Player1OnServer.TheLargeList.Count} vs Client List Count: {m_Player1OnClient1.TheLargeList.Count}\n" + $"Server Delegate Triggered: {m_Player1OnServer.ListDelegateTriggered} | Client Delegate Triggered: {m_Player1OnClient1.ListDelegateTriggered}\n"; } diff --git a/Tests/Runtime/OwnerPermissionTests.cs b/Tests/Runtime/OwnerPermissionTests.cs new file mode 100644 index 0000000..abe4355 --- /dev/null +++ b/Tests/Runtime/OwnerPermissionTests.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.TestTools; +using Unity.Netcode.TestHelpers.Runtime; + +namespace Unity.Netcode.RuntimeTests +{ + public class OwnerPermissionObject : NetworkBehaviour + { + // indexed by [object, machine] + public static OwnerPermissionObject[,] Objects = new OwnerPermissionObject[3, 3]; + public static int CurrentlySpawning = 0; + + public static List ClientTargetedNetworkObjects = new List(); + // a client-owned NetworkVariable + public NetworkVariable MyNetworkVariableOwner; + // a server-owned NetworkVariable + public NetworkVariable MyNetworkVariableServer; + + // a client-owned NetworkVariable + public NetworkList MyNetworkListOwner; + // a server-owned NetworkVariable + public NetworkList MyNetworkListServer; + + // verifies two lists are identical + public static void CheckLists(NetworkList listA, NetworkList listB) + { + Debug.Assert(listA.Count == listB.Count); + for (var i = 0; i < listA.Count; i++) + { + Debug.Assert(listA[i] == listB[i]); + } + } + + // verifies all objects have consistent lists on all clients + public static void VerifyConsistency() + { + for (var objectIndex = 0; objectIndex < 3; objectIndex++) + { + CheckLists(Objects[objectIndex, 0].MyNetworkListOwner, Objects[objectIndex, 1].MyNetworkListOwner); + CheckLists(Objects[objectIndex, 0].MyNetworkListOwner, Objects[objectIndex, 2].MyNetworkListOwner); + + CheckLists(Objects[objectIndex, 0].MyNetworkListServer, Objects[objectIndex, 1].MyNetworkListServer); + CheckLists(Objects[objectIndex, 0].MyNetworkListServer, Objects[objectIndex, 2].MyNetworkListServer); + } + } + + public override void OnNetworkSpawn() + { + Objects[CurrentlySpawning, NetworkManager.LocalClientId] = GetComponent(); + Debug.Log($"Object index ({CurrentlySpawning}) spawned on client {NetworkManager.LocalClientId}"); + } + + private void Awake() + { + MyNetworkVariableOwner = new NetworkVariable(writePerm: NetworkVariableWritePermission.Owner); + MyNetworkVariableOwner.OnValueChanged += OwnerChanged; + + MyNetworkVariableServer = new NetworkVariable(writePerm: NetworkVariableWritePermission.Server); + MyNetworkVariableServer.OnValueChanged += ServerChanged; + + MyNetworkListOwner = new NetworkList(writePerm: NetworkVariableWritePermission.Owner); + MyNetworkListOwner.OnListChanged += ListOwnerChanged; + + MyNetworkListServer = new NetworkList(writePerm: NetworkVariableWritePermission.Server); + MyNetworkListServer.OnListChanged += ListServerChanged; + } + + public void OwnerChanged(int before, int after) + { + } + + public void ServerChanged(int before, int after) + { + } + + public void ListOwnerChanged(NetworkListEvent listEvent) + { + } + + public void ListServerChanged(NetworkListEvent listEvent) + { + } + } + + public class OwnerPermissionHideTests : NetcodeIntegrationTest + { + protected override int NumberOfClients => 2; + + private GameObject m_PrefabToSpawn; + + protected override void OnServerAndClientsCreated() + { + m_PrefabToSpawn = CreateNetworkObjectPrefab("OwnerPermissionObject"); + m_PrefabToSpawn.AddComponent(); + } + + [UnityTest] + public IEnumerator OwnerPermissionTest() + { + // create 3 objects + for (var objectIndex = 0; objectIndex < 3; objectIndex++) + { + OwnerPermissionObject.CurrentlySpawning = objectIndex; + + NetworkManager ownerManager = m_ServerNetworkManager; + if (objectIndex != 0) + { + ownerManager = m_ClientNetworkManagers[objectIndex - 1]; + } + SpawnObject(m_PrefabToSpawn, ownerManager); + + // wait for each object to spawn on each client + for (var clientIndex = 0; clientIndex < 3; clientIndex++) + { + while (OwnerPermissionObject.Objects[objectIndex, clientIndex] == null) + { + yield return new WaitForSeconds(0.0f); + } + } + } + + var nextValueToWrite = 1; + var serverIndex = 0; + + for (var objectIndex = 0; objectIndex < 3; objectIndex++) + { + for (var clientWriting = 0; clientWriting < 3; clientWriting++) + { + // ==== Server-writable NetworkVariable ==== + var gotException = false; + Debug.Log($"Writing to server-write variable on object {objectIndex} on client {clientWriting}"); + + try + { + nextValueToWrite++; + OwnerPermissionObject.Objects[objectIndex, clientWriting].MyNetworkVariableServer.Value = nextValueToWrite; + } + catch (Exception) + { + gotException = true; + } + + // Verify server-owned netvar can only be written by server + Debug.Assert(gotException == (clientWriting != serverIndex)); + + // ==== Owner-writable NetworkVariable ==== + gotException = false; + Debug.Log($"Writing to owner-write variable on object {objectIndex} on client {clientWriting}"); + + try + { + nextValueToWrite++; + OwnerPermissionObject.Objects[objectIndex, clientWriting].MyNetworkVariableOwner.Value = nextValueToWrite; + } + catch (Exception) + { + gotException = true; + } + + // Verify client-owned netvar can only be written by owner + Debug.Assert(gotException == (clientWriting != objectIndex)); + + // ==== Server-writable NetworkList ==== + gotException = false; + Debug.Log($"Writing to server-write list on object {objectIndex} on client {clientWriting}"); + + try + { + nextValueToWrite++; + OwnerPermissionObject.Objects[objectIndex, clientWriting].MyNetworkListServer.Add(nextValueToWrite); + } + catch (Exception) + { + gotException = true; + } + + // Verify server-owned networkList can only be written by server + Debug.Assert(gotException == (clientWriting != serverIndex)); + + // ==== Owner-writable NetworkList ==== + gotException = false; + Debug.Log($"Writing to owner-write list on object {objectIndex} on client {clientWriting}"); + + try + { + nextValueToWrite++; + OwnerPermissionObject.Objects[objectIndex, clientWriting].MyNetworkListOwner.Add(nextValueToWrite); + } + catch (Exception) + { + gotException = true; + } + + // Verify client-owned networkList can only be written by owner + Debug.Assert(gotException == (clientWriting != objectIndex)); + + yield return NetcodeIntegrationTestHelpers.WaitForTicks(m_ServerNetworkManager, 5); + yield return NetcodeIntegrationTestHelpers.WaitForTicks(m_ClientNetworkManagers[0], 5); + yield return NetcodeIntegrationTestHelpers.WaitForTicks(m_ClientNetworkManagers[1], 5); + + OwnerPermissionObject.VerifyConsistency(); + } + } + } + } +} diff --git a/Tests/Runtime/OwnerPermissionTests.cs.meta b/Tests/Runtime/OwnerPermissionTests.cs.meta new file mode 100644 index 0000000..a7d90d5 --- /dev/null +++ b/Tests/Runtime/OwnerPermissionTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 88c657dcbe9a2414ba551b60dab19acd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Serialization/NetworkObjectReferenceTests.cs b/Tests/Runtime/Serialization/NetworkObjectReferenceTests.cs index 49c4a1f..07cd576 100644 --- a/Tests/Runtime/Serialization/NetworkObjectReferenceTests.cs +++ b/Tests/Runtime/Serialization/NetworkObjectReferenceTests.cs @@ -110,6 +110,32 @@ public void TestSerializeGameObject() } } + [Test] + public void TestImplicitConversionToGameObject() + { + using var networkObjectContext = UnityObjectContext.CreateNetworkObject(); + networkObjectContext.Object.Spawn(); + + NetworkObjectReference outReference = networkObjectContext.Object.gameObject; + + GameObject go = outReference; + Assert.AreEqual(networkObjectContext.Object.gameObject, go); + } + + [Test] + public void TestImplicitToGameObjectIsNullWhenNotFound() + { + using var networkObjectContext = UnityObjectContext.CreateNetworkObject(); + networkObjectContext.Object.Spawn(); + + NetworkObjectReference outReference = networkObjectContext.Object.gameObject; + + networkObjectContext.Object.Despawn(); + Object.DestroyImmediate(networkObjectContext.Object.gameObject); + + GameObject go = outReference; + Assert.IsNull(go); + } [Test] public void TestTryGet() { diff --git a/Tests/Runtime/TestHelpers.meta b/Tests/Runtime/TestHelpers.meta new file mode 100644 index 0000000..ce3ac00 --- /dev/null +++ b/Tests/Runtime/TestHelpers.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1f398e1797944b5db4d3aa473629f46e +timeCreated: 1661800773 \ No newline at end of file diff --git a/Tests/Runtime/TestHelpers/MessageCatcher.cs b/Tests/Runtime/TestHelpers/MessageCatcher.cs new file mode 100644 index 0000000..0448185 --- /dev/null +++ b/Tests/Runtime/TestHelpers/MessageCatcher.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using Unity.Collections; + +namespace Unity.Netcode.RuntimeTests +{ + internal class MessageCatcher : INetworkHooks where TMessageType : INetworkMessage + { + private NetworkManager m_OwnerNetworkManager; + + public MessageCatcher(NetworkManager ownerNetworkManager) + { + m_OwnerNetworkManager = ownerNetworkManager; + } + + private struct TriggerData + { + public FastBufferReader Reader; + public MessageHeader Header; + public ulong SenderId; + public float Timestamp; + public int SerializedHeaderSize; + } + private readonly List m_CaughtMessages = new List(); + + public void ReleaseMessages() + { + + foreach (var caughtSpawn in m_CaughtMessages) + { + // Reader will be disposed within HandleMessage + m_OwnerNetworkManager.MessagingSystem.HandleMessage(caughtSpawn.Header, caughtSpawn.Reader, caughtSpawn.SenderId, caughtSpawn.Timestamp, caughtSpawn.SerializedHeaderSize); + } + } + + public int CaughtMessageCount => m_CaughtMessages.Count; + + public void OnBeforeSendMessage(ulong clientId, ref T message, NetworkDelivery delivery) where T : INetworkMessage + { + } + + public void OnAfterSendMessage(ulong clientId, ref T message, NetworkDelivery delivery, int messageSizeBytes) where T : INetworkMessage + { + } + + public void OnBeforeReceiveMessage(ulong senderId, Type messageType, int messageSizeBytes) + { + } + + public void OnAfterReceiveMessage(ulong senderId, Type messageType, int messageSizeBytes) + { + } + + public void OnBeforeSendBatch(ulong clientId, int messageCount, int batchSizeInBytes, NetworkDelivery delivery) + { + } + + public void OnAfterSendBatch(ulong clientId, int messageCount, int batchSizeInBytes, NetworkDelivery delivery) + { + } + + public void OnBeforeReceiveBatch(ulong senderId, int messageCount, int batchSizeInBytes) + { + } + + public void OnAfterReceiveBatch(ulong senderId, int messageCount, int batchSizeInBytes) + { + } + + public bool OnVerifyCanSend(ulong destinationId, Type messageType, NetworkDelivery delivery) + { + return true; + } + + public bool OnVerifyCanReceive(ulong senderId, Type messageType, FastBufferReader messageContent, ref NetworkContext context) + { + if (messageType == typeof(TMessageType)) + { + m_CaughtMessages.Add(new TriggerData + { + Reader = new FastBufferReader(messageContent, Allocator.Persistent), + Header = context.Header, + Timestamp = context.Timestamp, + SenderId = context.SenderId, + SerializedHeaderSize = context.SerializedHeaderSize + }); + return false; + } + + return true; + } + + public void OnBeforeHandleMessage(ref T message, ref NetworkContext context) where T : INetworkMessage + { + } + + public void OnAfterHandleMessage(ref T message, ref NetworkContext context) where T : INetworkMessage + { + } + } +} diff --git a/Tests/Runtime/TestHelpers/MessageCatcher.cs.meta b/Tests/Runtime/TestHelpers/MessageCatcher.cs.meta new file mode 100644 index 0000000..93a0c6f --- /dev/null +++ b/Tests/Runtime/TestHelpers/MessageCatcher.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f008d074bcb841ae90b1949f9e2f0854 +timeCreated: 1661796973 \ No newline at end of file diff --git a/Tests/Runtime/TestHelpers/MessageLogger.cs b/Tests/Runtime/TestHelpers/MessageLogger.cs new file mode 100644 index 0000000..3ed7ac3 --- /dev/null +++ b/Tests/Runtime/TestHelpers/MessageLogger.cs @@ -0,0 +1,69 @@ +using System; +using UnityEngine; + +namespace Unity.Netcode.RuntimeTests +{ + internal class MessageLogger : INetworkHooks + { + private NetworkManager m_OwningNetworkManager; + public MessageLogger(NetworkManager owningNetworkManager) + { + m_OwningNetworkManager = owningNetworkManager; + } + + public void OnBeforeSendMessage(ulong clientId, ref T message, NetworkDelivery delivery) where T : INetworkMessage + { + Debug.Log($"{(m_OwningNetworkManager.IsServer ? "Server" : "Client")} {m_OwningNetworkManager.LocalClientId}: Sending {message.GetType().FullName} to {clientId} with {delivery}"); + } + + public void OnAfterSendMessage(ulong clientId, ref T message, NetworkDelivery delivery, int messageSizeBytes) where T : INetworkMessage + { + } + + public void OnBeforeReceiveMessage(ulong senderId, Type messageType, int messageSizeBytes) + { + Debug.Log($"{(m_OwningNetworkManager.IsServer ? "Server" : "Client")} {m_OwningNetworkManager.LocalClientId}: Receiving {messageType.FullName} from {senderId}"); + } + + public void OnAfterReceiveMessage(ulong senderId, Type messageType, int messageSizeBytes) + { + } + + public void OnBeforeSendBatch(ulong clientId, int messageCount, int batchSizeInBytes, NetworkDelivery delivery) + { + Debug.Log($"{(m_OwningNetworkManager.IsServer ? "Server" : "Client")} {m_OwningNetworkManager.LocalClientId}: Sending a batch of to {clientId}: {messageCount} messages, {batchSizeInBytes} bytes, with {delivery}"); + } + + public void OnAfterSendBatch(ulong clientId, int messageCount, int batchSizeInBytes, NetworkDelivery delivery) + { + } + + public void OnBeforeReceiveBatch(ulong senderId, int messageCount, int batchSizeInBytes) + { + Debug.Log($"{(m_OwningNetworkManager.IsServer ? "Server" : "Client")} {m_OwningNetworkManager.LocalClientId}: Received a batch from {senderId}, {messageCount} messages, {batchSizeInBytes} bytes"); + } + + public void OnAfterReceiveBatch(ulong senderId, int messageCount, int batchSizeInBytes) + { + } + + public bool OnVerifyCanSend(ulong destinationId, Type messageType, NetworkDelivery delivery) + { + return true; + } + + public bool OnVerifyCanReceive(ulong senderId, Type messageType, FastBufferReader messageContent, ref NetworkContext context) + { + return true; + } + + public void OnBeforeHandleMessage(ref T message, ref NetworkContext context) where T : INetworkMessage + { + Debug.Log($"{(m_OwningNetworkManager.IsServer ? "Server" : "Client")} {m_OwningNetworkManager.LocalClientId}: Handling message {message.GetType().FullName}"); + } + + public void OnAfterHandleMessage(ref T message, ref NetworkContext context) where T : INetworkMessage + { + } + } +} diff --git a/Tests/Runtime/TestHelpers/MessageLogger.cs.meta b/Tests/Runtime/TestHelpers/MessageLogger.cs.meta new file mode 100644 index 0000000..290d149 --- /dev/null +++ b/Tests/Runtime/TestHelpers/MessageLogger.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4dbf404fdf544a409bf1bcab2c3f8b3e +timeCreated: 1661799489 \ No newline at end of file diff --git a/Tests/Runtime/Timing/NetworkTimeSystemTests.cs b/Tests/Runtime/Timing/NetworkTimeSystemTests.cs index c03de7f..a59d035 100644 --- a/Tests/Runtime/Timing/NetworkTimeSystemTests.cs +++ b/Tests/Runtime/Timing/NetworkTimeSystemTests.cs @@ -1,6 +1,7 @@ using System.Collections; using NUnit.Framework; using UnityEngine; +using UnityEngine.Assertions.Comparers; using UnityEngine.TestTools; using Unity.Netcode.TestHelpers.Runtime; @@ -11,25 +12,45 @@ namespace Unity.Netcode.RuntimeTests /// public class NetworkTimeSystemTests { - private MonoBehaviourTest m_MonoBehaviourTest; // cache for teardown + private MonoBehaviourTest m_PlayerLoopFixedTimeTestComponent; // cache for teardown + private MonoBehaviourTest m_PlayerLoopTimeTestComponent; // cache for teardown + + private float m_OriginalTimeScale = 1.0f; [SetUp] public void Setup() { + m_OriginalTimeScale = Time.timeScale; + // Create, instantiate, and host Assert.IsTrue(NetworkManagerHelper.StartNetworkManager(out _)); } /// /// Tests whether time is accessible and has correct values inside Update/FixedUpdate. + /// This test applies only when is 1. + /// + /// + [UnityTest] + public IEnumerator PlayerLoopFixedTimeTest() + { + m_PlayerLoopFixedTimeTestComponent = new MonoBehaviourTest(); + + yield return m_PlayerLoopFixedTimeTestComponent; + } + + /// + /// Tests whether time is accessible and has correct values inside Update, for multiples values. /// /// [UnityTest] - public IEnumerator PlayerLoopTimeTest() + public IEnumerator PlayerLoopTimeTest_WithDifferentTimeScale([Values(0.0f, 0.1f, 0.5f, 1.0f, 2.0f, 5.0f)] float timeScale) { - m_MonoBehaviourTest = new MonoBehaviourTest(); + Time.timeScale = timeScale; + + m_PlayerLoopTimeTestComponent = new MonoBehaviourTest(); - yield return m_MonoBehaviourTest; + yield return m_PlayerLoopTimeTestComponent; } /// @@ -40,10 +61,10 @@ public IEnumerator PlayerLoopTimeTest() [UnityTest] public IEnumerator CorrectAmountTicksTest() { - var tickSystem = NetworkManager.Singleton.NetworkTickSystem; - var delta = tickSystem.LocalTime.FixedDeltaTime; - var previous_localTickCalculated = 0; - var previous_serverTickCalculated = 0; + NetworkTickSystem tickSystem = NetworkManager.Singleton.NetworkTickSystem; + float delta = tickSystem.LocalTime.FixedDeltaTime; + int previous_localTickCalculated = 0; + int previous_serverTickCalculated = 0; while (tickSystem.LocalTime.Time < 3f) { @@ -70,7 +91,7 @@ public IEnumerator CorrectAmountTicksTest() Assert.AreEqual(previous_localTickCalculated, NetworkManager.Singleton.LocalTime.Tick, $"Calculated local tick {previous_localTickCalculated} does not match local tick {NetworkManager.Singleton.LocalTime.Tick}!"); Assert.AreEqual(previous_serverTickCalculated, NetworkManager.Singleton.ServerTime.Tick, $"Calculated server tick {previous_serverTickCalculated} does not match server tick {NetworkManager.Singleton.ServerTime.Tick}!"); - Assert.True(Mathf.Approximately((float)NetworkManager.Singleton.LocalTime.Time, (float)NetworkManager.Singleton.ServerTime.Time), $"Local time {(float)NetworkManager.Singleton.LocalTime.Time} is not approximately server time {(float)NetworkManager.Singleton.ServerTime.Time}!"); + Assert.AreEqual((float)NetworkManager.Singleton.LocalTime.Time, (float)NetworkManager.Singleton.ServerTime.Time, $"Local time {(float)NetworkManager.Singleton.LocalTime.Time} is not approximately server time {(float)NetworkManager.Singleton.ServerTime.Time}!", FloatComparer.s_ComparerWithDefaultTolerance); } } @@ -80,15 +101,23 @@ public void TearDown() // Stop, shutdown, and destroy NetworkManagerHelper.ShutdownNetworkManager(); - if (m_MonoBehaviourTest != null) + Time.timeScale = m_OriginalTimeScale; + + if (m_PlayerLoopFixedTimeTestComponent != null) { - Object.DestroyImmediate(m_MonoBehaviourTest.gameObject); + Object.DestroyImmediate(m_PlayerLoopFixedTimeTestComponent.gameObject); + m_PlayerLoopFixedTimeTestComponent = null; } - } + if (m_PlayerLoopTimeTestComponent != null) + { + Object.DestroyImmediate(m_PlayerLoopTimeTestComponent.gameObject); + m_PlayerLoopTimeTestComponent = null; + } + } } - public class PlayerLoopTimeTestComponent : MonoBehaviour, IMonoBehaviourTest + public class PlayerLoopFixedTimeTestComponent : MonoBehaviour, IMonoBehaviourTest { public const int Passes = 100; @@ -101,7 +130,7 @@ public class PlayerLoopTimeTestComponent : MonoBehaviour, IMonoBehaviourTest private NetworkTime m_ServerTimePreviousUpdate; private NetworkTime m_LocalTimePreviousFixedUpdate; - public void Start() + private void Start() { // Run fixed update at same rate as network tick Time.fixedDeltaTime = NetworkManager.Singleton.LocalTime.FixedDeltaTime; @@ -110,23 +139,23 @@ public void Start() Time.maximumDeltaTime = float.MaxValue; } - public void Update() + private void Update() { // This must run first else it wont run if there is an exception m_UpdatePasses++; - var localTime = NetworkManager.Singleton.LocalTime; - var serverTime = NetworkManager.Singleton.ServerTime; + NetworkTime localTime = NetworkManager.Singleton.LocalTime; + NetworkTime serverTime = NetworkManager.Singleton.ServerTime; // time should have advanced on the host/server - Assert.True(m_LocalTimePreviousUpdate.Time < localTime.Time); - Assert.True(m_ServerTimePreviousUpdate.Time < serverTime.Time); + Assert.Less(m_LocalTimePreviousUpdate.Time, localTime.Time); + Assert.Less(m_ServerTimePreviousUpdate.Time, serverTime.Time); // time should be further then last fixed step in update - Assert.True(m_LocalTimePreviousFixedUpdate.FixedTime < localTime.Time); + Assert.Less(m_LocalTimePreviousFixedUpdate.FixedTime, localTime.Time); // we should be in same or further tick then fixed update - Assert.True(m_LocalTimePreviousFixedUpdate.Tick <= localTime.Tick); + Assert.LessOrEqual(m_LocalTimePreviousFixedUpdate.Tick, localTime.Tick); // fixed update should result in same amounts of tick as network time if (m_TickOffset == -1) @@ -135,23 +164,61 @@ public void Update() } else { - // offset of 1 is ok, this happens due to different tick duration offsets - Assert.True(Mathf.Abs(serverTime.Tick - m_TickOffset - m_LastFixedUpdateTick) <= 1); + // offset of 1 is ok, this happens due to different tick duration offsets + Assert.LessOrEqual(Mathf.Abs(serverTime.Tick - m_TickOffset - m_LastFixedUpdateTick), 1); } m_LocalTimePreviousUpdate = localTime; + m_ServerTimePreviousUpdate = serverTime; } - public void FixedUpdate() + private void FixedUpdate() { - var time = NetworkManager.Singleton.LocalTime; + m_LocalTimePreviousFixedUpdate = NetworkManager.Singleton.LocalTime; - m_LocalTimePreviousFixedUpdate = time; + Assert.AreEqual(Time.fixedDeltaTime, m_LocalTimePreviousFixedUpdate.FixedDeltaTime); + Assert.AreEqual((float)NetworkManager.Singleton.LocalTime.Time, (float)NetworkManager.Singleton.ServerTime.Time, null, FloatComparer.s_ComparerWithDefaultTolerance); + m_LastFixedUpdateTick++; + } - Assert.AreEqual(Time.fixedDeltaTime, time.FixedDeltaTime); - Assert.True(Mathf.Approximately((float)NetworkManager.Singleton.LocalTime.Time, (float)NetworkManager.Singleton.ServerTime.Time)); + public bool IsTestFinished => m_UpdatePasses >= Passes; + } - m_LastFixedUpdateTick++; + public class PlayerLoopTimeTestComponent : MonoBehaviour, IMonoBehaviourTest + { + public const int Passes = 100; + + private int m_UpdatePasses = 0; + + private NetworkTime m_LocalTimePreviousUpdate; + private NetworkTime m_ServerTimePreviousUpdate; + private NetworkTime m_LocalTimePreviousFixedUpdate; + + private void Update() + { + // This must run first else it wont run if there is an exception + m_UpdatePasses++; + + NetworkTime localTime = NetworkManager.Singleton.LocalTime; + NetworkTime serverTime = NetworkManager.Singleton.ServerTime; + + // time should have advanced on the host/server + Assert.Less(m_LocalTimePreviousUpdate.Time, localTime.Time); + Assert.Less(m_ServerTimePreviousUpdate.Time, serverTime.Time); + + // time should be further then last fixed step in update + Assert.Less(m_LocalTimePreviousFixedUpdate.FixedTime, localTime.Time); + + // we should be in same or further tick then fixed update + Assert.LessOrEqual(m_LocalTimePreviousFixedUpdate.Tick, localTime.Tick); + + m_LocalTimePreviousUpdate = localTime; + m_ServerTimePreviousUpdate = serverTime; + } + + private void FixedUpdate() + { + m_LocalTimePreviousFixedUpdate = NetworkManager.Singleton.LocalTime; } public bool IsTestFinished => m_UpdatePasses >= Passes; diff --git a/Tests/Runtime/TransformInterpolationTests.cs b/Tests/Runtime/TransformInterpolationTests.cs index 8ed5b78..dd0b3ef 100644 --- a/Tests/Runtime/TransformInterpolationTests.cs +++ b/Tests/Runtime/TransformInterpolationTests.cs @@ -11,7 +11,7 @@ namespace Unity.Netcode.RuntimeTests public class TransformInterpolationObject : NetworkBehaviour { // Set the minimum threshold which we will use as our margin of error - public const float MinThreshold = 0.001f; + public const float MinThreshold = 0.005f; public bool CheckPosition; public bool IsMoving; @@ -24,7 +24,7 @@ private void Update() { if (transform.position.y < -MinThreshold || transform.position.y > 100.0f + MinThreshold) { - Debug.LogError($"Interpolation failure. transform.position.y is {transform.position.y}. Should be between 0.0 and 100.0"); + Debug.LogError($"Interpolation failure. transform.position.y is {transform.position.y}. Should be between 0.0 and 100.0. Current threshold is [+/- {MinThreshold}]."); } } diff --git a/Tests/Runtime/Transports/UnityTransportDriverClient.cs b/Tests/Runtime/Transports/UnityTransportDriverClient.cs index e61ce4b..d41d9bf 100644 --- a/Tests/Runtime/Transports/UnityTransportDriverClient.cs +++ b/Tests/Runtime/Transports/UnityTransportDriverClient.cs @@ -65,7 +65,11 @@ private void OnDestroy() public void Connect() { +#if UTP_TRANSPORT_2_0_ABOVE + var endpoint = NetworkEndpoint.LoopbackIpv4; +#else var endpoint = NetworkEndPoint.LoopbackIpv4; +#endif endpoint.Port = 7777; m_Connection = m_Driver.Connect(endpoint); diff --git a/Tests/Runtime/Transports/UnityTransportTestHelpers.cs b/Tests/Runtime/Transports/UnityTransportTestHelpers.cs index 254cc86..f6d73f9 100644 --- a/Tests/Runtime/Transports/UnityTransportTestHelpers.cs +++ b/Tests/Runtime/Transports/UnityTransportTestHelpers.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; using Unity.Netcode.Transports.UTP; +using Unity.Networking.Transport; using UnityEngine; namespace Unity.Netcode.RuntimeTests @@ -36,14 +37,23 @@ public static IEnumerator WaitForNetworkEvent(NetworkEvent type, List events, int maxPayloadSize = UnityTransport.InitialMaxPayloadSize) + public static void InitializeTransport(out UnityTransport transport, out List events, + int maxPayloadSize = UnityTransport.InitialMaxPayloadSize, int maxSendQueueSize = 0, NetworkFamily family = NetworkFamily.Ipv4) { var logger = new TransportEventLogger(); events = logger.Events; transport = new GameObject().AddComponent(); + transport.OnTransportEvent += logger.HandleEvent; - transport.SetMaxPayloadSize(maxPayloadSize); + transport.MaxPayloadSize = maxPayloadSize; + transport.MaxSendQueueSize = maxSendQueueSize; + + if (family == NetworkFamily.Ipv6) + { + transport.SetConnectionData("::1", 7777); + } + transport.Initialize(); } diff --git a/Tests/Runtime/Transports/UnityTransportTests.cs b/Tests/Runtime/Transports/UnityTransportTests.cs index 481366e..53fccb0 100644 --- a/Tests/Runtime/Transports/UnityTransportTests.cs +++ b/Tests/Runtime/Transports/UnityTransportTests.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Text; using Unity.Netcode.Transports.UTP; +using Unity.Networking.Transport; using UnityEngine; using UnityEngine.TestTools; using static Unity.Netcode.RuntimeTests.UnityTransportTestHelpers; @@ -21,6 +22,15 @@ public class UnityTransportTests NetworkDelivery.Reliable }; + private static readonly NetworkFamily[] k_NetworkFamiltyParameters = + { + NetworkFamily.Ipv4, +#if !(UNITY_SWITCH || UNITY_PS4 || UNITY_PS5) + // IPv6 is not supported on Switch, PS4, and PS5. + NetworkFamily.Ipv6 +#endif + }; + private UnityTransport m_Server, m_Client1, m_Client2; private List m_ServerEvents, m_Client1Events, m_Client2Events; @@ -60,10 +70,12 @@ public IEnumerator Cleanup() // Check if can make a simple data exchange. [UnityTest] - public IEnumerator PingPong([ValueSource("k_DeliveryParameters")] NetworkDelivery delivery) + public IEnumerator PingPong( + [ValueSource("k_DeliveryParameters")] NetworkDelivery delivery, + [ValueSource("k_NetworkFamiltyParameters")] NetworkFamily family) { - InitializeTransport(out m_Server, out m_ServerEvents); - InitializeTransport(out m_Client1, out m_Client1Events); + InitializeTransport(out m_Server, out m_ServerEvents, family: family); + InitializeTransport(out m_Client1, out m_Client1Events, family: family); m_Server.StartServer(); m_Client1.StartClient(); @@ -89,10 +101,12 @@ public IEnumerator PingPong([ValueSource("k_DeliveryParameters")] NetworkDeliver // Check if can make a simple data exchange (both ways at a time). [UnityTest] - public IEnumerator PingPongSimultaneous([ValueSource("k_DeliveryParameters")] NetworkDelivery delivery) + public IEnumerator PingPongSimultaneous( + [ValueSource("k_DeliveryParameters")] NetworkDelivery delivery, + [ValueSource("k_NetworkFamiltyParameters")] NetworkFamily family) { - InitializeTransport(out m_Server, out m_ServerEvents); - InitializeTransport(out m_Client1, out m_Client1Events); + InitializeTransport(out m_Server, out m_ServerEvents, family: family); + InitializeTransport(out m_Client1, out m_Client1Events, family: family); m_Server.StartServer(); m_Client1.StartClient(); @@ -126,13 +140,15 @@ public IEnumerator PingPongSimultaneous([ValueSource("k_DeliveryParameters")] Ne // loopback traffic are too small for the amount of data sent in a single update here. [UnityTest] [UnityPlatform(exclude = new[] { RuntimePlatform.Switch, RuntimePlatform.PS4, RuntimePlatform.PS5 })] - public IEnumerator SendMaximumPayloadSize([ValueSource("k_DeliveryParameters")] NetworkDelivery delivery) + public IEnumerator SendMaximumPayloadSize( + [ValueSource("k_DeliveryParameters")] NetworkDelivery delivery, + [ValueSource("k_NetworkFamiltyParameters")] NetworkFamily family) { // We want something that's over the old limit of ~44KB for reliable payloads. var payloadSize = 64 * 1024; - InitializeTransport(out m_Server, out m_ServerEvents, payloadSize); - InitializeTransport(out m_Client1, out m_Client1Events, payloadSize); + InitializeTransport(out m_Server, out m_ServerEvents, payloadSize, family: family); + InitializeTransport(out m_Client1, out m_Client1Events, payloadSize, family: family); m_Server.StartServer(); m_Client1.StartClient(); @@ -164,10 +180,12 @@ public IEnumerator SendMaximumPayloadSize([ValueSource("k_DeliveryParameters")] // Check making multiple sends to a client in a single frame. [UnityTest] - public IEnumerator MultipleSendsSingleFrame([ValueSource("k_DeliveryParameters")] NetworkDelivery delivery) + public IEnumerator MultipleSendsSingleFrame( + [ValueSource("k_DeliveryParameters")] NetworkDelivery delivery, + [ValueSource("k_NetworkFamiltyParameters")] NetworkFamily family) { - InitializeTransport(out m_Server, out m_ServerEvents); - InitializeTransport(out m_Client1, out m_Client1Events); + InitializeTransport(out m_Server, out m_ServerEvents, family: family); + InitializeTransport(out m_Client1, out m_Client1Events, family: family); m_Server.StartServer(); m_Client1.StartClient(); @@ -193,11 +211,13 @@ public IEnumerator MultipleSendsSingleFrame([ValueSource("k_DeliveryParameters") // Check sending data to multiple clients. [UnityTest] - public IEnumerator SendMultipleClients([ValueSource("k_DeliveryParameters")] NetworkDelivery delivery) + public IEnumerator SendMultipleClients( + [ValueSource("k_DeliveryParameters")] NetworkDelivery delivery, + [ValueSource("k_NetworkFamiltyParameters")] NetworkFamily family) { - InitializeTransport(out m_Server, out m_ServerEvents); - InitializeTransport(out m_Client1, out m_Client1Events); - InitializeTransport(out m_Client2, out m_Client2Events); + InitializeTransport(out m_Server, out m_ServerEvents, family: family); + InitializeTransport(out m_Client1, out m_Client1Events, family: family); + InitializeTransport(out m_Client2, out m_Client2Events, family: family); m_Server.StartServer(); m_Client1.StartClient(); @@ -234,11 +254,13 @@ public IEnumerator SendMultipleClients([ValueSource("k_DeliveryParameters")] Net // Check receiving data from multiple clients. [UnityTest] - public IEnumerator ReceiveMultipleClients([ValueSource("k_DeliveryParameters")] NetworkDelivery delivery) + public IEnumerator ReceiveMultipleClients( + [ValueSource("k_DeliveryParameters")] NetworkDelivery delivery, + [ValueSource("k_NetworkFamiltyParameters")] NetworkFamily family) { - InitializeTransport(out m_Server, out m_ServerEvents); - InitializeTransport(out m_Client1, out m_Client1Events); - InitializeTransport(out m_Client2, out m_Client2Events); + InitializeTransport(out m_Server, out m_ServerEvents, family: family); + InitializeTransport(out m_Client1, out m_Client1Events, family: family); + InitializeTransport(out m_Client2, out m_Client2Events, family: family); m_Server.StartServer(); m_Client1.StartClient(); @@ -273,8 +295,10 @@ public IEnumerator ReceiveMultipleClients([ValueSource("k_DeliveryParameters")] [UnityTest] public IEnumerator DisconnectOnReliableSendQueueOverflow() { - InitializeTransport(out m_Server, out m_ServerEvents); - InitializeTransport(out m_Client1, out m_Client1Events); + const int maxSendQueueSize = 16 * 1024; + + InitializeTransport(out m_Server, out m_ServerEvents, maxSendQueueSize: maxSendQueueSize); + InitializeTransport(out m_Client1, out m_Client1Events, maxSendQueueSize: maxSendQueueSize); m_Server.StartServer(); m_Client1.StartClient(); @@ -283,7 +307,7 @@ public IEnumerator DisconnectOnReliableSendQueueOverflow() m_Server.Shutdown(); - var numSends = (UnityTransport.InitialMaxSendQueueSize / 1024); + var numSends = (maxSendQueueSize / 1024); for (int i = 0; i < numSends; i++) { @@ -292,8 +316,7 @@ public IEnumerator DisconnectOnReliableSendQueueOverflow() } LogAssert.Expect(LogType.Error, "Couldn't add payload of size 1024 to reliable send queue. " + - $"Closing connection {m_Client1.ServerClientId} as reliability guarantees can't be maintained. " + - $"Perhaps 'Max Send Queue Size' ({UnityTransport.InitialMaxSendQueueSize}) is too small for workload."); + $"Closing connection {m_Client1.ServerClientId} as reliability guarantees can't be maintained."); Assert.AreEqual(2, m_Client1Events.Count); Assert.AreEqual(NetworkEvent.Disconnect, m_Client1Events[1].Type); @@ -308,15 +331,17 @@ public IEnumerator DisconnectOnReliableSendQueueOverflow() [UnityPlatform(exclude = new[] { RuntimePlatform.Switch, RuntimePlatform.PS4, RuntimePlatform.PS5 })] public IEnumerator SendCompletesOnUnreliableSendQueueOverflow() { - InitializeTransport(out m_Server, out m_ServerEvents); - InitializeTransport(out m_Client1, out m_Client1Events); + const int maxSendQueueSize = 16 * 1024; + + InitializeTransport(out m_Server, out m_ServerEvents, maxSendQueueSize: maxSendQueueSize); + InitializeTransport(out m_Client1, out m_Client1Events, maxSendQueueSize: maxSendQueueSize); m_Server.StartServer(); m_Client1.StartClient(); yield return WaitForNetworkEvent(NetworkEvent.Connect, m_Client1Events); - var numSends = (UnityTransport.InitialMaxSendQueueSize / 1024) + 1; + var numSends = (maxSendQueueSize / 1024) + 1; for (int i = 0; i < numSends; i++) { @@ -340,6 +365,7 @@ public IEnumerator SendCompletesOnUnreliableSendQueueOverflow() yield return null; } +#if !UTP_TRANSPORT_2_0_ABOVE // Check that simulator parameters are effective. We only check with the drop rate, because // that's easy to check and we only really want to make sure the simulator parameters are // configured properly (the simulator pipeline stage is already well-tested in UTP). @@ -394,6 +420,7 @@ public IEnumerator CurrentRttReportedCorrectly() yield return null; } +#endif [UnityTest] public IEnumerator SendQueuesFlushedOnShutdown([ValueSource("k_DeliveryParameters")] NetworkDelivery delivery) diff --git a/Tests/Runtime/com.unity.netcode.runtimetests.asmdef b/Tests/Runtime/com.unity.netcode.runtimetests.asmdef index fbb6f71..e245a30 100644 --- a/Tests/Runtime/com.unity.netcode.runtimetests.asmdef +++ b/Tests/Runtime/com.unity.netcode.runtimetests.asmdef @@ -39,6 +39,11 @@ "name": "com.unity.modules.physics", "expression": "", "define": "COM_UNITY_MODULES_PHYSICS" + }, + { + "name": "com.unity.transport", + "expression": "2.0.0-exp", + "define": "UTP_TRANSPORT_2_0_ABOVE" } ] -} \ No newline at end of file +} diff --git a/package.json b/package.json index d8c8b76..0ab3607 100644 --- a/package.json +++ b/package.json @@ -2,19 +2,19 @@ "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.0.2", + "version": "1.1.0", "unity": "2020.3", "dependencies": { "com.unity.nuget.mono-cecil": "1.10.1", - "com.unity.transport": "1.2.0" + "com.unity.transport": "1.3.0" }, "upmCi": { - "footprint": "01764b7751e27d1e2af672c49cec3ed5691b53b7" + "footprint": "4d959c429b2aabd0ba04a6a1a4e1c5e352e6366f" }, "repository": { "url": "https://github.com/Unity-Technologies/com.unity.netcode.gameobjects.git", "type": "git", - "revision": "fe0c300aa691f31d2aec1d4b73e2971f28122d3b" + "revision": "2c69184e5f85a025455c415145be3eeecbf98446" }, "samples": [ {