diff --git a/CHANGELOG.md b/CHANGELOG.md index 765dfed..627975e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ + # Changelog All notable changes to this project will be documented in this file. @@ -6,6 +7,34 @@ 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.0.1] - 2022-08-23 + +### Changed + +- 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) +- Preformance improvements for cases with large number of NetworkObjects, by not iterating over all unchanged NetworkObjects + +### Fixed + +- Fixed an issue where reading/writing more than 8 bits at a time with BitReader/BitWriter would write/read from the wrong place, returning and incorrect result. (#2130) +- Fixed issue with the internal `NetworkTransformState.m_Bitset` flag not getting cleared upon the next tick advancement. (#2110) +- Fixed interpolation issue with `NetworkTransform.Teleport`. (#2110) +- Fixed issue where the authoritative side was interpolating its transform. (#2110) +- Fixed Owner-written NetworkVariable infinitely write themselves (#2109) +- Fixed NetworkList issue that showed when inserting at the very end of a NetworkList (#2099) +- Fixed issue where a client owner of a `NetworkVariable` with both owner read and write permissions would not update the server side when changed. (#2097) +- Fixed issue when attempting to spawn a parent `GameObject`, with `NetworkObject` component attached, that has one or more child `GameObject`s, that are inactive in the hierarchy, with `NetworkBehaviour` components it will no longer attempt to spawn the associated `NetworkBehaviour`(s) or invoke ownership changed notifications but will log a warning message. (#2096) +- Fixed an issue where destroying a NetworkBehaviour would not deregister it from the parent NetworkObject, leading to exceptions when the parent was later destroyed. (#2091) +- Fixed issue where `NetworkObject.NetworkHide` was despawning and destroying, as opposed to only despawning, in-scene placed `NetworkObject`s. (#2086) +- Fixed `NetworkAnimator` synchronizing transitions twice due to it detecting the change in animation state once a transition is started by a trigger. (#2084) +- Fixed issue where `NetworkAnimator` would not synchronize a looping animation for late joining clients if it was at the very end of its loop. (#2076) +- Fixed issue where `NetworkAnimator` was not removing its subscription from `OnClientConnectedCallback` when despawned during the shutdown sequence. (#2074) +- Fixed IsServer and IsClient being set to false before object despawn during the shutdown sequence. (#2074) +- Fixed NetworkList Value event on the server. PreviousValue is now set correctly when a new value is set through property setter. (#2067) +- Fixed NetworkLists not populating on client. NetworkList now uses the most recent list as opposed to the list at the end of previous frame, when sending full updates to dynamically spawned NetworkObject. The difference in behaviour is required as scene management spawns those objects at a different time in the frame, relative to updates. (#2062) + ## [1.0.0] - 2022-06-27 ### Changed diff --git a/Components/NetworkAnimator.cs b/Components/NetworkAnimator.cs index 0b138ed..41ab79d 100644 --- a/Components/NetworkAnimator.cs +++ b/Components/NetworkAnimator.cs @@ -163,6 +163,7 @@ namespace Unity.Netcode.Components internal struct AnimationMessage : INetworkSerializable { // state hash per layer. if non-zero, then Play() this animation, skipping transitions + internal bool Transition; internal int StateHash; internal float NormalizedTime; internal int Layer; @@ -424,14 +425,10 @@ namespace Unity.Netcode.Components stateHash = nextState.fullPathHash; } - else - if (st.normalizedTime >= adjustedNormalizedMaxTime) - { - continue; - } var animMsg = new AnimationMessage { + Transition = m_Animator.IsInTransition(layer), StateHash = stateHash, NormalizedTime = normalizedTime, Layer = layer, @@ -447,6 +444,9 @@ namespace Unity.Netcode.Components m_NetworkAnimatorStateChangeHandler.SynchronizeClient(playerId); } + /// + /// Checks for changes in both Animator parameters and state. + /// internal void CheckForAnimatorChanges() { if (!IsOwner && !IsServerAuthoritative() || IsServerAuthoritative() && !IsServer) @@ -487,6 +487,7 @@ namespace Unity.Netcode.Components var animMsg = new AnimationMessage { + Transition = m_Animator.IsInTransition(layer), StateHash = stateHash, NormalizedTime = normalizedTime, Layer = layer, @@ -749,7 +750,13 @@ namespace Unity.Netcode.Components /// private unsafe void UpdateAnimationState(AnimationMessage animationState) { - if (animationState.StateHash != 0) + if (animationState.StateHash == 0) + { + return; + } + + var currentState = m_Animator.GetCurrentAnimatorStateInfo(animationState.Layer); + if (currentState.fullPathHash != animationState.StateHash || m_Animator.IsInTransition(animationState.Layer) != animationState.Transition) { m_Animator.Play(animationState.StateHash, animationState.Layer, animationState.NormalizedTime); } @@ -835,6 +842,10 @@ namespace Unity.Netcode.Components [ClientRpc] private unsafe void SendAnimStateClientRpc(AnimationMessage animSnapshot, ClientRpcParams clientRpcParams = default) { + if (IsServer) + { + return; + } var isServerAuthoritative = IsServerAuthoritative(); if (!isServerAuthoritative && !IsOwner || isServerAuthoritative) { @@ -884,11 +895,7 @@ namespace Unity.Netcode.Components [ClientRpc] internal void SendAnimTriggerClientRpc(AnimationTriggerMessage animationTriggerMessage, ClientRpcParams clientRpcParams = default) { - var isServerAuthoritative = IsServerAuthoritative(); - if (!isServerAuthoritative && !IsOwner || isServerAuthoritative) - { - m_Animator.SetBool(animationTriggerMessage.Hash, animationTriggerMessage.IsTriggerSet); - } + m_Animator.SetBool(animationTriggerMessage.Hash, animationTriggerMessage.IsTriggerSet); } /// @@ -905,8 +912,13 @@ namespace Unity.Netcode.Components /// sets (true) or resets (false) the trigger. The default is to set it (true). public void SetTrigger(int hash, bool setTrigger = true) { - var isServerAuthoritative = IsServerAuthoritative(); - if (IsOwner && !isServerAuthoritative || IsServer && isServerAuthoritative) + // MTT-3564: + // After fixing the issue with trigger controlled Transitions being synchronized twice, + // it exposed additional issues with this logic. Now, either the owner or the server can + // update triggers. Since server-side RPCs are immediately invoked, for a host a trigger + // will happen when SendAnimTriggerClientRpc is called. For a client owner, we call the + // SendAnimTriggerServerRpc and then trigger locally when running in owner authority mode. + if (IsOwner || IsServer) { var animTriggerMessage = new AnimationTriggerMessage() { Hash = hash, IsTriggerSet = setTrigger }; if (IsServer) @@ -916,9 +928,11 @@ namespace Unity.Netcode.Components else { SendAnimTriggerServerRpc(animTriggerMessage); + if (!IsServerAuthoritative()) + { + m_Animator.SetTrigger(hash); + } } - // trigger the animation locally on the server... - m_Animator.SetBool(hash, setTrigger); } } diff --git a/Components/NetworkTransform.cs b/Components/NetworkTransform.cs index 82c1ab8..73afd53 100644 --- a/Components/NetworkTransform.cs +++ b/Components/NetworkTransform.cs @@ -60,8 +60,8 @@ namespace Unity.Netcode.Components private const int k_ScaleYBit = 8; private const int k_ScaleZBit = 9; private const int k_TeleportingBit = 10; - // 11-15: + private ushort m_Bitset; internal bool InLocalSpace @@ -105,6 +105,14 @@ namespace Unity.Netcode.Components } } + internal bool HasPositionChange + { + get + { + return HasPositionX | HasPositionY | HasPositionZ; + } + } + // RotAngles internal bool HasRotAngleX { @@ -136,6 +144,15 @@ namespace Unity.Netcode.Components } } + internal bool HasRotAngleChange + { + get + { + return HasRotAngleX | HasRotAngleY | HasRotAngleZ; + } + } + + // Scale internal bool HasScaleX { @@ -167,6 +184,14 @@ namespace Unity.Netcode.Components } } + internal bool HasScaleChange + { + get + { + return HasScaleX | HasScaleY | HasScaleZ; + } + } + internal bool IsTeleportingNextFrame { get => (m_Bitset & (1 << k_TeleportingBit)) != 0; @@ -182,37 +207,16 @@ namespace Unity.Netcode.Components internal float ScaleX, ScaleY, ScaleZ; internal double SentTime; - internal Vector3 Position - { - get { return new Vector3(PositionX, PositionY, PositionZ); } - set - { - PositionX = value.x; - PositionY = value.y; - PositionZ = value.z; - } - } + internal bool IsDirty; - internal Vector3 Rotation + /// + /// This will reset the NetworkTransform BitSet + /// + internal void ClearBitSetForNextTick() { - get { return new Vector3(RotAngleX, RotAngleY, RotAngleZ); } - set - { - RotAngleX = value.x; - RotAngleY = value.y; - RotAngleZ = value.z; - } - } - - internal Vector3 Scale - { - get { return new Vector3(ScaleX, ScaleY, ScaleZ); } - set - { - ScaleX = value.x; - ScaleY = value.y; - ScaleZ = value.z; - } + // We need to preserve the local space settings for the current state + m_Bitset &= (ushort)(m_Bitset & (1 << k_InLocalSpaceBit)); + IsDirty = false; } public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter @@ -267,6 +271,21 @@ namespace Unity.Netcode.Components { serializer.SerializeValue(ref ScaleZ); } + + // Only if we are receiving state + if (serializer.IsReader) + { + // Go ahead and mark the local state dirty or not dirty as well + /// + if (HasPositionChange || HasRotAngleChange || HasScaleChange) + { + IsDirty = true; + } + else + { + IsDirty = false; + } + } } } @@ -328,22 +347,21 @@ namespace Unity.Netcode.Components /// public float ScaleThreshold = ScaleThresholdDefault; + /// - /// Sets whether this transform should sync in local space or in world space. - /// This is important to set since reparenting this transform could have issues, - /// if using world position (depending on who gets synced first: the parent or the child) - /// Having a child always at position 0,0,0 for example will have less possibilities of desync than when using world positions + /// Sets whether the transform should be treated as local (true) or world (false) space. /// + /// + /// This should only be changed by the authoritative side during runtime. Non-authoritative + /// changes will be overridden upon the next state update. + /// [Tooltip("Sets whether this transform should sync in local space or in world space")] public bool InLocalSpace = false; - private bool m_LastInterpolateLocal = false; // was the last frame local /// /// When enabled (default) interpolation is applied and when disabled no interpolation is applied - /// Note: can be changed during runtime. /// public bool Interpolate = true; - private bool m_LastInterpolate = true; // was the last frame interpolated /// /// Used to determine who can write to this transform. Server only for this transform. @@ -351,7 +369,6 @@ namespace Unity.Netcode.Components /// in the package samples for how to implement a NetworkTransform with client write support. /// If using different values, please use RPCs to write to the server. Netcode doesn't support client side network variable writing /// - // This is public to make sure that users don't depend on this IsClient && IsOwner check in their code. If this logic changes in the future, we can make it invisible here public bool CanCommitToTransform { get; protected set; } /// @@ -366,62 +383,107 @@ namespace Unity.Netcode.Components /// protected NetworkManager m_CachedNetworkManager; - private readonly NetworkVariable m_ReplicatedNetworkState = new NetworkVariable(new NetworkTransformState()); + /// + /// We have two internal NetworkVariables. + /// One for server authoritative and one for "client/owner" authoritative. + /// + private readonly NetworkVariable m_ReplicatedNetworkStateServer = new NetworkVariable(new NetworkTransformState(), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server); + private readonly NetworkVariable m_ReplicatedNetworkStateOwner = new NetworkVariable(new NetworkTransformState(), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); + + + internal NetworkVariable ReplicatedNetworkState + { + get + { + if (!IsServerAuthoritative()) + { + return m_ReplicatedNetworkStateOwner; + } + + return m_ReplicatedNetworkStateServer; + } + } private NetworkTransformState m_LocalAuthoritativeNetworkState; - private const int k_DebugDrawLineTime = 10; - private bool m_HasSentLastValue = false; // used to send one last value, so clients can make the difference between lost replication data (clients extrapolate) and no more data to send. + private ClientRpcParams m_ClientRpcParams = new ClientRpcParams() { Send = new ClientRpcSendParams() }; + private List m_ClientIds = new List() { 0 }; - private BufferedLinearInterpolator m_PositionXInterpolator; // = new BufferedLinearInterpolatorFloat(); - private BufferedLinearInterpolator m_PositionYInterpolator; // = new BufferedLinearInterpolatorFloat(); - private BufferedLinearInterpolator m_PositionZInterpolator; // = new BufferedLinearInterpolatorFloat(); - private BufferedLinearInterpolator m_RotationInterpolator; // = new BufferedLinearInterpolatorQuaternion(); // rotation is a single Quaternion since each euler axis will affect the quaternion's final value - private BufferedLinearInterpolator m_ScaleXInterpolator; // = new BufferedLinearInterpolatorFloat(); - private BufferedLinearInterpolator m_ScaleYInterpolator; // = new BufferedLinearInterpolatorFloat(); - private BufferedLinearInterpolator m_ScaleZInterpolator; // = new BufferedLinearInterpolatorFloat(); + private BufferedLinearInterpolator m_PositionXInterpolator; + private BufferedLinearInterpolator m_PositionYInterpolator; + private BufferedLinearInterpolator m_PositionZInterpolator; + private BufferedLinearInterpolator m_RotationInterpolator; // rotation is a single Quaternion since each Euler axis will affect the quaternion's final value + private BufferedLinearInterpolator m_ScaleXInterpolator; + private BufferedLinearInterpolator m_ScaleYInterpolator; + private BufferedLinearInterpolator m_ScaleZInterpolator; private readonly List> m_AllFloatInterpolators = new List>(6); - private Transform m_Transform; // cache the transform component to reduce unnecessary bounce between managed and native private int m_LastSentTick; private NetworkTransformState m_LastSentState; + internal NetworkTransformState GetLastSentState() + { + return m_LastSentState; + } + /// - /// Tries updating the server authoritative transform, only if allowed. - /// If this called server side, this will commit directly. - /// If no update is needed, nothing will be sent. This method should still be called every update, it'll self manage when it should and shouldn't send + /// This will try to send/commit the current transform delta states (if any) /// - /// - /// + /// + /// Only client owners or the server should invoke this method + /// + /// the transform to be committed + /// time it was marked dirty protected void TryCommitTransformToServer(Transform transformToCommit, double dirtyTime) { - var isDirty = ApplyTransformToNetworkState(ref m_LocalAuthoritativeNetworkState, dirtyTime, transformToCommit); - TryCommit(isDirty); - } - - private void TryCommitValuesToServer(Vector3 position, Vector3 rotation, Vector3 scale, double dirtyTime) - { - var isDirty = ApplyTransformToNetworkStateWithInfo(ref m_LocalAuthoritativeNetworkState, dirtyTime, position, rotation, scale); - - TryCommit(isDirty.isDirty); - } - - private void TryCommit(bool isDirty) - { - void Send(NetworkTransformState stateToSend) + // Only client owners or the server should invoke this method + if (!IsOwner && !m_CachedIsServer) { - if (m_CachedIsServer) + NetworkLog.LogError($"Non-owner instance, {name}, is trying to commit a transform!"); + return; + } + + /// If authority is invoking this, then treat it like we do with + if (CanCommitToTransform) + { + // If our replicated state is not dirty and our local authority state is dirty, clear it. + if (!ReplicatedNetworkState.IsDirty() && m_LocalAuthoritativeNetworkState.IsDirty) { - // server RPC takes a few frames to execute server side, we want this to execute immediately - CommitLocallyAndReplicate(stateToSend); + // Now clear our bitset and prepare for next network tick state update + m_LocalAuthoritativeNetworkState.ClearBitSetForNextTick(); } - else + + TryCommitTransform(transformToCommit, m_CachedNetworkManager.LocalTime.Time); + } + else + { + // We are an owner requesting to update our state + if (!m_CachedIsServer) { - CommitTransformServerRpc(stateToSend); + SetStateServerRpc(transformToCommit.position, transformToCommit.rotation, transformToCommit.localScale, false); + } + else // Server is always authoritative (including owner authoritative) + { + SetStateClientRpc(transformToCommit.position, transformToCommit.rotation, transformToCommit.localScale, false); } } + } + + /// + /// Authoritative side only + /// If there are any transform delta states, this method will synchronize the + /// state with all non-authority instances. + /// + private void TryCommitTransform(Transform transformToCommit, double dirtyTime) + { + if (!CanCommitToTransform && !IsOwner) + { + NetworkLog.LogError($"[{name}] is trying to commit the transform without authority!"); + return; + } + var isDirty = ApplyTransformToNetworkState(ref m_LocalAuthoritativeNetworkState, dirtyTime, transformToCommit); // if dirty, send // if not dirty anymore, but hasn't sent last value for limiting extrapolation, still set isDirty @@ -432,38 +494,24 @@ namespace Unity.Netcode.Components // making it immobile. if (isDirty) { - Send(m_LocalAuthoritativeNetworkState); + // Commit the state + ReplicatedNetworkState.Value = m_LocalAuthoritativeNetworkState; m_HasSentLastValue = false; m_LastSentTick = m_CachedNetworkManager.LocalTime.Tick; m_LastSentState = m_LocalAuthoritativeNetworkState; } else if (!m_HasSentLastValue && m_CachedNetworkManager.LocalTime.Tick >= m_LastSentTick + 1) // check for state.IsDirty since update can happen more than once per tick. No need for client, RPCs will just queue up { + // Since the last m_LocalAuthoritativeNetworkState could have included a IsTeleportingNextFrame + // we need to reset this here so only the deltas are applied and interpolation is not reset again. + m_LastSentState.IsTeleportingNextFrame = false; m_LastSentState.SentTime = m_CachedNetworkManager.LocalTime.Time; // time 1+ tick later - Send(m_LastSentState); + // Commit the state + ReplicatedNetworkState.Value = m_LastSentState; m_HasSentLastValue = true; } } - [ServerRpc(RequireOwnership = false)] - private void CommitTransformServerRpc(NetworkTransformState networkState, ServerRpcParams serverParams = default) - { - if (serverParams.Receive.SenderClientId == OwnerClientId) // RPC call when not authorized to write could happen during the RTT interval during which a server's ownership change hasn't reached the client yet - { - CommitLocallyAndReplicate(networkState); - } - } - - private void CommitLocallyAndReplicate(NetworkTransformState networkState) - { - m_ReplicatedNetworkState.Value = networkState; - - if (Interpolate) - { - AddInterpolatedState(networkState); - } - } - private void ResetInterpolatedStateToCurrentAuthoritativeState() { var serverTime = NetworkManager.ServerTime.Time; @@ -471,7 +519,7 @@ namespace Unity.Netcode.Components m_PositionYInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.PositionY, serverTime); m_PositionZInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.PositionZ, serverTime); - m_RotationInterpolator.ResetTo(Quaternion.Euler(m_LocalAuthoritativeNetworkState.Rotation), 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); @@ -479,38 +527,46 @@ namespace Unity.Netcode.Components } /// - /// Will apply the transform to the LocalAuthoritativeNetworkState and get detailed isDirty information returned. + /// Used for integration testing: + /// Will apply the transform to the LocalAuthoritativeNetworkState and get detailed dirty information returned + /// in the returned. /// /// transform to apply - /// bool isDirty, bool isPositionDirty, bool isRotationDirty, bool isScaleDirty - internal (bool isDirty, bool isPositionDirty, bool isRotationDirty, bool isScaleDirty) ApplyLocalNetworkState(Transform transform) + /// NetworkTransformState + internal NetworkTransformState ApplyLocalNetworkState(Transform transform) { - return ApplyTransformToNetworkStateWithInfo(ref m_LocalAuthoritativeNetworkState, m_CachedNetworkManager.LocalTime.Time, transform); + // Since we never commit these changes, we need to simulate that any changes were committed previously and the bitset + // value would already be reset prior to having the state applied + m_LocalAuthoritativeNetworkState.ClearBitSetForNextTick(); + + // Now check the transform for any threshold value changes + ApplyTransformToNetworkStateWithInfo(ref m_LocalAuthoritativeNetworkState, m_CachedNetworkManager.LocalTime.Time, transform); + + // Return the entire state to be used by the integration test + return m_LocalAuthoritativeNetworkState; } - // updates `NetworkState` properties if they need to and returns a `bool` indicating whether or not there was any changes made - // returned boolean would be useful to change encapsulating `NetworkVariable`'s dirty state, e.g. ReplNetworkState.SetDirty(isDirty); + /// + /// Used for integration testing + /// internal bool ApplyTransformToNetworkState(ref NetworkTransformState networkState, double dirtyTime, Transform transformToUse) { - return ApplyTransformToNetworkStateWithInfo(ref networkState, dirtyTime, transformToUse).isDirty; + return ApplyTransformToNetworkStateWithInfo(ref networkState, dirtyTime, transformToUse); } - private (bool isDirty, bool isPositionDirty, bool isRotationDirty, bool isScaleDirty) ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState networkState, double dirtyTime, Transform transformToUse) - { - var position = InLocalSpace ? transformToUse.localPosition : transformToUse.position; - var rotAngles = InLocalSpace ? transformToUse.localEulerAngles : transformToUse.eulerAngles; - var scale = transformToUse.localScale; - return ApplyTransformToNetworkStateWithInfo(ref networkState, dirtyTime, position, rotAngles, scale); - } - - private (bool isDirty, bool isPositionDirty, bool isRotationDirty, bool isScaleDirty) ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState networkState, double dirtyTime, Vector3 position, Vector3 rotAngles, Vector3 scale) + /// + /// Applies the transform to the specified. + /// + private bool ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState networkState, double dirtyTime, Transform transformToUse) { var isDirty = false; var isPositionDirty = false; var isRotationDirty = false; var isScaleDirty = false; - // hasPositionZ set to false when it should be true? + var position = InLocalSpace ? transformToUse.localPosition : transformToUse.position; + var rotAngles = InLocalSpace ? transformToUse.localEulerAngles : transformToUse.eulerAngles; + var scale = transformToUse.localScale; if (InLocalSpace != networkState.InLocalSpace) { @@ -518,77 +574,63 @@ namespace Unity.Netcode.Components isDirty = true; } - // we assume that if x, y or z are dirty then we'll have to send all 3 anyway, so for efficiency - // we skip doing the (quite expensive) Math.Approximately() and check against PositionThreshold - // this still is overly costly and could use more improvements. - // - // (ditto for scale components) - if (SyncPositionX && - Mathf.Abs(networkState.PositionX - position.x) > PositionThreshold) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + if (SyncScaleZ && Mathf.Abs(networkState.ScaleZ - scale.z) >= ScaleThreshold || networkState.IsTeleportingNextFrame) { networkState.ScaleZ = scale.z; networkState.HasScaleZ = true; @@ -602,37 +644,46 @@ namespace Unity.Netcode.Components networkState.SentTime = dirtyTime; } - return (isDirty, isPositionDirty, isRotationDirty, isScaleDirty); + /// We need to set this in order to know when we can reset our local authority state + /// If our state is already dirty or we just found deltas (i.e. isDirty == true) + networkState.IsDirty |= isDirty; + return isDirty; } - private void ApplyInterpolatedNetworkStateToTransform(NetworkTransformState networkState, Transform transformToUpdate) + /// + /// Applies the authoritative state to the transform + /// + private void ApplyAuthoritativeState() { - var interpolatedPosition = InLocalSpace ? transformToUpdate.localPosition : transformToUpdate.position; + var networkState = ReplicatedNetworkState.Value; + var interpolatedPosition = networkState.InLocalSpace ? transform.localPosition : transform.position; // todo: we should store network state w/ quats vs. euler angles - var interpolatedRotAngles = InLocalSpace ? transformToUpdate.localEulerAngles : transformToUpdate.eulerAngles; - var interpolatedScale = transformToUpdate.localScale; + var interpolatedRotAngles = networkState.InLocalSpace ? transform.localEulerAngles : transform.eulerAngles; + var interpolatedScale = transform.localScale; + var isTeleporting = networkState.IsTeleportingNextFrame; - // InLocalSpace Read + // InLocalSpace Read: InLocalSpace = networkState.InLocalSpace; - // Position Read - if (SyncPositionX) + + // Update the position values that were changed in this state update + if (networkState.HasPositionX) { - interpolatedPosition.x = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Position.x : m_PositionXInterpolator.GetInterpolatedValue(); + interpolatedPosition.x = isTeleporting || !Interpolate ? networkState.PositionX : m_PositionXInterpolator.GetInterpolatedValue(); } - if (SyncPositionY) + if (networkState.HasPositionY) { - interpolatedPosition.y = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Position.y : m_PositionYInterpolator.GetInterpolatedValue(); + interpolatedPosition.y = isTeleporting || !Interpolate ? networkState.PositionY : m_PositionYInterpolator.GetInterpolatedValue(); } - if (SyncPositionZ) + if (networkState.HasPositionZ) { - interpolatedPosition.z = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Position.z : m_PositionZInterpolator.GetInterpolatedValue(); + interpolatedPosition.z = isTeleporting || !Interpolate ? networkState.PositionZ : m_PositionZInterpolator.GetInterpolatedValue(); } - // again, we should be using quats here - if (SyncRotAngleX || SyncRotAngleY || SyncRotAngleZ) + // Update the rotation values that were changed in this state update + if (networkState.HasRotAngleChange) { var eulerAngles = new Vector3(); if (Interpolate) @@ -640,111 +691,174 @@ namespace Unity.Netcode.Components eulerAngles = m_RotationInterpolator.GetInterpolatedValue().eulerAngles; } - if (SyncRotAngleX) + if (networkState.HasRotAngleX) { - interpolatedRotAngles.x = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Rotation.x : eulerAngles.x; + interpolatedRotAngles.x = isTeleporting || !Interpolate ? networkState.RotAngleX : eulerAngles.x; } - if (SyncRotAngleY) + if (networkState.HasRotAngleY) { - interpolatedRotAngles.y = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Rotation.y : eulerAngles.y; + interpolatedRotAngles.y = isTeleporting || !Interpolate ? networkState.RotAngleY : eulerAngles.y; } - if (SyncRotAngleZ) + if (networkState.HasRotAngleZ) { - interpolatedRotAngles.z = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Rotation.z : eulerAngles.z; + interpolatedRotAngles.z = isTeleporting || !Interpolate ? networkState.RotAngleZ : eulerAngles.z; } } - // Scale Read - if (SyncScaleX) + // Update all scale axis that were changed in this state update + if (networkState.HasScaleX) { - interpolatedScale.x = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Scale.x : m_ScaleXInterpolator.GetInterpolatedValue(); + interpolatedScale.x = isTeleporting || !Interpolate ? networkState.ScaleX : m_ScaleXInterpolator.GetInterpolatedValue(); } - if (SyncScaleY) + if (networkState.HasScaleY) { - interpolatedScale.y = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Scale.y : m_ScaleYInterpolator.GetInterpolatedValue(); + interpolatedScale.y = isTeleporting || !Interpolate ? networkState.ScaleY : m_ScaleYInterpolator.GetInterpolatedValue(); } - if (SyncScaleZ) + if (networkState.HasScaleZ) { - interpolatedScale.z = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Scale.z : m_ScaleZInterpolator.GetInterpolatedValue(); + interpolatedScale.z = isTeleporting || !Interpolate ? networkState.ScaleZ : m_ScaleZInterpolator.GetInterpolatedValue(); } - // Position Apply - if (SyncPositionX || SyncPositionY || SyncPositionZ) + // Apply the new position + if (networkState.HasPositionChange) { if (InLocalSpace) { - transformToUpdate.localPosition = interpolatedPosition; + + transform.localPosition = interpolatedPosition; } else { - transformToUpdate.position = interpolatedPosition; + transform.position = interpolatedPosition; } } - // RotAngles Apply - if (SyncRotAngleX || SyncRotAngleY || SyncRotAngleZ) + // Apply the new rotation + if (networkState.HasRotAngleChange) { if (InLocalSpace) { - transformToUpdate.localRotation = Quaternion.Euler(interpolatedRotAngles); + transform.localRotation = Quaternion.Euler(interpolatedRotAngles); } else { - transformToUpdate.rotation = Quaternion.Euler(interpolatedRotAngles); + transform.rotation = Quaternion.Euler(interpolatedRotAngles); } } - // Scale Apply - if (SyncScaleX || SyncScaleY || SyncScaleZ) + // Apply the new scale + if (networkState.HasScaleChange) { - transformToUpdate.localScale = interpolatedScale; + transform.localScale = interpolatedScale; } } - private void AddInterpolatedState(NetworkTransformState newState, bool reset = false) + /// + /// Only non-authoritative instances should invoke this + /// + private void AddInterpolatedState(NetworkTransformState newState) { var sentTime = newState.SentTime; + var currentPosition = newState.InLocalSpace ? transform.localPosition : transform.position; + var currentRotation = newState.InLocalSpace ? transform.localRotation : transform.rotation; + var currentEulerAngles = currentRotation.eulerAngles; - if (reset) + // When there is a change in interpolation or if teleporting, we reset + if ((newState.InLocalSpace != InLocalSpace) || newState.IsTeleportingNextFrame) { + InLocalSpace = newState.InLocalSpace; + var currentScale = transform.localScale; + + // we should clear our float interpolators + foreach (var interpolator in m_AllFloatInterpolators) + { + interpolator.Clear(); + } + + // we should clear our quaternion interpolator + m_RotationInterpolator.Clear(); + + // Adjust based on which axis changed if (newState.HasPositionX) { m_PositionXInterpolator.ResetTo(newState.PositionX, sentTime); + currentPosition.x = newState.PositionX; } if (newState.HasPositionY) { m_PositionYInterpolator.ResetTo(newState.PositionY, sentTime); + currentPosition.y = newState.PositionY; } if (newState.HasPositionZ) { m_PositionZInterpolator.ResetTo(newState.PositionZ, sentTime); + currentPosition.z = newState.PositionZ; } - m_RotationInterpolator.ResetTo(Quaternion.Euler(newState.Rotation), sentTime); + // Apply the position + if (newState.InLocalSpace) + { + transform.localPosition = currentPosition; + } + else + { + transform.position = currentPosition; + } + // Adjust based on which axis changed if (newState.HasScaleX) { m_ScaleXInterpolator.ResetTo(newState.ScaleX, sentTime); + currentScale.x = newState.ScaleX; } if (newState.HasScaleY) { m_ScaleYInterpolator.ResetTo(newState.ScaleY, sentTime); + currentScale.y = newState.ScaleY; } if (newState.HasScaleZ) { m_ScaleZInterpolator.ResetTo(newState.ScaleZ, sentTime); + currentScale.z = newState.ScaleZ; } + // Apply the adjusted scale + transform.localScale = currentScale; + + // Adjust based on which axis changed + if (newState.HasRotAngleX) + { + currentEulerAngles.x = newState.RotAngleX; + } + + if (newState.HasRotAngleY) + { + currentEulerAngles.y = newState.RotAngleY; + } + + if (newState.HasRotAngleZ) + { + currentEulerAngles.z = newState.RotAngleZ; + } + + // Apply the rotation + currentRotation.eulerAngles = currentEulerAngles; + transform.rotation = currentRotation; + + // Reset the rotation interpolator + m_RotationInterpolator.ResetTo(currentRotation, sentTime); return; } + + // Apply axial changes from the new state if (newState.HasPositionX) { m_PositionXInterpolator.AddMeasurement(newState.PositionX, sentTime); @@ -760,8 +874,6 @@ namespace Unity.Netcode.Components m_PositionZInterpolator.AddMeasurement(newState.PositionZ, sentTime); } - m_RotationInterpolator.AddMeasurement(Quaternion.Euler(newState.Rotation), sentTime); - if (newState.HasScaleX) { m_ScaleXInterpolator.AddMeasurement(newState.ScaleX, sentTime); @@ -776,8 +888,35 @@ namespace Unity.Netcode.Components { m_ScaleZInterpolator.AddMeasurement(newState.ScaleZ, sentTime); } + + // With rotation, we check if there are any changes first and + // if so then apply the changes to the current Euler rotation + // values. + if (newState.HasRotAngleChange) + { + if (newState.HasRotAngleX) + { + currentEulerAngles.x = newState.RotAngleX; + } + + if (newState.HasRotAngleY) + { + currentEulerAngles.y = newState.RotAngleY; + } + + if (newState.HasRotAngleZ) + { + currentEulerAngles.z = newState.RotAngleZ; + } + + currentRotation.eulerAngles = currentEulerAngles; + m_RotationInterpolator.AddMeasurement(currentRotation, sentTime); + } } + /// + /// Only non-authoritative instances should invoke this method + /// private void OnNetworkStateChanged(NetworkTransformState oldState, NetworkTransformState newState) { if (!NetworkObject.IsSpawned) @@ -793,13 +932,7 @@ namespace Unity.Netcode.Components if (Interpolate) { - AddInterpolatedState(newState, (newState.InLocalSpace != m_LastInterpolateLocal)); - } - m_LastInterpolateLocal = newState.InLocalSpace; - - if (m_CachedNetworkManager.LogLevel == LogLevel.Developer) - { - var pos = new Vector3(newState.PositionX, newState.PositionY, newState.PositionZ); + AddInterpolatedState(newState); } } @@ -844,35 +977,39 @@ namespace Unity.Netcode.Components } } - /// public override void OnNetworkSpawn() { - // must set up m_Transform in OnNetworkSpawn because it's possible an object spawns but is disabled - // and thus awake won't be called. - // TODO: investigate further on not sending data for something that is not enabled - m_Transform = transform; - m_ReplicatedNetworkState.OnValueChanged += OnNetworkStateChanged; - - CanCommitToTransform = IsServer; m_CachedIsServer = IsServer; m_CachedNetworkManager = NetworkManager; + Initialize(); + + // This assures the initial spawning of the object synchronizes all connected clients + // with the current transform values. This should not be placed within Initialize since + // that can be invoked when ownership changes. if (CanCommitToTransform) { - TryCommitTransformToServer(m_Transform, m_CachedNetworkManager.LocalTime.Time); - } - m_LocalAuthoritativeNetworkState = m_ReplicatedNetworkState.Value; + // Teleport to current position + SetStateInternal(transform.position, transform.rotation, transform.localScale, true); - // crucial we do this to reset the interpolators so that recycled objects when using a pool will - // not have leftover interpolator state from the previous object - Initialize(); + // Force the state update to be sent + TryCommitTransform(transform, m_CachedNetworkManager.LocalTime.Time); + } } /// public override void OnNetworkDespawn() { - m_ReplicatedNetworkState.OnValueChanged -= OnNetworkStateChanged; + ReplicatedNetworkState.OnValueChanged -= OnNetworkStateChanged; + } + + /// + public override void OnDestroy() + { + base.OnDestroy(); + m_ReplicatedNetworkStateServer.Dispose(); + m_ReplicatedNetworkStateOwner.Dispose(); } /// @@ -887,22 +1024,36 @@ namespace Unity.Netcode.Components Initialize(); } + /// + /// Initializes NetworkTransform when spawned and ownership changes. + /// private void Initialize() { - ResetInterpolatedStateToCurrentAuthoritativeState(); // useful for late joining + if (!IsSpawned) + { + return; + } + + CanCommitToTransform = IsServerAuthoritative() ? IsServer : IsOwner; + var replicatedState = ReplicatedNetworkState; + m_LocalAuthoritativeNetworkState = replicatedState.Value; if (CanCommitToTransform) { - m_ReplicatedNetworkState.SetDirty(true); + replicatedState.OnValueChanged -= OnNetworkStateChanged; } - else if (m_Transform != null) + else { - ApplyInterpolatedNetworkStateToTransform(m_ReplicatedNetworkState.Value, m_Transform); + replicatedState.OnValueChanged += OnNetworkStateChanged; + + // In case we are late joining + ResetInterpolatedStateToCurrentAuthoritativeState(); } } /// /// Directly sets a state on the authoritative transform. + /// Owner clients can directly set the state on a server authoritative transform /// This will override any changes made previously to the transform /// This isn't resistant to network jitter. Server side changes due to this method won't be interpolated. /// The parameters are broken up into pos / rot / scale on purpose so that the caller can perturb @@ -916,36 +1067,86 @@ namespace Unity.Netcode.Components /// public void SetState(Vector3? posIn = null, Quaternion? rotIn = null, Vector3? scaleIn = null, bool shouldGhostsInterpolate = true) { - if (!IsOwner) - { - throw new Exception("Trying to set a state on a not owned transform"); - } - - if (m_CachedNetworkManager && !(m_CachedNetworkManager.IsConnectedClient || m_CachedNetworkManager.IsListening)) + if (!IsSpawned) { return; } - Vector3 pos = posIn == null ? transform.position : (Vector3)posIn; - Quaternion rot = rotIn == null ? transform.rotation : (Quaternion)rotIn; - Vector3 scale = scaleIn == null ? transform.localScale : (Vector3)scaleIn; + // Only the server or owner can invoke this method + if (!IsOwner && !m_CachedIsServer) + { + throw new Exception("Non-owner client instance cannot set the state of the NetworkTransform!"); + } + + Vector3 pos = posIn == null ? InLocalSpace ? transform.localPosition : transform.position : posIn.Value; + Quaternion rot = rotIn == null ? InLocalSpace ? transform.localRotation : transform.rotation : rotIn.Value; + Vector3 scale = scaleIn == null ? transform.localScale : scaleIn.Value; if (!CanCommitToTransform) { - if (!m_CachedIsServer) + // Preserving the ability for owner authoritative mode to accept state changes from server + if (m_CachedIsServer) { - SetStateServerRpc(pos, rot, scale, shouldGhostsInterpolate); + m_ClientIds[0] = OwnerClientId; + m_ClientRpcParams.Send.TargetClientIds = m_ClientIds; + SetStateClientRpc(pos, rot, scale, !shouldGhostsInterpolate, m_ClientRpcParams); } + else // Preserving the ability for server authoritative mode to accept state changes from owner + { + SetStateServerRpc(pos, rot, scale, !shouldGhostsInterpolate); + } + return; + } + + SetStateInternal(pos, rot, scale, !shouldGhostsInterpolate); + } + + /// + /// Authoritative only method + /// Sets the internal state (teleporting or just set state) of the authoritative + /// transform directly. + /// + private void SetStateInternal(Vector3 pos, Quaternion rot, Vector3 scale, bool shouldTeleport) + { + if (InLocalSpace) + { + transform.localPosition = pos; + transform.localRotation = rot; } else { - m_Transform.position = pos; - m_Transform.rotation = rot; - m_Transform.localScale = scale; - m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = shouldGhostsInterpolate; + transform.position = pos; + transform.rotation = rot; } + transform.localScale = scale; + m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = shouldTeleport; + + TryCommitTransform(transform, m_CachedNetworkManager.LocalTime.Time); } + /// + /// Invoked by , allows a non-owner server to update the transform state + /// + /// + /// Continued support for client-driven server authority model + /// + [ClientRpc] + 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); + } + + /// + /// Invoked by , allows an owner-client update the transform state + /// + /// + /// Continued support for client-driven server authority model + /// [ServerRpc] private void SetStateServerRpc(Vector3 pos, Quaternion rot, Vector3 scale, bool shouldTeleport) { @@ -954,15 +1155,23 @@ namespace Unity.Netcode.Components { (pos, rot, scale) = OnClientRequestChange(pos, rot, scale); } - m_Transform.position = pos; - m_Transform.rotation = rot; - m_Transform.localScale = scale; + + transform.position = pos; + transform.rotation = rot; + transform.localScale = scale; m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = shouldTeleport; + TryCommitTransform(transform, m_CachedNetworkManager.LocalTime.Time); } // todo: this is currently in update, to be able to catch any transform changes. A FixedUpdate mode could be added to be less intense, but it'd be // conditional to users only making transform update changes in FixedUpdate. /// + /// + /// If you override this method, be sure that: + /// - Non-owners always invoke this base class method when using interpolation. + /// - Authority can opt to use in place of invoking this base class method. + /// - Non-authority owners can use but should still invoke the this base class method when using interpolation. + /// protected virtual void Update() { if (!IsSpawned) @@ -970,36 +1179,25 @@ namespace Unity.Netcode.Components return; } - if (!Interpolate && m_LastInterpolate) - { - // if we just stopped interpolating, let's clear the interpolators - foreach (var interpolator in m_AllFloatInterpolators) - { - interpolator.Clear(); - } - } - - m_LastInterpolate = Interpolate; - if (CanCommitToTransform) { - if (m_CachedIsServer) + // If our replicated state is not dirty and our local authority state is dirty, clear it. + if (!ReplicatedNetworkState.IsDirty() && m_LocalAuthoritativeNetworkState.IsDirty) { - TryCommitTransformToServer(m_Transform, m_CachedNetworkManager.LocalTime.Time); + // Now clear our bitset and prepare for next network tick state update + m_LocalAuthoritativeNetworkState.ClearBitSetForNextTick(); } + TryCommitTransform(transform, m_CachedNetworkManager.LocalTime.Time); } - - // apply interpolated value - if (m_CachedNetworkManager.IsConnectedClient || m_CachedNetworkManager.IsListening) + else { - // eventually, we could hoist this calculation so that it happens once for all objects, not once per object - var cachedDeltaTime = Time.deltaTime; - var serverTime = NetworkManager.ServerTime; - var cachedServerTime = serverTime.Time; - var cachedRenderTime = serverTime.TimeTicksAgo(1).Time; - if (Interpolate) { + // eventually, we could hoist this calculation so that it happens once for all objects, not once per object + var serverTime = NetworkManager.ServerTime; + var cachedDeltaTime = Time.deltaTime; + var cachedServerTime = serverTime.Time; + var cachedRenderTime = serverTime.TimeTicksAgo(1).Time; foreach (var interpolator in m_AllFloatInterpolators) { interpolator.Update(cachedDeltaTime, cachedRenderTime, cachedServerTime); @@ -1007,18 +1205,13 @@ namespace Unity.Netcode.Components m_RotationInterpolator.Update(cachedDeltaTime, cachedRenderTime, cachedServerTime); } - - if (!CanCommitToTransform) - { - // Apply updated interpolated value - ApplyInterpolatedNetworkStateToTransform(m_ReplicatedNetworkState.Value, m_Transform); - } + // Now apply the current authoritative state + ApplyAuthoritativeState(); } - m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = false; } /// - /// Teleports the transform to the given values without interpolating + /// Teleport the transform to the given values without interpolating /// /// new position to move to. /// new rotation to rotate to. @@ -1028,21 +1221,11 @@ namespace Unity.Netcode.Components { if (!CanCommitToTransform) { - throw new Exception("Teleport not allowed"); + throw new Exception("Teleporting on non-authoritative side is not allowed!"); } - var newRotationEuler = newRotation.eulerAngles; - var stateToSend = m_LocalAuthoritativeNetworkState; - stateToSend.IsTeleportingNextFrame = true; - stateToSend.Position = newPosition; - stateToSend.Rotation = newRotationEuler; - stateToSend.Scale = newScale; - ApplyInterpolatedNetworkStateToTransform(stateToSend, transform); - // set teleport flag in state to signal to ghosts not to interpolate - m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = true; - // check server side - TryCommitValuesToServer(newPosition, newRotationEuler, newScale, m_CachedNetworkManager.LocalTime.Time); - m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = false; + // Teleporting now is as simple as setting the internal state and passing the teleport flag + SetStateInternal(newPosition, newRotation, newScale, true); } /// diff --git a/Editor/CodeGen/INetworkMessageILPP.cs b/Editor/CodeGen/INetworkMessageILPP.cs index a78a7e6..309e268 100644 --- a/Editor/CodeGen/INetworkMessageILPP.cs +++ b/Editor/CodeGen/INetworkMessageILPP.cs @@ -18,8 +18,7 @@ namespace Unity.Netcode.Editor.CodeGen public override ILPPInterface GetInstance() => this; public override bool WillProcess(ICompiledAssembly compiledAssembly) => - compiledAssembly.Name == CodeGenHelpers.RuntimeAssemblyName || - compiledAssembly.References.Any(filePath => Path.GetFileNameWithoutExtension(filePath) == CodeGenHelpers.RuntimeAssemblyName); + compiledAssembly.Name == CodeGenHelpers.RuntimeAssemblyName; private readonly List m_Diagnostics = new List(); diff --git a/Runtime/Core/NetworkBehaviour.cs b/Runtime/Core/NetworkBehaviour.cs index d2ec920..6f96f03 100644 --- a/Runtime/Core/NetworkBehaviour.cs +++ b/Runtime/Core/NetworkBehaviour.cs @@ -331,7 +331,8 @@ namespace Unity.Netcode // in Update and/or in FixedUpdate could still be checking NetworkBehaviour.NetworkObject directly (i.e. does it exist?) // or NetworkBehaviour.IsSpawned (i.e. to early exit if not spawned) which, in turn, could generate several Warning messages // per spawned NetworkObject. Checking for ShutdownInProgress prevents these unnecessary LogWarning messages. - if (m_NetworkObject == null && (NetworkManager.Singleton == null || !NetworkManager.Singleton.ShutdownInProgress)) + // We must check IsSpawned, otherwise a warning will be logged under certain valid conditions (see OnDestroy) + if (IsSpawned && m_NetworkObject == null && (NetworkManager.Singleton == null || !NetworkManager.Singleton.ShutdownInProgress)) { if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) { @@ -582,9 +583,11 @@ namespace Unity.Netcode { NetworkVariableFields[NetworkVariableIndexesToReset[i]].ResetDirty(); } + + MarkVariablesDirty(false); } - internal void VariableUpdate(ulong targetClientId) + internal void PreVariableUpdate() { if (!m_VarInit) { @@ -592,6 +595,10 @@ namespace Unity.Netcode } PreNetworkVariableWrite(); + } + + internal void VariableUpdate(ulong targetClientId) + { NetworkVariableUpdate(targetClientId, NetworkBehaviourId); } @@ -663,11 +670,11 @@ namespace Unity.Netcode return false; } - internal void MarkVariablesDirty() + internal void MarkVariablesDirty(bool dirty) { for (int j = 0; j < NetworkVariableFields.Count; j++) { - NetworkVariableFields[j].SetDirty(true); + NetworkVariableFields[j].SetDirty(dirty); } } @@ -759,6 +766,14 @@ namespace Unity.Netcode /// public virtual void OnDestroy() { + if (NetworkObject != null && NetworkObject.IsSpawned && IsSpawned) + { + // If the associated NetworkObject is still spawned then this + // NetworkBehaviour will be removed from the NetworkObject's + // ChildNetworkBehaviours list. + NetworkObject.OnNetworkBehaviourDestroyed(this); + } + // this seems odd to do here, but in fact especially in tests we can find ourselves // here without having called InitializedVariables, which causes problems if any // of those variables use native containers (e.g. NetworkList) as they won't be @@ -770,6 +785,7 @@ namespace Unity.Netcode InitializeVariables(); } + for (int i = 0; i < NetworkVariableFields.Count; i++) { NetworkVariableFields[i].Dispose(); diff --git a/Runtime/Core/NetworkBehaviourUpdater.cs b/Runtime/Core/NetworkBehaviourUpdater.cs index 56bdfdc..7e9176e 100644 --- a/Runtime/Core/NetworkBehaviourUpdater.cs +++ b/Runtime/Core/NetworkBehaviourUpdater.cs @@ -8,12 +8,17 @@ namespace Unity.Netcode /// public class NetworkBehaviourUpdater { - private HashSet m_Touched = new HashSet(); + private HashSet m_DirtyNetworkObjects = new HashSet(); #if DEVELOPMENT_BUILD || UNITY_EDITOR private ProfilerMarker m_NetworkBehaviourUpdate = new ProfilerMarker($"{nameof(NetworkBehaviour)}.{nameof(NetworkBehaviourUpdate)}"); #endif + internal void AddForUpdate(NetworkObject networkObject) + { + m_DirtyNetworkObjects.Add(networkObject); + } + internal void NetworkBehaviourUpdate(NetworkManager networkManager) { #if DEVELOPMENT_BUILD || UNITY_EDITOR @@ -23,57 +28,59 @@ namespace Unity.Netcode { if (networkManager.IsServer) { - m_Touched.Clear(); - for (int i = 0; i < networkManager.ConnectedClientsList.Count; i++) + foreach (var dirtyObj in m_DirtyNetworkObjects) { - var client = networkManager.ConnectedClientsList[i]; - var spawnedObjs = networkManager.SpawnManager.SpawnedObjectsList; - m_Touched.UnionWith(spawnedObjs); - foreach (var sobj in spawnedObjs) + for (int k = 0; k < dirtyObj.ChildNetworkBehaviours.Count; k++) { - if (sobj.IsNetworkVisibleTo(client.ClientId)) + dirtyObj.ChildNetworkBehaviours[k].PreVariableUpdate(); + } + + 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)) { // Sync just the variables for just the objects this client sees - for (int k = 0; k < sobj.ChildNetworkBehaviours.Count; k++) + for (int k = 0; k < dirtyObj.ChildNetworkBehaviours.Count; k++) { - sobj.ChildNetworkBehaviours[k].VariableUpdate(client.ClientId); + dirtyObj.ChildNetworkBehaviours[k].VariableUpdate(client.ClientId); } } } } - - // Now, reset all the no-longer-dirty variables - foreach (var sobj in m_Touched) - { - for (int k = 0; k < sobj.ChildNetworkBehaviours.Count; k++) - { - sobj.ChildNetworkBehaviours[k].PostNetworkVariableWrite(); - } - } } else { // when client updates the server, it tells it about all its objects - foreach (var sobj in networkManager.SpawnManager.SpawnedObjectsList) + foreach (var sobj in m_DirtyNetworkObjects) { if (sobj.IsOwner) { + for (int k = 0; k < sobj.ChildNetworkBehaviours.Count; k++) + { + sobj.ChildNetworkBehaviours[k].PreVariableUpdate(); + } for (int k = 0; k < sobj.ChildNetworkBehaviours.Count; k++) { sobj.ChildNetworkBehaviours[k].VariableUpdate(NetworkManager.ServerClientId); } } } - - // Now, reset all the no-longer-dirty variables - foreach (var sobj in networkManager.SpawnManager.SpawnedObjectsList) + } + // Now, reset all the no-longer-dirty variables + foreach (var dirtyobj in m_DirtyNetworkObjects) + { + for (int k = 0; k < dirtyobj.ChildNetworkBehaviours.Count; k++) { - for (int k = 0; k < sobj.ChildNetworkBehaviours.Count; k++) - { - sobj.ChildNetworkBehaviours[k].PostNetworkVariableWrite(); - } + dirtyobj.ChildNetworkBehaviours[k].PostNetworkVariableWrite(); } } + m_DirtyNetworkObjects.Clear(); } finally { diff --git a/Runtime/Core/NetworkManager.cs b/Runtime/Core/NetworkManager.cs index b2403fd..21c7082 100644 --- a/Runtime/Core/NetworkManager.cs +++ b/Runtime/Core/NetworkManager.cs @@ -54,7 +54,12 @@ namespace Unity.Netcode return $"{nameof(NetworkPrefab)} \"{networkPrefab.Prefab.gameObject.name}\""; } - internal NetworkBehaviourUpdater BehaviourUpdater { get; private set; } + internal NetworkBehaviourUpdater BehaviourUpdater { get; set; } + + internal void MarkNetworkObjectDirty(NetworkObject networkObject) + { + BehaviourUpdater.AddForUpdate(networkObject); + } internal MessagingSystem MessagingSystem { get; private set; } @@ -1384,6 +1389,19 @@ namespace Unity.Netcode } IsConnectedClient = false; + + // We need to clean up NetworkObjects before we reset the IsServer + // and IsClient properties. This provides consistency of these two + // property values for NetworkObjects that are still spawned when + // the shutdown cycle begins. + if (SpawnManager != null) + { + SpawnManager.DespawnAndDestroyNetworkObjects(); + SpawnManager.ServerResetShudownStateForSceneObjects(); + + SpawnManager = null; + } + IsServer = false; IsClient = false; @@ -1406,14 +1424,6 @@ namespace Unity.Netcode NetworkConfig.NetworkTransport.OnTransportEvent -= HandleRawTransportPoll; } - if (SpawnManager != null) - { - SpawnManager.DespawnAndDestroyNetworkObjects(); - SpawnManager.ServerResetShudownStateForSceneObjects(); - - SpawnManager = null; - } - if (DeferredMessageManager != null) { DeferredMessageManager.CleanupAllTriggers(); @@ -2060,6 +2070,20 @@ namespace Unity.Netcode SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, ownerClientId); + for (int index = 0; index < MessagingSystem.MessageHandlers.Length; index++) + { + if (MessagingSystem.MessageTypes[index] != null) + { + var orderingMessage = new OrderingMessage + { + Order = index, + Hash = XXHash.Hash32(MessagingSystem.MessageTypes[index].FullName) + }; + + SendMessage(ref orderingMessage, NetworkDelivery.ReliableFragmentedSequenced, ownerClientId); + } + } + // If scene management is enabled, then let NetworkSceneManager handle the initial scene and NetworkObject synchronization if (!NetworkConfig.EnableSceneManagement) { diff --git a/Runtime/Core/NetworkObject.cs b/Runtime/Core/NetworkObject.cs index 4b20130..0d889cb 100644 --- a/Runtime/Core/NetworkObject.cs +++ b/Runtime/Core/NetworkObject.cs @@ -235,9 +235,19 @@ namespace Unity.Netcode } /// - /// Shows a previously hidden to a client + /// Makes the previously hidden "netcode visible" to the targeted client. /// - /// The client to show the to + /// + /// Usage: Use to start sending updates for a previously hidden to the targeted client.
+ ///
+ /// Dynamically Spawned: s will be instantiated and spawned on the targeted client side.
+ /// In-Scene Placed: The instantiated but despawned s will be spawned on the targeted client side.
+ ///
+ /// See Also:
+ ///
+ /// or
+ ///
+ /// The targeted client public void NetworkShow(ulong clientId) { if (!IsSpawned) @@ -260,11 +270,22 @@ namespace Unity.Netcode NetworkManager.SpawnManager.SendSpawnCallForObject(clientId, this); } + /// - /// Shows a list of previously hidden s to a client + /// Makes a list of previously hidden s "netcode visible" for the client specified. /// - /// The s to show - /// The client to show the objects to + /// + /// Usage: Use to start sending updates for previously hidden s to the targeted client.
+ ///
+ /// Dynamically Spawned: s will be instantiated and spawned on the targeted client's side.
+ /// In-Scene Placed: Already instantiated but despawned s will be spawned on the targeted client's side.
+ ///
+ /// See Also:
+ ///
+ /// or
+ ///
+ /// The objects to become "netcode visible" to the targeted client + /// The targeted client public static void NetworkShow(List networkObjects, ulong clientId) { if (networkObjects == null || networkObjects.Count == 0) @@ -305,9 +326,19 @@ namespace Unity.Netcode } /// - /// Hides a object from a specific client + /// Hides the from the targeted client. /// - /// The client to hide the object for + /// + /// Usage: Use to stop sending updates to the targeted client, "netcode invisible", for a currently visible .
+ ///
+ /// Dynamically Spawned: s will be despawned and destroyed on the targeted client's side.
+ /// In-Scene Placed: s will only be despawned on the targeted client's side.
+ ///
+ /// See Also:
+ ///
+ /// or
+ ///
+ /// The targeted client public void NetworkHide(ulong clientId) { if (!IsSpawned) @@ -335,7 +366,7 @@ namespace Unity.Netcode var message = new DestroyObjectMessage { NetworkObjectId = NetworkObjectId, - DestroyGameObject = true + DestroyGameObject = !IsSceneObject.Value }; // Send destroy call var size = NetworkManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, clientId); @@ -343,10 +374,20 @@ namespace Unity.Netcode } /// - /// Hides a list of objects from a client + /// Hides a list of s from the targeted client. /// - /// The objects to hide - /// The client to hide the objects from + /// + /// Usage: Use to stop sending updates to the targeted client, "netcode invisible", for the currently visible s.
+ ///
+ /// Dynamically Spawned: s will be despawned and destroyed on the targeted client's side.
+ /// In-Scene Placed: s will only be despawned on the targeted client's side.
+ ///
+ /// See Also:
+ ///
+ /// or
+ ///
+ /// The s that will become "netcode invisible" to the targeted client + /// The targeted client public static void NetworkHide(List networkObjects, ulong clientId) { if (networkObjects == null || networkObjects.Count == 0) @@ -455,8 +496,8 @@ namespace Unity.Netcode /// /// Spawns a across the network and makes it the player object for the given client /// - /// The clientId whos player object this is - /// Should the object be destroyd when the scene is changed + /// The clientId who's player object this is + /// Should the object be destroyed when the scene is changed public void SpawnAsPlayerObject(ulong clientId, bool destroyWithScene = false) { SpawnInternal(destroyWithScene, clientId, true); @@ -512,7 +553,14 @@ namespace Unity.Netcode for (int i = 0; i < ChildNetworkBehaviours.Count; i++) { - ChildNetworkBehaviours[i].InternalOnGainedOwnership(); + if (ChildNetworkBehaviours[i].gameObject.activeInHierarchy) + { + ChildNetworkBehaviours[i].InternalOnGainedOwnership(); + } + else + { + Debug.LogWarning($"{ChildNetworkBehaviours[i].gameObject.name} is disabled! Netcode for GameObjects does not support disabled NetworkBehaviours! The {ChildNetworkBehaviours[i].GetType().Name} component was skipped during ownership assignment!"); + } } } @@ -697,7 +745,7 @@ namespace Unity.Netcode // For instance, if we're spawning NetworkObject 5 and its parent is 10, what should happen if we do not have 10 yet? // let's say 10 is on the way to be replicated in a few frames and we could fix that parent-child relationship later. // - // If you couldn't find your parent, we put you into OrphanChildren set and everytime we spawn another NetworkObject locally due to replication, + // If you couldn't find your parent, we put you into OrphanChildren set and every time we spawn another NetworkObject locally due to replication, // we call CheckOrphanChildren() method and quickly iterate over OrphanChildren set and see if we can reparent/adopt one. internal static HashSet OrphanChildren = new HashSet(); @@ -764,7 +812,14 @@ namespace Unity.Netcode for (int i = 0; i < ChildNetworkBehaviours.Count; i++) { - ChildNetworkBehaviours[i].InternalOnNetworkSpawn(); + if (ChildNetworkBehaviours[i].gameObject.activeInHierarchy) + { + ChildNetworkBehaviours[i].InternalOnNetworkSpawn(); + } + else + { + 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!"); + } } } @@ -813,12 +868,12 @@ namespace Unity.Netcode } } - internal void MarkVariablesDirty() + internal void MarkVariablesDirty(bool dirty) { for (int i = 0; i < ChildNetworkBehaviours.Count; i++) { var behavior = ChildNetworkBehaviours[i]; - behavior.MarkVariablesDirty(); + behavior.MarkVariablesDirty(dirty); } } @@ -1164,5 +1219,21 @@ namespace Unity.Netcode return GlobalObjectIdHash; } + + /// + /// Removes a NetworkBehaviour from the ChildNetworkBehaviours list when destroyed + /// while the NetworkObject is still spawned. + /// + internal void OnNetworkBehaviourDestroyed(NetworkBehaviour networkBehaviour) + { + if (networkBehaviour.IsSpawned && IsSpawned) + { + if (NetworkManager.LogLevel == LogLevel.Developer) + { + NetworkLog.LogWarning($"{nameof(NetworkBehaviour)}-{networkBehaviour.name} is being destroyed while {nameof(NetworkObject)}-{name} is still spawned! (could break state synchronization)"); + } + ChildNetworkBehaviours.Remove(networkBehaviour); + } + } } } diff --git a/Runtime/Messaging/Messages/OrderingMessage.cs b/Runtime/Messaging/Messages/OrderingMessage.cs new file mode 100644 index 0000000..6651e0e --- /dev/null +++ b/Runtime/Messaging/Messages/OrderingMessage.cs @@ -0,0 +1,50 @@ +using System; + +namespace Unity.Netcode +{ + /// + /// Upon connecting, the host sends a series of OrderingMessage to the client so that it can make sure both sides + /// have the same message types in the same positions in + /// - MessagingSystem.m_MessageHandlers + /// - MessagingSystem.m_ReverseTypeMap + /// even if one side has extra messages (compilation, version, patch, or platform differences, etc...) + /// + /// The ConnectionRequestedMessage, ConnectionApprovedMessage and OrderingMessage are prioritized at the beginning + /// of the mapping, to guarantee they can be exchanged before the two sides share their ordering + /// The sorting used in also stable so that even if MessageType names share hashes, it will work most of the time + /// + internal struct OrderingMessage : INetworkMessage + { + public int Order; + public uint Hash; + + public void Serialize(FastBufferWriter writer) + { + if (!writer.TryBeginWrite(FastBufferWriter.GetWriteSize(Order) + FastBufferWriter.GetWriteSize(Hash))) + { + throw new OverflowException($"Not enough space in the buffer to write {nameof(OrderingMessage)}"); + } + + writer.WriteValue(Order); + writer.WriteValue(Hash); + } + + public bool Deserialize(FastBufferReader reader, ref NetworkContext context) + { + if (!reader.TryBeginRead(FastBufferWriter.GetWriteSize(Order) + FastBufferWriter.GetWriteSize(Hash))) + { + throw new OverflowException($"Not enough data in the buffer to read {nameof(OrderingMessage)}"); + } + + reader.ReadValue(out Order); + reader.ReadValue(out Hash); + + return true; + } + + public void Handle(ref NetworkContext context) + { + ((NetworkManager)context.SystemOwner).MessagingSystem.ReorderMessage(Order, Hash); + } + } +} diff --git a/Tests/Runtime/NetworkAnimator/NetworkAnimatorTests.cs.meta b/Runtime/Messaging/Messages/OrderingMessage.cs.meta similarity index 83% rename from Tests/Runtime/NetworkAnimator/NetworkAnimatorTests.cs.meta rename to Runtime/Messaging/Messages/OrderingMessage.cs.meta index 19205b5..3a8bc03 100644 --- a/Tests/Runtime/NetworkAnimator/NetworkAnimatorTests.cs.meta +++ b/Runtime/Messaging/Messages/OrderingMessage.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: c2e5a740c1abd4315801e3f26ecf8adb +guid: 3ada9e8fd5bf94b1f9a6a21531c8a3ee MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Runtime/Messaging/MessagingSystem.cs b/Runtime/Messaging/MessagingSystem.cs index 4bec42e..82917a1 100644 --- a/Runtime/Messaging/MessagingSystem.cs +++ b/Runtime/Messaging/MessagingSystem.cs @@ -8,6 +8,11 @@ using UnityEngine; namespace Unity.Netcode { + internal class HandlerNotRegisteredException : SystemException + { + public HandlerNotRegisteredException() { } + public HandlerNotRegisteredException(string issue) : base(issue) { } + } internal class InvalidMessageStructureException : SystemException { @@ -44,8 +49,9 @@ namespace Unity.Netcode private NativeList m_IncomingMessageQueue = new NativeList(16, Allocator.Persistent); - private MessageHandler[] m_MessageHandlers = new MessageHandler[255]; - private Type[] m_ReverseTypeMap = new Type[255]; + // These array will grow as we need more message handlers. 4 is just a starting size. + private MessageHandler[] m_MessageHandlers = new MessageHandler[4]; + private Type[] m_ReverseTypeMap = new Type[4]; private Dictionary m_MessageTypes = new Dictionary(); private Dictionary> m_SendQueues = new Dictionary>(); @@ -59,6 +65,7 @@ namespace Unity.Netcode internal Type[] MessageTypes => m_ReverseTypeMap; internal MessageHandler[] MessageHandlers => m_MessageHandlers; + internal uint MessageHandlerCount => m_HighMessageType; internal uint GetMessageType(Type t) @@ -75,6 +82,35 @@ namespace Unity.Netcode public MessageHandler Handler; } + internal List PrioritizeMessageOrder(List allowedTypes) + { + var prioritizedTypes = new List(); + + // first pass puts the priority message in the first indices + // Those are the messages that must be delivered in order to allow re-ordering the others later + foreach (var t in allowedTypes) + { + if (t.MessageType.FullName == "Unity.Netcode.ConnectionRequestMessage" || + t.MessageType.FullName == "Unity.Netcode.ConnectionApprovedMessage" || + t.MessageType.FullName == "Unity.Netcode.OrderingMessage") + { + prioritizedTypes.Add(t); + } + } + + foreach (var t in allowedTypes) + { + if (t.MessageType.FullName != "Unity.Netcode.ConnectionRequestMessage" && + t.MessageType.FullName != "Unity.Netcode.ConnectionApprovedMessage" && + t.MessageType.FullName != "Unity.Netcode.OrderingMessage") + { + prioritizedTypes.Add(t); + } + } + + return prioritizedTypes; + } + public MessagingSystem(IMessageSender messageSender, object owner, IMessageProvider provider = null) { try @@ -89,6 +125,7 @@ namespace Unity.Netcode var allowedTypes = provider.GetMessages(); allowedTypes.Sort((a, b) => string.CompareOrdinal(a.MessageType.FullName, b.MessageType.FullName)); + allowedTypes = PrioritizeMessageOrder(allowedTypes); foreach (var type in allowedTypes) { RegisterMessageType(type); @@ -143,6 +180,13 @@ namespace Unity.Netcode private void RegisterMessageType(MessageWithHandler messageWithHandler) { + // if we are out of space, perform amortized linear growth + if (m_HighMessageType == m_MessageHandlers.Length) + { + Array.Resize(ref m_MessageHandlers, 2 * m_MessageHandlers.Length); + Array.Resize(ref m_ReverseTypeMap, 2 * m_ReverseTypeMap.Length); + } + m_MessageHandlers[m_HighMessageType] = messageWithHandler.Handler; m_ReverseTypeMap[m_HighMessageType] = messageWithHandler.MessageType; m_MessageTypes[messageWithHandler.MessageType] = m_HighMessageType++; @@ -226,6 +270,70 @@ namespace Unity.Netcode return true; } + // Moves the handler for the type having hash `targetHash` to the `desiredOrder` position, in the handler list + // This allows the server to tell the client which id it is using for which message and make sure the right + // message is used when deserializing. + internal void ReorderMessage(int desiredOrder, uint targetHash) + { + if (desiredOrder < 0) + { + throw new ArgumentException("ReorderMessage desiredOrder must be positive"); + } + + if (desiredOrder < m_ReverseTypeMap.Length && + XXHash.Hash32(m_ReverseTypeMap[desiredOrder].FullName) == targetHash) + { + // matching positions and hashes. All good. + return; + } + + Debug.Log($"Unexpected hash for {desiredOrder}"); + + // Since the message at `desiredOrder` is not the expected one, + // insert an empty placeholder and move the messages down + var typesAsList = new List(m_ReverseTypeMap); + + typesAsList.Insert(desiredOrder, null); + var handlersAsList = new List(m_MessageHandlers); + handlersAsList.Insert(desiredOrder, null); + + // we added a dummy message, bump the end up + m_HighMessageType++; + + // Here, we rely on the server telling us about all messages, in order. + // So, we know the handlers before desiredOrder are correct. + // We start at desiredOrder to not shift them when we insert. + int position = desiredOrder; + bool found = false; + while (position < typesAsList.Count) + { + if (typesAsList[position] != null && + XXHash.Hash32(typesAsList[position].FullName) == targetHash) + { + found = true; + break; + } + + position++; + } + + if (found) + { + // Copy the handler and type to the right index + + typesAsList[desiredOrder] = typesAsList[position]; + handlersAsList[desiredOrder] = handlersAsList[position]; + typesAsList.RemoveAt(position); + handlersAsList.RemoveAt(position); + + // we removed a copy after moving a message, reduce the high message index + m_HighMessageType--; + } + + m_ReverseTypeMap = typesAsList.ToArray(); + m_MessageHandlers = handlersAsList.ToArray(); + } + public void HandleMessage(in MessageHeader header, FastBufferReader reader, ulong senderId, float timestamp, int serializedHeaderSize) { if (header.MessageType >= m_HighMessageType) @@ -259,18 +367,29 @@ namespace Unity.Netcode var handler = m_MessageHandlers[header.MessageType]; using (reader) { - // No user-land message handler exceptions should escape the receive loop. - // If an exception is throw, the message is ignored. - // Example use case: A bad message is received that can't be deserialized and throws - // an OverflowException because it specifies a length greater than the number of bytes in it - // for some dynamic-length value. - try + // This will also log an exception is if the server knows about a message type the client doesn't know + // about. In this case the handler will be null. It is still an issue the user must deal with: If the + // two connecting builds know about different messages, the server should not send a message to a client + // that doesn't know about it + if (handler == null) { - handler.Invoke(reader, ref context, this); + Debug.LogException(new HandlerNotRegisteredException(header.MessageType.ToString())); } - catch (Exception e) + else { - Debug.LogException(e); + // No user-land message handler exceptions should escape the receive loop. + // If an exception is throw, the message is ignored. + // Example use case: A bad message is received that can't be deserialized and throws + // an OverflowException because it specifies a length greater than the number of bytes in it + // for some dynamic-length value. + try + { + handler.Invoke(reader, ref context, this); + } + catch (Exception e) + { + Debug.LogException(e); + } } } for (var hookIdx = 0; hookIdx < m_Hooks.Count; ++hookIdx) diff --git a/Runtime/NetworkVariable/Collections/NetworkList.cs b/Runtime/NetworkVariable/Collections/NetworkList.cs index 7fa8628..8f69b77 100644 --- a/Runtime/NetworkVariable/Collections/NetworkList.cs +++ b/Runtime/NetworkVariable/Collections/NetworkList.cs @@ -63,6 +63,11 @@ namespace Unity.Netcode return base.IsDirty() || m_DirtyEvents.Length > 0; } + internal void MarkNetworkObjectDirty() + { + m_NetworkBehaviour.NetworkManager.MarkNetworkObjectDirty(m_NetworkBehaviour.NetworkObject); + } + /// public override void WriteDelta(FastBufferWriter writer) { @@ -122,10 +127,26 @@ namespace Unity.Netcode /// public override void WriteField(FastBufferWriter writer) { - writer.WriteValueSafe((ushort)m_ListAtLastReset.Length); - for (int i = 0; i < m_ListAtLastReset.Length; i++) + // 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) { - NetworkVariableSerialization.Write(writer, ref m_ListAtLastReset.ElementAt(i)); + 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++) + { + NetworkVariableSerialization.Write(writer, ref m_List.ElementAt(i)); + } } } @@ -173,6 +194,7 @@ namespace Unity.Netcode Index = m_List.Length - 1, Value = m_List[m_List.Length - 1] }); + MarkNetworkObjectDirty(); } } break; @@ -180,8 +202,16 @@ namespace Unity.Netcode { reader.ReadValueSafe(out int index); NetworkVariableSerialization.Read(reader, out T value); - m_List.InsertRangeWithBeginEnd(index, index + 1); - m_List[index] = value; + + if (index < m_List.Length) + { + m_List.InsertRangeWithBeginEnd(index, index + 1); + m_List[index] = value; + } + else + { + m_List.Add(value); + } if (OnListChanged != null) { @@ -201,6 +231,7 @@ namespace Unity.Netcode Index = index, Value = m_List[index] }); + MarkNetworkObjectDirty(); } } break; @@ -233,6 +264,7 @@ namespace Unity.Netcode Index = index, Value = value }); + MarkNetworkObjectDirty(); } } break; @@ -260,6 +292,7 @@ namespace Unity.Netcode Index = index, Value = value }); + MarkNetworkObjectDirty(); } } break; @@ -295,6 +328,7 @@ namespace Unity.Netcode Value = value, PreviousValue = previousValue }); + MarkNetworkObjectDirty(); } } break; @@ -317,6 +351,7 @@ namespace Unity.Netcode { Type = eventType }); + MarkNetworkObjectDirty(); } } break; @@ -403,8 +438,15 @@ namespace Unity.Netcode /// public void Insert(int index, T item) { - m_List.InsertRangeWithBeginEnd(index, index + 1); - m_List[index] = item; + if (index < m_List.Length) + { + m_List.InsertRangeWithBeginEnd(index, index + 1); + m_List[index] = item; + } + else + { + m_List.Add(item); + } var listEvent = new NetworkListEvent() { @@ -436,13 +478,15 @@ namespace Unity.Netcode get => m_List[index]; set { + var previousValue = m_List[index]; m_List[index] = value; var listEvent = new NetworkListEvent() { Type = NetworkListEvent.EventType.Value, Index = index, - Value = value + Value = value, + PreviousValue = previousValue }; HandleAddListEvent(listEvent); @@ -452,6 +496,7 @@ namespace Unity.Netcode private void HandleAddListEvent(NetworkListEvent listEvent) { m_DirtyEvents.Add(listEvent); + MarkNetworkObjectDirty(); OnListChanged?.Invoke(listEvent); } diff --git a/Runtime/NetworkVariable/NetworkVariable.cs b/Runtime/NetworkVariable/NetworkVariable.cs index d32a80f..698fe01 100644 --- a/Runtime/NetworkVariable/NetworkVariable.cs +++ b/Runtime/NetworkVariable/NetworkVariable.cs @@ -87,7 +87,7 @@ namespace Unity.Netcode /// the new value of type `T` to be set/> private protected void Set(T value) { - m_IsDirty = true; + SetDirty(true); T previousValue = m_InternalValue; m_InternalValue = value; OnValueChanged?.Invoke(previousValue, m_InternalValue); @@ -119,7 +119,7 @@ namespace Unity.Netcode if (keepDirtyDelta) { - m_IsDirty = true; + SetDirty(true); } OnValueChanged?.Invoke(previousValue, m_InternalValue); diff --git a/Runtime/NetworkVariable/NetworkVariableBase.cs b/Runtime/NetworkVariable/NetworkVariableBase.cs index a35e31e..4b8ea84 100644 --- a/Runtime/NetworkVariable/NetworkVariableBase.cs +++ b/Runtime/NetworkVariable/NetworkVariableBase.cs @@ -54,7 +54,7 @@ namespace Unity.Netcode /// The property is used to determine if the /// value of the `NetworkVariable` has changed. /// - private protected bool m_IsDirty; + private bool m_IsDirty; /// /// Gets or sets the name of the network variable's instance @@ -79,6 +79,10 @@ namespace Unity.Netcode public virtual void SetDirty(bool isDirty) { m_IsDirty = isDirty; + if (m_IsDirty && m_NetworkBehaviour != null) + { + m_NetworkBehaviour.NetworkManager.MarkNetworkObjectDirty(m_NetworkBehaviour.NetworkObject); + } } /// @@ -111,7 +115,7 @@ namespace Unity.Netcode case NetworkVariableReadPermission.Everyone: return true; case NetworkVariableReadPermission.Owner: - return clientId == m_NetworkBehaviour.NetworkObject.OwnerClientId; + return clientId == m_NetworkBehaviour.NetworkObject.OwnerClientId || NetworkManager.ServerClientId == clientId; } } diff --git a/Runtime/Serialization/BitReader.cs b/Runtime/Serialization/BitReader.cs index 313e0c4..dcb30a9 100644 --- a/Runtime/Serialization/BitReader.cs +++ b/Runtime/Serialization/BitReader.cs @@ -21,6 +21,8 @@ namespace Unity.Netcode private const int k_BitsPerByte = 8; + private int BytePosition => m_BitPosition >> 3; + /// /// Whether or not the current BitPosition is evenly divisible by 8. I.e. whether or not the BitPosition is at a byte boundary. /// @@ -98,11 +100,6 @@ namespace Unity.Netcode throw new ArgumentOutOfRangeException(nameof(bitCount), "Cannot read more than 64 bits from a 64-bit value!"); } - if (bitCount < 0) - { - throw new ArgumentOutOfRangeException(nameof(bitCount), "Cannot read fewer than 0 bits!"); - } - int checkPos = (int)(m_BitPosition + bitCount); if (checkPos > m_AllowedBitwiseReadMark) { @@ -165,7 +162,7 @@ namespace Unity.Netcode #endif int offset = m_BitPosition & 7; - int pos = m_BitPosition >> 3; + int pos = BytePosition; bit = (m_BufferPointer[pos] & (1 << offset)) != 0; ++m_BitPosition; } @@ -175,7 +172,7 @@ namespace Unity.Netcode { var val = new T(); byte* ptr = ((byte*)&val) + offsetBytes; - byte* bufferPointer = m_BufferPointer + m_Position; + byte* bufferPointer = m_BufferPointer + BytePosition; UnsafeUtility.MemCpy(ptr, bufferPointer, bytesToRead); m_BitPosition += bytesToRead * k_BitsPerByte; diff --git a/Runtime/Serialization/BitWriter.cs b/Runtime/Serialization/BitWriter.cs index 842522a..9eacd56 100644 --- a/Runtime/Serialization/BitWriter.cs +++ b/Runtime/Serialization/BitWriter.cs @@ -29,6 +29,8 @@ namespace Unity.Netcode get => (m_BitPosition & 7) == 0; } + private int BytePosition => m_BitPosition >> 3; + internal unsafe BitWriter(FastBufferWriter writer) { m_Writer = writer; @@ -181,7 +183,7 @@ namespace Unity.Netcode #endif int offset = m_BitPosition & 7; - int pos = m_BitPosition >> 3; + int pos = BytePosition; ++m_BitPosition; m_BufferPointer[pos] = (byte)(bit ? (m_BufferPointer[pos] & ~(1 << offset)) | (1 << offset) : (m_BufferPointer[pos] & ~(1 << offset))); } @@ -190,7 +192,7 @@ namespace Unity.Netcode private unsafe void WritePartialValue(T value, int bytesToWrite, int offsetBytes = 0) where T : unmanaged { byte* ptr = ((byte*)&value) + offsetBytes; - byte* bufferPointer = m_BufferPointer + m_Position; + byte* bufferPointer = m_BufferPointer + BytePosition; UnsafeUtility.MemCpy(bufferPointer, ptr, bytesToWrite); m_BitPosition += bytesToWrite * k_BitsPerByte; diff --git a/Runtime/Spawning/NetworkSpawnManager.cs b/Runtime/Spawning/NetworkSpawnManager.cs index 0a24169..3300af9 100644 --- a/Runtime/Spawning/NetworkSpawnManager.cs +++ b/Runtime/Spawning/NetworkSpawnManager.cs @@ -576,7 +576,7 @@ namespace Unity.Netcode var size = NetworkManager.SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, clientId); NetworkManager.NetworkMetrics.TrackObjectSpawnSent(clientId, networkObject, size); - networkObject.MarkVariablesDirty(); + networkObject.MarkVariablesDirty(true); } internal ulong? GetSpawnParentId(NetworkObject networkObject) diff --git a/Runtime/Transports/UTP/UnityTransport.cs b/Runtime/Transports/UTP/UnityTransport.cs index ad2787f..5a06191 100644 --- a/Runtime/Transports/UTP/UnityTransport.cs +++ b/Runtime/Transports/UTP/UnityTransport.cs @@ -158,11 +158,11 @@ namespace Unity.Netcode.Transports.UTP set => m_MaxPacketQueueSize = value; } - [Tooltip("The maximum size of a payload that can be handled by the transport.")] + [Tooltip("The maximum size of an unreliable payload that can be handled by the transport.")] [SerializeField] private int m_MaxPayloadSize = InitialMaxPayloadSize; - /// The maximum size of a payload that can be handled by the transport. + /// The maximum size of an unreliable payload that can be handled by the transport. public int MaxPayloadSize { get => m_MaxPayloadSize; @@ -1148,14 +1148,14 @@ namespace Unity.Netcode.Transports.UTP /// The delivery type (QoS) to send data with public override void Send(ulong clientId, ArraySegment payload, NetworkDelivery networkDelivery) { - if (payload.Count > m_MaxPayloadSize) + var pipeline = SelectSendPipeline(networkDelivery); + + if (pipeline != m_ReliableSequencedPipeline && payload.Count > m_MaxPayloadSize) { - Debug.LogError($"Payload of size {payload.Count} larger than configured 'Max Payload Size' ({m_MaxPayloadSize})."); + Debug.LogError($"Unreliable payload of size {payload.Count} larger than configured 'Max Payload Size' ({m_MaxPayloadSize})."); return; } - var pipeline = SelectSendPipeline(networkDelivery); - var sendTarget = new SendTarget(clientId, pipeline); if (!m_SendQueue.TryGetValue(sendTarget, out var queue)) { @@ -1285,10 +1285,10 @@ namespace Unity.Netcode.Transports.UTP SendBatchedMessages(kvp.Key, kvp.Value); } - // The above flush only puts the message in UTP internal buffers, need the flush send - // job to execute to actually get things out on the wire. This will also ensure any - // disconnect messages are sent out. - m_Driver.ScheduleFlushSend(default).Complete(); + // The above flush only puts the message in UTP internal buffers, need an update to + // actually get the messages on the wire. (Normally a flush send would be sufficient, + // but there might be disconnect messages and those require an update call.) + m_Driver.ScheduleUpdate().Complete(); DisposeInternals(); @@ -1325,10 +1325,8 @@ namespace Unity.Netcode.Transports.UTP #if MULTIPLAYER_TOOLS_1_0_0_PRE_7 NetworkPipelineStageCollection.RegisterPipelineStage(new NetworkMetricsPipelineStage()); #endif - var maxFrameTimeMS = 0; #if UNITY_EDITOR || DEVELOPMENT_BUILD - maxFrameTimeMS = 100; ConfigureSimulator(); #endif @@ -1336,8 +1334,7 @@ namespace Unity.Netcode.Transports.UTP maxConnectAttempts: transport.m_MaxConnectAttempts, connectTimeoutMS: transport.m_ConnectTimeoutMS, disconnectTimeoutMS: transport.m_DisconnectTimeoutMS, - heartbeatTimeoutMS: transport.m_HeartbeatTimeoutMS, - maxFrameTimeMS: maxFrameTimeMS); + heartbeatTimeoutMS: transport.m_HeartbeatTimeoutMS); driver = NetworkDriver.Create(m_NetworkSettings); diff --git a/TestHelpers/Runtime/NetcodeIntegrationTest.cs b/TestHelpers/Runtime/NetcodeIntegrationTest.cs index 4dbcf1a..8ab5617 100644 --- a/TestHelpers/Runtime/NetcodeIntegrationTest.cs +++ b/TestHelpers/Runtime/NetcodeIntegrationTest.cs @@ -869,12 +869,14 @@ namespace Unity.Netcode.TestHelpers.Runtime /// Constructor that allows you To break tests up as a host /// and a server. /// Example: Decorate your child derived class with TestFixture - /// and then create a constructor at the child level + /// and then create a constructor at the child level. + /// Don't forget to set your constructor public, else Unity will + /// give you a hard to decipher error /// [TestFixture(HostOrServer.Host)] /// [TestFixture(HostOrServer.Server)] /// public class MyChildClass : NetcodeIntegrationTest /// { - /// MyChildClass(HostOrServer hostOrServer) : base(hostOrServer) { } + /// public MyChildClass(HostOrServer hostOrServer) : base(hostOrServer) { } /// } /// /// diff --git a/TestHelpers/Runtime/com.unity.netcode.testhelpers.runtime.asmdef b/TestHelpers/Runtime/com.unity.netcode.testhelpers.runtime.asmdef index 03c978b..2fb107c 100644 --- a/TestHelpers/Runtime/com.unity.netcode.testhelpers.runtime.asmdef +++ b/TestHelpers/Runtime/com.unity.netcode.testhelpers.runtime.asmdef @@ -11,6 +11,9 @@ "optionalUnityReferences": [ "TestAssemblies" ], + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], "versionDefines": [ { "name": "com.unity.multiplayer.tools", diff --git a/Tests/Editor/Build/BuildTests.cs b/Tests/Editor/Build/BuildTests.cs index 66a2b13..c8cde89 100644 --- a/Tests/Editor/Build/BuildTests.cs +++ b/Tests/Editor/Build/BuildTests.cs @@ -4,7 +4,6 @@ using NUnit.Framework; using UnityEditor; using UnityEditor.Build.Reporting; using UnityEngine; -using UnityEngine.TestTools; namespace Unity.Netcode.EditorTests { @@ -21,20 +20,20 @@ namespace Unity.Netcode.EditorTests var buildTargetGroup = BuildPipeline.GetBuildTargetGroup(buildTarget); var buildTargetSupported = BuildPipeline.IsBuildTargetSupported(buildTargetGroup, buildTarget); - var buildReport = BuildPipeline.BuildPlayer( - new[] { Path.Combine(packagePath, DefaultBuildScenePath) }, - Path.Combine(Path.GetDirectoryName(Application.dataPath), "Builds", nameof(BuildTests)), - buildTarget, - BuildOptions.None - ); - if (buildTargetSupported) { + var buildReport = BuildPipeline.BuildPlayer( + new[] { Path.Combine(packagePath, DefaultBuildScenePath) }, + Path.Combine(Path.GetDirectoryName(Application.dataPath), "Builds", nameof(BuildTests)), + buildTarget, + BuildOptions.None + ); + Assert.AreEqual(BuildResult.Succeeded, buildReport.summary.result); } else { - LogAssert.Expect(LogType.Error, "Error building player because build target was unsupported"); + Debug.Log($"Skipped building player due to Unsupported Build Target"); } } } diff --git a/Tests/Editor/Messaging/MessageRegistrationTests.cs b/Tests/Editor/Messaging/MessageRegistrationTests.cs index b606fd5..1228712 100644 --- a/Tests/Editor/Messaging/MessageRegistrationTests.cs +++ b/Tests/Editor/Messaging/MessageRegistrationTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using NUnit.Framework; @@ -179,5 +180,122 @@ namespace Unity.Netcode.EditorTests Assert.AreEqual(handlerFour, systemThree.MessageHandlers[systemThree.GetMessageType(typeof(TestMessageFour))]); } } + + internal class AAAEarlyLexicographicNetworkMessage : INetworkMessage + { + public void Serialize(FastBufferWriter writer) + { + } + + public bool Deserialize(FastBufferReader reader, ref NetworkContext context) + { + return true; + } + + public void Handle(ref NetworkContext context) + { + } + } + +#pragma warning disable IDE1006 + internal class zzzLateLexicographicNetworkMessage : AAAEarlyLexicographicNetworkMessage + { + } +#pragma warning restore IDE1006 + + internal class OrderingMessageProvider : IMessageProvider + { + public List GetMessages() + { + var listMessages = new List(); + + var messageWithHandler = new MessagingSystem.MessageWithHandler(); + + messageWithHandler.MessageType = typeof(zzzLateLexicographicNetworkMessage); + listMessages.Add(messageWithHandler); + + messageWithHandler.MessageType = typeof(ConnectionRequestMessage); + listMessages.Add(messageWithHandler); + + messageWithHandler.MessageType = typeof(ConnectionApprovedMessage); + listMessages.Add(messageWithHandler); + + messageWithHandler.MessageType = typeof(OrderingMessage); + listMessages.Add(messageWithHandler); + + messageWithHandler.MessageType = typeof(AAAEarlyLexicographicNetworkMessage); + listMessages.Add(messageWithHandler); + + return listMessages; + } + } + + [Test] + public void MessagesGetPrioritizedCorrectly() + { + var sender = new NopMessageSender(); + var provider = new OrderingMessageProvider(); + var messagingSystem = new MessagingSystem(sender, null, provider); + + // the 3 priority messages should appear first, in lexicographic order + Assert.AreEqual(messagingSystem.MessageTypes[0], typeof(ConnectionApprovedMessage)); + Assert.AreEqual(messagingSystem.MessageTypes[1], typeof(ConnectionRequestMessage)); + Assert.AreEqual(messagingSystem.MessageTypes[2], typeof(OrderingMessage)); + + // the other should follow after + Assert.AreEqual(messagingSystem.MessageTypes[3], typeof(AAAEarlyLexicographicNetworkMessage)); + Assert.AreEqual(messagingSystem.MessageTypes[4], typeof(zzzLateLexicographicNetworkMessage)); + + // there should not be any extras + Assert.AreEqual(messagingSystem.MessageHandlerCount, 5); + + // reorder the zzz one to position 3 + messagingSystem.ReorderMessage(3, XXHash.Hash32(typeof(zzzLateLexicographicNetworkMessage).FullName)); + + // the 3 priority messages should still appear first, in lexicographic order + Assert.AreEqual(messagingSystem.MessageTypes[0], typeof(ConnectionApprovedMessage)); + Assert.AreEqual(messagingSystem.MessageTypes[1], typeof(ConnectionRequestMessage)); + Assert.AreEqual(messagingSystem.MessageTypes[2], typeof(OrderingMessage)); + + // the other should follow after, but reordered + Assert.AreEqual(messagingSystem.MessageTypes[3], typeof(zzzLateLexicographicNetworkMessage)); + Assert.AreEqual(messagingSystem.MessageTypes[4], typeof(AAAEarlyLexicographicNetworkMessage)); + + // there should still not be any extras + Assert.AreEqual(messagingSystem.MessageHandlerCount, 5); + + // verify we get an exception when asking for an invalid position + try + { + messagingSystem.ReorderMessage(-1, XXHash.Hash32(typeof(zzzLateLexicographicNetworkMessage).FullName)); + Assert.Fail(); + } + catch (ArgumentException) + { + } + + // reorder the zzz one to position 3, again, to check nothing bad happens + messagingSystem.ReorderMessage(3, XXHash.Hash32(typeof(zzzLateLexicographicNetworkMessage).FullName)); + + // the two non-priority should not have moved + Assert.AreEqual(messagingSystem.MessageTypes[3], typeof(zzzLateLexicographicNetworkMessage)); + Assert.AreEqual(messagingSystem.MessageTypes[4], typeof(AAAEarlyLexicographicNetworkMessage)); + + // there should still not be any extras + Assert.AreEqual(messagingSystem.MessageHandlerCount, 5); + + // 4242 is a random hash that should not match anything + messagingSystem.ReorderMessage(3, 4242); + + // that should result in an extra entry + Assert.AreEqual(messagingSystem.MessageHandlerCount, 6); + + // with a null handler + Assert.AreEqual(messagingSystem.MessageHandlers[3], null); + + // and it should have bumped the previous messages down + Assert.AreEqual(messagingSystem.MessageTypes[4], typeof(zzzLateLexicographicNetworkMessage)); + Assert.AreEqual(messagingSystem.MessageTypes[5], typeof(AAAEarlyLexicographicNetworkMessage)); + } } } diff --git a/Tests/Editor/Messaging/MessageSendingTests.cs b/Tests/Editor/Messaging/MessageSendingTests.cs index dd1d3eb..5fce803 100644 --- a/Tests/Editor/Messaging/MessageSendingTests.cs +++ b/Tests/Editor/Messaging/MessageSendingTests.cs @@ -1,8 +1,12 @@ using System; using System.Collections.Generic; +using System.Text.RegularExpressions; using NUnit.Framework; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; +using UnityEngine; +using UnityEngine.TestTools; +using Random = System.Random; namespace Unity.Netcode.EditorTests { @@ -40,11 +44,24 @@ namespace Unity.Netcode.EditorTests } } - private class TestMessageProvider : IMessageProvider + private class TestMessageProvider : IMessageProvider, IDisposable { + // Keep track of what we sent + private List> m_CachedMessages = new List>(); + + public void Dispose() + { + foreach (var cachedItem in m_CachedMessages) + { + // Clear out any references to MessagingSystem.MessageWithHandlers + cachedItem.Clear(); + } + m_CachedMessages.Clear(); + } + public List GetMessages() { - return new List + var messageList = new List { new MessagingSystem.MessageWithHandler { @@ -52,9 +69,13 @@ namespace Unity.Netcode.EditorTests Handler = MessagingSystem.ReceiveMessage } }; + // Track messages sent + m_CachedMessages.Add(messageList); + return messageList; } } + private TestMessageProvider m_TestMessageProvider; private TestMessageSender m_MessageSender; private MessagingSystem m_MessagingSystem; private ulong[] m_Clients = { 0 }; @@ -63,15 +84,16 @@ namespace Unity.Netcode.EditorTests public void SetUp() { TestMessage.Serialized = false; - m_MessageSender = new TestMessageSender(); - m_MessagingSystem = new MessagingSystem(m_MessageSender, this, new TestMessageProvider()); + m_TestMessageProvider = new TestMessageProvider(); + m_MessagingSystem = new MessagingSystem(m_MessageSender, this, m_TestMessageProvider); m_MessagingSystem.ClientConnected(0); } [TearDown] public void TearDown() { + m_TestMessageProvider.Dispose(); m_MessagingSystem.Dispose(); } @@ -224,5 +246,56 @@ namespace Unity.Netcode.EditorTests Assert.AreEqual(message2, receivedMessage2); } } + + private class TestNoHandlerMessageProvider : IMessageProvider + { + public List GetMessages() + { + return new List + { + new MessagingSystem.MessageWithHandler + { + MessageType = typeof(TestMessage), + Handler = null + } + }; + } + } + + [Test] + public void WhenReceivingAMessageWithoutAHandler_ExceptionIsLogged() + { + // If a MessagingSystem already exists then dispose of it before creating a new MessagingSystem (otherwise memory leak) + if (m_MessagingSystem != null) + { + m_MessagingSystem.Dispose(); + m_MessagingSystem = null; + } + + // Since m_MessagingSystem is disposed during teardown we don't need to worry about that here. + m_MessagingSystem = new MessagingSystem(new NopMessageSender(), this, new TestNoHandlerMessageProvider()); + m_MessagingSystem.ClientConnected(0); + + var messageHeader = new MessageHeader + { + MessageSize = (ushort)UnsafeUtility.SizeOf(), + MessageType = m_MessagingSystem.GetMessageType(typeof(TestMessage)), + }; + var message = GetMessage(); + + var writer = new FastBufferWriter(1300, Allocator.Temp); + using (writer) + { + writer.TryBeginWrite(FastBufferWriter.GetWriteSize(message)); + writer.WriteValue(message); + + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + m_MessagingSystem.HandleMessage(messageHeader, reader, 0, 0, 0); + LogAssert.Expect(LogType.Exception, new Regex(".*HandlerNotRegisteredException.*")); + } + } + } } } diff --git a/Tests/Editor/NetworkVar/NetworkVarTests.cs b/Tests/Editor/NetworkVar/NetworkVarTests.cs index 61bc147..e871b23 100644 --- a/Tests/Editor/NetworkVar/NetworkVarTests.cs +++ b/Tests/Editor/NetworkVar/NetworkVarTests.cs @@ -1,41 +1,55 @@ using NUnit.Framework; +using UnityEngine; namespace Unity.Netcode.EditorTests.NetworkVar { public class NetworkVarTests { + public class NetworkVarComponent : NetworkBehaviour + { + public NetworkVariable NetworkVariable = new NetworkVariable(); + } [Test] public void TestAssignmentUnchanged() { - var intVar = new NetworkVariable(); - - intVar.Value = 314159265; - - intVar.OnValueChanged += (value, newValue) => + var gameObjectMan = new GameObject(); + var networkManager = gameObjectMan.AddComponent(); + networkManager.BehaviourUpdater = new NetworkBehaviourUpdater(); + var gameObject = new GameObject(); + var networkObject = gameObject.AddComponent(); + networkObject.NetworkManagerOwner = networkManager; + var networkVarComponent = gameObject.AddComponent(); + networkVarComponent.NetworkVariable.Initialize(networkVarComponent); + networkVarComponent.NetworkVariable.Value = 314159265; + networkVarComponent.NetworkVariable.OnValueChanged += (value, newValue) => { Assert.Fail("OnValueChanged was invoked when setting the same value"); }; - - intVar.Value = 314159265; + networkVarComponent.NetworkVariable.Value = 314159265; + Object.DestroyImmediate(gameObject); + Object.DestroyImmediate(gameObjectMan); } - [Test] public void TestAssignmentChanged() { - var intVar = new NetworkVariable(); - - intVar.Value = 314159265; - + var gameObjectMan = new GameObject(); + var networkManager = gameObjectMan.AddComponent(); + networkManager.BehaviourUpdater = new NetworkBehaviourUpdater(); + var gameObject = new GameObject(); + var networkObject = gameObject.AddComponent(); + var networkVarComponent = gameObject.AddComponent(); + networkObject.NetworkManagerOwner = networkManager; + networkVarComponent.NetworkVariable.Initialize(networkVarComponent); + networkVarComponent.NetworkVariable.Value = 314159265; var changed = false; - - intVar.OnValueChanged += (value, newValue) => + networkVarComponent.NetworkVariable.OnValueChanged += (value, newValue) => { changed = true; }; - - intVar.Value = 314159266; - + networkVarComponent.NetworkVariable.Value = 314159266; Assert.True(changed); + Object.DestroyImmediate(gameObject); + Object.DestroyImmediate(gameObjectMan); } } } diff --git a/Tests/Editor/Serialization/UserBitReaderAndBitWriterTests_NCCBUG175.cs b/Tests/Editor/Serialization/UserBitReaderAndBitWriterTests_NCCBUG175.cs new file mode 100644 index 0000000..5db3db1 --- /dev/null +++ b/Tests/Editor/Serialization/UserBitReaderAndBitWriterTests_NCCBUG175.cs @@ -0,0 +1,95 @@ +using NUnit.Framework; +using Unity.Collections; + +namespace Unity.Netcode.EditorTests +{ + public class UserBitReaderAndBitWriterTests_NCCBUG175 + { + + [Test] + public void WhenBitwiseWritingMoreThan8Bits_ValuesAreCorrect() + { + using var writer = new FastBufferWriter(1024, Allocator.Temp); + ulong inVal = 123456789; + + for (int i = 0; i < 100; ++i) + { + writer.WriteValueSafe(i); + } + + using (var bitWriter = writer.EnterBitwiseContext()) + { + for (int i = 0; i < 16; ++i) + { + Assert.IsTrue((bitWriter.TryBeginWriteBits(32))); + bitWriter.WriteBits(inVal, 31); + bitWriter.WriteBit(true); + } + } + + using var reader = new FastBufferReader(writer, Allocator.Temp); + + for (int i = 0; i < 100; ++i) + { + reader.ReadValueSafe(out int outVal); + Assert.AreEqual(i, outVal); + } + + using var bitReader = reader.EnterBitwiseContext(); + for (int i = 0; i < 16; ++i) + { + Assert.IsTrue(bitReader.TryBeginReadBits(32)); + bitReader.ReadBits(out ulong outVal, 31); + bitReader.ReadBit(out bool bit); + Assert.AreEqual(inVal, outVal); + Assert.AreEqual(true, bit); + } + } + + [Test] + public void WhenBitwiseReadingMoreThan8Bits_ValuesAreCorrect() + { + using var writer = new FastBufferWriter(1024, Allocator.Temp); + ulong inVal = 123456789; + + for (int i = 0; i < 100; ++i) + { + writer.WriteValueSafe(i); + } + + uint combined = (uint)inVal | (1u << 31); + writer.WriteValueSafe(combined); + writer.WriteValueSafe(combined); + writer.WriteValueSafe(combined); + + using var reader = new FastBufferReader(writer, Allocator.Temp); + + for (int i = 0; i < 100; ++i) + { + reader.ReadValueSafe(out int outVal); + Assert.AreEqual(i, outVal); + } + + using (var bitReader = reader.EnterBitwiseContext()) + { + Assert.IsTrue(bitReader.TryBeginReadBits(32)); + bitReader.ReadBits(out ulong outVal, 31); + bitReader.ReadBit(out bool bit); + Assert.AreEqual(inVal, outVal); + Assert.AreEqual(true, bit); + + Assert.IsTrue(bitReader.TryBeginReadBits(32)); + bitReader.ReadBits(out outVal, 31); + bitReader.ReadBit(out bit); + Assert.AreEqual(inVal, outVal); + Assert.AreEqual(true, bit); + + Assert.IsTrue(bitReader.TryBeginReadBits(32)); + bitReader.ReadBits(out outVal, 31); + bitReader.ReadBit(out bit); + Assert.AreEqual(inVal, outVal); + Assert.AreEqual(true, bit); + } + } + } +} diff --git a/Tests/Editor/Serialization/UserBitReaderAndBitWriterTests_NCCBUG175.cs.meta b/Tests/Editor/Serialization/UserBitReaderAndBitWriterTests_NCCBUG175.cs.meta new file mode 100644 index 0000000..b1a34e7 --- /dev/null +++ b/Tests/Editor/Serialization/UserBitReaderAndBitWriterTests_NCCBUG175.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: adfa622d42824b70a39a30b6aa22c9c5 +timeCreated: 1660758428 \ No newline at end of file diff --git a/Tests/Editor/com.unity.netcode.editortests.asmdef b/Tests/Editor/com.unity.netcode.editortests.asmdef index 36642d5..8bfcba5 100644 --- a/Tests/Editor/com.unity.netcode.editortests.asmdef +++ b/Tests/Editor/com.unity.netcode.editortests.asmdef @@ -15,6 +15,9 @@ "optionalUnityReferences": [ "TestAssemblies" ], + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], "includePlatforms": [ "Editor" ], diff --git a/Tests/Runtime/Components/NetworkVariableTestComponent.cs b/Tests/Runtime/Components/NetworkVariableTestComponent.cs index b887067..6b24f18 100644 --- a/Tests/Runtime/Components/NetworkVariableTestComponent.cs +++ b/Tests/Runtime/Components/NetworkVariableTestComponent.cs @@ -53,7 +53,6 @@ namespace Unity.Netcode.RuntimeTests public bool EnableTesting; - private bool m_Initialized; private bool m_FinishedTests; private bool m_ChangesAppliedToNetworkVariables; @@ -148,6 +147,11 @@ namespace Unity.Netcode.RuntimeTests return m_FinishedTests; } + public void Awake() + { + InitializeTest(); + } + // Update is called once per frame private void Update() { @@ -164,37 +168,29 @@ namespace Unity.Netcode.RuntimeTests { if (NetworkManager != null && NetworkManager.IsListening) { - if (!m_Initialized) - { - InitializeTest(); - m_Initialized = true; - } - else - { - //Now change all of the values to make sure we are at least testing the local callback - m_NetworkVariableBool.Value = false; - m_NetworkVariableByte.Value = 255; - m_NetworkVariableColor.Value = new Color(100, 100, 100); - m_NetworkVariableColor32.Value = new Color32(100, 100, 100, 100); - m_NetworkVariableDouble.Value = 1000; - m_NetworkVariableFloat.Value = 1000.0f; - m_NetworkVariableInt.Value = 1000; - m_NetworkVariableLong.Value = 100000; - m_NetworkVariableSByte.Value = -127; - m_NetworkVariableQuaternion.Value = new Quaternion(100, 100, 100, 100); - m_NetworkVariableShort.Value = short.MaxValue; - m_NetworkVariableVector4.Value = new Vector4(1000, 1000, 1000, 1000); - m_NetworkVariableVector3.Value = new Vector3(1000, 1000, 1000); - m_NetworkVariableVector2.Value = new Vector2(1000, 1000); - m_NetworkVariableRay.Value = new Ray(Vector3.one, Vector3.right); - m_NetworkVariableULong.Value = ulong.MaxValue; - m_NetworkVariableUInt.Value = uint.MaxValue; - m_NetworkVariableUShort.Value = ushort.MaxValue; + //Now change all of the values to make sure we are at least testing the local callback + m_NetworkVariableBool.Value = false; + m_NetworkVariableByte.Value = 255; + m_NetworkVariableColor.Value = new Color(100, 100, 100); + m_NetworkVariableColor32.Value = new Color32(100, 100, 100, 100); + m_NetworkVariableDouble.Value = 1000; + m_NetworkVariableFloat.Value = 1000.0f; + m_NetworkVariableInt.Value = 1000; + m_NetworkVariableLong.Value = 100000; + m_NetworkVariableSByte.Value = -127; + m_NetworkVariableQuaternion.Value = new Quaternion(100, 100, 100, 100); + m_NetworkVariableShort.Value = short.MaxValue; + m_NetworkVariableVector4.Value = new Vector4(1000, 1000, 1000, 1000); + m_NetworkVariableVector3.Value = new Vector3(1000, 1000, 1000); + m_NetworkVariableVector2.Value = new Vector2(1000, 1000); + m_NetworkVariableRay.Value = new Ray(Vector3.one, Vector3.right); + m_NetworkVariableULong.Value = ulong.MaxValue; + m_NetworkVariableUInt.Value = uint.MaxValue; + m_NetworkVariableUShort.Value = ushort.MaxValue; - //Set the timeout (i.e. how long we will wait for all NetworkVariables to have registered their changes) - m_WaitForChangesTimeout = Time.realtimeSinceStartup + 0.50f; - m_ChangesAppliedToNetworkVariables = true; - } + //Set the timeout (i.e. how long we will wait for all NetworkVariables to have registered their changes) + m_WaitForChangesTimeout = Time.realtimeSinceStartup + 0.50f; + m_ChangesAppliedToNetworkVariables = true; } } } diff --git a/Tests/Runtime/DeferredMessagingTests.cs b/Tests/Runtime/DeferredMessagingTests.cs index 05826ac..28ed379 100644 --- a/Tests/Runtime/DeferredMessagingTests.cs +++ b/Tests/Runtime/DeferredMessagingTests.cs @@ -813,6 +813,7 @@ namespace Unity.Netcode.RuntimeTests } [UnityTest] + [Ignore("This test is unstable (MTT-4146)")] public IEnumerator WhenAMessageIsDeferredForMoreThanTheConfiguredTime_ItIsRemoved([Values(1, 2, 3)] int timeout) { RegisterClientPrefabs(); diff --git a/Tests/Runtime/ListChangedTest.cs b/Tests/Runtime/ListChangedTest.cs new file mode 100644 index 0000000..8c9c3be --- /dev/null +++ b/Tests/Runtime/ListChangedTest.cs @@ -0,0 +1,82 @@ +using System.Collections; +using UnityEngine; +using UnityEngine.TestTools; +using Unity.Netcode.TestHelpers.Runtime; + +namespace Unity.Netcode.RuntimeTests +{ + public class NetworkListChangedTestComponent : NetworkBehaviour + { + + } + + public class ListChangedObject : NetworkBehaviour + { + public int ExpectedPreviousValue = 0; + public int ExpectedValue = 0; + public bool AddDone = false; + + public NetworkList MyNetworkList = new NetworkList(); + + public override void OnNetworkSpawn() + { + MyNetworkList.OnListChanged += Changed; + base.OnNetworkSpawn(); + } + + public void Changed(NetworkListEvent listEvent) + { + if (listEvent.Type == NetworkListEvent.EventType.Value) + { + if (listEvent.PreviousValue != ExpectedPreviousValue) + { + Debug.Log($"Expected previous value mismatch {listEvent.PreviousValue} versus {ExpectedPreviousValue}"); + Debug.Assert(listEvent.PreviousValue == ExpectedPreviousValue); + } + + if (listEvent.Value != ExpectedValue) + { + Debug.Log($"Expected value mismatch {listEvent.Value} versus {ExpectedValue}"); + Debug.Assert(listEvent.Value == ExpectedValue); + } + + AddDone = true; + } + } + } + + public class NetworkListChangedTests : NetcodeIntegrationTest + { + protected override int NumberOfClients => 2; + + private ulong m_ClientId0; + private GameObject m_PrefabToSpawn; + + private NetworkObject m_NetSpawnedObject1; + + protected override void OnServerAndClientsCreated() + { + m_PrefabToSpawn = CreateNetworkObjectPrefab("ListChangedObject"); + m_PrefabToSpawn.AddComponent(); + } + + [UnityTest] + public IEnumerator NetworkListChangedTest() + { + m_ClientId0 = m_ClientNetworkManagers[0].LocalClientId; + + // create 3 objects + var spawnedObject1 = SpawnObject(m_PrefabToSpawn, m_ServerNetworkManager); + m_NetSpawnedObject1 = spawnedObject1.GetComponent(); + + m_NetSpawnedObject1.GetComponent().MyNetworkList.Add(42); + m_NetSpawnedObject1.GetComponent().ExpectedPreviousValue = 42; + m_NetSpawnedObject1.GetComponent().ExpectedValue = 44; + m_NetSpawnedObject1.GetComponent().MyNetworkList[0] = 44; + + Debug.Assert(m_NetSpawnedObject1.GetComponent().AddDone); + + return null; + } + } +} diff --git a/Tests/Runtime/ListChangedTest.cs.meta b/Tests/Runtime/ListChangedTest.cs.meta new file mode 100644 index 0000000..1cc34f6 --- /dev/null +++ b/Tests/Runtime/ListChangedTest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b269e2a059f814075a737691bc02afa4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Metrics/NetworkVariableMetricsTests.cs b/Tests/Runtime/Metrics/NetworkVariableMetricsTests.cs index 2ab85fa..640cee4 100644 --- a/Tests/Runtime/Metrics/NetworkVariableMetricsTests.cs +++ b/Tests/Runtime/Metrics/NetworkVariableMetricsTests.cs @@ -25,10 +25,17 @@ namespace Unity.Netcode.RuntimeTests.Metrics var metricValues = waitForMetricValues.AssertMetricValuesHaveBeenFound(); - var networkVariableDeltaSent = metricValues.First(); - Assert.AreEqual(nameof(NetworkVariableComponent.MyNetworkVariable), networkVariableDeltaSent.Name); - Assert.AreEqual(Server.LocalClientId, networkVariableDeltaSent.Connection.Id); - Assert.AreNotEqual(0, networkVariableDeltaSent.BytesCount); + bool found = false; + foreach (var networkVariableDeltaSent in metricValues) + { + if (nameof(NetworkVariableComponent.MyNetworkVariable) == networkVariableDeltaSent.Name && + Client.LocalClientId == networkVariableDeltaSent.Connection.Id && + 0 != networkVariableDeltaSent.BytesCount) + { + found = true; + } + } + Assert.IsTrue(found); } [UnityTest] diff --git a/Tests/Runtime/NetworkAnimator/NetworkAnimatorTests.cs b/Tests/Runtime/NetworkAnimator/NetworkAnimatorTests.cs deleted file mode 100644 index 108e37e..0000000 --- a/Tests/Runtime/NetworkAnimator/NetworkAnimatorTests.cs +++ /dev/null @@ -1,239 +0,0 @@ -#if COM_UNITY_MODULES_ANIMATION -using System.Collections; -using System.Collections.Generic; -using NUnit.Framework; -using Unity.Netcode.Components; -using UnityEngine; -using UnityEngine.TestTools; -using Unity.Netcode.TestHelpers.Runtime; - -namespace Unity.Netcode.RuntimeTests -{ - [TestFixture(HostOrServer.Host)] - [TestFixture(HostOrServer.Server)] - public class NetworkAnimatorTests : NetcodeIntegrationTest - { - protected override int NumberOfClients => 1; - - private GameObject m_PlayerOnServer; - private GameObject m_PlayerOnClient; - - private Animator m_PlayerOnServerAnimator; - private Animator m_PlayerOnClientAnimator; - - public NetworkAnimatorTests(HostOrServer hostOrServer) : base(hostOrServer) { } - - protected override void OnCreatePlayerPrefab() - { - // ideally, we would build up the AnimatorController entirely in code and not need an asset, - // but after some attempts this doesn't seem readily doable. Instead, we load a controller - var controller = Resources.Load("TestAnimatorController") as RuntimeAnimatorController; - var animator = m_PlayerPrefab.AddComponent(); - animator.runtimeAnimatorController = controller; - - var networkAnimator = m_PlayerPrefab.AddComponent(); - networkAnimator.Animator = animator; - } - - protected override IEnumerator OnServerAndClientsConnected() - { - m_PlayerOnServer = m_PlayerNetworkObjects[m_ServerNetworkManager.LocalClientId][m_ClientNetworkManagers[0].LocalClientId].gameObject; - m_PlayerOnServerAnimator = m_PlayerOnServerAnimator = m_PlayerOnServer.GetComponent(); - - m_PlayerOnClient = m_PlayerNetworkObjects[m_ClientNetworkManagers[0].LocalClientId][m_ClientNetworkManagers[0].LocalClientId].gameObject; - m_PlayerOnClientAnimator = m_PlayerOnClient.GetComponent(); - - return base.OnServerAndClientsConnected(); - } - - // helper function to scan an animator and verify a given clip is present - private bool HasClip(Animator animator, string clipName) - { - var clips = new List(); - animator.GetCurrentAnimatorClipInfo(0, clips); - foreach (var clip in clips) - { - if (clip.clip.name == clipName) - { - return true; - } - } - return false; - } - - [UnityTest] - public IEnumerator AnimationTriggerReset([Values(true, false)] bool asHash) - { - // We have "UnboundTrigger" purposely not bound to any animations so we can test resetting. - // If we used a trigger that was bound to a transition, then the trigger would reset as soon as the - // transition happens. This way it will stay stuck on - string triggerString = "UnboundTrigger"; - int triggerHash = Animator.StringToHash(triggerString); - - // Verify trigger is off - Assert.True(m_PlayerOnServerAnimator.GetBool(triggerString) == false); - Assert.True(m_PlayerOnClientAnimator.GetBool(triggerString) == false); - - // trigger. - if (asHash) - { - m_PlayerOnServer.GetComponent().SetTrigger(triggerHash); - } - else - { - m_PlayerOnServer.GetComponent().SetTrigger(triggerString); - } - - // verify trigger is set for client and server - yield return WaitForConditionOrTimeOut(() => asHash ? m_PlayerOnServerAnimator.GetBool(triggerHash) : m_PlayerOnServerAnimator.GetBool(triggerString)); - Assert.False(s_GlobalTimeoutHelper.TimedOut, "Timed out on server trigger set check"); - - yield return WaitForConditionOrTimeOut(() => asHash ? m_PlayerOnClientAnimator.GetBool(triggerHash) : m_PlayerOnClientAnimator.GetBool(triggerString)); - Assert.False(s_GlobalTimeoutHelper.TimedOut, "Timed out on client trigger set check"); - - // reset the trigger - if (asHash) - { - m_PlayerOnServer.GetComponent().ResetTrigger(triggerHash); - } - else - { - m_PlayerOnServer.GetComponent().ResetTrigger(triggerString); - } - - // verify trigger is reset for client and server - yield return WaitForConditionOrTimeOut(() => asHash ? m_PlayerOnServerAnimator.GetBool(triggerHash) == false : m_PlayerOnServerAnimator.GetBool(triggerString) == false); - Assert.False(s_GlobalTimeoutHelper.TimedOut, "Timed out on server reset check"); - - yield return WaitForConditionOrTimeOut(() => asHash ? m_PlayerOnClientAnimator.GetBool(triggerHash) == false : m_PlayerOnClientAnimator.GetBool(triggerString) == false); - Assert.False(s_GlobalTimeoutHelper.TimedOut, "Timed out on client reset check"); - } - - - [UnityTest] - public IEnumerator AnimationStateSyncTest() - { - // check that we have started in the default state - Assert.True(m_PlayerOnServerAnimator.GetCurrentAnimatorStateInfo(0).IsName("DefaultState")); - Assert.True(m_PlayerOnClientAnimator.GetCurrentAnimatorStateInfo(0).IsName("DefaultState")); - - // cause a change to the AlphaState state by setting AlphaParameter, which is - // the variable bound to the transition from default to AlphaState (see the TestAnimatorController asset) - m_PlayerOnServerAnimator.SetBool("AlphaParameter", true); - - // ...and now we should be in the AlphaState having triggered the AlphaParameter - yield return WaitForConditionOrTimeOut(() => m_PlayerOnServerAnimator.GetCurrentAnimatorStateInfo(0).IsName("AlphaState")); - Assert.False(s_GlobalTimeoutHelper.TimedOut, "Server failed to reach its animation state"); - - // ...and now the client should also have sync'd and arrived at the correct state - yield return WaitForConditionOrTimeOut(() => m_PlayerOnClientAnimator.GetCurrentAnimatorStateInfo(0).IsName("AlphaState")); - Assert.False(s_GlobalTimeoutHelper.TimedOut, "Client failed to sync its animation state from the server"); - } - - [UnityTest] - public IEnumerator AnimationLayerStateSyncTest() - { - int layer = 1; - // check that we have started in the default state - Assert.True(m_PlayerOnServerAnimator.GetCurrentAnimatorStateInfo(layer).IsName("DefaultStateLayer2")); - Assert.True(m_PlayerOnClientAnimator.GetCurrentAnimatorStateInfo(layer).IsName("DefaultStateLayer2")); - - // cause a change to the AlphaState state by setting AlphaParameter, which is - // the variable bound to the transition from default to AlphaState (see the TestAnimatorController asset) - m_PlayerOnServerAnimator.SetBool("Layer2AlphaParameter", true); - - // ...and now we should be in the AlphaState having triggered the AlphaParameter - yield return WaitForConditionOrTimeOut(() => m_PlayerOnServerAnimator.GetCurrentAnimatorStateInfo(layer).IsName("Layer2AlphaState")); - Assert.False(s_GlobalTimeoutHelper.TimedOut, "Server failed to reach its animation state"); - - // ...and now the client should also have sync'd and arrived at the correct state - yield return WaitForConditionOrTimeOut(() => m_PlayerOnClientAnimator.GetCurrentAnimatorStateInfo(layer).IsName("Layer2AlphaState")); - Assert.False(s_GlobalTimeoutHelper.TimedOut, "Client failed to sync its animation state from the server"); - } - - [UnityTest] - public IEnumerator AnimationLayerWeightTest() - { - int layer = 1; - float targetWeight = 0.333f; - - // check that we have started in the default state - Assert.True(Mathf.Approximately(m_PlayerOnServerAnimator.GetLayerWeight(layer), 1f)); - Assert.True(Mathf.Approximately(m_PlayerOnClientAnimator.GetLayerWeight(layer), 1f)); - - m_PlayerOnServerAnimator.SetLayerWeight(layer, targetWeight); - - // ...and now we should be in the AlphaState having triggered the AlphaParameter - yield return WaitForConditionOrTimeOut(() => - Mathf.Approximately(m_PlayerOnServerAnimator.GetLayerWeight(layer), targetWeight) - ); - Assert.False(s_GlobalTimeoutHelper.TimedOut, "Server failed to reach its animation state"); - - // ...and now the client should also have sync'd and arrived at the correct state - yield return WaitForConditionOrTimeOut(() => - Mathf.Approximately(m_PlayerOnClientAnimator.GetLayerWeight(layer), targetWeight) - ); - Assert.False(s_GlobalTimeoutHelper.TimedOut, "Server failed to reach its animation state"); - } - - - [UnityTest] - public IEnumerator AnimationStateSyncTriggerTest([Values(true, false)] bool asHash) - { - string triggerString = "TestTrigger"; - int triggerHash = Animator.StringToHash(triggerString); - - // check that we have started in the default state - Assert.True(m_PlayerOnServerAnimator.GetCurrentAnimatorStateInfo(0).IsName("DefaultState")); - Assert.True(m_PlayerOnClientAnimator.GetCurrentAnimatorStateInfo(0).IsName("DefaultState")); - - // cause a change to the AlphaState state by setting TestTrigger - // note, we have a special test for triggers because activating triggers via the - // NetworkAnimator is special; for other parameters you set them on the Animator and NetworkAnimator - // listens. But because triggers are super short and transitory, we require users to call - // NetworkAnimator.SetTrigger so we don't miss it - if (asHash) - { - m_PlayerOnServer.GetComponent().SetTrigger(triggerHash); - } - else - { - m_PlayerOnServer.GetComponent().SetTrigger(triggerString); - } - - // ...and now we should be in the AlphaState having triggered the AlphaParameter - yield return WaitForConditionOrTimeOut(() => m_PlayerOnServerAnimator.GetCurrentAnimatorStateInfo(0).IsName("TriggeredState")); - Assert.False(s_GlobalTimeoutHelper.TimedOut, "Server failed to reach its animation state via trigger"); - - // ...and now the client should also have sync'd and arrived at the correct state - yield return WaitForConditionOrTimeOut(() => m_PlayerOnClientAnimator.GetCurrentAnimatorStateInfo(0).IsName("TriggeredState")); - Assert.False(s_GlobalTimeoutHelper.TimedOut, "Client failed to sync its animation state from the server via trigger"); - } - - [UnityTest] - public IEnumerator AnimationStateSyncTestWithOverride() - { - // set up the animation override controller - var overrideController = Resources.Load("TestAnimatorOverrideController") as AnimatorOverrideController; - m_PlayerOnServer.GetComponent().runtimeAnimatorController = overrideController; - m_PlayerOnClient.GetComponent().runtimeAnimatorController = overrideController; - - // in our default state, we should see the OverrideDefaultAnimation clip - Assert.True(HasClip(m_PlayerOnServerAnimator, "OverrideDefaultAnimation")); - Assert.True(HasClip(m_PlayerOnClientAnimator, "OverrideDefaultAnimation")); - - // cause a change to the AlphaState state by setting AlphaParameter, which is - // the variable bound to the transition from default to AlphaState (see the TestAnimatorController asset) - m_PlayerOnServerAnimator.SetBool("AlphaParameter", true); - - // ...and now we should be in the AlphaState having set the AlphaParameter - yield return WaitForConditionOrTimeOut(() => HasClip(m_PlayerOnServerAnimator, "OverrideAlphaAnimation")); - Assert.False(s_GlobalTimeoutHelper.TimedOut, "Server failed to reach its overriden animation state"); - - // ...and now the client should also have sync'd and arrived at the correct state - yield return WaitForConditionOrTimeOut(() => HasClip(m_PlayerOnServerAnimator, "OverrideAlphaAnimation")); - Assert.False(s_GlobalTimeoutHelper.TimedOut, "Client failed to reach its overriden animation state"); - } - } -} -#endif // COM_UNITY_MODULES_ANIMATION diff --git a/Tests/Runtime/NetworkAnimator/Resources.meta b/Tests/Runtime/NetworkAnimator/Resources.meta deleted file mode 100644 index d3faa16..0000000 --- a/Tests/Runtime/NetworkAnimator/Resources.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: c3a8707ef624947a7ae8843ca6c70c0a -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Tests/Runtime/NetworkAnimator/Resources/AlphaAnimation.anim b/Tests/Runtime/NetworkAnimator/Resources/AlphaAnimation.anim deleted file mode 100644 index cb964ee..0000000 --- a/Tests/Runtime/NetworkAnimator/Resources/AlphaAnimation.anim +++ /dev/null @@ -1,53 +0,0 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!74 &7400000 -AnimationClip: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_Name: AlphaAnimation - serializedVersion: 6 - m_Legacy: 0 - m_Compressed: 0 - m_UseHighQualityCurve: 1 - m_RotationCurves: [] - m_CompressedRotationCurves: [] - m_EulerCurves: [] - m_PositionCurves: [] - m_ScaleCurves: [] - m_FloatCurves: [] - m_PPtrCurves: [] - m_SampleRate: 60 - m_WrapMode: 0 - m_Bounds: - m_Center: {x: 0, y: 0, z: 0} - m_Extent: {x: 0, y: 0, z: 0} - m_ClipBindingConstant: - genericBindings: [] - pptrCurveMapping: [] - m_AnimationClipSettings: - serializedVersion: 2 - m_AdditiveReferencePoseClip: {fileID: 0} - m_AdditiveReferencePoseTime: 0 - m_StartTime: 0 - m_StopTime: 1 - m_OrientationOffsetY: 0 - m_Level: 0 - m_CycleOffset: 0 - m_HasAdditiveReferencePose: 0 - m_LoopTime: 0 - m_LoopBlend: 0 - m_LoopBlendOrientation: 0 - m_LoopBlendPositionY: 0 - m_LoopBlendPositionXZ: 0 - m_KeepOriginalOrientation: 0 - m_KeepOriginalPositionY: 1 - m_KeepOriginalPositionXZ: 0 - m_HeightFromFeet: 0 - m_Mirror: 0 - m_EditorCurves: [] - m_EulerEditorCurves: [] - m_HasGenericRootTransform: 0 - m_HasMotionFloatCurves: 0 - m_Events: [] diff --git a/Tests/Runtime/NetworkAnimator/Resources/AlphaAnimation.anim.meta b/Tests/Runtime/NetworkAnimator/Resources/AlphaAnimation.anim.meta deleted file mode 100644 index 46edd4d..0000000 --- a/Tests/Runtime/NetworkAnimator/Resources/AlphaAnimation.anim.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: db8faf64ca46248abb6624513ac1fb1b -NativeFormatImporter: - externalObjects: {} - mainObjectFileID: 7400000 - userData: - assetBundleName: - assetBundleVariant: diff --git a/Tests/Runtime/NetworkAnimator/Resources/DefaultAnimation.anim b/Tests/Runtime/NetworkAnimator/Resources/DefaultAnimation.anim deleted file mode 100644 index f19491b..0000000 --- a/Tests/Runtime/NetworkAnimator/Resources/DefaultAnimation.anim +++ /dev/null @@ -1,53 +0,0 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!74 &7400000 -AnimationClip: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_Name: DefaultAnimation - serializedVersion: 6 - m_Legacy: 0 - m_Compressed: 0 - m_UseHighQualityCurve: 1 - m_RotationCurves: [] - m_CompressedRotationCurves: [] - m_EulerCurves: [] - m_PositionCurves: [] - m_ScaleCurves: [] - m_FloatCurves: [] - m_PPtrCurves: [] - m_SampleRate: 60 - m_WrapMode: 0 - m_Bounds: - m_Center: {x: 0, y: 0, z: 0} - m_Extent: {x: 0, y: 0, z: 0} - m_ClipBindingConstant: - genericBindings: [] - pptrCurveMapping: [] - m_AnimationClipSettings: - serializedVersion: 2 - m_AdditiveReferencePoseClip: {fileID: 0} - m_AdditiveReferencePoseTime: 0 - m_StartTime: 0 - m_StopTime: 1 - m_OrientationOffsetY: 0 - m_Level: 0 - m_CycleOffset: 0 - m_HasAdditiveReferencePose: 0 - m_LoopTime: 0 - m_LoopBlend: 0 - m_LoopBlendOrientation: 0 - m_LoopBlendPositionY: 0 - m_LoopBlendPositionXZ: 0 - m_KeepOriginalOrientation: 0 - m_KeepOriginalPositionY: 1 - m_KeepOriginalPositionXZ: 0 - m_HeightFromFeet: 0 - m_Mirror: 0 - m_EditorCurves: [] - m_EulerEditorCurves: [] - m_HasGenericRootTransform: 0 - m_HasMotionFloatCurves: 0 - m_Events: [] diff --git a/Tests/Runtime/NetworkAnimator/Resources/DefaultAnimation.anim.meta b/Tests/Runtime/NetworkAnimator/Resources/DefaultAnimation.anim.meta deleted file mode 100644 index e79be12..0000000 --- a/Tests/Runtime/NetworkAnimator/Resources/DefaultAnimation.anim.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 1f6191147839943ab93e2171cc15c5e9 -NativeFormatImporter: - externalObjects: {} - mainObjectFileID: 7400000 - userData: - assetBundleName: - assetBundleVariant: diff --git a/Tests/Runtime/NetworkAnimator/Resources/Layer2Animation.anim b/Tests/Runtime/NetworkAnimator/Resources/Layer2Animation.anim deleted file mode 100644 index e0bbbe7..0000000 --- a/Tests/Runtime/NetworkAnimator/Resources/Layer2Animation.anim +++ /dev/null @@ -1,53 +0,0 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!74 &7400000 -AnimationClip: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_Name: Layer2Animation - serializedVersion: 6 - m_Legacy: 0 - m_Compressed: 0 - m_UseHighQualityCurve: 1 - m_RotationCurves: [] - m_CompressedRotationCurves: [] - m_EulerCurves: [] - m_PositionCurves: [] - m_ScaleCurves: [] - m_FloatCurves: [] - m_PPtrCurves: [] - m_SampleRate: 60 - m_WrapMode: 0 - m_Bounds: - m_Center: {x: 0, y: 0, z: 0} - m_Extent: {x: 0, y: 0, z: 0} - m_ClipBindingConstant: - genericBindings: [] - pptrCurveMapping: [] - m_AnimationClipSettings: - serializedVersion: 2 - m_AdditiveReferencePoseClip: {fileID: 0} - m_AdditiveReferencePoseTime: 0 - m_StartTime: 0 - m_StopTime: 1 - m_OrientationOffsetY: 0 - m_Level: 0 - m_CycleOffset: 0 - m_HasAdditiveReferencePose: 0 - m_LoopTime: 0 - m_LoopBlend: 0 - m_LoopBlendOrientation: 0 - m_LoopBlendPositionY: 0 - m_LoopBlendPositionXZ: 0 - m_KeepOriginalOrientation: 0 - m_KeepOriginalPositionY: 1 - m_KeepOriginalPositionXZ: 0 - m_HeightFromFeet: 0 - m_Mirror: 0 - m_EditorCurves: [] - m_EulerEditorCurves: [] - m_HasGenericRootTransform: 0 - m_HasMotionFloatCurves: 0 - m_Events: [] diff --git a/Tests/Runtime/NetworkAnimator/Resources/Layer2Animation.anim.meta b/Tests/Runtime/NetworkAnimator/Resources/Layer2Animation.anim.meta deleted file mode 100644 index 2d1fbd7..0000000 --- a/Tests/Runtime/NetworkAnimator/Resources/Layer2Animation.anim.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: d31c84f6372c54d7eb8decb27010d005 -NativeFormatImporter: - externalObjects: {} - mainObjectFileID: 7400000 - userData: - assetBundleName: - assetBundleVariant: diff --git a/Tests/Runtime/NetworkAnimator/Resources/OverrideAlphaAnimation.anim b/Tests/Runtime/NetworkAnimator/Resources/OverrideAlphaAnimation.anim deleted file mode 100644 index 6123420..0000000 --- a/Tests/Runtime/NetworkAnimator/Resources/OverrideAlphaAnimation.anim +++ /dev/null @@ -1,53 +0,0 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!74 &7400000 -AnimationClip: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_Name: OverrideAlphaAnimation - serializedVersion: 6 - m_Legacy: 0 - m_Compressed: 0 - m_UseHighQualityCurve: 1 - m_RotationCurves: [] - m_CompressedRotationCurves: [] - m_EulerCurves: [] - m_PositionCurves: [] - m_ScaleCurves: [] - m_FloatCurves: [] - m_PPtrCurves: [] - m_SampleRate: 60 - m_WrapMode: 0 - m_Bounds: - m_Center: {x: 0, y: 0, z: 0} - m_Extent: {x: 0, y: 0, z: 0} - m_ClipBindingConstant: - genericBindings: [] - pptrCurveMapping: [] - m_AnimationClipSettings: - serializedVersion: 2 - m_AdditiveReferencePoseClip: {fileID: 0} - m_AdditiveReferencePoseTime: 0 - m_StartTime: 0 - m_StopTime: 1 - m_OrientationOffsetY: 0 - m_Level: 0 - m_CycleOffset: 0 - m_HasAdditiveReferencePose: 0 - m_LoopTime: 0 - m_LoopBlend: 0 - m_LoopBlendOrientation: 0 - m_LoopBlendPositionY: 0 - m_LoopBlendPositionXZ: 0 - m_KeepOriginalOrientation: 0 - m_KeepOriginalPositionY: 1 - m_KeepOriginalPositionXZ: 0 - m_HeightFromFeet: 0 - m_Mirror: 0 - m_EditorCurves: [] - m_EulerEditorCurves: [] - m_HasGenericRootTransform: 0 - m_HasMotionFloatCurves: 0 - m_Events: [] diff --git a/Tests/Runtime/NetworkAnimator/Resources/OverrideAlphaAnimation.anim.meta b/Tests/Runtime/NetworkAnimator/Resources/OverrideAlphaAnimation.anim.meta deleted file mode 100644 index bf0e868..0000000 --- a/Tests/Runtime/NetworkAnimator/Resources/OverrideAlphaAnimation.anim.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 05a2afc2ff8884d32afc64ed6765880a -NativeFormatImporter: - externalObjects: {} - mainObjectFileID: 7400000 - userData: - assetBundleName: - assetBundleVariant: diff --git a/Tests/Runtime/NetworkAnimator/Resources/OverrideDefaultAnimation.anim b/Tests/Runtime/NetworkAnimator/Resources/OverrideDefaultAnimation.anim deleted file mode 100644 index 9ac6917..0000000 --- a/Tests/Runtime/NetworkAnimator/Resources/OverrideDefaultAnimation.anim +++ /dev/null @@ -1,53 +0,0 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!74 &7400000 -AnimationClip: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_Name: OverrideDefaultAnimation - serializedVersion: 6 - m_Legacy: 0 - m_Compressed: 0 - m_UseHighQualityCurve: 1 - m_RotationCurves: [] - m_CompressedRotationCurves: [] - m_EulerCurves: [] - m_PositionCurves: [] - m_ScaleCurves: [] - m_FloatCurves: [] - m_PPtrCurves: [] - m_SampleRate: 60 - m_WrapMode: 0 - m_Bounds: - m_Center: {x: 0, y: 0, z: 0} - m_Extent: {x: 0, y: 0, z: 0} - m_ClipBindingConstant: - genericBindings: [] - pptrCurveMapping: [] - m_AnimationClipSettings: - serializedVersion: 2 - m_AdditiveReferencePoseClip: {fileID: 0} - m_AdditiveReferencePoseTime: 0 - m_StartTime: 0 - m_StopTime: 1 - m_OrientationOffsetY: 0 - m_Level: 0 - m_CycleOffset: 0 - m_HasAdditiveReferencePose: 0 - m_LoopTime: 0 - m_LoopBlend: 0 - m_LoopBlendOrientation: 0 - m_LoopBlendPositionY: 0 - m_LoopBlendPositionXZ: 0 - m_KeepOriginalOrientation: 0 - m_KeepOriginalPositionY: 1 - m_KeepOriginalPositionXZ: 0 - m_HeightFromFeet: 0 - m_Mirror: 0 - m_EditorCurves: [] - m_EulerEditorCurves: [] - m_HasGenericRootTransform: 0 - m_HasMotionFloatCurves: 0 - m_Events: [] diff --git a/Tests/Runtime/NetworkAnimator/Resources/OverrideDefaultAnimation.anim.meta b/Tests/Runtime/NetworkAnimator/Resources/OverrideDefaultAnimation.anim.meta deleted file mode 100644 index bcfba8f..0000000 --- a/Tests/Runtime/NetworkAnimator/Resources/OverrideDefaultAnimation.anim.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: cf503a5569d0b4df4910a26d09ce4530 -NativeFormatImporter: - externalObjects: {} - mainObjectFileID: 7400000 - userData: - assetBundleName: - assetBundleVariant: diff --git a/Tests/Runtime/NetworkAnimator/Resources/TestAnimatorController.controller b/Tests/Runtime/NetworkAnimator/Resources/TestAnimatorController.controller deleted file mode 100644 index 247a8e4..0000000 --- a/Tests/Runtime/NetworkAnimator/Resources/TestAnimatorController.controller +++ /dev/null @@ -1,449 +0,0 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!1102 &-8144973961595650150 -AnimatorState: - serializedVersion: 6 - m_ObjectHideFlags: 1 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_Name: New State - m_Speed: 1 - m_CycleOffset: 0 - m_Transitions: [] - m_StateMachineBehaviours: [] - m_Position: {x: 50, y: 50, z: 0} - m_IKOnFeet: 0 - m_WriteDefaultValues: 1 - m_Mirror: 0 - m_SpeedParameterActive: 0 - m_MirrorParameterActive: 0 - m_CycleOffsetParameterActive: 0 - m_TimeParameterActive: 0 - m_Motion: {fileID: 0} - m_Tag: - m_SpeedParameter: - m_MirrorParameter: - m_CycleOffsetParameter: - m_TimeParameter: ---- !u!1102 &-7257898091357968356 -AnimatorState: - serializedVersion: 6 - m_ObjectHideFlags: 1 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_Name: New State - m_Speed: 1 - m_CycleOffset: 0 - m_Transitions: [] - m_StateMachineBehaviours: [] - m_Position: {x: 50, y: 50, z: 0} - m_IKOnFeet: 0 - m_WriteDefaultValues: 1 - m_Mirror: 0 - m_SpeedParameterActive: 0 - m_MirrorParameterActive: 0 - m_CycleOffsetParameterActive: 0 - m_TimeParameterActive: 0 - m_Motion: {fileID: 0} - m_Tag: - m_SpeedParameter: - m_MirrorParameter: - m_CycleOffsetParameter: - m_TimeParameter: ---- !u!1101 &-7235917949335567458 -AnimatorStateTransition: - m_ObjectHideFlags: 1 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_Name: - m_Conditions: - - m_ConditionMode: 2 - m_ConditionEvent: Layer2AlphaParameter - m_EventTreshold: 0 - m_DstStateMachine: {fileID: 0} - m_DstState: {fileID: 6016706997111698284} - m_Solo: 0 - m_Mute: 0 - m_IsExit: 0 - serializedVersion: 3 - m_TransitionDuration: 0.25 - m_TransitionOffset: 0 - m_ExitTime: 0.75 - m_HasExitTime: 1 - m_HasFixedDuration: 1 - m_InterruptionSource: 2 - m_OrderedInterruption: 1 - m_CanTransitionToSelf: 1 ---- !u!1101 &-6097014330458455406 -AnimatorStateTransition: - m_ObjectHideFlags: 1 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_Name: - m_Conditions: - - m_ConditionMode: 2 - m_ConditionEvent: AlphaParameter - m_EventTreshold: 0 - m_DstStateMachine: {fileID: 0} - m_DstState: {fileID: -1198466922477486815} - m_Solo: 0 - m_Mute: 0 - m_IsExit: 0 - serializedVersion: 3 - m_TransitionDuration: 0.25 - m_TransitionOffset: 0 - m_ExitTime: 0.75 - m_HasExitTime: 1 - m_HasFixedDuration: 1 - m_InterruptionSource: 0 - m_OrderedInterruption: 1 - m_CanTransitionToSelf: 1 ---- !u!1107 &-1914299053840757887 -AnimatorStateMachine: - serializedVersion: 6 - m_ObjectHideFlags: 1 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_Name: Base Layer - m_ChildStates: - - serializedVersion: 1 - m_State: {fileID: -1198466922477486815} - m_Position: {x: 70, y: 290, z: 0} - - serializedVersion: 1 - m_State: {fileID: 320527679719022362} - m_Position: {x: 110, y: 490, z: 0} - - serializedVersion: 1 - m_State: {fileID: 3942933370568001311} - m_Position: {x: 380, y: 280, z: 0} - m_ChildStateMachines: [] - m_AnyStateTransitions: [] - m_EntryTransitions: [] - m_StateMachineTransitions: {} - m_StateMachineBehaviours: [] - m_AnyStatePosition: {x: 50, y: 20, z: 0} - m_EntryPosition: {x: 30, y: 180, z: 0} - m_ExitPosition: {x: 800, y: 120, z: 0} - m_ParentStateMachinePosition: {x: 800, y: 20, z: 0} - m_DefaultState: {fileID: -1198466922477486815} ---- !u!1102 &-1198466922477486815 -AnimatorState: - serializedVersion: 6 - m_ObjectHideFlags: 1 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_Name: DefaultState - m_Speed: 1 - m_CycleOffset: 0 - m_Transitions: - - {fileID: 232953446134799302} - - {fileID: 8340347106517238820} - m_StateMachineBehaviours: [] - m_Position: {x: 50, y: 50, z: 0} - m_IKOnFeet: 0 - m_WriteDefaultValues: 1 - m_Mirror: 0 - m_SpeedParameterActive: 0 - m_MirrorParameterActive: 0 - m_CycleOffsetParameterActive: 0 - m_TimeParameterActive: 0 - m_Motion: {fileID: 7400000, guid: 1f6191147839943ab93e2171cc15c5e9, type: 2} - m_Tag: - m_SpeedParameter: - m_MirrorParameter: - m_CycleOffsetParameter: - m_TimeParameter: ---- !u!91 &9100000 -AnimatorController: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_Name: TestAnimatorController - serializedVersion: 5 - m_AnimatorParameters: - - m_Name: AlphaParameter - m_Type: 4 - m_DefaultFloat: 0 - m_DefaultInt: 0 - m_DefaultBool: 0 - m_Controller: {fileID: 9100000} - - m_Name: TestTrigger - m_Type: 9 - m_DefaultFloat: 0 - m_DefaultInt: 0 - m_DefaultBool: 0 - m_Controller: {fileID: 9100000} - - m_Name: UnboundTrigger - m_Type: 9 - m_DefaultFloat: 0 - m_DefaultInt: 0 - m_DefaultBool: 0 - m_Controller: {fileID: 9100000} - - m_Name: Layer2AlphaParameter - m_Type: 4 - m_DefaultFloat: 0 - m_DefaultInt: 0 - m_DefaultBool: 0 - m_Controller: {fileID: 9100000} - m_AnimatorLayers: - - serializedVersion: 5 - m_Name: Base Layer - m_StateMachine: {fileID: -1914299053840757887} - m_Mask: {fileID: 0} - m_Motions: [] - m_Behaviours: [] - m_BlendingMode: 0 - m_SyncedLayerIndex: -1 - m_DefaultWeight: 0 - m_IKPass: 0 - m_SyncedLayerAffectsTiming: 0 - m_Controller: {fileID: 9100000} - - serializedVersion: 5 - m_Name: Layer2 - m_StateMachine: {fileID: 1433017894673297828} - m_Mask: {fileID: 0} - m_Motions: [] - m_Behaviours: [] - m_BlendingMode: 0 - m_SyncedLayerIndex: -1 - m_DefaultWeight: 1 - m_IKPass: 0 - m_SyncedLayerAffectsTiming: 0 - m_Controller: {fileID: 9100000} ---- !u!1101 &232953446134799302 -AnimatorStateTransition: - m_ObjectHideFlags: 1 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_Name: - m_Conditions: - - m_ConditionMode: 1 - m_ConditionEvent: AlphaParameter - m_EventTreshold: 0 - m_DstStateMachine: {fileID: 0} - m_DstState: {fileID: 320527679719022362} - m_Solo: 0 - m_Mute: 0 - m_IsExit: 0 - serializedVersion: 3 - m_TransitionDuration: 0.25 - m_TransitionOffset: 0 - m_ExitTime: 0.75 - m_HasExitTime: 1 - m_HasFixedDuration: 1 - m_InterruptionSource: 0 - m_OrderedInterruption: 1 - m_CanTransitionToSelf: 1 ---- !u!1102 &320527679719022362 -AnimatorState: - serializedVersion: 6 - m_ObjectHideFlags: 1 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_Name: AlphaState - m_Speed: 1 - m_CycleOffset: 0 - m_Transitions: [] - m_StateMachineBehaviours: [] - m_Position: {x: 50, y: 50, z: 0} - m_IKOnFeet: 0 - m_WriteDefaultValues: 1 - m_Mirror: 0 - m_SpeedParameterActive: 0 - m_MirrorParameterActive: 0 - m_CycleOffsetParameterActive: 0 - m_TimeParameterActive: 0 - m_Motion: {fileID: 7400000, guid: db8faf64ca46248abb6624513ac1fb1b, type: 2} - m_Tag: - m_SpeedParameter: - m_MirrorParameter: - m_CycleOffsetParameter: - m_TimeParameter: ---- !u!1102 &927597079590233140 -AnimatorState: - serializedVersion: 6 - m_ObjectHideFlags: 1 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_Name: Layer2AlphaState - m_Speed: 1 - m_CycleOffset: 0 - m_Transitions: - - {fileID: -7235917949335567458} - m_StateMachineBehaviours: [] - m_Position: {x: 50, y: 50, z: 0} - m_IKOnFeet: 0 - m_WriteDefaultValues: 1 - m_Mirror: 0 - m_SpeedParameterActive: 0 - m_MirrorParameterActive: 0 - m_CycleOffsetParameterActive: 0 - m_TimeParameterActive: 0 - m_Motion: {fileID: 7400000, guid: d31c84f6372c54d7eb8decb27010d005, type: 2} - m_Tag: - m_SpeedParameter: - m_MirrorParameter: - m_CycleOffsetParameter: - m_TimeParameter: ---- !u!1107 &1433017894673297828 -AnimatorStateMachine: - serializedVersion: 6 - m_ObjectHideFlags: 1 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_Name: Layer2 - m_ChildStates: - - serializedVersion: 1 - m_State: {fileID: 6016706997111698284} - m_Position: {x: 160, y: 250, z: 0} - - serializedVersion: 1 - m_State: {fileID: 927597079590233140} - m_Position: {x: 270, y: 370, z: 0} - m_ChildStateMachines: [] - m_AnyStateTransitions: [] - m_EntryTransitions: [] - m_StateMachineTransitions: {} - m_StateMachineBehaviours: [] - m_AnyStatePosition: {x: 50, y: 20, z: 0} - m_EntryPosition: {x: 50, y: 120, z: 0} - m_ExitPosition: {x: 800, y: 120, z: 0} - m_ParentStateMachinePosition: {x: 800, y: 20, z: 0} - m_DefaultState: {fileID: 6016706997111698284} ---- !u!1102 &3942933370568001311 -AnimatorState: - serializedVersion: 6 - m_ObjectHideFlags: 1 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_Name: TriggeredState - m_Speed: 1 - m_CycleOffset: 0 - m_Transitions: [] - m_StateMachineBehaviours: [] - m_Position: {x: 50, y: 50, z: 0} - m_IKOnFeet: 0 - m_WriteDefaultValues: 1 - m_Mirror: 0 - m_SpeedParameterActive: 0 - m_MirrorParameterActive: 0 - m_CycleOffsetParameterActive: 0 - m_TimeParameterActive: 0 - m_Motion: {fileID: 7400000, guid: db8faf64ca46248abb6624513ac1fb1b, type: 2} - m_Tag: - m_SpeedParameter: - m_MirrorParameter: - m_CycleOffsetParameter: - m_TimeParameter: ---- !u!1101 &5326371122012901575 -AnimatorStateTransition: - m_ObjectHideFlags: 1 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_Name: - m_Conditions: - - m_ConditionMode: 2 - m_ConditionEvent: AlphaParameter - m_EventTreshold: 0 - m_DstStateMachine: {fileID: 0} - m_DstState: {fileID: -1198466922477486815} - m_Solo: 0 - m_Mute: 0 - m_IsExit: 0 - serializedVersion: 3 - m_TransitionDuration: 0.25 - m_TransitionOffset: 0 - m_ExitTime: 0.75 - m_HasExitTime: 1 - m_HasFixedDuration: 1 - m_InterruptionSource: 0 - m_OrderedInterruption: 1 - m_CanTransitionToSelf: 1 ---- !u!1102 &6016706997111698284 -AnimatorState: - serializedVersion: 6 - m_ObjectHideFlags: 1 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_Name: DefaultStateLayer2 - m_Speed: 1 - m_CycleOffset: 0 - m_Transitions: - - {fileID: 6324505406226331058} - m_StateMachineBehaviours: [] - m_Position: {x: 50, y: 50, z: 0} - m_IKOnFeet: 0 - m_WriteDefaultValues: 1 - m_Mirror: 0 - m_SpeedParameterActive: 0 - m_MirrorParameterActive: 0 - m_CycleOffsetParameterActive: 0 - m_TimeParameterActive: 0 - m_Motion: {fileID: 0} - m_Tag: - m_SpeedParameter: - m_MirrorParameter: - m_CycleOffsetParameter: - m_TimeParameter: ---- !u!1101 &6324505406226331058 -AnimatorStateTransition: - m_ObjectHideFlags: 1 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_Name: - m_Conditions: - - m_ConditionMode: 1 - m_ConditionEvent: Layer2AlphaParameter - m_EventTreshold: 0 - m_DstStateMachine: {fileID: 0} - m_DstState: {fileID: 927597079590233140} - m_Solo: 0 - m_Mute: 0 - m_IsExit: 0 - serializedVersion: 3 - m_TransitionDuration: 0.25 - m_TransitionOffset: 0 - m_ExitTime: 0.75 - m_HasExitTime: 1 - m_HasFixedDuration: 1 - m_InterruptionSource: 2 - m_OrderedInterruption: 1 - m_CanTransitionToSelf: 1 ---- !u!1101 &8340347106517238820 -AnimatorStateTransition: - m_ObjectHideFlags: 1 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_Name: - m_Conditions: - - m_ConditionMode: 1 - m_ConditionEvent: TestTrigger - m_EventTreshold: 0 - m_DstStateMachine: {fileID: 0} - m_DstState: {fileID: 3942933370568001311} - m_Solo: 0 - m_Mute: 0 - m_IsExit: 0 - serializedVersion: 3 - m_TransitionDuration: 0.25 - m_TransitionOffset: 0 - m_ExitTime: 0.75 - m_HasExitTime: 1 - m_HasFixedDuration: 1 - m_InterruptionSource: 0 - m_OrderedInterruption: 1 - m_CanTransitionToSelf: 1 diff --git a/Tests/Runtime/NetworkAnimator/Resources/TestAnimatorController.controller.meta b/Tests/Runtime/NetworkAnimator/Resources/TestAnimatorController.controller.meta deleted file mode 100644 index 9c9d715..0000000 --- a/Tests/Runtime/NetworkAnimator/Resources/TestAnimatorController.controller.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: a0b8ebecb362240989d16159bdfa067c -NativeFormatImporter: - externalObjects: {} - mainObjectFileID: 9100000 - userData: - assetBundleName: - assetBundleVariant: diff --git a/Tests/Runtime/NetworkAnimator/Resources/TestAnimatorOverrideController.overrideController b/Tests/Runtime/NetworkAnimator/Resources/TestAnimatorOverrideController.overrideController deleted file mode 100644 index 3d70809..0000000 --- a/Tests/Runtime/NetworkAnimator/Resources/TestAnimatorOverrideController.overrideController +++ /dev/null @@ -1,15 +0,0 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!221 &22100000 -AnimatorOverrideController: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_Name: TestAnimatorOverrideController - m_Controller: {fileID: 9100000, guid: a0b8ebecb362240989d16159bdfa067c, type: 2} - m_Clips: - - m_OriginalClip: {fileID: 7400000, guid: 1f6191147839943ab93e2171cc15c5e9, type: 2} - m_OverrideClip: {fileID: 7400000, guid: cf503a5569d0b4df4910a26d09ce4530, type: 2} - - m_OriginalClip: {fileID: 7400000, guid: db8faf64ca46248abb6624513ac1fb1b, type: 2} - m_OverrideClip: {fileID: 7400000, guid: 05a2afc2ff8884d32afc64ed6765880a, type: 2} diff --git a/Tests/Runtime/NetworkBehaviourGenericTests.cs b/Tests/Runtime/NetworkBehaviourGenericTests.cs index 7d66770..c51527a 100644 --- a/Tests/Runtime/NetworkBehaviourGenericTests.cs +++ b/Tests/Runtime/NetworkBehaviourGenericTests.cs @@ -2,6 +2,7 @@ using System.Collections; using UnityEngine; using UnityEngine.TestTools; using Unity.Netcode.TestHelpers.Runtime; +using Unity.Netcode.Components; namespace Unity.Netcode.RuntimeTests { @@ -21,6 +22,53 @@ namespace Unity.Netcode.RuntimeTests public class SimpleNetworkBehaviour : NetworkBehaviour { + public bool OnNetworkDespawnCalled; + + public override void OnNetworkDespawn() + { + OnNetworkDespawnCalled = true; + base.OnNetworkDespawn(); + } + } + + protected override IEnumerator OnSetup() + { + m_AllowServerToStart = false; + return base.OnSetup(); + } + + /// + /// This validates the fix for when a child GameObject with a NetworkBehaviour + /// is deleted while the parent GameObject with a NetworkObject is spawned and + /// is not deleted until a later time would cause an exception due to the + /// NetworkBehaviour not being removed from the NetworkObject.ChildNetworkBehaviours + /// list. + /// + [UnityTest] + public IEnumerator ValidatedDisableddNetworkBehaviourWarning() + { + m_AllowServerToStart = true; + + yield return s_DefaultWaitForTick; + + // Now just start the Host + yield return StartServerAndClients(); + + var parentObject = new GameObject(); + var childObject = new GameObject(); + childObject.name = "ChildObject"; + childObject.transform.parent = parentObject.transform; + var parentNetworkObject = parentObject.AddComponent(); + var childBehaviour = childObject.AddComponent(); + + // Set the child object to be inactive in the hierarchy + childObject.SetActive(false); + + LogAssert.Expect(LogType.Warning, $"{childObject.name} is disabled! Netcode for GameObjects does not support disabled NetworkBehaviours! The {childBehaviour.GetType().Name} component was skipped during ownership assignment!"); + LogAssert.Expect(LogType.Warning, $"{childObject.name} is disabled! Netcode for GameObjects does not support spawning disabled NetworkBehaviours! The {childBehaviour.GetType().Name} component was skipped during spawn!"); + + parentNetworkObject.Spawn(); + yield return s_DefaultWaitForTick; } /// @@ -42,6 +90,9 @@ namespace Unity.Netcode.RuntimeTests // set the log level to developer m_ServerNetworkManager.LogLevel = LogLevel.Developer; + // The only valid condition for this would be if the NetworkBehaviour is spawned. + simpleNetworkBehaviour.IsSpawned = true; + // Verify the warning gets logged under normal conditions var isNull = simpleNetworkBehaviour.NetworkObject == null; LogAssert.Expect(LogType.Warning, $"[Netcode] Could not get {nameof(NetworkObject)} for the {nameof(NetworkBehaviour)}. Are you missing a {nameof(NetworkObject)} component?"); @@ -57,5 +108,44 @@ namespace Unity.Netcode.RuntimeTests networkObjectToTest.Despawn(); Object.Destroy(networkObjectToTest); } + + /// + /// This validates the fix for when a child GameObject with a NetworkBehaviour + /// is deleted while the parent GameObject with a NetworkObject is spawned and + /// is not deleted until a later time would cause an exception due to the + /// NetworkBehaviour not being removed from the NetworkObject.ChildNetworkBehaviours + /// list. + /// + [UnityTest] + public IEnumerator ValidateDeleteChildNetworkBehaviour() + { + m_AllowServerToStart = true; + + yield return s_DefaultWaitForTick; + + // Now just start the Host + yield return StartServerAndClients(); + + var parentObject = new GameObject(); + var childObject = new GameObject(); + childObject.transform.parent = parentObject.transform; + var parentNetworkObject = parentObject.AddComponent(); + childObject.AddComponent(); + + parentNetworkObject.Spawn(); + yield return s_DefaultWaitForTick; + + // Destroy the child object with child NetworkBehaviour + Object.Destroy(childObject); + + yield return s_DefaultWaitForTick; + + // Assure no log messages are logged when they should not be logged + LogAssert.NoUnexpectedReceived(); + + // Destroy the parent object which should not cause any exceptions + // (validating the fix) + Object.Destroy(parentObject); + } } } diff --git a/Tests/Runtime/NetworkShowHideTests.cs b/Tests/Runtime/NetworkShowHideTests.cs index 15f9ec7..bd95fa4 100644 --- a/Tests/Runtime/NetworkShowHideTests.cs +++ b/Tests/Runtime/NetworkShowHideTests.cs @@ -50,7 +50,7 @@ namespace Unity.Netcode.RuntimeTests public NetworkVariable MyNetworkVariable; - private void Start() + private void Awake() { MyNetworkVariable = new NetworkVariable(); MyNetworkVariable.OnValueChanged += Changed; diff --git a/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs b/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs index 0d6c58f..207b691 100644 --- a/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs +++ b/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs @@ -1,11 +1,6 @@ -using System; using System.Collections; -#if NGO_TRANSFORM_DEBUG -using System.Text.RegularExpressions; -#endif using Unity.Netcode.Components; using NUnit.Framework; -// using Unity.Netcode.Samples; using UnityEngine; using UnityEngine.TestTools; using Unity.Netcode.TestHelpers.Runtime; @@ -14,8 +9,15 @@ namespace Unity.Netcode.RuntimeTests { public class NetworkTransformTestComponent : NetworkTransform { + public bool ServerAuthority; public bool ReadyToReceivePositionUpdate = false; + + protected override bool OnIsServerAuthoritative() + { + return ServerAuthority; + } + public override void OnNetworkSpawn() { base.OnNetworkSpawn(); @@ -23,243 +25,531 @@ namespace Unity.Netcode.RuntimeTests ReadyToReceivePositionUpdate = true; } + public void CommitToTransform() + { + TryCommitTransformToServer(transform, NetworkManager.LocalTime.Time); + } + public (bool isDirty, bool isPositionDirty, bool isRotationDirty, bool isScaleDirty) ApplyState() { - return ApplyLocalNetworkState(transform); + var transformState = ApplyLocalNetworkState(transform); + return (transformState.IsDirty, transformState.HasPositionChange, transformState.HasRotAngleChange, transformState.HasScaleChange); } } - // [TestFixture(true, true)] - [TestFixture(true, false)] - // [TestFixture(false, true)] - [TestFixture(false, false)] + [TestFixture(HostOrServer.Host, Authority.Server)] + [TestFixture(HostOrServer.Host, Authority.Owner)] + [TestFixture(HostOrServer.Server, Authority.Server)] + [TestFixture(HostOrServer.Server, Authority.Owner)] + public class NetworkTransformTests : NetcodeIntegrationTest { - private NetworkObject m_ClientSideClientPlayer; - private NetworkObject m_ServerSideClientPlayer; + private NetworkObject m_AuthoritativePlayer; + private NetworkObject m_NonAuthoritativePlayer; - private readonly bool m_TestWithClientNetworkTransform; + private NetworkTransformTestComponent m_AuthoritativeTransform; + private NetworkTransformTestComponent m_NonAuthoritativeTransform; + private NetworkTransformTestComponent m_OwnerTransform; - public NetworkTransformTests(bool testWithHost, bool testWithClientNetworkTransform) + private readonly Authority m_Authority; + + public enum Authority { - m_UseHost = testWithHost; // from test fixture - m_TestWithClientNetworkTransform = testWithClientNetworkTransform; + Server, + Owner + } + + public enum Interpolation + { + DisableInterpolate, + EnableInterpolate + } + + /// + /// Constructor + /// + /// Value is set by TestFixture + /// Value is set by TestFixture + public NetworkTransformTests(HostOrServer testWithHost, Authority authority) + { + m_UseHost = testWithHost == HostOrServer.Host ? true : false; + m_Authority = authority; } protected override int NumberOfClients => 1; protected override void OnCreatePlayerPrefab() { - if (m_TestWithClientNetworkTransform) - { - // m_PlayerPrefab.AddComponent(); - } - else - { - var networkTransform = m_PlayerPrefab.AddComponent(); - networkTransform.Interpolate = false; - } + var networkTransformTestComponent = m_PlayerPrefab.AddComponent(); + networkTransformTestComponent.ServerAuthority = m_Authority == Authority.Server; } protected override void OnServerAndClientsCreated() { -#if NGO_TRANSFORM_DEBUG - // Log assert for writing without authority is a developer log... - // TODO: This is why monolithic test base classes and test helpers are an anti-pattern - this is part of an individual test case setup but is separated from the code verifying it! - m_ServerNetworkManager.LogLevel = LogLevel.Developer; - m_ClientNetworkManagers[0].LogLevel = LogLevel.Developer; -#endif + if (m_EnableVerboseDebug) + { + m_ServerNetworkManager.LogLevel = LogLevel.Developer; + foreach (var clientNetworkManager in m_ClientNetworkManagers) + { + clientNetworkManager.LogLevel = LogLevel.Developer; + } + } } protected override IEnumerator OnServerAndClientsConnected() { // Get the client player representation on both the server and the client side - m_ServerSideClientPlayer = m_PlayerNetworkObjects[m_ServerNetworkManager.LocalClientId][m_ClientNetworkManagers[0].LocalClientId]; - m_ClientSideClientPlayer = m_PlayerNetworkObjects[m_ClientNetworkManagers[0].LocalClientId][m_ClientNetworkManagers[0].LocalClientId]; + var serverSideClientPlayer = m_ServerNetworkManager.ConnectedClients[m_ClientNetworkManagers[0].LocalClientId].PlayerObject; + var clientSideClientPlayer = m_ClientNetworkManagers[0].LocalClient.PlayerObject; + + m_AuthoritativePlayer = m_Authority == Authority.Server ? serverSideClientPlayer : clientSideClientPlayer; + m_NonAuthoritativePlayer = m_Authority == Authority.Server ? clientSideClientPlayer : serverSideClientPlayer; // Get the NetworkTransformTestComponent to make sure the client side is ready before starting test - var otherSideNetworkTransformComponent = m_ClientSideClientPlayer.GetComponent(); + m_AuthoritativeTransform = m_AuthoritativePlayer.GetComponent(); + m_NonAuthoritativeTransform = m_NonAuthoritativePlayer.GetComponent(); + + m_OwnerTransform = m_AuthoritativeTransform.IsOwner ? m_AuthoritativeTransform : m_NonAuthoritativeTransform; // Wait for the client-side to notify it is finished initializing and spawning. - yield return WaitForConditionOrTimeOut(() => otherSideNetworkTransformComponent.ReadyToReceivePositionUpdate == true); + yield return WaitForConditionOrTimeOut(() => m_NonAuthoritativeTransform.ReadyToReceivePositionUpdate == true); + AssertOnTimeout("Timed out waiting for client-side to notify it is ready!"); + + Assert.True(m_AuthoritativeTransform.CanCommitToTransform); + Assert.False(m_NonAuthoritativeTransform.CanCommitToTransform); - Assert.False(s_GlobalTimeoutHelper.TimedOut, "Timed out waiting for client-side to notify it is ready!"); yield return base.OnServerAndClientsConnected(); } - // TODO: rewrite after perms & authority changes - [UnityTest] - public IEnumerator TestAuthoritativeTransformChangeOneAtATime([Values] bool testLocalTransform) + public enum TransformSpace { - // Get the client player's NetworkTransform for both instances - var authoritativeNetworkTransform = m_ServerSideClientPlayer.GetComponent(); - var otherSideNetworkTransform = m_ClientSideClientPlayer.GetComponent(); - - Assert.That(!otherSideNetworkTransform.CanCommitToTransform); - Assert.That(authoritativeNetworkTransform.CanCommitToTransform); - - if (authoritativeNetworkTransform.CanCommitToTransform) - { - authoritativeNetworkTransform.InLocalSpace = testLocalTransform; - } - - if (otherSideNetworkTransform.CanCommitToTransform) - { - otherSideNetworkTransform.InLocalSpace = testLocalTransform; - } - - float approximation = 0.05f; - - // test position - var authPlayerTransform = authoritativeNetworkTransform.transform; - - Assert.AreEqual(Vector3.zero, otherSideNetworkTransform.transform.position, "server side pos should be zero at first"); // sanity check - - authPlayerTransform.position = new Vector3(10, 20, 30); - - yield return WaitForConditionOrTimeOut(() => otherSideNetworkTransform.transform.position.x > approximation); - - Assert.False(s_GlobalTimeoutHelper.TimedOut, $"timeout while waiting for position change! Otherside value {otherSideNetworkTransform.transform.position.x} vs. Approximation {approximation}"); - - Assert.True(new Vector3(10, 20, 30) == otherSideNetworkTransform.transform.position, $"wrong position on ghost, {otherSideNetworkTransform.transform.position}"); // Vector3 already does float approximation with == - - // test rotation - authPlayerTransform.rotation = Quaternion.Euler(45, 40, 35); // using euler angles instead of quaternions directly to really see issues users might encounter - Assert.AreEqual(Quaternion.identity, otherSideNetworkTransform.transform.rotation, "wrong initial value for rotation"); // sanity check - - yield return WaitForConditionOrTimeOut(() => otherSideNetworkTransform.transform.rotation.eulerAngles.x > approximation); - - Assert.False(s_GlobalTimeoutHelper.TimedOut, "timeout while waiting for rotation change"); - - // approximation needed here since eulerAngles isn't super precise. - Assert.LessOrEqual(Math.Abs(45 - otherSideNetworkTransform.transform.rotation.eulerAngles.x), approximation, $"wrong rotation on ghost on x, got {otherSideNetworkTransform.transform.rotation.eulerAngles.x}"); - Assert.LessOrEqual(Math.Abs(40 - otherSideNetworkTransform.transform.rotation.eulerAngles.y), approximation, $"wrong rotation on ghost on y, got {otherSideNetworkTransform.transform.rotation.eulerAngles.y}"); - Assert.LessOrEqual(Math.Abs(35 - otherSideNetworkTransform.transform.rotation.eulerAngles.z), approximation, $"wrong rotation on ghost on z, got {otherSideNetworkTransform.transform.rotation.eulerAngles.z}"); - - // test scale - UnityEngine.Assertions.Assert.AreApproximatelyEqual(1f, otherSideNetworkTransform.transform.lossyScale.x, "wrong initial value for scale"); // sanity check - UnityEngine.Assertions.Assert.AreApproximatelyEqual(1f, otherSideNetworkTransform.transform.lossyScale.y, "wrong initial value for scale"); // sanity check - UnityEngine.Assertions.Assert.AreApproximatelyEqual(1f, otherSideNetworkTransform.transform.lossyScale.z, "wrong initial value for scale"); // sanity check - authPlayerTransform.localScale = new Vector3(2, 3, 4); - - yield return WaitForConditionOrTimeOut(() => otherSideNetworkTransform.transform.lossyScale.x > 1f + approximation); - - Assert.False(s_GlobalTimeoutHelper.TimedOut, "timeout while waiting for scale change"); - - UnityEngine.Assertions.Assert.AreApproximatelyEqual(2f, otherSideNetworkTransform.transform.lossyScale.x, "wrong scale on ghost"); - UnityEngine.Assertions.Assert.AreApproximatelyEqual(3f, otherSideNetworkTransform.transform.lossyScale.y, "wrong scale on ghost"); - UnityEngine.Assertions.Assert.AreApproximatelyEqual(4f, otherSideNetworkTransform.transform.lossyScale.z, "wrong scale on ghost"); - - // todo reparent and test - // todo test all public API + World, + Local } - [UnityTest] - public IEnumerator TestCantChangeTransformFromOtherSideAuthority([Values] bool testClientAuthority) + public enum OverrideState { - // Get the client player's NetworkTransform for both instances - var authoritativeNetworkTransform = m_ServerSideClientPlayer.GetComponent(); - var otherSideNetworkTransform = m_ClientSideClientPlayer.GetComponent(); + Update, + CommitToTransform + } - Assert.AreEqual(Vector3.zero, otherSideNetworkTransform.transform.position, "other side pos should be zero at first"); // sanity check + /// + /// Tests changing all axial values one at a time. + /// These tests are performed: + /// - While in local space and world space + /// - While interpolation is enabled and disabled + /// - Using the TryCommitTransformToServer "override" that can be used + /// from a child derived or external class. + /// + [UnityTest] + public IEnumerator TestAuthoritativeTransformChangeOneAtATime([Values] TransformSpace testLocalTransform, [Values] Interpolation interpolation, [Values] OverrideState overideState) + { + var overrideUpdate = overideState == OverrideState.CommitToTransform; + m_AuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; + m_NonAuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; - otherSideNetworkTransform.transform.position = new Vector3(4, 5, 6); + m_AuthoritativeTransform.InLocalSpace = testLocalTransform == TransformSpace.Local; + + // test position + var authPlayerTransform = overrideUpdate ? m_OwnerTransform.transform : m_AuthoritativeTransform.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) + { + m_OwnerTransform.CommitToTransform(); + } + + yield return WaitForConditionOrTimeOut(PositionsMatch); + AssertOnTimeout($"Timed out waiting for positions to match"); + + // 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) + { + m_OwnerTransform.CommitToTransform(); + } + + yield return WaitForConditionOrTimeOut(RotationsMatch); + AssertOnTimeout($"Timed out waiting for rotations to match"); + + authPlayerTransform.localScale = new Vector3(2, 3, 4); + if (overrideUpdate) + { + m_OwnerTransform.CommitToTransform(); + } + + yield return WaitForConditionOrTimeOut(ScaleValuesMatch); + AssertOnTimeout($"Timed out waiting for scale values to match"); + } + + /// + /// Test to verify nonAuthority cannot change the transform directly + /// + [UnityTest] + public IEnumerator VerifyNonAuthorityCantChangeTransform([Values] Interpolation interpolation) + { + m_AuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; + m_NonAuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; + Assert.AreEqual(Vector3.zero, m_NonAuthoritativeTransform.transform.position, "other side pos should be zero at first"); // sanity check + + m_NonAuthoritativeTransform.transform.position = new Vector3(4, 5, 6); yield return s_DefaultWaitForTick; - Assert.AreEqual(Vector3.zero, otherSideNetworkTransform.transform.position, "got authority error, but other side still moved!"); -#if NGO_TRANSFORM_DEBUG - // We are no longer emitting this warning, and we are banishing tests that rely on console output, so - // needs re-implementation - // TODO: This should be a separate test - verify 1 behavior per test - LogAssert.Expect(LogType.Warning, new Regex(".*without authority detected.*")); -#endif - } + Assert.AreEqual(Vector3.zero, m_NonAuthoritativeTransform.transform.position, "[Position] NonAuthority was able to change the position!"); + var nonAuthorityRotation = m_NonAuthoritativeTransform.transform.rotation; + var originalNonAuthorityEulerRotation = nonAuthorityRotation.eulerAngles; + var nonAuthorityEulerRotation = originalNonAuthorityEulerRotation; + // Verify rotation is not marked dirty when rotated by half of the threshold + nonAuthorityEulerRotation.y += 20.0f; + nonAuthorityRotation.eulerAngles = nonAuthorityEulerRotation; + m_NonAuthoritativeTransform.transform.rotation = nonAuthorityRotation; + yield return s_DefaultWaitForTick; + var nonAuthorityCurrentEuler = m_NonAuthoritativeTransform.transform.rotation.eulerAngles; + Assert.True(originalNonAuthorityEulerRotation.Equals(nonAuthorityCurrentEuler), "[Rotation] NonAuthority was able to change the rotation!"); + + var nonAuthorityScale = m_NonAuthoritativeTransform.transform.localScale; + m_NonAuthoritativeTransform.transform.localScale = nonAuthorityScale * 100; + + yield return s_DefaultWaitForTick; + + Assert.True(nonAuthorityScale.Equals(m_NonAuthoritativeTransform.transform.localScale), "[Scale] NonAuthority was able to change the scale!"); + } /// /// Validates that rotation checks don't produce false positive /// results when rolling over between 0 and 360 degrees /// [UnityTest] - public IEnumerator TestRotationThresholdDeltaCheck() + public IEnumerator TestRotationThresholdDeltaCheck([Values] Interpolation interpolation) { - // Get the client player's NetworkTransform for both instances - var authoritativeNetworkTransform = m_ServerSideClientPlayer.GetComponent(); - var otherSideNetworkTransform = m_ClientSideClientPlayer.GetComponent(); - otherSideNetworkTransform.RotAngleThreshold = authoritativeNetworkTransform.RotAngleThreshold = 5.0f; + m_AuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; + m_NonAuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; - var halfThreshold = authoritativeNetworkTransform.RotAngleThreshold * 0.5001f; - var serverRotation = authoritativeNetworkTransform.transform.rotation; - var serverEulerRotation = serverRotation.eulerAngles; + m_NonAuthoritativeTransform.RotAngleThreshold = m_AuthoritativeTransform.RotAngleThreshold = 5.0f; + + var halfThreshold = m_AuthoritativeTransform.RotAngleThreshold * 0.5001f; + var authorityRotation = m_AuthoritativeTransform.transform.rotation; + var authorityEulerRotation = authorityRotation.eulerAngles; // Verify rotation is not marked dirty when rotated by half of the threshold - serverEulerRotation.y += halfThreshold; - serverRotation.eulerAngles = serverEulerRotation; - authoritativeNetworkTransform.transform.rotation = serverRotation; - var results = authoritativeNetworkTransform.ApplyState(); - Assert.IsFalse(results.isRotationDirty, $"Rotation is dirty when rotation threshold is {authoritativeNetworkTransform.RotAngleThreshold} degrees and only adjusted by {halfThreshold} degrees!"); + authorityEulerRotation.y += halfThreshold; + authorityRotation.eulerAngles = authorityEulerRotation; + m_AuthoritativeTransform.transform.rotation = authorityRotation; + var results = m_AuthoritativeTransform.ApplyState(); + Assert.IsFalse(results.isRotationDirty, $"Rotation is dirty when rotation threshold is {m_AuthoritativeTransform.RotAngleThreshold} degrees and only adjusted by {halfThreshold} degrees!"); yield return s_DefaultWaitForTick; // Verify rotation is marked dirty when rotated by another half threshold value - serverEulerRotation.y += halfThreshold; - serverRotation.eulerAngles = serverEulerRotation; - authoritativeNetworkTransform.transform.rotation = serverRotation; - results = authoritativeNetworkTransform.ApplyState(); - Assert.IsTrue(results.isRotationDirty, $"Rotation was not dirty when rotated by the threshold value: {authoritativeNetworkTransform.RotAngleThreshold} degrees!"); + authorityEulerRotation.y += halfThreshold; + authorityRotation.eulerAngles = authorityEulerRotation; + m_AuthoritativeTransform.transform.rotation = authorityRotation; + results = m_AuthoritativeTransform.ApplyState(); + Assert.IsTrue(results.isRotationDirty, $"Rotation was not dirty when rotated by the threshold value: {m_AuthoritativeTransform.RotAngleThreshold} degrees!"); yield return s_DefaultWaitForTick; //Reset rotation back to zero on all axis - serverRotation.eulerAngles = serverEulerRotation = Vector3.zero; - authoritativeNetworkTransform.transform.rotation = serverRotation; + authorityRotation.eulerAngles = authorityEulerRotation = Vector3.zero; + m_AuthoritativeTransform.transform.rotation = authorityRotation; yield return s_DefaultWaitForTick; // Rotate by 360 minus halfThreshold (which is really just negative halfThreshold) and verify rotation is not marked dirty - serverEulerRotation.y = 360 - halfThreshold; - serverRotation.eulerAngles = serverEulerRotation; - authoritativeNetworkTransform.transform.rotation = serverRotation; - results = authoritativeNetworkTransform.ApplyState(); + authorityEulerRotation.y = 360 - halfThreshold; + authorityRotation.eulerAngles = authorityEulerRotation; + m_AuthoritativeTransform.transform.rotation = authorityRotation; + results = m_AuthoritativeTransform.ApplyState(); - Assert.IsFalse(results.isRotationDirty, $"Rotation is dirty when rotation threshold is {authoritativeNetworkTransform.RotAngleThreshold} degrees and only adjusted by " + - $"{Mathf.DeltaAngle(0, serverEulerRotation.y)} degrees!"); + Assert.IsFalse(results.isRotationDirty, $"Rotation is dirty when rotation threshold is {m_AuthoritativeTransform.RotAngleThreshold} degrees and only adjusted by " + + $"{Mathf.DeltaAngle(0, authorityEulerRotation.y)} degrees!"); - serverEulerRotation.y -= halfThreshold; - serverRotation.eulerAngles = serverEulerRotation; - authoritativeNetworkTransform.transform.rotation = serverRotation; - results = authoritativeNetworkTransform.ApplyState(); + authorityEulerRotation.y -= halfThreshold; + authorityRotation.eulerAngles = authorityEulerRotation; + m_AuthoritativeTransform.transform.rotation = authorityRotation; + results = m_AuthoritativeTransform.ApplyState(); - Assert.IsTrue(results.isRotationDirty, $"Rotation was not dirty when rotated by {Mathf.DeltaAngle(0, serverEulerRotation.y)} degrees!"); + Assert.IsTrue(results.isRotationDirty, $"Rotation was not dirty when rotated by {Mathf.DeltaAngle(0, authorityEulerRotation.y)} degrees!"); //Reset rotation back to zero on all axis - serverRotation.eulerAngles = serverEulerRotation = Vector3.zero; - authoritativeNetworkTransform.transform.rotation = serverRotation; + authorityRotation.eulerAngles = authorityEulerRotation = Vector3.zero; + m_AuthoritativeTransform.transform.rotation = authorityRotation; yield return s_DefaultWaitForTick; - serverEulerRotation.y -= halfThreshold; - serverRotation.eulerAngles = serverEulerRotation; - authoritativeNetworkTransform.transform.rotation = serverRotation; - results = authoritativeNetworkTransform.ApplyState(); - Assert.IsFalse(results.isRotationDirty, $"Rotation is dirty when rotation threshold is {authoritativeNetworkTransform.RotAngleThreshold} degrees and only adjusted by " + - $"{Mathf.DeltaAngle(0, serverEulerRotation.y)} degrees!"); + authorityEulerRotation.y -= halfThreshold; + authorityRotation.eulerAngles = authorityEulerRotation; + m_AuthoritativeTransform.transform.rotation = authorityRotation; + results = m_AuthoritativeTransform.ApplyState(); + Assert.IsFalse(results.isRotationDirty, $"Rotation is dirty when rotation threshold is {m_AuthoritativeTransform.RotAngleThreshold} degrees and only adjusted by " + + $"{Mathf.DeltaAngle(0, authorityEulerRotation.y)} degrees!"); - serverEulerRotation.y -= halfThreshold; - serverRotation.eulerAngles = serverEulerRotation; - authoritativeNetworkTransform.transform.rotation = serverRotation; - results = authoritativeNetworkTransform.ApplyState(); + authorityEulerRotation.y -= halfThreshold; + authorityRotation.eulerAngles = authorityEulerRotation; + m_AuthoritativeTransform.transform.rotation = authorityRotation; + results = m_AuthoritativeTransform.ApplyState(); - Assert.IsTrue(results.isRotationDirty, $"Rotation was not dirty when rotated by {Mathf.DeltaAngle(0, serverEulerRotation.y)} degrees!"); + Assert.IsTrue(results.isRotationDirty, $"Rotation was not dirty when rotated by {Mathf.DeltaAngle(0, authorityEulerRotation.y)} degrees!"); + } + + private bool ValidateBitSetValues(NetworkTransform.NetworkTransformState serverState, NetworkTransform.NetworkTransformState clientState) + { + if (serverState.HasPositionX == clientState.HasPositionX && serverState.HasPositionY == clientState.HasPositionY && serverState.HasPositionZ == clientState.HasPositionZ && + serverState.HasRotAngleX == clientState.HasRotAngleX && serverState.HasRotAngleY == clientState.HasRotAngleY && serverState.HasRotAngleZ == clientState.HasRotAngleZ && + serverState.HasScaleX == clientState.HasScaleX && serverState.HasScaleY == clientState.HasScaleY && serverState.HasScaleZ == clientState.HasScaleZ) + { + return true; + } + return false; + } + + /// + /// Test to make sure that the bitset value is updated properly + /// + [UnityTest] + public IEnumerator TestBitsetValue([Values] Interpolation interpolation) + { + m_AuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; + m_NonAuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; + m_NonAuthoritativeTransform.RotAngleThreshold = m_AuthoritativeTransform.RotAngleThreshold = 0.1f; + yield return s_DefaultWaitForTick; + + m_AuthoritativeTransform.transform.rotation = Quaternion.Euler(1, 2, 3); + var serverLastSentState = m_AuthoritativeTransform.GetLastSentState(); + var clientReplicatedState = m_NonAuthoritativeTransform.ReplicatedNetworkState.Value; + yield return WaitForConditionOrTimeOut(() => ValidateBitSetValues(serverLastSentState, clientReplicatedState)); + AssertOnTimeout($"Timed out waiting for Authoritative Bitset state to equal NonAuthoritative replicated Bitset state!"); + + yield return WaitForConditionOrTimeOut(RotationsMatch); + AssertOnTimeout($"[Timed-Out] Authoritative rotation {m_AuthoritativeTransform.transform.rotation.eulerAngles} != Non-Authoritative rotation {m_NonAuthoritativeTransform.transform.rotation.eulerAngles}"); + } + + private float m_DetectedPotentialInterpolatedTeleport; + + /// + /// The tests teleporting with and without interpolation + /// + [UnityTest] + public IEnumerator TeleportTest([Values] Interpolation interpolation) + { + m_AuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; + m_NonAuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; + var authTransform = m_AuthoritativeTransform.transform; + var nonAuthPosition = m_NonAuthoritativeTransform.transform.position; + var currentTick = m_AuthoritativeTransform.NetworkManager.ServerTime.Tick; + m_DetectedPotentialInterpolatedTeleport = 0.0f; + var teleportDestination = new Vector3(100.00f, 100.00f, 100.00f); + var targetDistance = Mathf.Abs(Vector3.Distance(nonAuthPosition, teleportDestination)); + m_AuthoritativeTransform.Teleport(new Vector3(100.00f, 100.00f, 100.00f), authTransform.rotation, authTransform.localScale); + yield return WaitForConditionOrTimeOut(() => TeleportPositionMatches(nonAuthPosition)); + + AssertOnTimeout($"[Timed-Out][Teleport] Timed out waiting for NonAuthoritative position to !"); + Assert.IsTrue(m_DetectedPotentialInterpolatedTeleport == 0.0f, $"Detected possible interpolation on non-authority side! NonAuthority distance: {m_DetectedPotentialInterpolatedTeleport} | Target distance: {targetDistance}"); + } + + + /// + /// This test validates the method + /// usage for the non-authoritative side. It will either be the owner or the server making/requesting state changes. + /// This validates that: + /// - The owner authoritative mode can still be controlled by the server (i.e. owner authoritative with server authority override capabilities) + /// - The server authoritative mode can still be directed by the client owner. + /// + /// + /// This also tests that the original server authoritative model with client-owner driven NetworkTransforms is preserved. + /// + [UnityTest] + public IEnumerator NonAuthorityOwnerSettingStateTest([Values] Interpolation interpolation) + { + var interpolate = interpolation == Interpolation.EnableInterpolate; + m_AuthoritativeTransform.Interpolate = interpolate; + m_NonAuthoritativeTransform.Interpolate = interpolate; + m_NonAuthoritativeTransform.RotAngleThreshold = m_AuthoritativeTransform.RotAngleThreshold = 0.1f; + + // Test one parameter at a time first + var newPosition = new Vector3(125f, 35f, 65f); + var newRotation = Quaternion.Euler(1, 2, 3); + var newScale = new Vector3(2.0f, 2.0f, 2.0f); + m_NonAuthoritativeTransform.SetState(newPosition, null, null, interpolate); + yield return WaitForConditionOrTimeOut(() => PositionsMatchesValue(newPosition)); + AssertOnTimeout($"Timed out waiting for non-authoritative position state request to be applied!"); + Assert.True(Aproximately(newPosition, m_AuthoritativeTransform.transform.position), "Authoritative position does not match!"); + Assert.True(Aproximately(newPosition, m_NonAuthoritativeTransform.transform.position), "Non-Authoritative position does not match!"); + + m_NonAuthoritativeTransform.SetState(null, newRotation, null, interpolate); + yield return WaitForConditionOrTimeOut(() => RotationMatchesValue(newRotation.eulerAngles)); + AssertOnTimeout($"Timed out waiting for non-authoritative rotation state request to be applied!"); + Assert.True(Aproximately(newRotation.eulerAngles, m_AuthoritativeTransform.transform.rotation.eulerAngles), "Authoritative rotation does not match!"); + Assert.True(Aproximately(newRotation.eulerAngles, m_NonAuthoritativeTransform.transform.rotation.eulerAngles), "Non-Authoritative rotation does not match!"); + + m_NonAuthoritativeTransform.SetState(null, null, newScale, interpolate); + yield return WaitForConditionOrTimeOut(() => ScaleMatchesValue(newScale)); + AssertOnTimeout($"Timed out waiting for non-authoritative scale state request to be applied!"); + Assert.True(Aproximately(newScale, m_AuthoritativeTransform.transform.localScale), "Authoritative scale does not match!"); + Assert.True(Aproximately(newScale, m_NonAuthoritativeTransform.transform.localScale), "Non-Authoritative scale does not match!"); + + // Test all parameters at once + newPosition = new Vector3(55f, 95f, -25f); + newRotation = Quaternion.Euler(20, 5, 322); + newScale = new Vector3(0.5f, 0.5f, 0.5f); + + m_NonAuthoritativeTransform.SetState(newPosition, newRotation, newScale, interpolate); + yield return WaitForConditionOrTimeOut(() => PositionRotationScaleMatches(newPosition, newRotation.eulerAngles, newScale)); + AssertOnTimeout($"Timed out waiting for non-authoritative position, rotation, and scale state request to be applied!"); + Assert.True(Aproximately(newPosition, m_AuthoritativeTransform.transform.position), "Authoritative position does not match!"); + Assert.True(Aproximately(newPosition, m_NonAuthoritativeTransform.transform.position), "Non-Authoritative position does not match!"); + Assert.True(Aproximately(newRotation.eulerAngles, m_AuthoritativeTransform.transform.rotation.eulerAngles), "Authoritative rotation does not match!"); + Assert.True(Aproximately(newRotation.eulerAngles, m_NonAuthoritativeTransform.transform.rotation.eulerAngles), "Non-Authoritative rotation does not match!"); + Assert.True(Aproximately(newScale, m_AuthoritativeTransform.transform.localScale), "Authoritative scale does not match!"); + Assert.True(Aproximately(newScale, m_NonAuthoritativeTransform.transform.localScale), "Non-Authoritative scale does not match!"); + } + + private bool Aproximately(float x, float y) + { + return Mathf.Abs(x - y) <= k_AproximateDeltaVariance; + } + + private bool Aproximately(Vector3 a, Vector3 b) + { + return Mathf.Abs(a.x - b.x) <= k_AproximateDeltaVariance && + Mathf.Abs(a.y - b.y) <= k_AproximateDeltaVariance && + Mathf.Abs(a.z - b.z) <= k_AproximateDeltaVariance; + } + + private const float k_AproximateDeltaVariance = 0.01f; + private bool PositionsMatchesValue(Vector3 positionToMatch) + { + var authorityPosition = m_AuthoritativeTransform.transform.position; + var nonAuthorityPosition = m_NonAuthoritativeTransform.transform.position; + var auhtorityIsEqual = Aproximately(authorityPosition, positionToMatch); + var nonauthorityIsEqual = Aproximately(nonAuthorityPosition, positionToMatch); + + if (!auhtorityIsEqual) + { + VerboseDebug($"Authority position {authorityPosition} != position to match: {positionToMatch}!"); + } + if (!nonauthorityIsEqual) + { + VerboseDebug($"NonAuthority position {nonAuthorityPosition} != position to match: {positionToMatch}!"); + } + return auhtorityIsEqual && nonauthorityIsEqual; + } + + private bool RotationMatchesValue(Vector3 rotationEulerToMatch) + { + var authorityRotationEuler = m_AuthoritativeTransform.transform.rotation.eulerAngles; + var nonAuthorityRotationEuler = m_NonAuthoritativeTransform.transform.rotation.eulerAngles; + var auhtorityIsEqual = Aproximately(authorityRotationEuler, rotationEulerToMatch); + var nonauthorityIsEqual = Aproximately(nonAuthorityRotationEuler, rotationEulerToMatch); + + if (!auhtorityIsEqual) + { + VerboseDebug($"Authority rotation {authorityRotationEuler} != rotation to match: {rotationEulerToMatch}!"); + } + if (!nonauthorityIsEqual) + { + VerboseDebug($"NonAuthority position {nonAuthorityRotationEuler} != rotation to match: {rotationEulerToMatch}!"); + } + return auhtorityIsEqual && nonauthorityIsEqual; + } + + private bool ScaleMatchesValue(Vector3 scaleToMatch) + { + var authorityScale = m_AuthoritativeTransform.transform.localScale; + var nonAuthorityScale = m_NonAuthoritativeTransform.transform.localScale; + var auhtorityIsEqual = Aproximately(authorityScale, scaleToMatch); + var nonauthorityIsEqual = Aproximately(nonAuthorityScale, scaleToMatch); + + if (!auhtorityIsEqual) + { + VerboseDebug($"Authority scale {authorityScale} != scale to match: {scaleToMatch}!"); + } + if (!nonauthorityIsEqual) + { + VerboseDebug($"NonAuthority scale {nonAuthorityScale} != scale to match: {scaleToMatch}!"); + } + return auhtorityIsEqual && nonauthorityIsEqual; + } + + + private bool TeleportPositionMatches(Vector3 nonAuthorityOriginalPosition) + { + var nonAuthorityPosition = m_NonAuthoritativeTransform.transform.position; + var authorityPosition = m_AuthoritativeTransform.transform.position; + var targetDistance = Mathf.Abs(Vector3.Distance(nonAuthorityOriginalPosition, authorityPosition)); + var nonAuthorityCurrentDistance = Mathf.Abs(Vector3.Distance(nonAuthorityPosition, nonAuthorityOriginalPosition)); + if (!Aproximately(targetDistance, nonAuthorityCurrentDistance)) + { + if (nonAuthorityCurrentDistance >= 0.15f * targetDistance && nonAuthorityCurrentDistance <= 0.75f * targetDistance) + { + m_DetectedPotentialInterpolatedTeleport = nonAuthorityCurrentDistance; + } + return false; + } + var xIsEqual = Aproximately(authorityPosition.x, nonAuthorityPosition.x); + var yIsEqual = Aproximately(authorityPosition.y, nonAuthorityPosition.y); + var zIsEqual = Aproximately(authorityPosition.z, nonAuthorityPosition.z); + if (!xIsEqual || !yIsEqual || !zIsEqual) + { + VerboseDebug($"Authority position {authorityPosition} != NonAuthority position {nonAuthorityPosition}"); + } + return xIsEqual && yIsEqual && zIsEqual; ; + } + + private bool PositionRotationScaleMatches(Vector3 position, Vector3 eulerRotation, Vector3 scale) + { + return PositionsMatchesValue(position) && RotationMatchesValue(eulerRotation) && ScaleMatchesValue(scale); + } + + private bool RotationsMatch() + { + var authorityEulerRotation = m_AuthoritativeTransform.transform.rotation.eulerAngles; + var nonAuthorityEulerRotation = m_NonAuthoritativeTransform.transform.rotation.eulerAngles; + var xIsEqual = Aproximately(authorityEulerRotation.x, nonAuthorityEulerRotation.x); + var yIsEqual = Aproximately(authorityEulerRotation.y, nonAuthorityEulerRotation.y); + var zIsEqual = Aproximately(authorityEulerRotation.z, nonAuthorityEulerRotation.z); + if (!xIsEqual || !yIsEqual || !zIsEqual) + { + VerboseDebug($"Authority rotation {authorityEulerRotation} != NonAuthority rotation {nonAuthorityEulerRotation}"); + } + return xIsEqual && yIsEqual && zIsEqual; + } + + private bool PositionsMatch() + { + var authorityPosition = m_AuthoritativeTransform.transform.position; + var nonAuthorityPosition = m_NonAuthoritativeTransform.transform.position; + var xIsEqual = Aproximately(authorityPosition.x, nonAuthorityPosition.x); + var yIsEqual = Aproximately(authorityPosition.y, nonAuthorityPosition.y); + var zIsEqual = Aproximately(authorityPosition.z, nonAuthorityPosition.z); + if (!xIsEqual || !yIsEqual || !zIsEqual) + { + VerboseDebug($"Authority position {authorityPosition} != NonAuthority position {nonAuthorityPosition}"); + } + return xIsEqual && yIsEqual && zIsEqual; + } + + private bool ScaleValuesMatch() + { + var authorityScale = m_AuthoritativeTransform.transform.localScale; + var nonAuthorityScale = m_NonAuthoritativeTransform.transform.localScale; + var xIsEqual = Aproximately(authorityScale.x, nonAuthorityScale.x); + var yIsEqual = Aproximately(authorityScale.y, nonAuthorityScale.y); + var zIsEqual = Aproximately(authorityScale.z, nonAuthorityScale.z); + if (!xIsEqual || !yIsEqual || !zIsEqual) + { + VerboseDebug($"Authority scale {authorityScale} != NonAuthority scale {nonAuthorityScale}"); + } + return xIsEqual && yIsEqual && zIsEqual; } - /* - * ownership change - * test teleport with interpolation - * test teleport without interpolation - * test dynamic spawning - */ protected override IEnumerator OnTearDown() { - UnityEngine.Object.DestroyImmediate(m_PlayerPrefab); + m_EnableVerboseDebug = false; + Object.DestroyImmediate(m_PlayerPrefab); yield return base.OnTearDown(); } } diff --git a/Tests/Runtime/NetworkVarBufferCopyTest.cs b/Tests/Runtime/NetworkVarBufferCopyTest.cs index b927cb1..228b230 100644 --- a/Tests/Runtime/NetworkVarBufferCopyTest.cs +++ b/Tests/Runtime/NetworkVarBufferCopyTest.cs @@ -15,17 +15,6 @@ namespace Unity.Netcode.RuntimeTests public bool FieldWritten; public bool DeltaRead; public bool FieldRead; - public bool Dirty = false; - - public override void ResetDirty() - { - Dirty = false; - } - - public override bool IsDirty() - { - return Dirty; - } public override void WriteDelta(FastBufferWriter writer) { @@ -138,12 +127,12 @@ namespace Unity.Netcode.RuntimeTests Assert.IsFalse(s_GlobalTimeoutHelper.TimedOut, "Timed out waiting for client side DummyNetBehaviour to register it was spawned!"); // Check that FieldWritten is written when dirty - serverComponent.NetVar.Dirty = true; + serverComponent.NetVar.SetDirty(true); yield return s_DefaultWaitForTick; Assert.True(serverComponent.NetVar.FieldWritten); // Check that DeltaWritten is written when dirty - serverComponent.NetVar.Dirty = true; + serverComponent.NetVar.SetDirty(true); yield return s_DefaultWaitForTick; Assert.True(serverComponent.NetVar.DeltaWritten); diff --git a/Tests/Runtime/NetworkVariableTests.cs b/Tests/Runtime/NetworkVariableTests.cs index fee8922..f8b7339 100644 --- a/Tests/Runtime/NetworkVariableTests.cs +++ b/Tests/Runtime/NetworkVariableTests.cs @@ -14,6 +14,7 @@ namespace Unity.Netcode.RuntimeTests { public NetworkVariable OwnerWritable_Position = new NetworkVariable(Vector3.one, NetworkVariableBase.DefaultReadPerm, NetworkVariableWritePermission.Owner); public NetworkVariable ServerWritable_Position = new NetworkVariable(Vector3.one, NetworkVariableBase.DefaultReadPerm, NetworkVariableWritePermission.Server); + public NetworkVariable OwnerReadWrite_Position = new NetworkVariable(Vector3.one, NetworkVariableReadPermission.Owner, NetworkVariableWritePermission.Owner); } [TestFixtureSource(nameof(TestDataSource))] @@ -104,6 +105,42 @@ namespace Unity.Netcode.RuntimeTests return true; } + private bool CheckOwnerReadWriteAreEqualOnOwnerAndServer() + { + var testObjServer = m_ServerNetworkManager.SpawnManager.SpawnedObjects[m_TestObjId]; + var testCompServer = testObjServer.GetComponent(); + foreach (var clientNetworkManager in m_ClientNetworkManagers) + { + var testObjClient = clientNetworkManager.SpawnManager.SpawnedObjects[m_TestObjId]; + var testCompClient = testObjClient.GetComponent(); + if (testObjServer.OwnerClientId == testObjClient.OwnerClientId && + testCompServer.OwnerReadWrite_Position.Value == testCompClient.ServerWritable_Position.Value && + testCompServer.OwnerReadWrite_Position.ReadPerm == testCompClient.ServerWritable_Position.ReadPerm && + testCompServer.OwnerReadWrite_Position.WritePerm == testCompClient.ServerWritable_Position.WritePerm) + { + return true; + } + } + return false; + } + + private bool CheckOwnerReadWriteAreNotEqualOnNonOwnerClients(NetVarPermTestComp ownerReadWriteObject) + { + foreach (var clientNetworkManager in m_ClientNetworkManagers) + { + var testObjClient = clientNetworkManager.SpawnManager.SpawnedObjects[m_TestObjId]; + var testCompClient = testObjClient.GetComponent(); + if (testObjClient.OwnerClientId != ownerReadWriteObject.OwnerClientId || + ownerReadWriteObject.OwnerReadWrite_Position.Value == testCompClient.ServerWritable_Position.Value || + ownerReadWriteObject.OwnerReadWrite_Position.ReadPerm != testCompClient.ServerWritable_Position.ReadPerm || + ownerReadWriteObject.OwnerReadWrite_Position.WritePerm != testCompClient.ServerWritable_Position.WritePerm) + { + return false; + } + } + return true; + } + [UnityTest] public IEnumerator ServerChangesOwnerWritableNetVar() { @@ -164,6 +201,44 @@ namespace Unity.Netcode.RuntimeTests yield return WaitForOwnerWritableAreEqualOnAll(); } + /// + /// This tests the scenario where a client owner has both read and write + /// permissions set. The server should be the only instance that can read + /// the NetworkVariable. ServerCannotChangeOwnerWritableNetVar performs + /// the same check to make sure the server cannot write to a client owner + /// NetworkVariable with owner write permissions. + /// + [UnityTest] + public IEnumerator ClientOwnerWithReadWriteChangesNetVar() + { + yield return WaitForOwnerWritableAreEqualOnAll(); + + var testObjServer = m_ServerNetworkManager.SpawnManager.SpawnedObjects[m_TestObjId]; + + int clientManagerIndex = m_ClientNetworkManagers.Length - 1; + var newOwnerClientId = m_ClientNetworkManagers[clientManagerIndex].LocalClientId; + testObjServer.ChangeOwnership(newOwnerClientId); + yield return NetcodeIntegrationTestHelpers.WaitForTicks(m_ServerNetworkManager, 2); + + yield return WaitForOwnerWritableAreEqualOnAll(); + + var testObjClient = m_ClientNetworkManagers[clientManagerIndex].SpawnManager.SpawnedObjects[m_TestObjId]; + var testCompClient = testObjClient.GetComponent(); + + var oldValue = testCompClient.OwnerReadWrite_Position.Value; + var newValue = oldValue + new Vector3(Random.Range(0, 100.0f), Random.Range(0, 100.0f), Random.Range(0, 100.0f)); + + testCompClient.OwnerWritable_Position.Value = newValue; + yield return WaitForPositionsAreEqual(testCompClient.OwnerWritable_Position, newValue); + + // Verify the client owner and server match + yield return CheckOwnerReadWriteAreEqualOnOwnerAndServer(); + + // Verify the non-owner clients do not have the same Value but do have the same permissions + yield return CheckOwnerReadWriteAreNotEqualOnNonOwnerClients(testCompClient); + } + + [UnityTest] public IEnumerator ClientCannotChangeServerWritableNetVar() { diff --git a/Tests/Runtime/OwnerModifiedTests.cs b/Tests/Runtime/OwnerModifiedTests.cs new file mode 100644 index 0000000..52ddb2b --- /dev/null +++ b/Tests/Runtime/OwnerModifiedTests.cs @@ -0,0 +1,103 @@ +using System.Collections; +using System.Collections.Generic; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; +using Unity.Netcode.TestHelpers.Runtime; + +namespace Unity.Netcode.RuntimeTests +{ + // This is a bit of a quirky test. + // Addresses MTT-4386 #2109 + // Where the NetworkVariable updates would be repeated on some clients. + // The twist comes fom the updates needing to happens very specifically for the issue to repro in tests + + public class OwnerModifiedObject : NetworkBehaviour, INetworkUpdateSystem + { + public NetworkList MyNetworkList; + + static internal int Updates = 0; + + private void Awake() + { + MyNetworkList = new NetworkList(new List(), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); + MyNetworkList.OnListChanged += Changed; + } + + public void Changed(NetworkListEvent listEvent) + { + var expected = 0; + var listString = ""; + foreach (var i in MyNetworkList) + { + Assert.AreEqual(i, expected); + expected++; + listString += i.ToString(); + } + Debug.Log($"[{NetworkManager.LocalClientId}] Value changed to {listString}"); + Updates++; + } + + public bool AddValues; + + public NetworkUpdateStage NetworkUpdateStageToCheck; + + private int m_ValueToUpdate; + + public void NetworkUpdate(NetworkUpdateStage updateStage) + { + if (updateStage == NetworkUpdateStageToCheck) + { + if (AddValues) + { + MyNetworkList.Add(m_ValueToUpdate++); + AddValues = false; + } + } + } + + public override void OnDestroy() + { + NetworkUpdateLoop.UnregisterAllNetworkUpdates(this); + base.OnDestroy(); + } + + public void InitializeLastCient() + { + NetworkUpdateLoop.RegisterAllNetworkUpdates(this); + } + } + + public class OwnerModifiedTests : NetcodeIntegrationTest + { + protected override int NumberOfClients => 2; + + protected override void OnCreatePlayerPrefab() + { + m_PlayerPrefab.AddComponent(); + } + + [UnityTest] + public IEnumerator OwnerModifiedTest() + { + // We use this to assure we are the "last client" connected. + yield return CreateAndStartNewClient(); + var ownerModLastClient = m_ClientNetworkManagers[2].LocalClient.PlayerObject.GetComponent(); + ownerModLastClient.InitializeLastCient(); + + // Run through all update loops setting the value once every 5 frames + foreach (var updateLoopType in System.Enum.GetValues(typeof(NetworkUpdateStage))) + { + ownerModLastClient.NetworkUpdateStageToCheck = (NetworkUpdateStage)updateLoopType; + Debug.Log($"Testing Update Stage: {ownerModLastClient.NetworkUpdateStageToCheck}"); + ownerModLastClient.AddValues = true; + yield return NetcodeIntegrationTestHelpers.WaitForTicks(m_ServerNetworkManager, 5); + } + + yield return NetcodeIntegrationTestHelpers.WaitForTicks(m_ServerNetworkManager, 5); + + // We'll have at least one update per stage per client, if all goes well. + Assert.True(OwnerModifiedObject.Updates > 20); + } + } +} diff --git a/Tests/Runtime/OwnerModifiedTests.cs.meta b/Tests/Runtime/OwnerModifiedTests.cs.meta new file mode 100644 index 0000000..700bc20 --- /dev/null +++ b/Tests/Runtime/OwnerModifiedTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 419d83ebac7544ea9b0a9d5c3eab2c71 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/TransformInterpolationTests.cs b/Tests/Runtime/TransformInterpolationTests.cs index 2fca8ea..8ed5b78 100644 --- a/Tests/Runtime/TransformInterpolationTests.cs +++ b/Tests/Runtime/TransformInterpolationTests.cs @@ -10,21 +10,19 @@ 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 bool CheckPosition; public bool IsMoving; public bool IsFixed; private void Update() { - // Since the local position is transformed from local to global and vice-versa on the server and client - // it may accumulate some error. We allow an error of 0.01 over the range of 1000 used in this test. - // This requires precision to 5 digits, so it doesn't weaken the test, while preventing spurious failures - const float maxRoundingError = 0.01f; - // Check the position of the nested object on the client if (CheckPosition) { - if (transform.position.y < -maxRoundingError || transform.position.y > 100.0f + maxRoundingError) + 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"); } @@ -65,7 +63,8 @@ namespace Unity.Netcode.RuntimeTests protected override void OnServerAndClientsCreated() { m_PrefabToSpawn = CreateNetworkObjectPrefab("InterpTestObject"); - m_PrefabToSpawn.AddComponent(); + var networkTransform = m_PrefabToSpawn.AddComponent(); + networkTransform.PositionThreshold = TransformInterpolationObject.MinThreshold; m_PrefabToSpawn.AddComponent(); } @@ -85,8 +84,6 @@ namespace Unity.Netcode.RuntimeTests m_SpawnedObjectOnClient = s_GlobalNetworkObjects[clientId][m_SpawnedAsNetworkObject.NetworkObjectId]; // make sure the objects are set with the right network manager m_SpawnedObjectOnClient.NetworkManagerOwner = m_ClientNetworkManagers[0]; - - } [UnityTest] diff --git a/Tests/Runtime/Transports/UnityTransportTests.cs b/Tests/Runtime/Transports/UnityTransportTests.cs index 5dec849..481366e 100644 --- a/Tests/Runtime/Transports/UnityTransportTests.cs +++ b/Tests/Runtime/Transports/UnityTransportTests.cs @@ -457,5 +457,26 @@ namespace Unity.Netcode.RuntimeTests yield return null; } + + [UnityTest] + public IEnumerator ReliablePayloadsCanBeLargerThanMaximum() + { + InitializeTransport(out m_Server, out m_ServerEvents); + InitializeTransport(out m_Client1, out m_Client1Events); + + m_Server.StartServer(); + m_Client1.StartClient(); + + yield return WaitForNetworkEvent(NetworkEvent.Connect, m_Client1Events); + + var payloadSize = UnityTransport.InitialMaxPayloadSize + 1; + var data = new ArraySegment(new byte[payloadSize]); + + m_Server.Send(m_Client1.ServerClientId, data, NetworkDelivery.Reliable); + + yield return WaitForNetworkEvent(NetworkEvent.Data, m_Client1Events); + + yield return null; + } } } diff --git a/Tests/Runtime/com.unity.netcode.runtimetests.asmdef b/Tests/Runtime/com.unity.netcode.runtimetests.asmdef index 45ae434..fbb6f71 100644 --- a/Tests/Runtime/com.unity.netcode.runtimetests.asmdef +++ b/Tests/Runtime/com.unity.netcode.runtimetests.asmdef @@ -16,6 +16,9 @@ "optionalUnityReferences": [ "TestAssemblies" ], + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], "versionDefines": [ { "name": "com.unity.multiplayer.tools", diff --git a/package.json b/package.json index 70e4ac7..06a9a9a 100644 --- a/package.json +++ b/package.json @@ -2,22 +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.0", + "version": "1.0.1", "unity": "2020.3", "dependencies": { "com.unity.nuget.mono-cecil": "1.10.1", - "com.unity.transport": "1.1.0" - }, - "_upm": { - "changelog": "### Changed\n\n- Changed version to 1.0.0. (#2046)" + "com.unity.transport": "1.2.0" }, "upmCi": { - "footprint": "382d762a40cdcb42ebaf495e373effb00362baf1" + "footprint": "8824c99a21c438135052b8a8d42b6a8cb865bea3" }, "repository": { "url": "https://github.com/Unity-Technologies/com.unity.netcode.gameobjects.git", "type": "git", - "revision": "fddb7cd920e1db9e49d44846d7121e38f59bd137" + "revision": "ce1ab3ca9495caf3f906d8ca5459677614214837" }, "samples": [ {