This repository has been archived on 2025-04-22. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs
Unity Technologies 1e7078c160 com.unity.netcode.gameobjects@1.1.0
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

Additional documentation and release notes are available at [Multiplayer Documentation](https://docs-multiplayer.unity3d.com).

## [1.1.0] - 2022-10-21

### Added

- Added `NetworkManager.IsApproved` flag that is set to `true` a client has been approved.(#2261)
- `UnityTransport` now provides a way to set the Relay server data directly from the `RelayServerData` structure (provided by the Unity Transport package) throuh its `SetRelayServerData` method. This allows making use of the new APIs in UTP 1.3 that simplify integration of the Relay SDK. (#2235)
- IPv6 is now supported for direct connections when using `UnityTransport`. (#2232)
- Added WebSocket support when using UTP 2.0 with `UseWebSockets` property in the `UnityTransport` component of the `NetworkManager` allowing to pick WebSockets for communication. When building for WebGL, this selection happens automatically. (#2201)
- Added position, rotation, and scale to the `ParentSyncMessage` which provides users the ability to specify the final values on the server-side when `OnNetworkObjectParentChanged` is invoked just before the message is created (when the `Transform` values are applied to the message). (#2146)
- Added `NetworkObject.TryRemoveParent` method for convenience purposes opposed to having to cast null to either `GameObject` or `NetworkObject`. (#2146)

### Changed

- Updated `UnityTransport` dependency on `com.unity.transport` to 1.3.0. (#2231)
- The send queues of `UnityTransport` are now dynamically-sized. This means that there shouldn't be any need anymore to tweak the 'Max Send Queue Size' value. In fact, this field is now removed from the inspector and will not be serialized anymore. It is still possible to set it manually using the `MaxSendQueueSize` property, but it is not recommended to do so aside from some specific needs (e.g. limiting the amount of memory used by the send queues in very constrained environments). (#2212)
- As a consequence of the above change, the `UnityTransport.InitialMaxSendQueueSize` field is now deprecated. There is no default value anymore since send queues are dynamically-sized. (#2212)
- The debug simulator in `UnityTransport` is now non-deterministic. Its random number generator used to be seeded with a constant value, leading to the same pattern of packet drops, delays, and jitter in every run. (#2196)
- `NetworkVariable<>` now supports managed `INetworkSerializable` types, as well as other managed types with serialization/deserialization delegates registered to `UserNetworkVariableSerialization<T>.WriteValue` and `UserNetworkVariableSerialization<T>.ReadValue` (#2219)
- `NetworkVariable<>` and `BufferSerializer<BufferSerializerReader>` now deserialize `INetworkSerializable` types in-place, rather than constructing new ones. (#2219)

### Fixed

- Fixed `NetworkManager.ApprovalTimeout` will not timeout due to slower client synchronization times as it now uses the added `NetworkManager.IsApproved` flag to determined if the client has been approved or not.(#2261)
- Fixed issue caused when changing ownership of objects hidden to some clients (#2242)
- Fixed issue where an in-scene placed NetworkObject would not invoke NetworkBehaviour.OnNetworkSpawn if the GameObject was disabled when it was despawned. (#2239)
- Fixed issue where clients were not rebuilding the `NetworkConfig` hash value for each unique connection request. (#2226)
- Fixed the issue where player objects were not taking the `DontDestroyWithOwner` property into consideration when a client disconnected. (#2225)
- Fixed issue where `SceneEventProgress` would not complete if a client late joins while it is still in progress. (#2222)
- Fixed issue where `SceneEventProgress` would not complete if a client disconnects. (#2222)
- Fixed issues with detecting if a `SceneEventProgress` has timed out. (#2222)
- Fixed issue #1924 where `UnityTransport` would fail to restart after a first failure (even if what caused the initial failure was addressed). (#2220)
- Fixed issue where `NetworkTransform.SetStateServerRpc` and `NetworkTransform.SetStateClientRpc` were not honoring local vs world space settings when applying the position and rotation. (#2203)
- Fixed ILPP `TypeLoadException` on WebGL on MacOS Editor and potentially other platforms. (#2199)
- Implicit conversion of NetworkObjectReference to GameObject will now return null instead of throwing an exception if the referenced object could not be found (i.e., was already despawned) (#2158)
- Fixed warning resulting from a stray NetworkAnimator.meta file (#2153)
- Fixed Connection Approval Timeout not working client side. (#2164)
- Fixed issue where the `WorldPositionStays` parenting parameter was not being synchronized with clients. (#2146)
- Fixed issue where parented in-scene placed `NetworkObject`s would fail for late joining clients. (#2146)
- Fixed issue where scale was not being synchronized which caused issues with nested parenting and scale when `WorldPositionStays` was true. (#2146)
- Fixed issue with `NetworkTransform.ApplyTransformToNetworkStateWithInfo` where it was not honoring axis sync settings when `NetworkTransformState.IsTeleportingNextFrame` was true. (#2146)
- Fixed issue with `NetworkTransform.TryCommitTransformToServer` where it was not honoring the `InLocalSpace` setting. (#2146)
- Fixed ClientRpcs always reporting in the profiler view as going to all clients, even when limited to a subset of clients by `ClientRpcParams`. (#2144)
- Fixed RPC codegen failing to choose the correct extension methods for `FastBufferReader` and `FastBufferWriter` when the parameters were a generic type (i.e., List<int>) and extensions for multiple instantiations of that type have been defined (i.e., List<int> and List<string>) (#2142)
- Fixed the issue where running a server (i.e. not host) the second player would not receive updates (unless a third player joined). (#2127)
- Fixed issue where late-joining client transition synchronization could fail when more than one transition was occurring.(#2127)
- Fixed throwing an exception in `OnNetworkUpdate` causing other `OnNetworkUpdate` calls to not be executed. (#1739)
- Fixed synchronization when Time.timeScale is set to 0. This changes timing update to use unscaled deltatime. Now network updates rate are independent from the local time scale. (#2171)
- Fixed not sending all NetworkVariables to all clients when a client connects to a server. (#1987)
- Fixed IsOwner/IsOwnedByServer being wrong on the server after calling RemoveOwnership (#2211)
2022-10-21 00:00:00 +00:00

953 lines
48 KiB
C#

using System.Collections;
using System.Collections.Generic;
using Unity.Netcode.Components;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
using Unity.Netcode.TestHelpers.Runtime;
namespace Unity.Netcode.RuntimeTests
{
/// <summary>
/// Helper component for all NetworkTransformTests
/// </summary>
public class NetworkTransformTestComponent : NetworkTransform
{
public bool ServerAuthority;
public bool ReadyToReceivePositionUpdate = false;
protected override bool OnIsServerAuthoritative()
{
return ServerAuthority;
}
public override void OnNetworkSpawn()
{
base.OnNetworkSpawn();
ReadyToReceivePositionUpdate = true;
}
public void CommitToTransform()
{
TryCommitTransformToServer(transform, NetworkManager.LocalTime.Time);
}
public (bool isDirty, bool isPositionDirty, bool isRotationDirty, bool isScaleDirty) ApplyState()
{
var transformState = ApplyLocalNetworkState(transform);
return (transformState.IsDirty, transformState.HasPositionChange, transformState.HasRotAngleChange, transformState.HasScaleChange);
}
}
/// <summary>
/// Helper component for NetworkTransform parenting tests
/// </summary>
public class ChildObjectComponent : NetworkBehaviour
{
public readonly static List<ChildObjectComponent> Instances = new List<ChildObjectComponent>();
public static ChildObjectComponent ServerInstance { get; internal set; }
public readonly static Dictionary<ulong, NetworkObject> ClientInstances = new Dictionary<ulong, NetworkObject>();
public static void Reset()
{
ServerInstance = null;
ClientInstances.Clear();
Instances.Clear();
}
public override void OnNetworkSpawn()
{
if (IsServer)
{
ServerInstance = this;
}
else
{
ClientInstances.Add(NetworkManager.LocalClientId, NetworkObject);
}
Instances.Add(this);
base.OnNetworkSpawn();
}
}
/// <summary>
/// Integration tests for NetworkTransform that will test both
/// server and host operating modes and will test both authoritative
/// models for each operating mode.
/// </summary>
[TestFixture(HostOrServer.Host, Authority.ServerAuthority)]
[TestFixture(HostOrServer.Host, Authority.OwnerAuthority)]
[TestFixture(HostOrServer.Server, Authority.ServerAuthority)]
[TestFixture(HostOrServer.Server, Authority.OwnerAuthority)]
public class NetworkTransformTests : NetcodeIntegrationTest
{
private NetworkObject m_AuthoritativePlayer;
private NetworkObject m_NonAuthoritativePlayer;
private NetworkObject m_ChildObjectToBeParented;
private NetworkTransformTestComponent m_AuthoritativeTransform;
private NetworkTransformTestComponent m_NonAuthoritativeTransform;
private NetworkTransformTestComponent m_OwnerTransform;
private readonly Authority m_Authority;
public enum Authority
{
ServerAuthority,
OwnerAuthority
}
public enum Interpolation
{
DisableInterpolate,
EnableInterpolate
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="testWithHost">Value is set by TestFixture</param>
/// <param name="testWithClientNetworkTransform">Value is set by TestFixture</param>
public NetworkTransformTests(HostOrServer testWithHost, Authority authority)
{
m_UseHost = testWithHost == HostOrServer.Host ? true : false;
m_Authority = authority;
}
protected override int NumberOfClients => 1;
protected override IEnumerator OnSetup()
{
ChildObjectComponent.Reset();
return base.OnSetup();
}
protected override void OnCreatePlayerPrefab()
{
var networkTransformTestComponent = m_PlayerPrefab.AddComponent<NetworkTransformTestComponent>();
networkTransformTestComponent.ServerAuthority = m_Authority == Authority.ServerAuthority;
}
protected override void OnServerAndClientsCreated()
{
var childObject = CreateNetworkObjectPrefab("ChildObject");
childObject.AddComponent<ChildObjectComponent>();
var childNetworkTransform = childObject.AddComponent<NetworkTransform>();
childNetworkTransform.InLocalSpace = true;
m_ChildObjectToBeParented = childObject.GetComponent<NetworkObject>();
// Now apply local transform values
m_ChildObjectToBeParented.transform.position = m_ChildObjectLocalPosition;
var childRotation = m_ChildObjectToBeParented.transform.rotation;
childRotation.eulerAngles = m_ChildObjectLocalRotation;
m_ChildObjectToBeParented.transform.rotation = childRotation;
m_ChildObjectToBeParented.transform.localScale = m_ChildObjectLocalScale;
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
var serverSideClientPlayer = m_ServerNetworkManager.ConnectedClients[m_ClientNetworkManagers[0].LocalClientId].PlayerObject;
var clientSideClientPlayer = m_ClientNetworkManagers[0].LocalClient.PlayerObject;
m_AuthoritativePlayer = m_Authority == Authority.ServerAuthority ? serverSideClientPlayer : clientSideClientPlayer;
m_NonAuthoritativePlayer = m_Authority == Authority.ServerAuthority ? clientSideClientPlayer : serverSideClientPlayer;
// Get the NetworkTransformTestComponent to make sure the client side is ready before starting test
m_AuthoritativeTransform = m_AuthoritativePlayer.GetComponent<NetworkTransformTestComponent>();
m_NonAuthoritativeTransform = m_NonAuthoritativePlayer.GetComponent<NetworkTransformTestComponent>();
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(() => 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);
yield return base.OnServerAndClientsConnected();
}
public enum TransformSpace
{
World,
Local
}
public enum OverrideState
{
Update,
CommitToTransform,
SetState
}
/// <summary>
/// Returns true when the server-host and all clients have
/// instantiated the child object to be used in <see cref="NetworkTransformParentingLocalSpaceOffsetTests"/>
/// </summary>
/// <returns></returns>
private bool AllChildObjectInstancesAreSpawned()
{
if (ChildObjectComponent.ServerInstance == null)
{
return false;
}
foreach (var clientNetworkManager in m_ClientNetworkManagers)
{
if (!ChildObjectComponent.ClientInstances.ContainsKey(clientNetworkManager.LocalClientId))
{
return false;
}
}
return true;
}
private bool AllChildObjectInstancesHaveChild()
{
foreach (var instance in ChildObjectComponent.ClientInstances.Values)
{
if (instance.transform.parent == null)
{
return false;
}
}
return true;
}
// To test that local position, rotation, and scale remain the same when parented.
private Vector3 m_ChildObjectLocalPosition = new Vector3(5.0f, 0.0f, -5.0f);
private Vector3 m_ChildObjectLocalRotation = new Vector3(-35.0f, 90.0f, 270.0f);
private Vector3 m_ChildObjectLocalScale = new Vector3(0.1f, 0.5f, 0.4f);
/// <summary>
/// A wait condition specific method that assures the local space coordinates
/// are not impacted by NetworkTransform when parented.
/// </summary>
private bool AllInstancesKeptLocalTransformValues()
{
foreach (var childInstance in ChildObjectComponent.Instances)
{
var childLocalPosition = childInstance.transform.localPosition;
var childLocalRotation = childInstance.transform.localRotation.eulerAngles;
var childLocalScale = childInstance.transform.localScale;
if (!Aproximately(childLocalPosition, m_ChildObjectLocalPosition))
{
return false;
}
if (!AproximatelyEuler(childLocalRotation, m_ChildObjectLocalRotation))
{
return false;
}
if (!Aproximately(childLocalScale, m_ChildObjectLocalScale))
{
return false;
}
}
return true;
}
/// <summary>
/// Handles validating the local space values match the original local space values.
/// If not, it generates a message containing the axial values that did not match
/// the target/start local space values.
/// </summary>
private IEnumerator WaitForAllChildrenLocalTransformValuesToMatch()
{
yield return WaitForConditionOrTimeOut(AllInstancesKeptLocalTransformValues);
var infoMessage = string.Empty;
if (s_GlobalTimeoutHelper.TimedOut)
{
foreach (var childInstance in ChildObjectComponent.Instances)
{
var childLocalPosition = childInstance.transform.localPosition;
var childLocalRotation = childInstance.transform.localRotation.eulerAngles;
var childLocalScale = childInstance.transform.localScale;
if (!Aproximately(childLocalPosition, m_ChildObjectLocalPosition))
{
infoMessage += $"[{childInstance.name}] Child's Local Position ({childLocalPosition}) | Original Local Position ({m_ChildObjectLocalPosition})\n";
}
if (!AproximatelyEuler(childLocalRotation, m_ChildObjectLocalRotation))
{
infoMessage += $"[{childInstance.name}] Child's Local Rotation ({childLocalRotation}) | Original Local Rotation ({m_ChildObjectLocalRotation})\n";
}
if (!Aproximately(childLocalScale, m_ChildObjectLocalScale))
{
infoMessage += $"[{childInstance.name}] Child's Local Scale ({childLocalScale}) | Original Local Rotation ({m_ChildObjectLocalScale})\n";
}
}
AssertOnTimeout($"Timed out waiting for all children to have the correct local space values:\n {infoMessage}");
}
yield return null;
}
/// <summary>
/// Validates that local space transform values remain the same when a NetworkTransform is
/// parented under another NetworkTransform
/// </summary>
[UnityTest]
public IEnumerator NetworkTransformParentedLocalSpaceTest([Values] Interpolation interpolation)
{
m_AuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate;
m_NonAuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate;
var authoritativeChildObject = SpawnObject(m_ChildObjectToBeParented.gameObject, m_AuthoritativeTransform.NetworkManager);
// Assure all of the child object instances are spawned
yield return WaitForConditionOrTimeOut(AllChildObjectInstancesAreSpawned);
AssertOnTimeout("Timed out waiting for all child instances to be spawned!");
// Just a sanity check as it should have timed out before this check
Assert.IsNotNull(ChildObjectComponent.ServerInstance, $"The server-side {nameof(ChildObjectComponent)} instance is null!");
// This determines which parent on the server side should be the parent
if (m_AuthoritativeTransform.IsServerAuthoritative())
{
Assert.True(ChildObjectComponent.ServerInstance.NetworkObject.TrySetParent(m_AuthoritativeTransform.transform, false), "[Authoritative] Failed to parent the child object!");
}
else
{
Assert.True(ChildObjectComponent.ServerInstance.NetworkObject.TrySetParent(m_NonAuthoritativeTransform.transform, false), "[Non-Authoritative] Failed to parent the child object!");
}
// This waits for all child instances to be parented
yield return WaitForConditionOrTimeOut(AllChildObjectInstancesHaveChild);
AssertOnTimeout("Timed out waiting for all instances to have parented a child!");
// This validates each child instance has preserved their local space values
yield return WaitForAllChildrenLocalTransformValuesToMatch();
}
/// <summary>
/// Validates that moving, rotating, and scaling the authority side with a single
/// tick will properly synchronize the non-authoritative side with the same values.
/// </summary>
private IEnumerator MoveRotateAndScaleAuthority(Vector3 position, Vector3 rotation, Vector3 scale, OverrideState overrideState)
{
switch (overrideState)
{
case OverrideState.SetState:
{
m_AuthoritativeTransform.SetState(position, Quaternion.Euler(rotation), scale);
break;
}
case OverrideState.Update:
default:
{
m_AuthoritativeTransform.transform.position = position;
yield return null;
var authoritativeRotation = m_AuthoritativeTransform.transform.rotation;
authoritativeRotation.eulerAngles = rotation;
m_AuthoritativeTransform.transform.rotation = authoritativeRotation;
yield return null;
m_AuthoritativeTransform.transform.localScale = scale;
break;
}
}
}
/// <summary>
/// Validates we don't extrapolate beyond the target value
/// </summary>
/// <remarks>
/// This will first wait for any authoritative changes to have been synchronized
/// with the non-authoritative side. It will then wait for the specified number
/// of tick periods to assure the values don't change
/// </remarks>
private IEnumerator WaitForPositionRotationAndScaleToMatch(int ticksToWait)
{
// Validate we interpolate to the appropriate position and rotation
yield return WaitForConditionOrTimeOut(PositionRotationScaleMatches);
AssertOnTimeout("Timed out waiting for non-authority to match authority's position or rotation");
// Wait for the specified number of ticks
for (int i = 0; i < ticksToWait; i++)
{
yield return s_DefaultWaitForTick;
}
// Verify both sides match (i.e. no drifting or over-extrapolating)
Assert.IsTrue(PositionsMatch(), $"Non-authority position did not match after waiting for {ticksToWait} ticks! " +
$"Authority ({m_AuthoritativeTransform.transform.position}) Non-Authority ({m_NonAuthoritativeTransform.transform.position})");
Assert.IsTrue(RotationsMatch(), $"Non-authority rotation did not match after waiting for {ticksToWait} ticks! " +
$"Authority ({m_AuthoritativeTransform.transform.rotation.eulerAngles}) Non-Authority ({m_NonAuthoritativeTransform.transform.rotation.eulerAngles})");
}
/// <summary>
/// Waits until the next tick
/// </summary>
private IEnumerator WaitForNextTick()
{
var currentTick = m_AuthoritativeTransform.NetworkManager.LocalTime.Tick;
while (m_AuthoritativeTransform.NetworkManager.LocalTime.Tick == currentTick)
{
yield return null;
}
}
// The number of iterations to change position, rotation, and scale for NetworkTransformMultipleChangesOverTime
private const int k_PositionRotationScaleIterations = 8;
protected override void OnNewClientCreated(NetworkManager networkManager)
{
networkManager.NetworkConfig.NetworkPrefabs = m_ServerNetworkManager.NetworkConfig.NetworkPrefabs;
base.OnNewClientCreated(networkManager);
}
/// <summary>
/// This validates that multiple changes can occur within the same tick or over
/// several ticks while still keeping non-authoritative instances synchronized.
/// </summary>
[UnityTest]
public IEnumerator NetworkTransformMultipleChangesOverTime([Values] TransformSpace testLocalTransform, [Values] OverrideState overideState)
{
m_AuthoritativeTransform.InLocalSpace = testLocalTransform == TransformSpace.Local;
var positionStart = new Vector3(1.0f, 0.5f, 2.0f);
var rotationStart = new Vector3(0.0f, 45.0f, 0.0f);
var scaleStart = new Vector3(1.0f, 1.0f, 1.0f);
var position = positionStart;
var rotation = rotationStart;
var scale = scaleStart;
// Move and rotate within the same tick, validate the non-authoritative instance updates
// to each set of changes. Repeat several times.
for (int i = 1; i < k_PositionRotationScaleIterations + 1; i++)
{
position = positionStart * i;
rotation = rotationStart * i;
scale = scaleStart * i;
// Wait for tick to change so we cam start close to the beginning the next tick in order
// to apply both deltas within the same tick period.
yield return WaitForNextTick();
// Apply deltas
MoveRotateAndScaleAuthority(position, rotation, scale, overideState);
// Wait for deltas to synchronize on non-authoritative side
yield return WaitForPositionRotationAndScaleToMatch(4);
}
// Check scale for all player instances when a client late joins
// NOTE: This validates the use of the spawned object's transform values as opposed to the replicated state (which now is only the last deltas)
yield return CreateAndStartNewClient();
var newClientNetworkManager = m_ClientNetworkManagers[NumberOfClients];
foreach (var playerRelativeEntry in m_PlayerNetworkObjects)
{
foreach (var playerInstanceEntry in playerRelativeEntry.Value)
{
var playerInstance = playerInstanceEntry.Value;
if (newClientNetworkManager.LocalClientId == playerInstance.OwnerClientId)
{
Assert.IsTrue(Aproximately(m_PlayerPrefab.transform.localScale, playerInstance.transform.localScale), $"{playerInstance.name}'s cloned instance's scale does not match original scale!\n" +
$"[ClientId-{playerRelativeEntry.Key} Relative] Player-{playerInstance.OwnerClientId}'s LocalScale ({playerInstance.transform.localScale}) vs Target Scale ({m_PlayerPrefab.transform.localScale})");
}
}
}
// Repeat this in the opposite direction
for (int i = -1; i > -1 * (k_PositionRotationScaleIterations + 1); i--)
{
position = positionStart * i;
rotation = rotationStart * i;
scale = scaleStart * i;
// Wait for tick to change so we cam start close to the beginning the next tick in order
// to apply both deltas within the same tick period.
yield return WaitForNextTick();
MoveRotateAndScaleAuthority(position, rotation, scale, overideState);
yield return WaitForPositionRotationAndScaleToMatch(4);
}
// Wait for tick to change so we cam start close to the beginning the next tick in order
// to apply as many deltas within the same tick period as we can (if not all)
yield return WaitForNextTick();
// Move and rotate within the same tick several times, then validate the non-authoritative
// instance updates to the authoritative instance's final position and rotation.
for (int i = 1; i < k_PositionRotationScaleIterations + 1; i++)
{
position = positionStart * i;
rotation = rotationStart * i;
scale = scaleStart * i;
MoveRotateAndScaleAuthority(position, rotation, scale, overideState);
}
yield return WaitForPositionRotationAndScaleToMatch(1);
// Wait for tick to change so we cam start close to the beginning the next tick in order
// to apply as many deltas within the same tick period as we can (if not all)
yield return WaitForNextTick();
// Repeat this in the opposite direction and rotation
for (int i = -1; i > -1 * (k_PositionRotationScaleIterations + 1); i--)
{
position = positionStart * i;
rotation = rotationStart * i;
scale = scaleStart * i;
MoveRotateAndScaleAuthority(position, rotation, scale, overideState);
}
yield return WaitForPositionRotationAndScaleToMatch(1);
}
/// <summary>
/// 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.
/// </summary>
[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;
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
var nextPosition = new Vector3(10, 20, 30);
if (overideState != OverrideState.SetState)
{
authPlayerTransform.position = nextPosition;
m_OwnerTransform.CommitToTransform();
}
else
{
m_OwnerTransform.SetState(nextPosition, null, null, m_AuthoritativeTransform.Interpolate);
}
yield return WaitForConditionOrTimeOut(PositionsMatch);
AssertOnTimeout($"Timed out waiting for positions to match");
// test rotation
Assert.AreEqual(Quaternion.identity, m_NonAuthoritativeTransform.transform.rotation, "wrong initial value for rotation"); // sanity check
var nextRotation = Quaternion.Euler(45, 40, 35); // using euler angles instead of quaternions directly to really see issues users might encounter
if (overideState != OverrideState.SetState)
{
authPlayerTransform.rotation = nextRotation;
m_OwnerTransform.CommitToTransform();
}
else
{
m_OwnerTransform.SetState(null, nextRotation, null, m_AuthoritativeTransform.Interpolate);
}
yield return WaitForConditionOrTimeOut(RotationsMatch);
AssertOnTimeout($"Timed out waiting for rotations to match");
var nextScale = new Vector3(2, 3, 4);
if (overrideUpdate)
{
authPlayerTransform.localScale = nextScale;
m_OwnerTransform.CommitToTransform();
}
else
{
m_OwnerTransform.SetState(null, null, nextScale, m_AuthoritativeTransform.Interpolate);
}
yield return WaitForConditionOrTimeOut(ScaleValuesMatch);
AssertOnTimeout($"Timed out waiting for scale values to match");
}
/// <summary>
/// Test to verify nonAuthority cannot change the transform directly
/// </summary>
[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, 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!");
}
/// <summary>
/// Validates that rotation checks don't produce false positive
/// results when rolling over between 0 and 360 degrees
/// </summary>
[UnityTest]
public IEnumerator TestRotationThresholdDeltaCheck([Values] Interpolation interpolation)
{
m_AuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate;
m_NonAuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate;
m_NonAuthoritativeTransform.RotAngleThreshold = m_AuthoritativeTransform.RotAngleThreshold = 5.0f;
var halfThreshold = m_AuthoritativeTransform.RotAngleThreshold * 0.5001f;
var authorityRotation = m_AuthoritativeTransform.transform.rotation;
var authorityEulerRotation = authorityRotation.eulerAngles;
// Verify rotation is not marked dirty when rotated by half of the threshold
authorityEulerRotation.y += halfThreshold;
authorityRotation.eulerAngles = authorityEulerRotation;
m_AuthoritativeTransform.transform.rotation = authorityRotation;
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
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
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
authorityEulerRotation.y = 360 - halfThreshold;
authorityRotation.eulerAngles = authorityEulerRotation;
m_AuthoritativeTransform.transform.rotation = authorityRotation;
results = m_AuthoritativeTransform.ApplyState();
Assert.IsFalse(results.isRotationDirty, $"Rotation is dirty when rotation threshold is {m_AuthoritativeTransform.RotAngleThreshold} degrees and only adjusted by " +
$"{Mathf.DeltaAngle(0, authorityEulerRotation.y)} degrees!");
authorityEulerRotation.y -= halfThreshold;
authorityRotation.eulerAngles = authorityEulerRotation;
m_AuthoritativeTransform.transform.rotation = authorityRotation;
results = m_AuthoritativeTransform.ApplyState();
Assert.IsTrue(results.isRotationDirty, $"Rotation was not dirty when rotated by {Mathf.DeltaAngle(0, authorityEulerRotation.y)} degrees!");
//Reset rotation back to zero on all axis
authorityRotation.eulerAngles = authorityEulerRotation = Vector3.zero;
m_AuthoritativeTransform.transform.rotation = authorityRotation;
yield return s_DefaultWaitForTick;
authorityEulerRotation.y -= halfThreshold;
authorityRotation.eulerAngles = authorityEulerRotation;
m_AuthoritativeTransform.transform.rotation = authorityRotation;
results = m_AuthoritativeTransform.ApplyState();
Assert.IsFalse(results.isRotationDirty, $"Rotation is dirty when rotation threshold is {m_AuthoritativeTransform.RotAngleThreshold} degrees and only adjusted by " +
$"{Mathf.DeltaAngle(0, authorityEulerRotation.y)} degrees!");
authorityEulerRotation.y -= halfThreshold;
authorityRotation.eulerAngles = authorityEulerRotation;
m_AuthoritativeTransform.transform.rotation = authorityRotation;
results = m_AuthoritativeTransform.ApplyState();
Assert.IsTrue(results.isRotationDirty, $"Rotation was not dirty when rotated by {Mathf.DeltaAngle(0, authorityEulerRotation.y)} degrees!");
}
private bool ValidateBitSetValues(NetworkTransform.NetworkTransformState serverState, NetworkTransform.NetworkTransformState clientState)
{
if (serverState.HasPositionX == clientState.HasPositionX && serverState.HasPositionY == clientState.HasPositionY && serverState.HasPositionZ == clientState.HasPositionZ &&
serverState.HasRotAngleX == clientState.HasRotAngleX && serverState.HasRotAngleY == clientState.HasRotAngleY && serverState.HasRotAngleZ == clientState.HasRotAngleZ &&
serverState.HasScaleX == clientState.HasScaleX && serverState.HasScaleY == clientState.HasScaleY && serverState.HasScaleZ == clientState.HasScaleZ)
{
return true;
}
return false;
}
/// <summary>
/// Test to make sure that the bitset value is updated properly
/// </summary>
[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;
/// <summary>
/// The tests teleporting with and without interpolation
/// </summary>
[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}");
}
/// <summary>
/// This test validates the <see cref="NetworkTransform.SetState(Vector3?, Quaternion?, Vector3?, bool)"/> 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.
/// </summary>
/// <remarks>
/// This also tests that the original server authoritative model with client-owner driven NetworkTransforms is preserved.
/// </remarks>
[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 bool AproximatelyEuler(Vector3 a, Vector3 b)
{
return Mathf.DeltaAngle(a.x, b.x) <= k_AproximateDeltaVariance &&
Mathf.DeltaAngle(a.y, b.y) <= k_AproximateDeltaVariance &&
Mathf.DeltaAngle(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 PositionRotationScaleMatches()
{
return RotationsMatch() && PositionsMatch() && ScaleValuesMatch();
}
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;
}
protected override IEnumerator OnTearDown()
{
m_EnableVerboseDebug = false;
Object.DestroyImmediate(m_PlayerPrefab);
yield return base.OnTearDown();
}
}
}