From b5abc3ff7c6c598ec07aeacc4eaae7358f4c0b81 Mon Sep 17 00:00:00 2001
From: Unity Technologies <@unity>
Date: Mon, 10 Apr 2023 00:00:00 +0000
Subject: [PATCH] com.unity.netcode.gameobjects@1.4.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.4.0] - 2023-04-10
### Added
- Added a way to access the GlobalObjectIdHash via PrefabIdHash for use in the Connection Approval Callback. (#2437)
- Added `OnServerStarted` and `OnServerStopped` events that will trigger only on the server (or host player) to notify that the server just started or is no longer active (#2420)
- Added `OnClientStarted` and `OnClientStopped` events that will trigger only on the client (or host player) to notify that the client just started or is no longer active (#2420)
- Added `NetworkTransform.UseHalfFloatPrecision` property that, when enabled, will use half float values for position, rotation, and scale. This yields a 50% bandwidth savings a the cost of precision. (#2388)
- Added `NetworkTransform.UseQuaternionSynchronization` property that, when enabled, will synchronize the entire quaternion. (#2388)
- Added `NetworkTransform.UseQuaternionCompression` property that, when enabled, will use a smallest three implementation reducing a full quaternion synchronization update to the size of an unsigned integer. (#2388)
- Added `NetworkTransform.SlerpPosition` property that, when enabled along with interpolation being enabled, will interpolate using `Vector3.Slerp`. (#2388)
- Added `BufferedLinearInterpolatorVector3` that replaces the float version, is now used by `NetworkTransform`, and provides the ability to enable or disable `Slerp`. (#2388)
- Added `HalfVector3` used for scale when half float precision is enabled. (#2388)
- Added `HalfVector4` used for rotation when half float precision and quaternion synchronization is enabled. (#2388)
- Added `HalfVector3DeltaPosition` used for position when half float precision is enabled. This handles loss in position precision by updating only the delta position as opposed to the full position. (#2388)
- Added `NetworkTransform.GetSpaceRelativePosition` and `NetworkTransform.GetSpaceRelativeRotation` helper methods to return the proper values depending upon whether local or world space. (#2388)
- Added `NetworkTransform.OnAuthorityPushTransformState` virtual method that is invoked just prior to sending the `NetworkTransformState` to non-authoritative instances. This provides users with the ability to obtain more precise delta values for prediction related calculations. (#2388)
- Added `NetworkTransform.OnNetworkTransformStateUpdated` virtual method that is invoked just after the authoritative `NetworkTransformState` is applied. This provides users with the ability to obtain more precise delta values for prediction related calculations. (#2388)
- Added `NetworkTransform.OnInitialize`virtual method that is invoked after the `NetworkTransform` has been initialized or re-initialized when ownership changes. This provides for a way to make adjustments when `NetworkTransform` is initialized (i.e. resetting client prediction etc) (#2388)
- Added `NetworkObject.SynchronizeTransform` property (default is true) that provides users with another way to help with bandwidth optimizations where, when set to false, the `NetworkObject`'s associated transform will not be included when spawning and/or synchronizing late joining players. (#2388)
- Added `NetworkSceneManager.ActiveSceneSynchronizationEnabled` property, disabled by default, that enables client synchronization of server-side active scene changes. (#2383)
- Added `NetworkObject.ActiveSceneSynchronization`, disabled by default, that will automatically migrate a `NetworkObject` to a newly assigned active scene. (#2383)
- Added `NetworkObject.SceneMigrationSynchronization`, enabled by default, that will synchronize client(s) when a `NetworkObject` is migrated into a new scene on the server side via `SceneManager.MoveGameObjectToScene`. (#2383)
### Changed
- Made sure the `CheckObjectVisibility` delegate is checked and applied, upon `NetworkShow` attempt. Found while supporting (#2454), although this is not a fix for this (already fixed) issue. (#2463)
- Changed `NetworkTransform` authority handles delta checks on each new network tick and no longer consumes processing cycles checking for deltas for all frames in-between ticks. (#2388)
- Changed the `NetworkTransformState` structure is now public and now has public methods that provide access to key properties of the `NetworkTransformState` structure. (#2388)
- Changed `NetworkTransform` interpolation adjusts its interpolation "ticks ago" to be 2 ticks latent if it is owner authoritative and the instance is not the server or 1 tick latent if the instance is the server and/or is server authoritative. (#2388)
- Updated `NetworkSceneManager` to migrate dynamically spawned `NetworkObject`s with `DestroyWithScene` set to false into the active scene if their current scene is unloaded. (#2383)
- Updated the server to synchronize its local `NetworkSceneManager.ClientSynchronizationMode` during the initial client synchronization. (#2383)
### Fixed
- Fixed issue where during client synchronization the synchronizing client could receive a ObjectSceneChanged message before the client-side NetworkObject instance had been instantiated and spawned. (#2502)
- Fixed issue where `NetworkAnimator` was building client RPC parameters to exclude the host from sending itself messages but was not including it in the ClientRpc parameters. (#2492)
- Fixed issue where `NetworkAnimator` was not properly detecting and synchronizing cross fade initiated transitions. (#2481)
- Fixed issue where `NetworkAnimator` was not properly synchronizing animation state updates. (#2481)
- Fixed float NetworkVariables not being rendered properly in the inspector of NetworkObjects. (#2441)
- Fixed an issue where Named Message Handlers could remove themselves causing an exception when the metrics tried to access the name of the message.(#2426)
- Fixed registry of public `NetworkVariable`s in derived `NetworkBehaviour`s (#2423)
- Fixed issue where runtime association of `Animator` properties to `AnimationCurve`s would cause `NetworkAnimator` to attempt to update those changes. (#2416)
- Fixed issue where `NetworkAnimator` would not check if its associated `Animator` was valid during serialization and would spam exceptions in the editor console. (#2416)
- Fixed issue with a child's rotation rolling over when interpolation is enabled on a `NetworkTransform`. Now using half precision or full quaternion synchronization will always update all axis. (#2388)
- Fixed issue where `NetworkTransform` was not setting the teleport flag when the `NetworkTransform.InLocalSpace` value changed. This issue only impacted `NetworkTransform` when interpolation was enabled. (#2388)
- Fixed issue when the `NetworkSceneManager.ClientSynchronizationMode` is `LoadSceneMode.Additive` and the server changes the currently active scene prior to a client connecting then upon a client connecting and being synchronized the NetworkSceneManager would clear its internal ScenePlacedObjects list that could already be populated. (#2383)
- Fixed issue where a client would load duplicate scenes of already preloaded scenes during the initial client synchronization and `NetworkSceneManager.ClientSynchronizationMode` was set to `LoadSceneMode.Additive`. (#2383)
---
CHANGELOG.md | 49 +
Components/HalfVector3.cs | 159 ++
Components/HalfVector3.cs.meta | 11 +
Components/HalfVector4.cs | 137 +
Components/HalfVector4.cs.meta | 11 +
.../BufferedLinearInterpolator.cs | 71 +-
Components/NetworkAnimator.cs | 394 +--
Components/NetworkDeltaPosition.cs | 205 ++
Components/NetworkDeltaPosition.cs.meta | 11 +
Components/NetworkTransform.cs | 2414 +++++++++++++----
Components/QuaternionCompressor.cs | 123 +
Components/QuaternionCompressor.cs.meta | 11 +
.../com.unity.netcode.components.asmdef | 3 +-
Editor/CodeGen/INetworkMessageILPP.cs | 2 +-
Editor/CodeGen/INetworkSerializableILPP.cs | 2 +-
Editor/CodeGen/NetworkBehaviourILPP.cs | 84 +-
Editor/NetworkBehaviourEditor.cs | 18 +-
Editor/NetworkManagerEditor.cs | 24 +-
Editor/NetworkManagerHelper.cs | 2 +-
Editor/NetworkObjectEditor.cs | 2 +-
Editor/NetworkTransformEditor.cs | 31 +-
Runtime/AssemblyInfo.cs | 2 +-
Runtime/Configuration/NetworkConfig.cs | 14 +-
Runtime/Configuration/NetworkPrefabs.cs | 11 +
Runtime/Core/ComponentFactory.cs | 2 +
Runtime/Core/NetworkBehaviour.cs | 41 +-
Runtime/Core/NetworkManager.cs | 117 +-
Runtime/Core/NetworkObject.cs | 275 +-
Runtime/Core/NetworkUpdateLoop.cs | 2 +-
Runtime/Hashing/XXHash.cs | 2 +-
Runtime/Messaging/CustomMessageManager.cs | 16 +-
Runtime/Messaging/DeferredMessageManager.cs | 5 +-
Runtime/Messaging/DisconnectReasonMessage.cs | 6 +-
Runtime/Messaging/Messages/RpcMessages.cs | 2 +-
Runtime/Messaging/MessagingSystem.cs | 64 +-
Runtime/NetworkVariable/NetworkVariable.cs | 2 +-
.../DefaultSceneManagerHandler.cs | 377 +++
.../DefaultSceneManagerHandler.cs.meta | 11 +
.../SceneManagement/ISceneManagerHandler.cs | 20 +
.../SceneManagement/NetworkSceneManager.cs | 756 ++++--
Runtime/SceneManagement/SceneEventData.cs | 203 +-
Runtime/SceneManagement/SceneEventProgress.cs | 4 +-
Runtime/Serialization/FastBufferReader.cs | 12 +-
.../Serialization/NetworkObjectReference.cs | 10 +-
Runtime/Spawning/NetworkSpawnManager.cs | 13 +-
Runtime/Timing/IRealTimeProvider.cs | 10 +
Runtime/Timing/IRealTimeProvider.cs.meta | 3 +
Runtime/Timing/RealTimeProvider.cs | 12 +
Runtime/Timing/RealTimeProvider.cs.meta | 3 +
Runtime/Transports/UNET/UNetTransport.cs | 6 +-
Runtime/Transports/UTP/UnityTransport.cs | 34 +-
Runtime/com.unity.netcode.runtime.asmdef | 3 +-
.../Runtime/IntegrationTestSceneHandler.cs | 545 +++-
.../IntegrationTestWithApproximation.cs | 79 +
.../IntegrationTestWithApproximation.cs.meta | 11 +
TestHelpers/Runtime/MockTimeProvider.cs | 30 +
TestHelpers/Runtime/MockTimeProvider.cs.meta | 3 +
TestHelpers/Runtime/MockTransport.cs | 89 +
TestHelpers/Runtime/MockTransport.cs.meta | 3 +
TestHelpers/Runtime/NetcodeIntegrationTest.cs | 517 +++-
.../Runtime/NetcodeIntegrationTestHelpers.cs | 102 +-
TestHelpers/Runtime/NetworkManagerHelper.cs | 2 +-
TestHelpers/Runtime/TimeoutHelper.cs | 109 +-
Tests/Editor/DisconnectMessageTests.cs | 18 +-
.../Messaging/MessageRegistrationTests.cs | 9 +-
.../Messaging/MessageVersioningTests.cs | 2 +-
.../NetworkManagerConfigurationTests.cs | 47 +-
Tests/Editor/NetworkPrefabProcessorTests.cs | 2 +-
.../BaseFastBufferReaderWriterTest.cs | 19 +-
Tests/Editor/Serialization/BytePackerTests.cs | 4 -
.../Serialization/FastBufferReaderTests.cs | 8 -
.../Serialization/FastBufferWriterTests.cs | 13 +-
.../Transports/BatchedReceiveQueueTests.cs | 10 +-
.../com.unity.netcode.editortests.asmdef | 3 +-
Tests/Runtime/AddNetworkPrefabTests.cs | 2 +-
Tests/Runtime/ClientOnlyConnectionTests.cs | 4 +-
.../BufferDataValidationComponent.cs | 2 +-
.../NetworkVariableTestComponent.cs | 6 +-
Tests/Runtime/ConnectionApproval.cs | 14 +-
Tests/Runtime/DeferredMessagingTests.cs | 298 +-
Tests/Runtime/DisconnectTests.cs | 2 +-
Tests/Runtime/HiddenVariableTests.cs | 2 +-
Tests/Runtime/IntegrationTestExamples.cs | 4 +-
Tests/Runtime/InvalidConnectionEventsTest.cs | 2 +-
Tests/Runtime/ListChangedTest.cs | 2 +-
.../Messaging/DisconnectReasonTests.cs | 2 +-
Tests/Runtime/Messaging/NamedMessageTests.cs | 2 +-
.../Runtime/Messaging/UnnamedMessageTests.cs | 2 +-
Tests/Runtime/NestedNetworkManagerTests.cs | 4 +-
Tests/Runtime/NetworkBehaviourGenericTests.cs | 12 +-
Tests/Runtime/NetworkBehaviourUpdaterTests.cs | 2 +-
...NetworkManagerCustomMessageManagerTests.cs | 11 +-
Tests/Runtime/NetworkManagerEventsTests.cs | 258 ++
.../Runtime/NetworkManagerEventsTests.cs.meta | 11 +
.../NetworkManagerSceneManagerTests.cs | 8 +-
Tests/Runtime/NetworkManagerTransportTests.cs | 2 +-
.../NetworkObjectDestroyTests.cs | 2 +-
.../NetworkObjectDontDestroyWithOwnerTests.cs | 2 +-
...orkObjectNetworkClientOwnedObjectsTests.cs | 2 +-
.../NetworkObjectOnNetworkDespawnTests.cs | 2 +-
.../NetworkObjectOnSpawnTests.cs | 2 +-
.../NetworkObjectOwnershipTests.cs | 2 +-
.../NetworkObjectPropertyTests.cs | 45 +
.../NetworkObjectPropertyTests.cs.meta | 11 +
.../NetworkObjectSpawnManyObjectsTests.cs | 2 +-
.../NetworkObjectSynchronizationTests.cs | 4 +-
Tests/Runtime/NetworkPrefabHandlerTests.cs | 4 +-
Tests/Runtime/NetworkShowHideTests.cs | 19 +-
Tests/Runtime/NetworkSpawnManagerTests.cs | 2 +-
.../NetworkTransformOwnershipTests.cs | 45 +-
.../NetworkTransformStateTests.cs | 108 +-
.../NetworkTransform/NetworkTransformTests.cs | 731 +++--
Tests/Runtime/NetworkUpdateLoopTests.cs | 4 +-
Tests/Runtime/NetworkVarBufferCopyTest.cs | 6 +-
Tests/Runtime/NetworkVariableTests.cs | 431 ++-
...tworkVariableUserSerializableTypesTests.cs | 2 +-
Tests/Runtime/NetworkVisibilityTests.cs | 4 +-
Tests/Runtime/OwnerModifiedTests.cs | 4 +-
Tests/Runtime/OwnerPermissionTests.cs | 2 +-
Tests/Runtime/Physics/NetworkRigidbodyTest.cs | 2 +-
Tests/Runtime/PlayerObjectTests.cs | 2 +-
Tests/Runtime/RpcManyClientsTests.cs | 36 +-
Tests/Runtime/RpcQueueTests.cs | 4 +-
Tests/Runtime/RpcTests.cs | 2 +-
.../NetworkBehaviourReferenceTests.cs | 2 +-
.../NetworkObjectReferenceTests.cs | 2 +-
Tests/Runtime/StopStartRuntimeTests.cs | 2 +-
.../Runtime/Timing/NetworkTimeSystemTests.cs | 2 +-
.../Runtime/Timing/TimeInitializationTest.cs | 2 +-
...InstanceTest.cs => TimeIntegrationTest.cs} | 33 +-
...st.cs.meta => TimeIntegrationTest.cs.meta} | 0
Tests/Runtime/TransformInterpolationTests.cs | 114 +-
.../UnityTransportConnectionTests.cs | 2 +-
.../Transports/UnityTransportDriverClient.cs | 7 +-
.../Transports/UnityTransportTestHelpers.cs | 2 +-
.../Runtime/Transports/UnityTransportTests.cs | 2 +-
.../com.unity.netcode.runtimetests.asmdef | 3 +-
package.json | 10 +-
138 files changed, 7892 insertions(+), 1852 deletions(-)
create mode 100644 Components/HalfVector3.cs
create mode 100644 Components/HalfVector3.cs.meta
create mode 100644 Components/HalfVector4.cs
create mode 100644 Components/HalfVector4.cs.meta
create mode 100644 Components/NetworkDeltaPosition.cs
create mode 100644 Components/NetworkDeltaPosition.cs.meta
create mode 100644 Components/QuaternionCompressor.cs
create mode 100644 Components/QuaternionCompressor.cs.meta
create mode 100644 Runtime/SceneManagement/DefaultSceneManagerHandler.cs
create mode 100644 Runtime/SceneManagement/DefaultSceneManagerHandler.cs.meta
create mode 100644 Runtime/Timing/IRealTimeProvider.cs
create mode 100644 Runtime/Timing/IRealTimeProvider.cs.meta
create mode 100644 Runtime/Timing/RealTimeProvider.cs
create mode 100644 Runtime/Timing/RealTimeProvider.cs.meta
create mode 100644 TestHelpers/Runtime/IntegrationTestWithApproximation.cs
create mode 100644 TestHelpers/Runtime/IntegrationTestWithApproximation.cs.meta
create mode 100644 TestHelpers/Runtime/MockTimeProvider.cs
create mode 100644 TestHelpers/Runtime/MockTimeProvider.cs.meta
create mode 100644 TestHelpers/Runtime/MockTransport.cs
create mode 100644 TestHelpers/Runtime/MockTransport.cs.meta
create mode 100644 Tests/Runtime/NetworkManagerEventsTests.cs
create mode 100644 Tests/Runtime/NetworkManagerEventsTests.cs.meta
create mode 100644 Tests/Runtime/NetworkObject/NetworkObjectPropertyTests.cs
create mode 100644 Tests/Runtime/NetworkObject/NetworkObjectPropertyTests.cs.meta
rename Tests/Runtime/Timing/{TimeMultiInstanceTest.cs => TimeIntegrationTest.cs} (85%)
rename Tests/Runtime/Timing/{TimeMultiInstanceTest.cs.meta => TimeIntegrationTest.cs.meta} (100%)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 970adac..098ab31 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,55 @@ 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.4.0] - 2023-04-10
+
+### Added
+
+- Added a way to access the GlobalObjectIdHash via PrefabIdHash for use in the Connection Approval Callback. (#2437)
+- Added `OnServerStarted` and `OnServerStopped` events that will trigger only on the server (or host player) to notify that the server just started or is no longer active (#2420)
+- Added `OnClientStarted` and `OnClientStopped` events that will trigger only on the client (or host player) to notify that the client just started or is no longer active (#2420)
+- Added `NetworkTransform.UseHalfFloatPrecision` property that, when enabled, will use half float values for position, rotation, and scale. This yields a 50% bandwidth savings a the cost of precision. (#2388)
+- Added `NetworkTransform.UseQuaternionSynchronization` property that, when enabled, will synchronize the entire quaternion. (#2388)
+- Added `NetworkTransform.UseQuaternionCompression` property that, when enabled, will use a smallest three implementation reducing a full quaternion synchronization update to the size of an unsigned integer. (#2388)
+- Added `NetworkTransform.SlerpPosition` property that, when enabled along with interpolation being enabled, will interpolate using `Vector3.Slerp`. (#2388)
+- Added `BufferedLinearInterpolatorVector3` that replaces the float version, is now used by `NetworkTransform`, and provides the ability to enable or disable `Slerp`. (#2388)
+- Added `HalfVector3` used for scale when half float precision is enabled. (#2388)
+- Added `HalfVector4` used for rotation when half float precision and quaternion synchronization is enabled. (#2388)
+- Added `HalfVector3DeltaPosition` used for position when half float precision is enabled. This handles loss in position precision by updating only the delta position as opposed to the full position. (#2388)
+- Added `NetworkTransform.GetSpaceRelativePosition` and `NetworkTransform.GetSpaceRelativeRotation` helper methods to return the proper values depending upon whether local or world space. (#2388)
+- Added `NetworkTransform.OnAuthorityPushTransformState` virtual method that is invoked just prior to sending the `NetworkTransformState` to non-authoritative instances. This provides users with the ability to obtain more precise delta values for prediction related calculations. (#2388)
+- Added `NetworkTransform.OnNetworkTransformStateUpdated` virtual method that is invoked just after the authoritative `NetworkTransformState` is applied. This provides users with the ability to obtain more precise delta values for prediction related calculations. (#2388)
+- Added `NetworkTransform.OnInitialize`virtual method that is invoked after the `NetworkTransform` has been initialized or re-initialized when ownership changes. This provides for a way to make adjustments when `NetworkTransform` is initialized (i.e. resetting client prediction etc) (#2388)
+- Added `NetworkObject.SynchronizeTransform` property (default is true) that provides users with another way to help with bandwidth optimizations where, when set to false, the `NetworkObject`'s associated transform will not be included when spawning and/or synchronizing late joining players. (#2388)
+- Added `NetworkSceneManager.ActiveSceneSynchronizationEnabled` property, disabled by default, that enables client synchronization of server-side active scene changes. (#2383)
+- Added `NetworkObject.ActiveSceneSynchronization`, disabled by default, that will automatically migrate a `NetworkObject` to a newly assigned active scene. (#2383)
+- Added `NetworkObject.SceneMigrationSynchronization`, enabled by default, that will synchronize client(s) when a `NetworkObject` is migrated into a new scene on the server side via `SceneManager.MoveGameObjectToScene`. (#2383)
+
+### Changed
+
+- Made sure the `CheckObjectVisibility` delegate is checked and applied, upon `NetworkShow` attempt. Found while supporting (#2454), although this is not a fix for this (already fixed) issue. (#2463)
+- Changed `NetworkTransform` authority handles delta checks on each new network tick and no longer consumes processing cycles checking for deltas for all frames in-between ticks. (#2388)
+- Changed the `NetworkTransformState` structure is now public and now has public methods that provide access to key properties of the `NetworkTransformState` structure. (#2388)
+- Changed `NetworkTransform` interpolation adjusts its interpolation "ticks ago" to be 2 ticks latent if it is owner authoritative and the instance is not the server or 1 tick latent if the instance is the server and/or is server authoritative. (#2388)
+- Updated `NetworkSceneManager` to migrate dynamically spawned `NetworkObject`s with `DestroyWithScene` set to false into the active scene if their current scene is unloaded. (#2383)
+- Updated the server to synchronize its local `NetworkSceneManager.ClientSynchronizationMode` during the initial client synchronization. (#2383)
+
+### Fixed
+
+- Fixed issue where during client synchronization the synchronizing client could receive a ObjectSceneChanged message before the client-side NetworkObject instance had been instantiated and spawned. (#2502)
+- Fixed issue where `NetworkAnimator` was building client RPC parameters to exclude the host from sending itself messages but was not including it in the ClientRpc parameters. (#2492)
+- Fixed issue where `NetworkAnimator` was not properly detecting and synchronizing cross fade initiated transitions. (#2481)
+- Fixed issue where `NetworkAnimator` was not properly synchronizing animation state updates. (#2481)
+- Fixed float NetworkVariables not being rendered properly in the inspector of NetworkObjects. (#2441)
+- Fixed an issue where Named Message Handlers could remove themselves causing an exception when the metrics tried to access the name of the message.(#2426)
+- Fixed registry of public `NetworkVariable`s in derived `NetworkBehaviour`s (#2423)
+- Fixed issue where runtime association of `Animator` properties to `AnimationCurve`s would cause `NetworkAnimator` to attempt to update those changes. (#2416)
+- Fixed issue where `NetworkAnimator` would not check if its associated `Animator` was valid during serialization and would spam exceptions in the editor console. (#2416)
+- Fixed issue with a child's rotation rolling over when interpolation is enabled on a `NetworkTransform`. Now using half precision or full quaternion synchronization will always update all axis. (#2388)
+- Fixed issue where `NetworkTransform` was not setting the teleport flag when the `NetworkTransform.InLocalSpace` value changed. This issue only impacted `NetworkTransform` when interpolation was enabled. (#2388)
+- Fixed issue when the `NetworkSceneManager.ClientSynchronizationMode` is `LoadSceneMode.Additive` and the server changes the currently active scene prior to a client connecting then upon a client connecting and being synchronized the NetworkSceneManager would clear its internal ScenePlacedObjects list that could already be populated. (#2383)
+- Fixed issue where a client would load duplicate scenes of already preloaded scenes during the initial client synchronization and `NetworkSceneManager.ClientSynchronizationMode` was set to `LoadSceneMode.Additive`. (#2383)
+
## [1.3.1] - 2023-03-27
### Added
diff --git a/Components/HalfVector3.cs b/Components/HalfVector3.cs
new file mode 100644
index 0000000..5ceca12
--- /dev/null
+++ b/Components/HalfVector3.cs
@@ -0,0 +1,159 @@
+using System.Runtime.CompilerServices;
+using Unity.Mathematics;
+using UnityEngine;
+
+namespace Unity.Netcode.Components
+{
+ ///
+ /// Half float precision .
+ ///
+ ///
+ /// The Vector3T values are half float values returned by for each
+ /// individual axis and the 16 bits of the half float are stored as values since C# does not have
+ /// a half float type.
+ ///
+ public struct HalfVector3 : INetworkSerializable
+ {
+ internal const int Length = 3;
+
+ ///
+ /// The half float precision value of the x-axis as a .
+ ///
+ public half X => Axis.x;
+ ///
+ /// The half float precision value of the y-axis as a .
+ ///
+ public half Y => Axis.y;
+ ///
+ /// The half float precision value of the z-axis as a .
+ ///
+ public half Z => Axis.x;
+
+ ///
+ /// Used to store the half float precision values as a
+ ///
+ public half3 Axis;
+
+ ///
+ /// Determine which axis will be synchronized during serialization
+ ///
+ public bool3 AxisToSynchronize;
+
+ private void SerializeWrite(FastBufferWriter writer)
+ {
+ for (int i = 0; i < Length; i++)
+ {
+ if (AxisToSynchronize[i])
+ {
+ writer.WriteUnmanagedSafe(Axis[i]);
+ }
+ }
+ }
+
+ private void SerializeRead(FastBufferReader reader)
+ {
+ for (int i = 0; i < Length; i++)
+ {
+ if (AxisToSynchronize[i])
+ {
+ var axisValue = Axis[i];
+ reader.ReadUnmanagedSafe(out axisValue);
+ Axis[i] = axisValue;
+ }
+ }
+ }
+
+ ///
+ /// The serialization implementation of .
+ ///
+ public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter
+ {
+ if (serializer.IsReader)
+ {
+ SerializeRead(serializer.GetFastBufferReader());
+ }
+ else
+ {
+ SerializeWrite(serializer.GetFastBufferWriter());
+ }
+ }
+
+ ///
+ /// Gets the full precision value as a .
+ ///
+ /// a as the full precision value.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Vector3 ToVector3()
+ {
+ Vector3 fullPrecision = Vector3.zero;
+ Vector3 fullConversion = math.float3(Axis);
+ for (int i = 0; i < Length; i++)
+ {
+ if (AxisToSynchronize[i])
+ {
+ fullPrecision[i] = fullConversion[i];
+ }
+ }
+ return fullPrecision;
+ }
+
+ ///
+ /// Converts a full precision to half precision and updates the current instance.
+ ///
+ /// The to convert.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void UpdateFrom(ref Vector3 vector3)
+ {
+ var half3Full = math.half3(vector3);
+ for (int i = 0; i < Length; i++)
+ {
+ if (AxisToSynchronize[i])
+ {
+ Axis[i] = half3Full[i];
+ }
+ }
+ }
+
+ ///
+ /// Constructor
+ ///
+ /// The initial axial values (converted to half floats) when instantiated.
+ /// The axis to synchronize.
+ public HalfVector3(Vector3 vector3, bool3 axisToSynchronize)
+ {
+ Axis = half3.zero;
+ AxisToSynchronize = axisToSynchronize;
+ UpdateFrom(ref vector3);
+ }
+
+ ///
+ /// Constructor that defaults to all axis being synchronized.
+ ///
+ /// The initial axial values (converted to half floats) when instantiated.
+ public HalfVector3(Vector3 vector3) : this(vector3, math.bool3(true))
+ {
+
+ }
+
+ ///
+ /// Constructor
+ ///
+ /// The initial x axis (converted to half float) value when instantiated.
+ /// The initial y axis (converted to half float) value when instantiated.
+ /// The initial z axis (converted to half float) value when instantiated.
+ /// The axis to synchronize.
+ public HalfVector3(float x, float y, float z, bool3 axisToSynchronize) : this(new Vector3(x, y, z), axisToSynchronize)
+ {
+ }
+
+ ///
+ /// Constructor that defaults to all axis being synchronized.
+ ///
+ /// The initial x axis (converted to half float) value when instantiated.
+ /// The initial y axis (converted to half float) value when instantiated.
+ /// The initial z axis (converted to half float) value when instantiated.
+ public HalfVector3(float x, float y, float z) : this(new Vector3(x, y, z), math.bool3(true))
+ {
+ }
+ }
+}
diff --git a/Components/HalfVector3.cs.meta b/Components/HalfVector3.cs.meta
new file mode 100644
index 0000000..cb9ad6c
--- /dev/null
+++ b/Components/HalfVector3.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b0e371533eaeac446b16b10886f64f84
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Components/HalfVector4.cs b/Components/HalfVector4.cs
new file mode 100644
index 0000000..35ba138
--- /dev/null
+++ b/Components/HalfVector4.cs
@@ -0,0 +1,137 @@
+using System.Runtime.CompilerServices;
+using Unity.Mathematics;
+using UnityEngine;
+
+namespace Unity.Netcode.Components
+{
+ ///
+ /// Half Precision that can also be used to convert a to half precision.
+ ///
+ ///
+ /// The Vector4T values are half float values returned by for each
+ /// individual axis and the 16 bits of the half float are stored as values since C# does not have
+ /// a half float type.
+ ///
+ public struct HalfVector4 : INetworkSerializable
+ {
+ internal const int Length = 4;
+ ///
+ /// The half float precision value of the x-axis as a .
+ ///
+ public half X => Axis.x;
+
+ ///
+ /// The half float precision value of the y-axis as a .
+ ///
+ public half Y => Axis.y;
+
+ ///
+ /// The half float precision value of the z-axis as a .
+ ///
+ public half Z => Axis.z;
+
+ ///
+ /// The half float precision value of the w-axis as a .
+ ///
+ public half W => Axis.w;
+
+ ///
+ /// Used to store the half float precision values as a
+ ///
+ public half4 Axis;
+
+ private void SerializeWrite(FastBufferWriter writer)
+ {
+ for (int i = 0; i < Length; i++)
+ {
+ writer.WriteUnmanagedSafe(Axis[i]);
+ }
+ }
+
+ private void SerializeRead(FastBufferReader reader)
+ {
+ for (int i = 0; i < Length; i++)
+ {
+ var axisValue = Axis[i];
+ reader.ReadUnmanagedSafe(out axisValue);
+ Axis[i] = axisValue;
+ }
+ }
+
+ ///
+ /// The serialization implementation of .
+ ///
+ public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter
+ {
+ if (serializer.IsReader)
+ {
+ SerializeRead(serializer.GetFastBufferReader());
+ }
+ else
+ {
+ SerializeWrite(serializer.GetFastBufferWriter());
+ }
+ }
+
+ ///
+ /// Converts this instance to a full precision .
+ ///
+ /// A as the full precision value.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Vector4 ToVector4()
+ {
+ return math.float4(Axis);
+ }
+
+ ///
+ /// Converts this instance to a full precision .
+ ///
+ /// A as the full precision value.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Quaternion ToQuaternion()
+ {
+ return math.quaternion(Axis);
+ }
+
+ ///
+ /// Converts a full precision to half precision and updates the current instance.
+ ///
+ /// The to convert and update this instance with.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void UpdateFrom(ref Vector4 vector4)
+ {
+ Axis = math.half4(vector4);
+ }
+
+ ///
+ /// Converts a full precision to half precision and updates the current instance.
+ ///
+ /// The to convert and update this instance with.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void UpdateFrom(ref Quaternion quaternion)
+ {
+ Axis = math.half4(math.half(quaternion.x), math.half(quaternion.y), math.half(quaternion.z), math.half(quaternion.w));
+ }
+
+ ///
+ /// Constructor
+ ///
+ /// The initial axial values (converted to half floats) when instantiated.
+ public HalfVector4(Vector4 vector4)
+ {
+ Axis = default;
+ UpdateFrom(ref vector4);
+ }
+
+ ///
+ /// Constructor
+ ///
+ /// The initial x axis (converted to half float) value when instantiated.
+ /// The initial y axis (converted to half float) value when instantiated.
+ /// The initial z axis (converted to half float) value when instantiated.
+ /// The initial w axis (converted to half float) value when instantiated.
+ public HalfVector4(float x, float y, float z, float w) : this(new Vector4(x, y, z, w))
+ {
+ }
+ }
+}
diff --git a/Components/HalfVector4.cs.meta b/Components/HalfVector4.cs.meta
new file mode 100644
index 0000000..1c54b8c
--- /dev/null
+++ b/Components/HalfVector4.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 03c78136f41ff84499e2a6ac4a7dd7a5
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Components/Interpolator/BufferedLinearInterpolator.cs b/Components/Interpolator/BufferedLinearInterpolator.cs
index 9d24829..a938942 100644
--- a/Components/Interpolator/BufferedLinearInterpolator.cs
+++ b/Components/Interpolator/BufferedLinearInterpolator.cs
@@ -312,20 +312,79 @@ namespace Unity.Netcode
///
public class BufferedLinearInterpolatorQuaternion : BufferedLinearInterpolator
{
+ ///
+ /// Use when .
+ /// Use when
+ ///
+ ///
+ /// When using half precision (due to the imprecision) using is
+ /// less processor intensive (i.e. precision is already "imprecise").
+ /// When using full precision (to maintain precision) using is
+ /// more processor intensive yet yields more precise results.
+ ///
+ public bool IsSlerp;
+
///
protected override Quaternion InterpolateUnclamped(Quaternion start, Quaternion end, float time)
{
- // Disabling Extrapolation:
- // TODO: Add Jira Ticket
- return Quaternion.Slerp(start, end, time);
+ if (IsSlerp)
+ {
+ return Quaternion.Slerp(start, end, time);
+ }
+ else
+ {
+ return Quaternion.Lerp(start, end, time);
+ }
}
///
protected override Quaternion Interpolate(Quaternion start, Quaternion end, float time)
{
- // Disabling Extrapolation:
- // TODO: Add Jira Ticket
- return Quaternion.Slerp(start, end, time);
+ if (IsSlerp)
+ {
+ return Quaternion.Slerp(start, end, time);
+ }
+ else
+ {
+ return Quaternion.Lerp(start, end, time);
+ }
+ }
+ }
+
+ ///
+ /// A implementation.
+ ///
+ public class BufferedLinearInterpolatorVector3 : BufferedLinearInterpolator
+ {
+ ///
+ /// Use when .
+ /// Use when
+ ///
+ public bool IsSlerp;
+ ///
+ protected override Vector3 InterpolateUnclamped(Vector3 start, Vector3 end, float time)
+ {
+ if (IsSlerp)
+ {
+ return Vector3.Slerp(start, end, time);
+ }
+ else
+ {
+ return Vector3.Lerp(start, end, time);
+ }
+ }
+
+ ///
+ protected override Vector3 Interpolate(Vector3 start, Vector3 end, float time)
+ {
+ if (IsSlerp)
+ {
+ return Vector3.Slerp(start, end, time);
+ }
+ else
+ {
+ return Vector3.Lerp(start, end, time);
+ }
}
}
}
diff --git a/Components/NetworkAnimator.cs b/Components/NetworkAnimator.cs
index a942a54..64f6017 100644
--- a/Components/NetworkAnimator.cs
+++ b/Components/NetworkAnimator.cs
@@ -23,6 +23,13 @@ namespace Unity.Netcode.Components
///
private void FlushMessages()
{
+ foreach (var animationUpdate in m_SendAnimationUpdates)
+ {
+ m_NetworkAnimator.SendAnimStateClientRpc(animationUpdate.AnimationMessage, animationUpdate.ClientRpcParams);
+ }
+
+ m_SendAnimationUpdates.Clear();
+
foreach (var sendEntry in m_SendParameterUpdates)
{
m_NetworkAnimator.SendParametersUpdateClientRpc(sendEntry.ParametersUpdateMessage, sendEntry.ClientRpcParams);
@@ -64,9 +71,11 @@ namespace Unity.Netcode.Components
m_NetworkAnimator.UpdateParameters(ref parameterUpdate);
}
m_ProcessParameterUpdates.Clear();
+ var isServerAuthority = m_NetworkAnimator.IsServerAuthoritative();
- // Only owners check for Animator changes
- if (m_NetworkAnimator.IsOwner && !m_NetworkAnimator.IsServerAuthoritative() || m_NetworkAnimator.IsServerAuthoritative() && m_NetworkAnimator.NetworkManager.IsServer)
+ // owners when owner authoritative or the server when server authoritative are the only instances that
+ // checks for Animator changes
+ if ((!isServerAuthority && m_NetworkAnimator.IsOwner) || (isServerAuthority && m_NetworkAnimator.IsServer))
{
m_NetworkAnimator.CheckForAnimatorChanges();
}
@@ -157,11 +166,11 @@ namespace Unity.Netcode.Components
[AddComponentMenu("Netcode/Network Animator")]
[RequireComponent(typeof(Animator))]
public class NetworkAnimator : NetworkBehaviour, ISerializationCallbackReceiver
-
{
[Serializable]
internal class TransitionStateinfo
{
+ public bool IsCrossFadeExit;
public int Layer;
public int OriginatingState;
public int DestinationState;
@@ -279,6 +288,11 @@ namespace Unity.Netcode.Components
{
return;
}
+ if (m_Animator == null)
+ {
+ return;
+ }
+
TransitionStateInfoList = new List();
var animatorController = m_Animator.runtimeAnimatorController as AnimatorController;
if (animatorController == null)
@@ -312,9 +326,19 @@ namespace Unity.Netcode.Components
internal float NormalizedTime;
internal int Layer;
internal float Weight;
+ internal float Duration;
// For synchronizing transitions
internal bool Transition;
+ internal bool CrossFade;
+
+ // Flags for bool states
+ private const byte k_IsTransition = 0x01;
+ private const byte k_IsCrossFade = 0x02;
+
+ // Used to serialize the bool states
+ private byte m_StateFlags;
+
// The StateHash is where the transition starts
// and the DestinationStateHash is the destination state
internal int DestinationStateHash;
@@ -324,65 +348,46 @@ namespace Unity.Netcode.Components
if (serializer.IsWriter)
{
var writer = serializer.GetFastBufferWriter();
- var writeSize = FastBufferWriter.GetWriteSize(Transition);
- writeSize += FastBufferWriter.GetWriteSize(StateHash);
- writeSize += FastBufferWriter.GetWriteSize(NormalizedTime);
- writeSize += FastBufferWriter.GetWriteSize(Layer);
- writeSize += FastBufferWriter.GetWriteSize(Weight);
+ m_StateFlags = 0x00;
if (Transition)
{
- writeSize += FastBufferWriter.GetWriteSize(DestinationStateHash);
+ m_StateFlags |= k_IsTransition;
}
-
- if (!writer.TryBeginWrite(writeSize))
+ if (CrossFade)
{
- throw new OverflowException($"[{GetType().Name}] Could not serialize: Out of buffer space.");
+ m_StateFlags |= k_IsCrossFade;
}
+ serializer.SerializeValue(ref m_StateFlags);
- writer.WriteValue(Transition);
- writer.WriteValue(StateHash);
- writer.WriteValue(NormalizedTime);
- writer.WriteValue(Layer);
- writer.WriteValue(Weight);
+ BytePacker.WriteValuePacked(writer, StateHash);
+ BytePacker.WriteValuePacked(writer, Layer);
if (Transition)
{
- writer.WriteValue(DestinationStateHash);
+ BytePacker.WriteValuePacked(writer, DestinationStateHash);
}
}
else
{
var reader = serializer.GetFastBufferReader();
- // Begin reading the Transition flag
- if (!reader.TryBeginRead(FastBufferWriter.GetWriteSize(Transition)))
- {
- throw new OverflowException($"[{GetType().Name}] Could not deserialize: Out of buffer space.");
- }
- reader.ReadValue(out Transition);
+ serializer.SerializeValue(ref m_StateFlags);
+ Transition = (m_StateFlags & k_IsTransition) == k_IsTransition;
+ CrossFade = (m_StateFlags & k_IsCrossFade) == k_IsCrossFade;
- // Now determine what remains to be read
- var readSize = FastBufferWriter.GetWriteSize(StateHash);
- readSize += FastBufferWriter.GetWriteSize(NormalizedTime);
- readSize += FastBufferWriter.GetWriteSize(Layer);
- readSize += FastBufferWriter.GetWriteSize(Weight);
+ ByteUnpacker.ReadValuePacked(reader, out StateHash);
+ ByteUnpacker.ReadValuePacked(reader, out Layer);
if (Transition)
{
- readSize += FastBufferWriter.GetWriteSize(DestinationStateHash);
+ ByteUnpacker.ReadValuePacked(reader, out DestinationStateHash);
}
+ }
- // Now read the remaining information about this AnimationState
- if (!reader.TryBeginRead(readSize))
- {
- throw new OverflowException($"[{GetType().Name}] Could not deserialize: Out of buffer space.");
- }
+ serializer.SerializeValue(ref NormalizedTime);
+ serializer.SerializeValue(ref Weight);
- reader.ReadValue(out StateHash);
- reader.ReadValue(out NormalizedTime);
- reader.ReadValue(out Layer);
- reader.ReadValue(out Weight);
- if (Transition)
- {
- reader.ReadValue(out DestinationStateHash);
- }
+ // Cross fading includes the duration of the cross fade.
+ if (CrossFade)
+ {
+ serializer.SerializeValue(ref Duration);
}
}
}
@@ -565,8 +570,10 @@ namespace Unity.Netcode.Components
// We initialize the m_AnimationMessage for all instances in the event that
// ownership or authority changes during runtime.
- m_AnimationMessage = new AnimationMessage();
- m_AnimationMessage.AnimationStates = new List();
+ m_AnimationMessage = new AnimationMessage
+ {
+ AnimationStates = new List()
+ };
// Store off our current layer weights and create our animation
// state entries per layer.
@@ -588,17 +595,13 @@ namespace Unity.Netcode.Components
m_CachedAnimatorParameters = new NativeArray(parameters.Length, Allocator.Persistent);
m_ParametersToUpdate = new List(parameters.Length);
+ // Include all parameters including any controlled by an AnimationCurve as this could change during runtime.
+ // We ignore changes to any parameter controlled by an AnimationCurve when we are checking for changes in
+ // the Animator's parameters.
for (var i = 0; i < parameters.Length; i++)
{
var parameter = parameters[i];
- if (m_Animator.IsParameterControlledByCurve(parameter.nameHash))
- {
- // we are ignoring parameters that are controlled by animation curves - syncing the layer
- // states indirectly syncs the values that are driven by the animation curves
- continue;
- }
-
var cacheParam = new AnimatorParamCache
{
Type = UnsafeUtility.EnumToInt(parameter.type),
@@ -643,12 +646,22 @@ namespace Unity.Netcode.Components
///
public override void OnNetworkSpawn()
{
+ // If there is no assigned Animator then generate a server network warning (logged locally and if applicable on the server-host side as well).
+ if (m_Animator == null)
+ {
+ NetworkLog.LogWarningServer($"[{gameObject.name}][{nameof(NetworkAnimator)}] {nameof(Animator)} is not assigned! Animation synchronization will not work for this instance!");
+ }
+
if (IsServer)
{
m_ClientSendList = new List(128);
- m_ClientRpcParams = new ClientRpcParams();
- m_ClientRpcParams.Send = new ClientRpcSendParams();
- m_ClientRpcParams.Send.TargetClientIds = m_ClientSendList;
+ m_ClientRpcParams = new ClientRpcParams
+ {
+ Send = new ClientRpcSendParams
+ {
+ TargetClientIds = m_ClientSendList
+ }
+ };
}
// Create a handler for state changes
@@ -691,10 +704,7 @@ namespace Unity.Netcode.Components
for (int layer = 0; layer < m_Animator.layerCount; layer++)
{
var synchronizationStateInfo = m_Animator.GetCurrentAnimatorStateInfo(layer);
- if (SynchronizationStateInfo != null)
- {
- SynchronizationStateInfo.Add(synchronizationStateInfo);
- }
+ SynchronizationStateInfo?.Add(synchronizationStateInfo);
var stateHash = synchronizationStateInfo.fullPathHash;
var normalizedTime = synchronizationStateInfo.normalizedTime;
var isInTransition = m_Animator.IsInTransition(layer);
@@ -767,11 +777,97 @@ namespace Unity.Netcode.Components
else
{
var parameters = new ParametersUpdateMessage();
- var animationStates = new AnimationMessage();
+ var animationMessage = new AnimationMessage();
serializer.SerializeValue(ref parameters);
UpdateParameters(ref parameters);
- serializer.SerializeValue(ref animationStates);
- HandleAnimStateUpdate(ref animationStates);
+ serializer.SerializeValue(ref animationMessage);
+ foreach (var animationState in animationMessage.AnimationStates)
+ {
+ UpdateAnimationState(animationState);
+ }
+ }
+ }
+
+ ///
+ /// Checks for animation state changes in:
+ /// -Layer weights
+ /// -Cross fades
+ /// -Transitions
+ /// -Layer AnimationStates
+ ///
+ private void CheckForStateChange(int layer)
+ {
+ var stateChangeDetected = false;
+ var animState = m_AnimationMessage.AnimationStates[m_AnimationMessage.IsDirtyCount];
+ float layerWeightNow = m_Animator.GetLayerWeight(layer);
+ animState.CrossFade = false;
+ animState.Transition = false;
+ animState.NormalizedTime = 0.0f;
+ animState.Layer = layer;
+ animState.Duration = 0.0f;
+ animState.Weight = m_LayerWeights[layer];
+ animState.DestinationStateHash = 0;
+
+ if (layerWeightNow != m_LayerWeights[layer])
+ {
+ m_LayerWeights[layer] = layerWeightNow;
+ stateChangeDetected = true;
+ animState.Weight = layerWeightNow;
+ }
+
+ AnimatorStateInfo st = m_Animator.GetCurrentAnimatorStateInfo(layer);
+
+ if (m_Animator.IsInTransition(layer))
+ {
+ AnimatorTransitionInfo tt = m_Animator.GetAnimatorTransitionInfo(layer);
+ AnimatorStateInfo nt = m_Animator.GetNextAnimatorStateInfo(layer);
+ if (tt.anyState && tt.fullPathHash == 0 && m_TransitionHash[layer] != nt.fullPathHash)
+ {
+ m_TransitionHash[layer] = nt.fullPathHash;
+ m_AnimationHash[layer] = 0;
+ animState.DestinationStateHash = nt.fullPathHash; // Next state is the destination state for cross fade
+ animState.CrossFade = true;
+ animState.Transition = true;
+ animState.Duration = tt.duration;
+ animState.NormalizedTime = tt.normalizedTime;
+ stateChangeDetected = true;
+ //Debug.Log($"[Cross-Fade] To-Hash: {nt.fullPathHash} | TI-Duration: ({tt.duration}) | TI-Norm: ({tt.normalizedTime}) | From-Hash: ({m_AnimationHash[layer]}) | SI-FPHash: ({st.fullPathHash}) | SI-Norm: ({st.normalizedTime})");
+ }
+ else
+ if (!tt.anyState && tt.fullPathHash != m_TransitionHash[layer])
+ {
+ // first time in this transition for this layer
+ m_TransitionHash[layer] = tt.fullPathHash;
+ m_AnimationHash[layer] = 0;
+ animState.StateHash = tt.fullPathHash; // Transitioning from state
+ animState.CrossFade = false;
+ animState.Transition = true;
+ animState.NormalizedTime = tt.normalizedTime;
+ stateChangeDetected = true;
+ //Debug.Log($"[Transition] TI-Duration: ({tt.duration}) | TI-Norm: ({tt.normalizedTime}) | From-Hash: ({m_AnimationHash[layer]}) |SI-FPHash: ({st.fullPathHash}) | SI-Norm: ({st.normalizedTime})");
+ }
+ }
+ else
+ {
+ if (st.fullPathHash != m_AnimationHash[layer])
+ {
+ m_TransitionHash[layer] = 0;
+ m_AnimationHash[layer] = st.fullPathHash;
+ // first time in this animation state
+ if (m_AnimationHash[layer] != 0)
+ {
+ // came from another animation directly - from Play()
+ animState.StateHash = st.fullPathHash;
+ animState.NormalizedTime = st.normalizedTime;
+ }
+ stateChangeDetected = true;
+ //Debug.Log($"[State] From-Hash: ({m_AnimationHash[layer]}) |SI-FPHash: ({st.fullPathHash}) | SI-Norm: ({st.normalizedTime})");
+ }
+ }
+ if (stateChangeDetected)
+ {
+ m_AnimationMessage.AnimationStates[m_AnimationMessage.IsDirtyCount] = animState;
+ m_AnimationMessage.IsDirtyCount++;
}
}
@@ -784,11 +880,6 @@ namespace Unity.Netcode.Components
///
internal void CheckForAnimatorChanges()
{
- if (!IsSpawned || (!IsOwner && !IsServerAuthoritative()) || (IsServerAuthoritative() && !IsServer))
- {
- return;
- }
-
if (CheckParametersChanged())
{
SendParametersUpdate();
@@ -803,9 +894,6 @@ namespace Unity.Netcode.Components
return;
}
- int stateHash;
- float normalizedTime;
-
// Reset the dirty count before checking for AnimationState updates
m_AnimationMessage.IsDirtyCount = 0;
@@ -815,26 +903,7 @@ namespace Unity.Netcode.Components
AnimatorStateInfo st = m_Animator.GetCurrentAnimatorStateInfo(layer);
var totalSpeed = st.speed * st.speedMultiplier;
var adjustedNormalizedMaxTime = totalSpeed > 0.0f ? 1.0f / totalSpeed : 0.0f;
-
- if (!CheckAnimStateChanged(out stateHash, out normalizedTime, layer))
- {
- continue;
- }
-
- // If we made it here, then we need to synchronize this layer's animation state.
- // Get one of the preallocated AnimationState entries and populate it with the
- // current layer's state.
- var animationState = m_AnimationMessage.AnimationStates[m_AnimationMessage.IsDirtyCount];
-
- animationState.Transition = false; // Only used during synchronization
- animationState.StateHash = stateHash;
- animationState.NormalizedTime = normalizedTime;
- animationState.Layer = layer;
- animationState.Weight = m_LayerWeights[layer];
-
- // Apply the changes
- m_AnimationMessage.AnimationStates[m_AnimationMessage.IsDirtyCount] = animationState;
- m_AnimationMessage.IsDirtyCount++;
+ CheckForStateChange(layer);
}
// Send an AnimationMessage only if there are dirty AnimationStates to send
@@ -851,7 +920,7 @@ namespace Unity.Netcode.Components
m_ClientSendList.AddRange(NetworkManager.ConnectedClientsIds);
m_ClientSendList.Remove(NetworkManager.LocalClientId);
m_ClientRpcParams.Send.TargetClientIds = m_ClientSendList;
- SendAnimStateClientRpc(m_AnimationMessage);
+ SendAnimStateClientRpc(m_AnimationMessage, m_ClientRpcParams);
}
}
}
@@ -885,7 +954,7 @@ namespace Unity.Netcode.Components
///
/// Helper function to get the cached value
///
- unsafe private T GetValue(ref AnimatorParamCache animatorParamCache)
+ private unsafe T GetValue(ref AnimatorParamCache animatorParamCache)
{
T currentValue;
fixed (void* value = animatorParamCache.Value)
@@ -900,12 +969,20 @@ namespace Unity.Netcode.Components
/// If so, it fills out m_ParametersToUpdate with the indices of the parameters
/// that have changed. Returns true if any parameters changed.
///
- unsafe private bool CheckParametersChanged()
+ private unsafe bool CheckParametersChanged()
{
m_ParametersToUpdate.Clear();
for (int i = 0; i < m_CachedAnimatorParameters.Length; i++)
{
ref var cacheValue = ref UnsafeUtility.ArrayElementAsRef(m_CachedAnimatorParameters.GetUnsafePtr(), i);
+
+ // If a parameter gets controlled by a curve during runtime after initialization of NetworkAnimator
+ // then ignore changes to this parameter. We are not removing the parameter in the event that
+ // it no longer is controlled by a curve.
+ if (m_Animator.IsParameterControlledByCurve(cacheValue.Hash))
+ {
+ continue;
+ }
var hash = cacheValue.Hash;
if (cacheValue.Type == AnimationParamEnumWrapper.AnimatorControllerParameterInt)
{
@@ -941,52 +1018,6 @@ namespace Unity.Netcode.Components
return m_ParametersToUpdate.Count > 0;
}
- ///
- /// Checks if any of the Animator's states have changed
- ///
- private bool CheckAnimStateChanged(out int stateHash, out float normalizedTime, int layer)
- {
- stateHash = 0;
- normalizedTime = 0;
-
- float layerWeightNow = m_Animator.GetLayerWeight(layer);
- if (layerWeightNow != m_LayerWeights[layer])
- {
- m_LayerWeights[layer] = layerWeightNow;
- return true;
- }
-
- if (m_Animator.IsInTransition(layer))
- {
- AnimatorTransitionInfo tt = m_Animator.GetAnimatorTransitionInfo(layer);
- if (tt.fullPathHash != m_TransitionHash[layer])
- {
- // first time in this transition for this layer
- m_TransitionHash[layer] = tt.fullPathHash;
- m_AnimationHash[layer] = 0;
- return true;
- }
- }
- else
- {
- AnimatorStateInfo st = m_Animator.GetCurrentAnimatorStateInfo(layer);
- if (st.fullPathHash != m_AnimationHash[layer])
- {
- // first time in this animation state
- if (m_AnimationHash[layer] != 0)
- {
- // came from another animation directly - from Play()
- stateHash = st.fullPathHash;
- normalizedTime = st.normalizedTime;
- }
- m_TransitionHash[layer] = 0;
- m_AnimationHash[layer] = st.fullPathHash;
- return true;
- }
- }
- return false;
- }
-
///
/// Writes all of the Animator's parameters
/// This uses the m_ParametersToUpdate list to write out only
@@ -1110,14 +1141,14 @@ namespace Unity.Netcode.Components
}
// If there is no state transition then return
- if (animationState.StateHash == 0)
+ if (animationState.StateHash == 0 && !animationState.Transition)
{
return;
}
var currentState = m_Animator.GetCurrentAnimatorStateInfo(animationState.Layer);
// If it is a transition, then we are synchronizing transitions in progress when a client late joins
- if (animationState.Transition)
+ if (animationState.Transition && !animationState.CrossFade)
{
// We should have all valid entries for any animation state transition update
// Verify the AnimationState's assigned Layer exists
@@ -1150,9 +1181,14 @@ namespace Unity.Netcode.Components
NetworkLog.LogError($"[DestinationState To Transition Info] Layer ({animationState.Layer}) does not exist!");
}
}
+ else if (animationState.Transition && animationState.CrossFade)
+ {
+ m_Animator.CrossFade(animationState.DestinationStateHash, animationState.Duration, animationState.Layer, animationState.NormalizedTime);
+ }
else
{
- if (currentState.fullPathHash != animationState.StateHash)
+ // Make sure we are not just updating the weight of a layer.
+ if (currentState.fullPathHash != animationState.StateHash && m_Animator.HasState(animationState.Layer, animationState.StateHash))
{
m_Animator.Play(animationState.StateHash, animationState.Layer, animationState.NormalizedTime);
}
@@ -1237,23 +1273,11 @@ namespace Unity.Netcode.Components
}
}
- internal void HandleAnimStateUpdate(ref AnimationMessage animationMessage)
- {
- var isServerAuthoritative = IsServerAuthoritative();
- if (!isServerAuthoritative && !IsOwner || isServerAuthoritative)
- {
- foreach (var animationState in animationMessage.AnimationStates)
- {
- UpdateAnimationState(animationState);
- }
- }
- }
-
///
/// Internally-called RPC client receiving function to update some animation state on a client
///
[ClientRpc]
- private unsafe void SendAnimStateClientRpc(AnimationMessage animationMessage, ClientRpcParams clientRpcParams = default)
+ internal unsafe void SendAnimStateClientRpc(AnimationMessage animationMessage, ClientRpcParams clientRpcParams = default)
{
// This should never happen
if (IsHost)
@@ -1264,7 +1288,10 @@ namespace Unity.Netcode.Components
}
return;
}
- HandleAnimStateUpdate(ref animationMessage);
+ foreach (var animationState in animationMessage.AnimationStates)
+ {
+ UpdateAnimationState(animationState);
+ }
}
///
@@ -1274,44 +1301,31 @@ namespace Unity.Netcode.Components
[ServerRpc]
internal void SendAnimTriggerServerRpc(AnimationTriggerMessage animationTriggerMessage, ServerRpcParams serverRpcParams = default)
{
- // If it is server authoritative
+ // Ignore if a non-owner sent this.
+ if (serverRpcParams.Receive.SenderClientId != OwnerClientId)
+ {
+ if (NetworkManager.LogLevel == LogLevel.Developer)
+ {
+ NetworkLog.LogWarning($"[Owner Authoritative] Detected the a non-authoritative client is sending the server animation trigger updates. If you recently changed ownership of the {name} object, then this could be the reason.");
+ }
+ return;
+ }
+
+ // set the trigger locally on the server
+ InternalSetTrigger(animationTriggerMessage.Hash, animationTriggerMessage.IsTriggerSet);
+
+ m_ClientSendList.Clear();
+ m_ClientSendList.AddRange(NetworkManager.ConnectedClientsIds);
+ m_ClientSendList.Remove(NetworkManager.ServerClientId);
+
if (IsServerAuthoritative())
{
- // The only condition where this should (be allowed to) happen is when the owner sends the server a trigger message
- if (OwnerClientId == serverRpcParams.Receive.SenderClientId)
- {
- m_NetworkAnimatorStateChangeHandler.QueueTriggerUpdateToClient(animationTriggerMessage);
- }
- else if (NetworkManager.LogLevel == LogLevel.Developer)
- {
- NetworkLog.LogWarning($"[Server Authoritative] Detected the a non-authoritative client is sending the server animation trigger updates. If you recently changed ownership of the {name} object, then this could be the reason.");
- }
+ m_NetworkAnimatorStateChangeHandler.QueueTriggerUpdateToClient(animationTriggerMessage, m_ClientRpcParams);
}
- else
+ else if (NetworkManager.ConnectedClientsIds.Count > (IsHost ? 2 : 1))
{
- // Ignore if a non-owner sent this.
- if (serverRpcParams.Receive.SenderClientId != OwnerClientId)
- {
- if (NetworkManager.LogLevel == LogLevel.Developer)
- {
- NetworkLog.LogWarning($"[Owner Authoritative] Detected the a non-authoritative client is sending the server animation trigger updates. If you recently changed ownership of the {name} object, then this could be the reason.");
- }
- return;
- }
-
- // set the trigger locally on the server
- InternalSetTrigger(animationTriggerMessage.Hash, animationTriggerMessage.IsTriggerSet);
-
- // send the message to all non-authority clients excluding the server and the owner
- if (NetworkManager.ConnectedClientsIds.Count > (IsHost ? 2 : 1))
- {
- m_ClientSendList.Clear();
- m_ClientSendList.AddRange(NetworkManager.ConnectedClientsIds);
- m_ClientSendList.Remove(serverRpcParams.Receive.SenderClientId);
- m_ClientSendList.Remove(NetworkManager.ServerClientId);
- m_ClientRpcParams.Send.TargetClientIds = m_ClientSendList;
- m_NetworkAnimatorStateChangeHandler.QueueTriggerUpdateToClient(animationTriggerMessage, m_ClientRpcParams);
- }
+ m_ClientSendList.Remove(serverRpcParams.Receive.SenderClientId);
+ m_NetworkAnimatorStateChangeHandler.QueueTriggerUpdateToClient(animationTriggerMessage, m_ClientRpcParams);
}
}
diff --git a/Components/NetworkDeltaPosition.cs b/Components/NetworkDeltaPosition.cs
new file mode 100644
index 0000000..a7ed563
--- /dev/null
+++ b/Components/NetworkDeltaPosition.cs
@@ -0,0 +1,205 @@
+using System.Runtime.CompilerServices;
+using Unity.Mathematics;
+using UnityEngine;
+
+namespace Unity.Netcode.Components
+{
+ ///
+ /// Used to synchromnize delta position when half float precision is enabled
+ ///
+ public struct NetworkDeltaPosition : INetworkSerializable
+ {
+ internal const float MaxDeltaBeforeAdjustment = 64f;
+
+ ///
+ /// The HalfVector3 used to synchronize the delta in position
+ ///
+ public HalfVector3 HalfVector3;
+
+ internal Vector3 CurrentBasePosition;
+ internal Vector3 PrecisionLossDelta;
+ internal Vector3 HalfDeltaConvertedBack;
+ internal Vector3 PreviousPosition;
+ internal Vector3 DeltaPosition;
+ internal int NetworkTick;
+
+ ///
+ /// The serialization implementation of
+ ///
+ public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter
+ {
+ HalfVector3.NetworkSerialize(serializer);
+ }
+
+ ///
+ /// Gets the full precision value of Vector3 position while also potentially updating the current base position.
+ ///
+ /// Use the current network tick value.
+ /// The full position as a .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Vector3 ToVector3(int networkTick)
+ {
+ // When synchronizing, it is possible to have a state update arrive
+ // for the same synchronization network tick. Under this scenario,
+ // we only want to return the existing CurrentBasePosition + DeltaPosition
+ // values and not process the X, Y, or Z values.
+ // (See the constructors below)
+ if (networkTick == NetworkTick)
+ {
+ return CurrentBasePosition + DeltaPosition;
+ }
+ for (int i = 0; i < HalfVector3.Length; i++)
+ {
+ if (HalfVector3.AxisToSynchronize[i])
+ {
+ DeltaPosition[i] = Mathf.HalfToFloat(HalfVector3.Axis[i].value);
+ // If we exceed or are equal to the maximum delta value then we need to
+ // apply the delta to the CurrentBasePosition value and reset the delta
+ // position for the axis.
+ if (Mathf.Abs(DeltaPosition[i]) >= MaxDeltaBeforeAdjustment)
+ {
+ CurrentBasePosition[i] += DeltaPosition[i];
+ DeltaPosition[i] = 0.0f;
+ HalfVector3.Axis[i] = half.zero;
+ }
+ }
+ }
+ return CurrentBasePosition + DeltaPosition;
+ }
+
+ ///
+ /// Returns the current base position (excluding the delta position offset).
+ ///
+ /// The current base position as a
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Vector3 GetCurrentBasePosition()
+ {
+ return CurrentBasePosition;
+ }
+
+ ///
+ /// Returns the full position which includes the delta offset position.
+ ///
+ /// The full position as a .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Vector3 GetFullPosition()
+ {
+ return CurrentBasePosition + DeltaPosition;
+ }
+
+ ///
+ /// The half float vector3 version of the current delta position.
+ ///
+ ///
+ /// Only applies to the authoritative side for instances.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Vector3 GetConvertedDelta()
+ {
+ return HalfDeltaConvertedBack;
+ }
+
+ ///
+ /// The full precision current delta position.
+ ///
+ ///
+ /// Authoritative: Will have no precision loss
+ /// Non-Authoritative: Has the current network tick's loss of precision.
+ /// Precision loss adjustments are one network tick behind on the
+ /// non-authoritative side.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Vector3 GetDeltaPosition()
+ {
+ return DeltaPosition;
+ }
+
+ ///
+ /// Updates the position delta based off of the current base position.
+ ///
+ /// The full precision value to (converted to half floats) used to determine the delta offset positon.
+ /// Set the current network tick value when updating.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void UpdateFrom(ref Vector3 vector3, int networkTick)
+ {
+ NetworkTick = networkTick;
+ DeltaPosition = (vector3 + PrecisionLossDelta) - CurrentBasePosition;
+ for (int i = 0; i < HalfVector3.Length; i++)
+ {
+ if (HalfVector3.AxisToSynchronize[i])
+ {
+ HalfVector3.Axis[i] = math.half(DeltaPosition[i]);
+ HalfDeltaConvertedBack[i] = Mathf.HalfToFloat(HalfVector3.Axis[i].value);
+ PrecisionLossDelta[i] = DeltaPosition[i] - HalfDeltaConvertedBack[i];
+ if (Mathf.Abs(HalfDeltaConvertedBack[i]) >= MaxDeltaBeforeAdjustment)
+ {
+ CurrentBasePosition[i] += HalfDeltaConvertedBack[i];
+ HalfDeltaConvertedBack[i] = 0.0f;
+ DeltaPosition[i] = 0.0f;
+ }
+ }
+ }
+
+ for (int i = 0; i < HalfVector3.Length; i++)
+ {
+ if (HalfVector3.AxisToSynchronize[i])
+ {
+ PreviousPosition[i] = vector3[i];
+ }
+ }
+ }
+
+ ///
+ /// Constructor
+ ///
+ /// The initial axial values (converted to half floats) when instantiated.
+ /// Set the network tick value to the current network tick when instantiating.
+ /// The axis to be synchronized.
+ public NetworkDeltaPosition(Vector3 vector3, int networkTick, bool3 axisToSynchronize)
+ {
+ NetworkTick = networkTick;
+ CurrentBasePosition = vector3;
+ PreviousPosition = vector3;
+ PrecisionLossDelta = Vector3.zero;
+ DeltaPosition = Vector3.zero;
+ HalfDeltaConvertedBack = Vector3.zero;
+ HalfVector3 = new HalfVector3(vector3, axisToSynchronize);
+ UpdateFrom(ref vector3, networkTick);
+ }
+
+ ///
+ /// Constructor that defaults to all axis being synchronized.
+ ///
+ /// The initial axial values (converted to half floats) when instantiated.
+ /// Set the network tick value to the current network tick when instantiating.
+ public NetworkDeltaPosition(Vector3 vector3, int networkTick) : this(vector3, networkTick, math.bool3(true))
+ {
+
+ }
+
+ ///
+ /// Constructor
+ ///
+ /// The initial x axis (converted to half float) value when instantiated.
+ /// The initial y axis (converted to half float) value when instantiated.
+ /// The initial z axis (converted to half float) value when instantiated.
+ /// Set the network tick value to the current network tick when instantiating.
+ /// The axis to be synchronized.
+ public NetworkDeltaPosition(float x, float y, float z, int networkTick, bool3 axisToSynchronize) :
+ this(new Vector3(x, y, z), networkTick, axisToSynchronize)
+ {
+ }
+
+ ///
+ /// Constructor
+ ///
+ /// The initial x axis (converted to half float) value when instantiated.
+ /// The initial y axis (converted to half float) value when instantiated.
+ /// The initial z axis (converted to half float) value when instantiated.
+ /// Set the network tick value to the current network tick when instantiating.
+ public NetworkDeltaPosition(float x, float y, float z, int networkTick) :
+ this(new Vector3(x, y, z), networkTick, math.bool3(true))
+ {
+ }
+ }
+}
diff --git a/Components/NetworkDeltaPosition.cs.meta b/Components/NetworkDeltaPosition.cs.meta
new file mode 100644
index 0000000..66d0494
--- /dev/null
+++ b/Components/NetworkDeltaPosition.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e45e6886578116f4c92fa0fe0d77fb85
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Components/NetworkTransform.cs b/Components/NetworkTransform.cs
index d1a08c1..04a8837 100644
--- a/Components/NetworkTransform.cs
+++ b/Components/NetworkTransform.cs
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using Unity.Mathematics;
using UnityEngine;
namespace Unity.Netcode.Components
@@ -47,65 +49,135 @@ namespace Unity.Netcode.Components
///
public OnClientRequestChangeDelegate OnClientRequestChange;
- internal struct NetworkTransformState : INetworkSerializable
+ ///
+ /// Data structure used to synchronize the
+ ///
+ public struct NetworkTransformState : INetworkSerializable
{
- private const int k_InLocalSpaceBit = 0;
- private const int k_PositionXBit = 1;
- private const int k_PositionYBit = 2;
- private const int k_PositionZBit = 3;
- private const int k_RotAngleXBit = 4;
- private const int k_RotAngleYBit = 5;
- private const int k_RotAngleZBit = 6;
- private const int k_ScaleXBit = 7;
- private const int k_ScaleYBit = 8;
- private const int k_ScaleZBit = 9;
- private const int k_TeleportingBit = 10;
- // 11-15:
+ private const int k_InLocalSpaceBit = 0x00000001; // Persists between state updates (authority dictates if this is set)
+ private const int k_PositionXBit = 0x00000002;
+ private const int k_PositionYBit = 0x00000004;
+ private const int k_PositionZBit = 0x00000008;
+ private const int k_RotAngleXBit = 0x00000010;
+ private const int k_RotAngleYBit = 0x00000020;
+ private const int k_RotAngleZBit = 0x00000040;
+ private const int k_ScaleXBit = 0x00000080;
+ private const int k_ScaleYBit = 0x00000100;
+ private const int k_ScaleZBit = 0x00000200;
+ private const int k_TeleportingBit = 0x00000400;
+ private const int k_Interpolate = 0x00000800; // Persists between state updates (authority dictates if this is set)
+ private const int k_QuaternionSync = 0x00001000; // Persists between state updates (authority dictates if this is set)
+ private const int k_QuaternionCompress = 0x00002000; // Persists between state updates (authority dictates if this is set)
+ private const int k_UseHalfFloats = 0x00004000; // Persists between state updates (authority dictates if this is set)
+ private const int k_Synchronization = 0x00008000;
+ private const int k_PositionSlerp = 0x00010000; // Persists between state updates (authority dictates if this is set)
- private ushort m_Bitset;
+ // Stores persistent and state relative flags
+ private uint m_Bitset;
- internal bool InLocalSpace
+ // Used to store the tick calculated sent time
+ internal double SentTime;
+
+ // Used for full precision position updates
+ internal float PositionX, PositionY, PositionZ;
+
+ // Used for full precision Euler updates
+ internal float RotAngleX, RotAngleY, RotAngleZ;
+
+ // Used for full precision quaternion updates
+ internal Quaternion Rotation;
+
+ // Used for full precision scale updates
+ internal float ScaleX, ScaleY, ScaleZ;
+
+ // Used for half precision delta position updates
+ internal Vector3 CurrentPosition;
+ internal Vector3 DeltaPosition;
+ internal NetworkDeltaPosition NetworkDeltaPosition;
+
+ // Used for half precision scale
+ internal HalfVector3 HalfVectorScale;
+ internal Vector3 Scale;
+
+ // Used for half precision quaternion
+ internal HalfVector4 HalfVectorRotation;
+
+ // Used to store a compressed quaternion
+ internal uint QuaternionCompressed;
+
+ // Authoritative and non-authoritative sides use this to determine if a NetworkTransformState is
+ // dirty or not.
+ internal bool IsDirty { get; set; }
+
+ ///
+ /// The last byte size of the updated.
+ ///
+ public int LastSerializedSize { get; internal set; }
+
+ // Used for NetworkDeltaPosition delta position synchronization
+ internal int NetworkTick;
+
+ // Used when tracking by state ID is enabled
+ internal bool TrackByStateId;
+ internal int StateId;
+
+ // Used during serialization
+ private FastBufferReader m_Reader;
+ private FastBufferWriter m_Writer;
+
+ ///
+ /// When set, the is operates in local space
+ ///
+ public bool InLocalSpace
{
- get => (m_Bitset & (1 << k_InLocalSpaceBit)) != 0;
- set
+ get => GetFlag(k_InLocalSpaceBit);
+ internal set
{
- if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_InLocalSpaceBit)); }
- else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_InLocalSpaceBit)); }
+ SetFlag(value, k_InLocalSpaceBit);
}
}
// Position
- internal bool HasPositionX
+ ///
+ /// When set, the X-Axis position value has changed
+ ///
+ public bool HasPositionX
{
- get => (m_Bitset & (1 << k_PositionXBit)) != 0;
- set
+ get => GetFlag(k_PositionXBit);
+ internal set
{
- if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_PositionXBit)); }
- else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_PositionXBit)); }
+ SetFlag(value, k_PositionXBit);
}
}
- internal bool HasPositionY
+ ///
+ /// When set, the Y-Axis position value has changed
+ ///
+ public bool HasPositionY
{
- get => (m_Bitset & (1 << k_PositionYBit)) != 0;
- set
+ get => GetFlag(k_PositionYBit);
+ internal set
{
- if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_PositionYBit)); }
- else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_PositionYBit)); }
+ SetFlag(value, k_PositionYBit);
}
}
- internal bool HasPositionZ
+ ///
+ /// When set, the Z-Axis position value has changed
+ ///
+ public bool HasPositionZ
{
- get => (m_Bitset & (1 << k_PositionZBit)) != 0;
- set
+ get => GetFlag(k_PositionZBit);
+ internal set
{
- if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_PositionZBit)); }
- else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_PositionZBit)); }
+ SetFlag(value, k_PositionZBit);
}
}
- internal bool HasPositionChange
+ ///
+ /// When set, at least one of the position axis values has changed.
+ ///
+ public bool HasPositionChange
{
get
{
@@ -114,37 +186,58 @@ namespace Unity.Netcode.Components
}
// RotAngles
- internal bool HasRotAngleX
+ ///
+ /// When set, the Euler rotation X-Axis value has changed.
+ ///
+ ///
+ /// When quaternion synchronization is enabled all axis are always updated.
+ ///
+ public bool HasRotAngleX
{
- get => (m_Bitset & (1 << k_RotAngleXBit)) != 0;
- set
+ get => GetFlag(k_RotAngleXBit);
+ internal set
{
- if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_RotAngleXBit)); }
- else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_RotAngleXBit)); }
+ SetFlag(value, k_RotAngleXBit);
}
}
- internal bool HasRotAngleY
+ ///
+ /// When set, the Euler rotation Y-Axis value has changed.
+ ///
+ ///
+ /// When quaternion synchronization is enabled all axis are always updated.
+ ///
+ public bool HasRotAngleY
{
- get => (m_Bitset & (1 << k_RotAngleYBit)) != 0;
- set
+ get => GetFlag(k_RotAngleYBit);
+ internal set
{
- if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_RotAngleYBit)); }
- else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_RotAngleYBit)); }
+ SetFlag(value, k_RotAngleYBit);
}
}
- internal bool HasRotAngleZ
+ ///
+ /// When set, the Euler rotation Z-Axis value has changed.
+ ///
+ ///
+ /// When quaternion synchronization is enabled all axis are always updated.
+ ///
+ public bool HasRotAngleZ
{
- get => (m_Bitset & (1 << k_RotAngleZBit)) != 0;
- set
+ get => GetFlag(k_RotAngleZBit);
+ internal set
{
- if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_RotAngleZBit)); }
- else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_RotAngleZBit)); }
+ SetFlag(value, k_RotAngleZBit);
}
}
- internal bool HasRotAngleChange
+ ///
+ /// When set, at least one of the rotation axis values has changed.
+ ///
+ ///
+ /// When quaternion synchronization is enabled all axis are always updated.
+ ///
+ public bool HasRotAngleChange
{
get
{
@@ -152,39 +245,59 @@ namespace Unity.Netcode.Components
}
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal bool HasScale(int axisIndex)
+ {
+ return GetFlag(k_ScaleXBit << axisIndex);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal void SetHasScale(int axisIndex, bool isSet)
+ {
+ SetFlag(isSet, k_ScaleXBit << axisIndex);
+ }
// Scale
- internal bool HasScaleX
+ ///
+ /// When set, the X-Axis scale value has changed.
+ ///
+ public bool HasScaleX
{
- get => (m_Bitset & (1 << k_ScaleXBit)) != 0;
- set
+ get => GetFlag(k_ScaleXBit);
+ internal set
{
- if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_ScaleXBit)); }
- else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_ScaleXBit)); }
+ SetFlag(value, k_ScaleXBit);
}
}
- internal bool HasScaleY
+ ///
+ /// When set, the Y-Axis scale value has changed.
+ ///
+ public bool HasScaleY
{
- get => (m_Bitset & (1 << k_ScaleYBit)) != 0;
- set
+ get => GetFlag(k_ScaleYBit);
+ internal set
{
- if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_ScaleYBit)); }
- else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_ScaleYBit)); }
+ SetFlag(value, k_ScaleYBit);
}
}
- internal bool HasScaleZ
+ ///
+ /// When set, the Z-Axis scale value has changed.
+ ///
+ public bool HasScaleZ
{
- get => (m_Bitset & (1 << k_ScaleZBit)) != 0;
- set
+ get => GetFlag(k_ScaleZBit);
+ internal set
{
- if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_ScaleZBit)); }
- else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_ScaleZBit)); }
+ SetFlag(value, k_ScaleZBit);
}
}
- internal bool HasScaleChange
+ ///
+ /// When set, at least one of the scale axis values has changed.
+ ///
+ public bool HasScaleChange
{
get
{
@@ -192,112 +305,537 @@ namespace Unity.Netcode.Components
}
}
- internal bool IsTeleportingNextFrame
+ ///
+ /// When set, the current state will be treated as a teleport.
+ ///
+ ///
+ /// When teleporting:
+ /// - Interpolation is reset.
+ /// - If using half precision, full precision values are used.
+ /// - All axis marked to be synchronized will be updated.
+ ///
+ public bool IsTeleportingNextFrame
{
- get => (m_Bitset & (1 << k_TeleportingBit)) != 0;
- set
+ get => GetFlag(k_TeleportingBit);
+ internal set
{
- if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_TeleportingBit)); }
- else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_TeleportingBit)); }
+ SetFlag(value, k_TeleportingBit);
}
}
- internal float PositionX, PositionY, PositionZ;
- internal float RotAngleX, RotAngleY, RotAngleZ;
- internal float ScaleX, ScaleY, ScaleZ;
- internal double SentTime;
-
- // Authoritative and non-authoritative sides use this to determine if a NetworkTransformState is
- // dirty or not.
- internal bool IsDirty;
-
- // Non-Authoritative side uses this for ending extrapolation of the last applied state
- internal int EndExtrapolationTick;
+ ///
+ /// When set the is uses interpolation.
+ ///
+ ///
+ /// Authority does not apply interpolation via .
+ /// Authority should handle its own motion/rotation/scale smoothing locally.
+ ///
+ public bool UseInterpolation
+ {
+ get => GetFlag(k_Interpolate);
+ internal set
+ {
+ SetFlag(value, k_Interpolate);
+ }
+ }
///
- /// This will reset the NetworkTransform BitSet
+ /// When enabled, this instance uses synchronization.
///
+ ///
+ /// Use quaternion synchronization if you are nesting s and rotation can occur on both the parent and child.
+ /// When quaternion synchronization is enabled, the entire quaternion is updated when there are any changes to any axial values.
+ /// You can use half float precision or quaternion compression to reduce the bandwidth cost.
+ ///
+ public bool QuaternionSync
+ {
+ get => GetFlag(k_QuaternionSync);
+ internal set
+ {
+ SetFlag(value, k_QuaternionSync);
+ }
+ }
+
+ ///
+ /// When set s will be compressed down to 4 bytes using a smallest three implementation.
+ ///
+ ///
+ /// This only will be applied when is enabled.
+ /// Half float precision provides a higher precision than quaternion compression but at the cost of 4 additional bytes per update.
+ /// - Quaternion Compression: 4 bytes per delta update
+ /// - Half float precision: 8 bytes per delta update
+ ///
+ public bool QuaternionCompression
+ {
+ get => GetFlag(k_QuaternionCompress);
+ internal set
+ {
+ SetFlag(value, k_QuaternionCompress);
+ }
+ }
+
+ ///
+ /// When set, the will use half float precision for position, rotation, and scale.
+ ///
+ ///
+ /// Postion is synchronized through delta position updates in order to reduce precision loss/drift and to extend to positions beyond the limitation of half float maximum values.
+ /// Rotation and scale both use half float precision ( and )
+ ///
+ public bool UseHalfFloatPrecision
+ {
+ get => GetFlag(k_UseHalfFloats);
+ internal set
+ {
+ SetFlag(value, k_UseHalfFloats);
+ }
+ }
+
+ ///
+ /// When set, this indicates it is the first state being synchronized.
+ /// Typically when the associate is spawned or a client is being synchronized after connecting to a network session in progress.
+ ///
+ public bool IsSynchronizing
+ {
+ get => GetFlag(k_Synchronization);
+ internal set
+ {
+ SetFlag(value, k_Synchronization);
+ }
+ }
+
+ ///
+ /// Determines if position interpolation will Slerp towards its target position.
+ /// This is only really useful if you are moving around a point in a circular pattern.
+ ///
+ public bool UsePositionSlerp
+ {
+ get => GetFlag(k_PositionSlerp);
+ internal set
+ {
+ SetFlag(value, k_PositionSlerp);
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private bool GetFlag(int flag)
+ {
+ return (m_Bitset & flag) != 0;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void SetFlag(bool set, int flag)
+ {
+ if (set) { m_Bitset = m_Bitset | (uint)flag; }
+ else { m_Bitset = m_Bitset & (uint)~flag; }
+ }
+
internal void ClearBitSetForNextTick()
{
- // We need to preserve the local space settings for the current state
- m_Bitset &= (ushort)(m_Bitset & (1 << k_InLocalSpaceBit));
+ // Clear everything but flags that should persist between state updates until changed by authority
+ m_Bitset &= k_InLocalSpaceBit | k_Interpolate | k_UseHalfFloats | k_QuaternionSync | k_QuaternionCompress | k_PositionSlerp;
IsDirty = false;
}
+ ///
+ /// Returns the current state's rotation. If there is no change in the rotation,
+ /// then it will return .
+ ///
+ ///
+ /// When there is no change in an updated state's rotation then there are no values to return.
+ /// Checking for is one way to detect this.
+ ///
+ ///
+ public Quaternion GetRotation()
+ {
+ if (HasRotAngleChange)
+ {
+ if (QuaternionSync)
+ {
+ return Rotation;
+ }
+ else
+ {
+ return Quaternion.Euler(RotAngleX, RotAngleY, RotAngleZ);
+ }
+ }
+ return Quaternion.identity;
+ }
+
+ ///
+ /// Returns the current state's position. If there is no change in position,
+ /// then it returns .
+ ///
+ ///
+ /// When there is no change in an updated state's position then there are no values to return.
+ /// Checking for is one way to detect this.
+ ///
+ ///
+ public Vector3 GetPosition()
+ {
+ if (HasPositionChange)
+ {
+ if (UseHalfFloatPrecision)
+ {
+ if (IsTeleportingNextFrame)
+ {
+ return CurrentPosition;
+ }
+ else
+ {
+ return NetworkDeltaPosition.GetFullPosition();
+ }
+ }
+ else
+ {
+ return new Vector3(PositionX, PositionY, PositionZ);
+ }
+ }
+ return Vector3.zero;
+ }
+
+ ///
+ /// Returns the current state's scale. If there is no change in scale,
+ /// then it returns .
+ ///
+ ///
+ /// When there is no change in an updated state's scale then there are no values to return.
+ /// Checking for is one way to detect this.
+ ///
+ ///
+ public Vector3 GetScale()
+ {
+ if (HasScaleChange)
+ {
+ if (UseHalfFloatPrecision)
+ {
+ if (IsTeleportingNextFrame)
+ {
+ return Scale;
+ }
+ else
+ {
+ return HalfVectorScale.ToVector3();
+ }
+ }
+ else
+ {
+ return new Vector3(ScaleX, ScaleY, ScaleZ);
+ }
+ }
+ return Vector3.zero;
+ }
+
+ ///
+ /// The network tick that this state was sent by the authoritative instance.
+ ///
+ ///
+ public int GetNetworkTick()
+ {
+ return NetworkTick;
+ }
+
+ ///
+ /// Serializes this
+ ///
public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter
{
- serializer.SerializeValue(ref SentTime);
- // InLocalSpace + HasXXX Bits
- serializer.SerializeValue(ref m_Bitset);
- // Position Values
- if (HasPositionX)
+ // Used to calculate the LastSerializedSize value
+ var positionStart = 0;
+ var isWriting = serializer.IsWriter;
+ if (isWriting)
{
- serializer.SerializeValue(ref PositionX);
+ m_Writer = serializer.GetFastBufferWriter();
+ positionStart = m_Writer.Position;
+ }
+ else
+ {
+ m_Reader = serializer.GetFastBufferReader();
+ positionStart = m_Reader.Position;
}
- if (HasPositionY)
+ if (TrackByStateId)
{
- serializer.SerializeValue(ref PositionY);
+ var stateId = StateId;
+ if (IsSynchronizing)
+ {
+ StateId = -1;
+ }
+ else
+ {
+ if (serializer.IsWriter)
+ {
+ StateId++;
+ }
+ serializer.SerializeValue(ref StateId);
+ }
}
- if (HasPositionZ)
+ // Synchronize State Flags and Network Tick
{
- serializer.SerializeValue(ref PositionZ);
+ if (isWriting)
+ {
+ BytePacker.WriteValueBitPacked(m_Writer, m_Bitset);
+ // We use network ticks as opposed to absolute time as the authoritative
+ // side updates on every new tick.
+ BytePacker.WriteValueBitPacked(m_Writer, NetworkTick);
+
+ }
+ else
+ {
+ ByteUnpacker.ReadValueBitPacked(m_Reader, out m_Bitset);
+ // We use network ticks as opposed to absolute time as the authoritative
+ // side updates on every new tick.
+ ByteUnpacker.ReadValueBitPacked(m_Reader, out NetworkTick);
+ }
}
- // RotAngle Values
- if (HasRotAngleX)
+ // Synchronize Position
+ if (HasPositionChange)
{
- serializer.SerializeValue(ref RotAngleX);
+ if (UseHalfFloatPrecision)
+ {
+ if (IsTeleportingNextFrame)
+ {
+ // **Always use full precision when teleporting and UseHalfFloatPrecision is enabled**
+ serializer.SerializeValue(ref CurrentPosition);
+ // If we are synchronizing, then include the half vector position's delta offset
+ if (IsSynchronizing)
+ {
+ serializer.SerializeValue(ref DeltaPosition);
+ if (!isWriting)
+ {
+ NetworkDeltaPosition = new NetworkDeltaPosition(Vector3.zero, 0, math.bool3(HasPositionX, HasPositionY, HasPositionZ));
+ NetworkDeltaPosition.NetworkSerialize(serializer);
+ }
+ else
+ {
+ serializer.SerializeNetworkSerializable(ref NetworkDeltaPosition);
+ }
+ }
+ }
+ else
+ {
+ if (!isWriting)
+ {
+ NetworkDeltaPosition = new NetworkDeltaPosition(Vector3.zero, 0, math.bool3(HasPositionX, HasPositionY, HasPositionZ));
+ NetworkDeltaPosition.NetworkSerialize(serializer);
+ }
+ else
+ {
+ serializer.SerializeNetworkSerializable(ref NetworkDeltaPosition);
+ }
+ }
+ }
+ else // Legacy Position Synchronization
+ {
+ // Position Values
+ if (HasPositionX)
+ {
+ serializer.SerializeValue(ref PositionX);
+ }
+
+ if (HasPositionY)
+ {
+ serializer.SerializeValue(ref PositionY);
+ }
+
+ if (HasPositionZ)
+ {
+ serializer.SerializeValue(ref PositionZ);
+ }
+ }
}
- if (HasRotAngleY)
+ // Synchronize Rotation
+ if (HasRotAngleChange)
{
- serializer.SerializeValue(ref RotAngleY);
+ if (QuaternionSync)
+ {
+ // Always use the full quaternion if teleporting
+ if (IsTeleportingNextFrame)
+ {
+ serializer.SerializeValue(ref Rotation);
+ }
+ else
+ {
+ // Use the quaternion compressor if enabled
+ if (QuaternionCompression)
+ {
+ if (isWriting)
+ {
+ QuaternionCompressed = QuaternionCompressor.CompressQuaternion(ref Rotation);
+ }
+
+ serializer.SerializeValue(ref QuaternionCompressed);
+
+ if (!isWriting)
+ {
+ QuaternionCompressor.DecompressQuaternion(ref Rotation, QuaternionCompressed);
+ }
+ }
+ else
+ {
+ if (UseHalfFloatPrecision)
+ {
+ if (isWriting)
+ {
+ HalfVectorRotation.UpdateFrom(ref Rotation);
+ }
+
+ serializer.SerializeNetworkSerializable(ref HalfVectorRotation);
+
+ if (!isWriting)
+ {
+ Rotation = HalfVectorRotation.ToQuaternion();
+ }
+ }
+ else
+ {
+ serializer.SerializeValue(ref Rotation);
+ }
+ }
+ }
+ }
+ else // Euler Rotation Synchronization
+ {
+ // Half float precision (full precision when teleporting)
+ if (UseHalfFloatPrecision && !IsTeleportingNextFrame)
+ {
+ if (HasRotAngleChange)
+ {
+ var halfPrecisionRotation = new HalfVector3(RotAngleX, RotAngleY, RotAngleZ, math.bool3(HasRotAngleX, HasRotAngleY, HasRotAngleZ));
+ serializer.SerializeValue(ref halfPrecisionRotation);
+ if (!isWriting)
+ {
+ var eulerRotation = halfPrecisionRotation.ToVector3();
+ if (HasRotAngleX)
+ {
+ RotAngleX = eulerRotation.x;
+ }
+
+ if (HasRotAngleY)
+ {
+ RotAngleY = eulerRotation.y;
+ }
+
+ if (HasRotAngleZ)
+ {
+ RotAngleZ = eulerRotation.z;
+ }
+ }
+ }
+ }
+ else // Full precision Euler
+ {
+ // RotAngle Values
+ if (HasRotAngleX)
+ {
+ serializer.SerializeValue(ref RotAngleX);
+ }
+
+ if (HasRotAngleY)
+ {
+ serializer.SerializeValue(ref RotAngleY);
+ }
+
+ if (HasRotAngleZ)
+ {
+ serializer.SerializeValue(ref RotAngleZ);
+ }
+ }
+ }
}
- if (HasRotAngleZ)
+ // Synchronize Scale
+ if (HasScaleChange)
{
- serializer.SerializeValue(ref RotAngleZ);
- }
+ // Half precision scale synchronization
+ if (UseHalfFloatPrecision)
+ {
+ if (IsTeleportingNextFrame)
+ {
+ serializer.SerializeValue(ref Scale);
+ }
+ else
+ {
+ // For scale, when half precision is enabled we can still only send the axis with deltas
+ HalfVectorScale = new HalfVector3(Scale, math.bool3(HasScaleX, HasScaleY, HasScaleZ));
+ serializer.SerializeValue(ref HalfVectorScale);
+ if (!isWriting)
+ {
+ Scale = HalfVectorScale.ToVector3();
+ if (HasScaleX)
+ {
+ ScaleX = Scale.x;
+ }
- // Scale Values
- if (HasScaleX)
- {
- serializer.SerializeValue(ref ScaleX);
- }
+ if (HasScaleY)
+ {
+ ScaleY = Scale.y;
+ }
- if (HasScaleY)
- {
- serializer.SerializeValue(ref ScaleY);
- }
+ if (HasScaleZ)
+ {
+ ScaleZ = Scale.x;
+ }
+ }
+ }
+ }
+ else // Full precision scale synchronization
+ {
+ if (HasScaleX)
+ {
+ serializer.SerializeValue(ref ScaleX);
+ }
- if (HasScaleZ)
- {
- serializer.SerializeValue(ref ScaleZ);
+ if (HasScaleY)
+ {
+ serializer.SerializeValue(ref ScaleY);
+ }
+
+ if (HasScaleZ)
+ {
+ serializer.SerializeValue(ref ScaleZ);
+ }
+ }
}
// Only if we are receiving state
- if (serializer.IsReader)
+ if (!isWriting)
{
- // Go ahead and mark the local state dirty or not dirty as well
- ///
+ // Go ahead and mark the local state dirty
IsDirty = HasPositionChange || HasRotAngleChange || HasScaleChange;
+ LastSerializedSize = m_Reader.Position - positionStart;
+ }
+ else
+ {
+ LastSerializedSize = m_Writer.Position - positionStart;
}
}
}
///
- /// Whether or not x component of position will be replicated
+ /// When enabled (default), the x component of position will be synchronized by authority.
///
+ ///
+ /// Changes to this on non-authoritative instances has no effect.
+ ///
public bool SyncPositionX = true;
+
///
- /// Whether or not y component of position will be replicated
+ /// When enabled (default), the y component of position will be synchronized by authority.
///
+ ///
+ /// Changes to this on non-authoritative instances has no effect.
+ ///
public bool SyncPositionY = true;
+
///
- /// Whether or not z component of position will be replicated
+ /// When enabled (default), the z component of position will be synchronized by authority.
///
+ ///
+ /// Changes to this on non-authoritative instances has no effect.
+ ///
public bool SyncPositionZ = true;
private bool SynchronizePosition
@@ -309,16 +847,30 @@ namespace Unity.Netcode.Components
}
///
- /// Whether or not x component of rotation will be replicated
+ /// When enabled (default), the x component of rotation will be synchronized by authority.
///
+ ///
+ /// When is enabled this does not apply.
+ /// Changes to this on non-authoritative instances has no effect.
+ ///
public bool SyncRotAngleX = true;
+
///
- /// Whether or not y component of rotation will be replicated
+ /// When enabled (default), the y component of rotation will be synchronized by authority.
///
+ ///
+ /// When is enabled this does not apply.
+ /// Changes to this on non-authoritative instances has no effect.
+ ///
public bool SyncRotAngleY = true;
+
///
- /// Whether or not z component of rotation will be replicated
+ /// When enabled (default), the z component of rotation will be synchronized by authority.
///
+ ///
+ /// When is enabled this does not apply.
+ /// Changes to this on non-authoritative instances has no effect.
+ ///
public bool SyncRotAngleZ = true;
private bool SynchronizeRotation
@@ -330,18 +882,28 @@ namespace Unity.Netcode.Components
}
///
- /// Whether or not x component of scale will be replicated
+ /// When enabled (default), the x component of scale will be synchronized by authority.
///
+ ///
+ /// Changes to this on non-authoritative instances has no effect.
+ ///
public bool SyncScaleX = true;
- ///
- /// Whether or not y component of scale will be replicated
- ///
- public bool SyncScaleY = true;
- ///
- /// Whether or not z component of scale will be replicated
- ///
- public bool SyncScaleZ = true;
+ ///
+ /// When enabled (default), the y component of scale will be synchronized by authority.
+ ///
+ ///
+ /// Changes to this on non-authoritative instances has no effect.
+ ///
+ public bool SyncScaleY = true;
+
+ ///
+ /// When enabled (default), the z component of scale will be synchronized by authority.
+ ///
+ ///
+ /// Changes to this on non-authoritative instances has no effect.
+ ///
+ public bool SyncScaleZ = true;
private bool SynchronizeScale
{
@@ -352,41 +914,101 @@ namespace Unity.Netcode.Components
}
///
- /// The current position threshold value
- /// Any changes to the position that exceeds the current threshold value will be replicated
+ /// The position threshold value that triggers a delta state update by the authoritative instance.
///
+ ///
+ /// Note: setting this to zero will update position every network tick whether it changed or not.
+ ///
public float PositionThreshold = PositionThresholdDefault;
///
- /// The current rotation threshold value
- /// Any changes to the rotation that exceeds the current threshold value will be replicated
- /// Minimum Value: 0.001
- /// Maximum Value: 360.0
+ /// The rotation threshold value that triggers a delta state update by the authoritative instance.
///
- [Range(0.001f, 360.0f)]
+ ///
+ /// Minimum Value: 0.00001
+ /// Maximum Value: 360.0
+ ///
+ [Range(0.00001f, 360.0f)]
public float RotAngleThreshold = RotAngleThresholdDefault;
///
- /// The current scale threshold value
- /// Any changes to the scale that exceeds the current threshold value will be replicated
+ /// The scale threshold value that triggers a delta state update by the authoritative instance.
///
+ ///
+ /// Note: setting this to zero will update position every network tick whether it changed or not.
+ ///
public float ScaleThreshold = ScaleThresholdDefault;
+ ///
+ /// Enable this on the authority side for quaternion synchronization
+ ///
+ ///
+ /// This is synchronized by authority. During runtime, this should only be changed by the
+ /// authoritative side. Non-authoritative instances will be overridden by the next
+ /// authoritative state update.
+ ///
+ [Tooltip("When enabled, this will synchronize the full Quaternion (i.e. all Euler rotation axis are updated if one axis has a delta)")]
+ public bool UseQuaternionSynchronization = false;
+
+ ///
+ /// Enabled this on the authority side for quaternion compression
+ ///
+ ///
+ /// This has a lower precision than half float precision. Recommended only for low precision
+ /// scenarios. provides better precision at roughly half
+ /// the cost of a full quaternion update.
+ /// This is synchronized by authority. During runtime, this should only be changed by the
+ /// authoritative side. Non-authoritative instances will be overridden by the next
+ /// authoritative state update.
+ ///
+ [Tooltip("When enabled, this uses a smallest three implementation that reduces full Quaternion updates down to the size of an unsigned integer (ignores half float precision settings).")]
+ public bool UseQuaternionCompression = false;
+
+ ///
+ /// Enable this to use half float precision for position, rotation, and scale.
+ /// When enabled, delta position synchronization is used.
+ ///
+ ///
+ /// This is synchronized by authority. During runtime, this should only be changed by the
+ /// authoritative side. Non-authoritative instances will be overridden by the next
+ /// authoritative state update.
+ ///
+ [Tooltip("When enabled, this will use half float precision values for position (uses delta position updating), rotation (except when Quaternion compression is enabled), and scale.")]
+ public bool UseHalfFloatPrecision = false;
+
///
/// 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.
+ /// This is synchronized by authority. During runtime, this should only be changed by the
+ /// authoritative side. Non-authoritative instances will be overridden by the next
+ /// authoritative state update.
///
[Tooltip("Sets whether this transform should sync in local space or in world space")]
public bool InLocalSpace = false;
///
- /// When enabled (default) interpolation is applied and when disabled no interpolation is applied
+ /// When enabled (default) interpolation is applied.
+ /// When disabled interpolation is disabled.
///
+ ///
+ /// This is synchronized by authority and changes to interpolation during runtime forces a
+ /// teleport/full update. During runtime, this should only be changed by the authoritative
+ /// side. Non-authoritative instances will be overridden by the next authoritative state update.
+ ///
public bool Interpolate = true;
+ ///
+ /// When true and interpolation is enabled, this will Slerp to the target position.
+ ///
+ ///
+ /// This is synchronized by authority and only applies to position interpolation.
+ /// During runtime, this should only be changed by the authoritative side. Non-authoritative
+ /// instances will be overridden by the next authoritative state update.
+ ///
+ [Tooltip("When enabled the position interpolator will Slerp towards its current target position.")]
+ public bool SlerpPosition = false;
+
///
/// Used to determine who can write to this transform. Server only for this transform.
/// Changing this value alone in a child implementation will not allow you to create a NetworkTransform which can be written to by clients. See the ClientNetworkTransform Sample
@@ -399,13 +1021,13 @@ namespace Unity.Netcode.Components
/// Internally used by to keep track of whether this derived class instance
/// was instantiated on the server side or not.
///
- protected bool m_CachedIsServer;
+ protected bool m_CachedIsServer; // Note: we no longer use this and are only keeping it until we decide to deprecate it
///
/// Internally used by to keep track of the instance assigned to this
/// this derived class instance.
///
- protected NetworkManager m_CachedNetworkManager;
+ protected NetworkManager m_CachedNetworkManager; // Note: we no longer use this and are only keeping it until we decide to deprecate it
///
/// We have two internal NetworkVariables.
@@ -427,6 +1049,102 @@ namespace Unity.Netcode.Components
}
}
+ ///
+ /// Helper method that returns the space relative position of the transform.
+ ///
+ ///
+ /// If InLocalSpace is then it returns the transform.localPosition
+ /// If InLocalSpace is then it returns the transform.position
+ /// When invoked on the non-authority side:
+ /// If is true then it will return the most
+ /// current authority position from the most recent state update. This can be useful
+ /// if interpolation is enabled and you need to determine the final target position.
+ /// When invoked on the authority side:
+ /// It will always return the space relative position.
+ ///
+ ///
+ /// Authority always returns the space relative transform position (whether true or false).
+ /// Non-authority:
+ /// When false (default): returns the space relative transform position
+ /// When true: returns the authority position from the most recent state update.
+ ///
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Vector3 GetSpaceRelativePosition(bool getCurrentState = false)
+ {
+ if (!getCurrentState || CanCommitToTransform)
+ {
+ return InLocalSpace ? transform.localPosition : transform.position;
+ }
+ else
+ {
+ return m_CurrentPosition;
+ }
+ }
+
+ ///
+ /// Helper method that returns the space relative rotation of the transform.
+ ///
+ ///
+ /// If InLocalSpace is then it returns the transform.localRotation
+ /// If InLocalSpace is then it returns the transform.rotation
+ /// When invoked on the non-authority side:
+ /// If is true then it will return the most
+ /// current authority rotation from the most recent state update. This can be useful
+ /// if interpolation is enabled and you need to determine the final target rotation.
+ /// When invoked on the authority side:
+ /// It will always return the space relative rotation.
+ ///
+ ///
+ /// Authority always returns the space relative transform rotation (whether true or false).
+ /// Non-authority:
+ /// When false (default): returns the space relative transform rotation
+ /// When true: returns the authority rotation from the most recent state update.
+ ///
+ ///
+ public Quaternion GetSpaceRelativeRotation(bool getCurrentState = false)
+ {
+ if (!getCurrentState || CanCommitToTransform)
+ {
+ return InLocalSpace ? transform.localRotation : transform.rotation;
+ }
+ else
+ {
+ return m_CurrentRotation;
+ }
+ }
+
+ ///
+ /// Helper method that returns the scale of the transform.
+ ///
+ ///
+ /// When invoked on the non-authority side:
+ /// If is true then it will return the most
+ /// current authority scale from the most recent state update. This can be useful
+ /// if interpolation is enabled and you need to determine the final target scale.
+ /// When invoked on the authority side:
+ /// It will always return the space relative scale.
+ ///
+ ///
+ /// Authority always returns the space relative transform scale (whether true or false).
+ /// Non-authority:
+ /// When false (default): returns the space relative transform scale
+ /// When true: returns the authority scale from the most recent state update.
+ ///
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Vector3 GetScale(bool getCurrentState = false)
+ {
+ if (!getCurrentState || CanCommitToTransform)
+ {
+ return transform.localScale;
+ }
+ else
+ {
+ return m_CurrentScale;
+ }
+ }
+
// Used by both authoritative and non-authoritative instances.
// This represents the most recent local authoritative state.
private NetworkTransformState m_LocalAuthoritativeNetworkState;
@@ -434,21 +1152,93 @@ namespace Unity.Netcode.Components
private ClientRpcParams m_ClientRpcParams = new ClientRpcParams() { Send = new ClientRpcSendParams() };
private List m_ClientIds = new List() { 0 };
- 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 BufferedLinearInterpolatorVector3 m_PositionInterpolator;
+ private BufferedLinearInterpolatorVector3 m_ScaleInterpolator;
+ private BufferedLinearInterpolatorQuaternion m_RotationInterpolator; // rotation is a single Quaternion since each Euler axis will affect the quaternion's final value
- // Used by integration test
- private NetworkTransformState m_LastSentState;
+ // Non-Authoritative's current position, scale, and rotation that is used to assure the non-authoritative side cannot make adjustments to
+ // the portions of the transform being synchronized.
+ private Vector3 m_CurrentPosition;
+ private Vector3 m_CurrentScale;
+ private Quaternion m_CurrentRotation;
- internal NetworkTransformState GetLastSentState()
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal void UpdatePositionInterpolator(Vector3 position, double time, bool resetInterpolator = false)
{
- return m_LastSentState;
+ if (!CanCommitToTransform)
+ {
+ if (resetInterpolator)
+ {
+ m_PositionInterpolator.ResetTo(position, time);
+ }
+ else
+ {
+ m_PositionInterpolator.AddMeasurement(position, time);
+ }
+ }
+ }
+
+#if DEBUG_NETWORKTRANSFORM || UNITY_INCLUDE_TESTS
+ ///
+ /// For debugging delta position and half vector3
+ ///
+ protected delegate void AddLogEntryHandler(ref NetworkTransformState networkTransformState, ulong targetClient, bool preUpdate = false);
+ protected AddLogEntryHandler m_AddLogEntry;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void AddLogEntry(ref NetworkTransformState networkTransformState, ulong targetClient, bool preUpdate = false)
+ {
+ m_AddLogEntry?.Invoke(ref networkTransformState, targetClient, preUpdate);
+ }
+
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected int GetStateId(ref NetworkTransformState state)
+ {
+ return state.StateId;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected NetworkDeltaPosition GetHalfPositionState()
+ {
+ return m_HalfPositionState;
+ }
+
+#else
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void AddLogEntry(ref NetworkTransformState networkTransformState, ulong targetClient, bool preUpdate = false)
+ {
+ }
+#endif
+
+ ///
+ /// Only used when UseHalfFloatPrecision is enabled
+ ///
+ private NetworkDeltaPosition m_HalfPositionState = new NetworkDeltaPosition(Vector3.zero, 0);
+
+ internal void UpdatePositionSlerp()
+ {
+ if (m_PositionInterpolator != null)
+ {
+ m_PositionInterpolator.IsSlerp = SlerpPosition;
+ }
+ }
+
+ ///
+ /// Determines if synchronization is needed.
+ /// Basically only if we are running in owner authoritative mode and it
+ /// is the owner being synchronized we don't want to synchronize with
+ /// the exception of the NetworkObject being owned by the server.
+ ///
+ private bool ShouldSynchronizeHalfFloat(ulong targetClientId)
+ {
+ if (!IsServerAuthoritative() && NetworkObject.OwnerClientId == targetClientId)
+ {
+ // Return false for all client owners but return true for the server
+ return NetworkObject.IsOwnedByServer;
+ }
+ return true;
}
///
@@ -456,25 +1246,43 @@ namespace Unity.Netcode.Components
/// Server Side: Serializes as if we were teleporting (everything is sent via NetworkTransformState)
/// Client Side: Adds the interpolated state which applies the NetworkTransformState as well
///
+ ///
+ /// If a derived class overrides this, then make sure to invoke this base method!
+ ///
+ ///
+ ///
+ /// the clientId being synchronized (both reading and writing)
protected override void OnSynchronize(ref BufferSerializer serializer)
{
- // We don't need to synchronize NetworkTransforms that are on the same
- // GameObject as the NetworkObject.
- if (NetworkObject.gameObject == gameObject)
- {
- return;
- }
+ var targetClientId = m_TargetIdBeingSynchronized;
var synchronizationState = new NetworkTransformState();
+
if (serializer.IsWriter)
{
synchronizationState.IsTeleportingNextFrame = true;
- ApplyTransformToNetworkStateWithInfo(ref synchronizationState, m_CachedNetworkManager.LocalTime.Time, transform);
+ var transformToCommit = transform;
+ // If we are using Half Float Precision, then we want to only synchronize the authority's m_HalfPositionState.FullPosition in order for
+ // for the non-authority side to be able to properly synchronize delta position updates.
+ ApplyTransformToNetworkStateWithInfo(ref synchronizationState, ref transformToCommit, true, targetClientId);
synchronizationState.NetworkSerialize(serializer);
}
else
{
synchronizationState.NetworkSerialize(serializer);
- AddInterpolatedState(synchronizationState);
+ // Set the transform's synchronization modes
+ InLocalSpace = synchronizationState.InLocalSpace;
+ Interpolate = synchronizationState.UseInterpolation;
+ UseQuaternionSynchronization = synchronizationState.QuaternionSync;
+ UseHalfFloatPrecision = synchronizationState.UseHalfFloatPrecision;
+ UseQuaternionCompression = synchronizationState.QuaternionCompression;
+ SlerpPosition = synchronizationState.UsePositionSlerp;
+ UpdatePositionSlerp();
+
+ // Teleport/Fully Initialize based on the state
+ ApplyTeleportingState(synchronizationState);
+
+ m_LocalAuthoritativeNetworkState = synchronizationState;
+ m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = false;
}
}
@@ -488,24 +1296,33 @@ namespace Unity.Netcode.Components
/// time it was marked dirty
protected void TryCommitTransformToServer(Transform transformToCommit, double dirtyTime)
{
- // Only client owners or the server should invoke this method
- if (!IsOwner && !m_CachedIsServer)
+ if (!IsSpawned)
{
- NetworkLog.LogError($"Non-owner instance, {name}, is trying to commit a transform!");
+ NetworkLog.LogError($"Cannot commit transform when not spawned!");
+ return;
+ }
+
+ // Only the server or the owner is allowed to commit a transform
+ if (!IsServer && !IsOwner)
+ {
+ var errorMessage = gameObject != NetworkObject.gameObject ?
+ $"Non-authority instance of {NetworkObject.gameObject.name} is trying to commit a transform on {gameObject.name}!" :
+ $"Non-authority instance of {NetworkObject.gameObject.name} is trying to commit a transform!";
+ NetworkLog.LogError(errorMessage);
return;
}
// If we are authority, update the authoritative state
if (CanCommitToTransform)
{
- UpdateAuthoritativeState(transform);
+ OnUpdateAuthoritativeState(ref transformToCommit);
}
else // Non-Authority
{
var position = InLocalSpace ? transformToCommit.localPosition : transformToCommit.position;
var rotation = InLocalSpace ? transformToCommit.localRotation : transformToCommit.rotation;
// We are an owner requesting to update our state
- if (!m_CachedIsServer)
+ if (!IsServer)
{
SetStateServerRpc(position, rotation, transformToCommit.localScale, false);
}
@@ -516,24 +1333,43 @@ namespace Unity.Netcode.Components
}
}
+ ///
+ /// Invoked just prior to being pushed to non-authority instances.
+ ///
+ ///
+ /// This is useful to know the exact position, rotation, or scale values sent
+ /// to non-authoritative instances. This is only invoked on the authoritative
+ /// instance.
+ ///
+ /// the state being pushed
+ protected virtual void OnAuthorityPushTransformState(ref NetworkTransformState networkTransformState)
+ {
+ }
+
///
/// 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)
+ private void TryCommitTransform(ref Transform transformToCommit, bool synchronize = false)
{
- if (!CanCommitToTransform && !IsOwner)
+ // Only the server or the owner is allowed to commit a transform
+ if (!IsServer && !IsOwner)
{
NetworkLog.LogError($"[{name}] is trying to commit the transform without authority!");
return;
}
// If the transform has deltas (returns dirty) then...
- if (ApplyTransformToNetworkState(ref m_LocalAuthoritativeNetworkState, dirtyTime, transformToCommit))
+ if (ApplyTransformToNetworkStateWithInfo(ref m_LocalAuthoritativeNetworkState, ref transformToCommit, synchronize))
{
- // ...commit the state
+ m_LocalAuthoritativeNetworkState.LastSerializedSize = ReplicatedNetworkState.Value.LastSerializedSize;
+ OnAuthorityPushTransformState(ref m_LocalAuthoritativeNetworkState);
+
+ // "push"/commit the state
ReplicatedNetworkState.Value = m_LocalAuthoritativeNetworkState;
+
+ m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = false;
}
}
@@ -543,18 +1379,12 @@ namespace Unity.Netcode.Components
private void ResetInterpolatedStateToCurrentAuthoritativeState()
{
var serverTime = NetworkManager.ServerTime.Time;
- var position = InLocalSpace ? transform.localPosition : transform.position;
- m_PositionXInterpolator.ResetTo(position.x, serverTime);
- m_PositionYInterpolator.ResetTo(position.y, serverTime);
- m_PositionZInterpolator.ResetTo(position.z, serverTime);
- var rotation = InLocalSpace ? transform.localRotation : transform.rotation;
- m_RotationInterpolator.ResetTo(rotation, serverTime);
+ UpdatePositionInterpolator(GetSpaceRelativePosition(), serverTime, true);
+ UpdatePositionSlerp();
- var scale = transform.localScale;
- m_ScaleXInterpolator.ResetTo(scale.x, serverTime);
- m_ScaleYInterpolator.ResetTo(scale.y, serverTime);
- m_ScaleZInterpolator.ResetTo(scale.z, serverTime);
+ m_ScaleInterpolator.ResetTo(transform.localScale, serverTime);
+ m_RotationInterpolator.ResetTo(GetSpaceRelativeRotation(), serverTime);
}
///
@@ -571,7 +1401,7 @@ namespace Unity.Netcode.Components
m_LocalAuthoritativeNetworkState.ClearBitSetForNextTick();
// Now check the transform for any threshold value changes
- ApplyTransformToNetworkStateWithInfo(ref m_LocalAuthoritativeNetworkState, m_CachedNetworkManager.LocalTime.Time, transform);
+ ApplyTransformToNetworkStateWithInfo(ref m_LocalAuthoritativeNetworkState, ref transform);
// Return the entire state to be used by the integration test
return m_LocalAuthoritativeNetworkState;
@@ -582,101 +1412,341 @@ namespace Unity.Netcode.Components
///
internal bool ApplyTransformToNetworkState(ref NetworkTransformState networkState, double dirtyTime, Transform transformToUse)
{
- return ApplyTransformToNetworkStateWithInfo(ref networkState, dirtyTime, transformToUse);
+ // Apply the interpolate and PostionDeltaCompression flags, otherwise we get false positives whether something changed or not.
+ networkState.UseInterpolation = Interpolate;
+ networkState.QuaternionSync = UseQuaternionSynchronization;
+ networkState.UseHalfFloatPrecision = UseHalfFloatPrecision;
+ networkState.QuaternionCompression = UseQuaternionCompression;
+ m_HalfPositionState = new NetworkDeltaPosition(Vector3.zero, 0, math.bool3(SyncPositionX, SyncPositionY, SyncPositionZ));
+
+ return ApplyTransformToNetworkStateWithInfo(ref networkState, ref transformToUse);
}
///
/// Applies the transform to the specified.
///
- private bool ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState networkState, double dirtyTime, Transform transformToUse)
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private bool ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState networkState, ref Transform transformToUse, bool isSynchronization = false, ulong targetClientId = 0)
{
+ var isTeleportingAndNotSynchronizing = networkState.IsTeleportingNextFrame && !isSynchronization;
var isDirty = false;
- var isPositionDirty = false;
- var isRotationDirty = false;
- var isScaleDirty = false;
+ var isPositionDirty = isTeleportingAndNotSynchronizing ? networkState.HasPositionChange : false;
+ var isRotationDirty = isTeleportingAndNotSynchronizing ? networkState.HasRotAngleChange : false;
+ var isScaleDirty = isTeleportingAndNotSynchronizing ? networkState.HasScaleChange : false;
var position = InLocalSpace ? transformToUse.localPosition : transformToUse.position;
var rotAngles = InLocalSpace ? transformToUse.localEulerAngles : transformToUse.eulerAngles;
var scale = transformToUse.localScale;
+ networkState.IsSynchronizing = isSynchronization;
+
if (InLocalSpace != networkState.InLocalSpace)
{
networkState.InLocalSpace = InLocalSpace;
isDirty = true;
+ networkState.IsTeleportingNextFrame = true;
}
- if (SyncPositionX && (Mathf.Abs(networkState.PositionX - position.x) >= PositionThreshold || networkState.IsTeleportingNextFrame))
+ if (Interpolate != networkState.UseInterpolation)
{
- networkState.PositionX = position.x;
- networkState.HasPositionX = true;
- isPositionDirty = true;
+ networkState.UseInterpolation = Interpolate;
+ isDirty = true;
+ // When we change from interpolating to not interpolating (or vice versa) we need to synchronize/reset everything
+ networkState.IsTeleportingNextFrame = true;
}
- if (SyncPositionY && (Mathf.Abs(networkState.PositionY - position.y) >= PositionThreshold || networkState.IsTeleportingNextFrame))
+ if (UseQuaternionSynchronization != networkState.QuaternionSync)
{
- networkState.PositionY = position.y;
- networkState.HasPositionY = true;
- isPositionDirty = true;
+ networkState.QuaternionSync = UseQuaternionSynchronization;
+ isDirty = true;
+ networkState.IsTeleportingNextFrame = true;
}
- if (SyncPositionZ && (Mathf.Abs(networkState.PositionZ - position.z) >= PositionThreshold || networkState.IsTeleportingNextFrame))
+ if (UseQuaternionCompression != networkState.QuaternionCompression)
{
- networkState.PositionZ = position.z;
- networkState.HasPositionZ = true;
- isPositionDirty = true;
+ networkState.QuaternionCompression = UseQuaternionCompression;
+ isDirty = true;
+ networkState.IsTeleportingNextFrame = true;
}
- if (SyncRotAngleX && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleX, rotAngles.x)) >= RotAngleThreshold || networkState.IsTeleportingNextFrame))
+ if (UseHalfFloatPrecision != networkState.UseHalfFloatPrecision)
{
- networkState.RotAngleX = rotAngles.x;
- networkState.HasRotAngleX = true;
- isRotationDirty = true;
+ networkState.UseHalfFloatPrecision = UseHalfFloatPrecision;
+ isDirty = true;
+ networkState.IsTeleportingNextFrame = true;
}
- if (SyncRotAngleY && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleY, rotAngles.y)) >= RotAngleThreshold || networkState.IsTeleportingNextFrame))
+ if (SlerpPosition != networkState.UsePositionSlerp)
{
- networkState.RotAngleY = rotAngles.y;
- networkState.HasRotAngleY = true;
- isRotationDirty = true;
+ networkState.UsePositionSlerp = SlerpPosition;
+ isDirty = true;
+ networkState.IsTeleportingNextFrame = true;
}
- if (SyncRotAngleZ && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleZ, rotAngles.z)) >= RotAngleThreshold || networkState.IsTeleportingNextFrame))
+ if (!UseHalfFloatPrecision)
{
- networkState.RotAngleZ = rotAngles.z;
- networkState.HasRotAngleZ = true;
- isRotationDirty = true;
+ if (SyncPositionX && (Mathf.Abs(networkState.PositionX - position.x) >= PositionThreshold || networkState.IsTeleportingNextFrame))
+ {
+ networkState.PositionX = position.x;
+ networkState.HasPositionX = true;
+ isPositionDirty = true;
+ }
+
+ if (SyncPositionY && (Mathf.Abs(networkState.PositionY - position.y) >= PositionThreshold || networkState.IsTeleportingNextFrame))
+ {
+ networkState.PositionY = position.y;
+ networkState.HasPositionY = true;
+ isPositionDirty = true;
+ }
+
+ if (SyncPositionZ && (Mathf.Abs(networkState.PositionZ - position.z) >= PositionThreshold || networkState.IsTeleportingNextFrame))
+ {
+ networkState.PositionZ = position.z;
+ networkState.HasPositionZ = true;
+ isPositionDirty = true;
+ }
+ }
+ else if (SynchronizePosition)
+ {
+ // If we are teleporting then we can skip the delta threshold check
+ isPositionDirty = networkState.IsTeleportingNextFrame;
+
+ // For NetworkDeltaPosition, if any axial value is dirty then we always send a full update
+ if (!isPositionDirty)
+ {
+ for (int i = 0; i < 3; i++)
+ {
+ if (Math.Abs(position[i] - m_HalfPositionState.PreviousPosition[i]) >= PositionThreshold)
+ {
+ isPositionDirty = i == 0 ? SyncPositionX : i == 1 ? SyncPositionY : SyncPositionZ;
+ if (!isPositionDirty)
+ {
+ continue;
+ }
+ break;
+ }
+ }
+ }
+
+ // If the position is dirty or we are teleporting (which includes synchronization)
+ // then determine what parts of the NetworkDeltaPosition should be updated
+ if (isPositionDirty)
+ {
+ // If we are not synchronizing the transform state for the first time
+ if (!isSynchronization)
+ {
+ // With global teleporting (broadcast to all non-authority instances)
+ // we re-initialize authority's NetworkDeltaPosition and synchronize all
+ // non-authority instances with the new full precision position
+ if (networkState.IsTeleportingNextFrame)
+ {
+ m_HalfPositionState = new NetworkDeltaPosition(position, networkState.NetworkTick, math.bool3(SyncPositionX, SyncPositionY, SyncPositionZ));
+ networkState.CurrentPosition = position;
+ }
+ else // Otherwise, just synchronize the delta position value
+ {
+ m_HalfPositionState.HalfVector3.AxisToSynchronize = math.bool3(SyncPositionX, SyncPositionY, SyncPositionZ);
+ m_HalfPositionState.UpdateFrom(ref position, networkState.NetworkTick);
+ }
+
+ networkState.NetworkDeltaPosition = m_HalfPositionState;
+ }
+ else // If synchronizing is set, then use the current full position value on the server side
+ {
+ if (ShouldSynchronizeHalfFloat(targetClientId))
+ {
+ // If we have a NetworkDeltaPosition that has a state applied, then we want to determine
+ // what needs to be synchronized. For owner authoritative mode, the server side
+ // will have no valid state yet.
+ if (m_HalfPositionState.NetworkTick > 0)
+ {
+ // Always synchronize the base position and the ushort values of the
+ // current m_HalfPositionState
+ networkState.CurrentPosition = m_HalfPositionState.CurrentBasePosition;
+ networkState.NetworkDeltaPosition = m_HalfPositionState;
+ // If the server is the owner, in both server and owner authoritative modes,
+ // or we are running in server authoritative mode, then we use the
+ // HalfDeltaConvertedBack value as the delta position
+ if (NetworkObject.IsOwnedByServer || IsServerAuthoritative())
+ {
+ networkState.DeltaPosition = m_HalfPositionState.HalfDeltaConvertedBack;
+ }
+ else
+ {
+ // Otherwise, we are in owner authoritative mode and the server's NetworkDeltaPosition
+ // state is "non-authoritative" relative so we use the DeltaPosition.
+ networkState.DeltaPosition = m_HalfPositionState.DeltaPosition;
+ }
+ }
+ else // Reset everything and just send the current position
+ {
+ networkState.NetworkDeltaPosition = new NetworkDeltaPosition(Vector3.zero, 0, math.bool3(SyncPositionX, SyncPositionY, SyncPositionZ));
+ networkState.DeltaPosition = Vector3.zero;
+ networkState.CurrentPosition = position;
+ }
+ }
+ else
+ {
+ networkState.NetworkDeltaPosition = new NetworkDeltaPosition(Vector3.zero, 0, math.bool3(SyncPositionX, SyncPositionY, SyncPositionZ));
+ networkState.CurrentPosition = position;
+ }
+ // Add log entry for this update relative to the client being synchronized
+ AddLogEntry(ref networkState, targetClientId, true);
+ }
+ networkState.HasPositionX = SyncPositionX;
+ networkState.HasPositionY = SyncPositionY;
+ networkState.HasPositionZ = SyncPositionZ;
+ }
}
- if (SyncScaleX && (Mathf.Abs(networkState.ScaleX - scale.x) >= ScaleThreshold || networkState.IsTeleportingNextFrame))
+ if (!UseQuaternionSynchronization)
{
- networkState.ScaleX = scale.x;
+ if (SyncRotAngleX && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleX, rotAngles.x)) >= RotAngleThreshold || networkState.IsTeleportingNextFrame))
+ {
+ networkState.RotAngleX = rotAngles.x;
+ networkState.HasRotAngleX = true;
+ isRotationDirty = true;
+ }
+
+ if (SyncRotAngleY && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleY, rotAngles.y)) >= RotAngleThreshold || networkState.IsTeleportingNextFrame))
+ {
+ networkState.RotAngleY = rotAngles.y;
+ networkState.HasRotAngleY = true;
+ isRotationDirty = true;
+ }
+
+ if (SyncRotAngleZ && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleZ, rotAngles.z)) >= RotAngleThreshold || networkState.IsTeleportingNextFrame))
+ {
+ networkState.RotAngleZ = rotAngles.z;
+ networkState.HasRotAngleZ = true;
+ isRotationDirty = true;
+ }
+ }
+ else if (SynchronizeRotation)
+ {
+ // If we are teleporting then we can skip the delta threshold check
+ isRotationDirty = networkState.IsTeleportingNextFrame;
+ // For quaternion synchronization, if one angle is dirty we send a full update
+ if (!isRotationDirty)
+ {
+ var previousRotation = networkState.Rotation.eulerAngles;
+ for (int i = 0; i < 3; i++)
+ {
+ if (Mathf.Abs(Mathf.DeltaAngle(previousRotation[i], rotAngles[i])) >= RotAngleThreshold)
+ {
+ isRotationDirty = true;
+ break;
+ }
+ }
+ }
+ if (isRotationDirty)
+ {
+ networkState.Rotation = InLocalSpace ? transformToUse.localRotation : transformToUse.rotation;
+ networkState.HasRotAngleX = true;
+ networkState.HasRotAngleY = true;
+ networkState.HasRotAngleZ = true;
+ }
+ }
+
+ // Only if we are not synchronizing...
+ if (!isSynchronization)
+ {
+ if (!UseHalfFloatPrecision)
+ {
+ if (SyncScaleX && (Mathf.Abs(networkState.ScaleX - scale.x) >= ScaleThreshold || networkState.IsTeleportingNextFrame))
+ {
+ networkState.ScaleX = scale.x;
+ networkState.HasScaleX = true;
+ isScaleDirty = true;
+ }
+
+ if (SyncScaleY && (Mathf.Abs(networkState.ScaleY - scale.y) >= ScaleThreshold || networkState.IsTeleportingNextFrame))
+ {
+ networkState.ScaleY = scale.y;
+ networkState.HasScaleY = true;
+ isScaleDirty = true;
+ }
+
+ if (SyncScaleZ && (Mathf.Abs(networkState.ScaleZ - scale.z) >= ScaleThreshold || networkState.IsTeleportingNextFrame))
+ {
+ networkState.ScaleZ = scale.z;
+ networkState.HasScaleZ = true;
+ isScaleDirty = true;
+ }
+ }
+ else if (SynchronizeScale)
+ {
+ var previousScale = networkState.Scale;
+ for (int i = 0; i < 3; i++)
+ {
+ if (Mathf.Abs(Mathf.DeltaAngle(previousScale[i], scale[i])) >= ScaleThreshold || networkState.IsTeleportingNextFrame)
+ {
+ isScaleDirty = true;
+ networkState.Scale[i] = scale[i];
+ networkState.SetHasScale(i, i == 0 ? SyncScaleX : i == 1 ? SyncScaleY : SyncScaleZ);
+ }
+ }
+ }
+ }
+ else // If we are synchronizing then we need to determine which scale to use
+ if (SynchronizeScale)
+ {
+ // This all has to do with complex nested hierarchies and how it impacts scale
+ // when set for the first time.
+ var hasParentNetworkObject = false;
+
+ // If the NetworkObject belonging to this NetworkTransform instance has a parent
+ // (i.e. this handles nested NetworkTransforms under a parent at some layer above)
+ if (NetworkObject.transform.parent != null)
+ {
+ var parentNetworkObject = NetworkObject.transform.parent.GetComponent();
+
+ // In-scene placed NetworkObjects parented under a GameObject with no
+ // NetworkObject preserve their lossyScale when synchronizing.
+ if (parentNetworkObject == null && NetworkObject.IsSceneObject != false)
+ {
+ hasParentNetworkObject = true;
+ }
+ else
+ {
+ // Or if the relative NetworkObject has a parent NetworkObject
+ hasParentNetworkObject = parentNetworkObject != null;
+ }
+ }
+
+ // If world position stays is set and the relative NetworkObject is parented under a NetworkObject
+ // then we want to use the lossy scale for the initial synchronization.
+ var useLossy = NetworkObject.WorldPositionStays() && hasParentNetworkObject;
+ var scaleToUse = useLossy ? transform.lossyScale : transform.localScale;
+
+ if (!UseHalfFloatPrecision)
+ {
+ networkState.ScaleX = scaleToUse.x;
+ networkState.ScaleY = scaleToUse.y;
+ networkState.ScaleZ = scaleToUse.z;
+ }
+ else
+ {
+ networkState.Scale = scaleToUse;
+ }
networkState.HasScaleX = true;
- isScaleDirty = true;
- }
-
- if (SyncScaleY && (Mathf.Abs(networkState.ScaleY - scale.y) >= ScaleThreshold || networkState.IsTeleportingNextFrame))
- {
- networkState.ScaleY = scale.y;
networkState.HasScaleY = true;
- isScaleDirty = true;
- }
-
- if (SyncScaleZ && (Mathf.Abs(networkState.ScaleZ - scale.z) >= ScaleThreshold || networkState.IsTeleportingNextFrame))
- {
- networkState.ScaleZ = scale.z;
networkState.HasScaleZ = true;
isScaleDirty = true;
}
-
isDirty |= isPositionDirty || isRotationDirty || isScaleDirty;
if (isDirty)
{
- networkState.SentTime = dirtyTime;
+ // Some integration/unit tests disable the NetworkTransform and there is no
+ // NetworkManager
+ if (enabled)
+ {
+ networkState.NetworkTick = NetworkManager.ServerTime.Tick;
+ }
}
- /// 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)
+ // Mark the state dirty for the next network tick update to clear out the bitset values
networkState.IsDirty |= isDirty;
return isDirty;
}
@@ -686,139 +1756,259 @@ namespace Unity.Netcode.Components
///
private void ApplyAuthoritativeState()
{
- var networkState = ReplicatedNetworkState.Value;
- var adjustedPosition = networkState.InLocalSpace ? transform.localPosition : transform.position;
+ var networkState = m_LocalAuthoritativeNetworkState;
+ // The m_CurrentPosition, m_CurrentRotation, and m_CurrentScale values are continually updated
+ // at the end of this method and assure that when not interpolating the non-authoritative side
+ // cannot make adjustments to any portions the transform not being synchronized.
+ var adjustedPosition = m_CurrentPosition;
+ var adjustedRotation = m_CurrentRotation;
+ var adjustedRotAngles = adjustedRotation.eulerAngles;
+ var adjustedScale = m_CurrentScale;
- // TODO: We should store network state w/ quats vs. euler angles
- var adjustedRotAngles = networkState.InLocalSpace ? transform.localEulerAngles : transform.eulerAngles;
- var adjustedScale = transform.localScale;
-
- // InLocalSpace Read:
+ // Non-Authority Preservers the authority's transform state update modes
InLocalSpace = networkState.InLocalSpace;
+ Interpolate = networkState.UseInterpolation;
+ UseHalfFloatPrecision = networkState.UseHalfFloatPrecision;
+ UseQuaternionSynchronization = networkState.QuaternionSync;
+ UseQuaternionCompression = networkState.QuaternionCompression;
+ if (SlerpPosition != networkState.UsePositionSlerp)
+ {
+ SlerpPosition = networkState.UsePositionSlerp;
+ UpdatePositionSlerp();
+ }
// NOTE ABOUT INTERPOLATING AND THE CODE BELOW:
// We always apply the interpolated state for any axis we are synchronizing even when the state has no deltas
// to assure we fully interpolate to our target even after we stop extrapolating 1 tick later.
- var useInterpolatedValue = !networkState.IsTeleportingNextFrame && Interpolate;
- if (useInterpolatedValue)
+ if (Interpolate)
{
- if (SyncPositionX) { adjustedPosition.x = m_PositionXInterpolator.GetInterpolatedValue(); }
- if (SyncPositionY) { adjustedPosition.y = m_PositionYInterpolator.GetInterpolatedValue(); }
- if (SyncPositionZ) { adjustedPosition.z = m_PositionZInterpolator.GetInterpolatedValue(); }
+ if (SynchronizePosition)
+ {
+ var interpolatedPosition = m_PositionInterpolator.GetInterpolatedValue();
+ if (UseHalfFloatPrecision)
+ {
+ adjustedPosition = interpolatedPosition;
+ }
+ else
+ {
+ if (SyncPositionX) { adjustedPosition.x = interpolatedPosition.x; }
+ if (SyncPositionY) { adjustedPosition.y = interpolatedPosition.y; }
+ if (SyncPositionZ) { adjustedPosition.z = interpolatedPosition.z; }
+ }
+ }
- if (SyncScaleX) { adjustedScale.x = m_ScaleXInterpolator.GetInterpolatedValue(); }
- if (SyncScaleY) { adjustedScale.y = m_ScaleYInterpolator.GetInterpolatedValue(); }
- if (SyncScaleZ) { adjustedScale.z = m_ScaleZInterpolator.GetInterpolatedValue(); }
+ if (SynchronizeScale)
+ {
+ if (UseHalfFloatPrecision)
+ {
+ adjustedScale = m_ScaleInterpolator.GetInterpolatedValue();
+ }
+ else
+ {
+ var interpolatedScale = m_ScaleInterpolator.GetInterpolatedValue();
+ if (SyncScaleX) { adjustedScale.x = interpolatedScale.x; }
+ if (SyncScaleY) { adjustedScale.y = interpolatedScale.y; }
+ if (SyncScaleZ) { adjustedScale.z = interpolatedScale.z; }
+ }
+ }
if (SynchronizeRotation)
{
- var interpolatedEulerAngles = m_RotationInterpolator.GetInterpolatedValue().eulerAngles;
- if (SyncRotAngleX) { adjustedRotAngles.x = interpolatedEulerAngles.x; }
- if (SyncRotAngleY) { adjustedRotAngles.y = interpolatedEulerAngles.y; }
- if (SyncRotAngleZ) { adjustedRotAngles.z = interpolatedEulerAngles.z; }
+ var interpolatedRotation = m_RotationInterpolator.GetInterpolatedValue();
+ if (UseQuaternionSynchronization)
+ {
+ adjustedRotation = interpolatedRotation;
+ }
+ else
+ {
+ var interpolatedEulerAngles = interpolatedRotation.eulerAngles;
+ if (SyncRotAngleX) { adjustedRotAngles.x = interpolatedEulerAngles.x; }
+ if (SyncRotAngleY) { adjustedRotAngles.y = interpolatedEulerAngles.y; }
+ if (SyncRotAngleZ) { adjustedRotAngles.z = interpolatedEulerAngles.z; }
+ adjustedRotation.eulerAngles = adjustedRotAngles;
+ }
}
}
else
{
- if (networkState.HasPositionX) { adjustedPosition.x = networkState.PositionX; }
- if (networkState.HasPositionY) { adjustedPosition.y = networkState.PositionY; }
- if (networkState.HasPositionZ) { adjustedPosition.z = networkState.PositionZ; }
-
- if (networkState.HasScaleX) { adjustedScale.x = networkState.ScaleX; }
- if (networkState.HasScaleY) { adjustedScale.y = networkState.ScaleY; }
- if (networkState.HasScaleZ) { adjustedScale.z = networkState.ScaleZ; }
-
- if (networkState.HasRotAngleX) { adjustedRotAngles.x = networkState.RotAngleX; }
- if (networkState.HasRotAngleY) { adjustedRotAngles.y = networkState.RotAngleY; }
- if (networkState.HasRotAngleZ) { adjustedRotAngles.z = networkState.RotAngleZ; }
- }
-
- // NOTE: The below conditional checks for applying axial values are required in order to
- // prevent the non-authoritative side from making adjustments when interpolation is off.
-
- // TODO: Determine if we want to enforce, frame by frame, the non-authoritative transform values.
- // We would want save the position, rotation, and scale (each individually) after applying each
- // authoritative transform state received. Otherwise, the non-authoritative side could make
- // changes to an axial value (if interpolation is turned off) until authority sends an update for
- // that same axial value. When interpolation is on, the state's values being synchronized are
- // always applied each frame.
-
- // Apply the new position if it has changed or we are interpolating and synchronizing position
- if (networkState.HasPositionChange || (useInterpolatedValue && SynchronizePosition))
- {
- if (InLocalSpace)
+ // Non-Interpolated Position and Scale
+ if (UseHalfFloatPrecision)
{
- transform.localPosition = adjustedPosition;
+ if (networkState.HasPositionChange && SynchronizePosition)
+ {
+ adjustedPosition = networkState.CurrentPosition;
+ }
+
+ if (networkState.HasScaleChange && SynchronizeScale)
+ {
+ for (int i = 0; i < 3; i++)
+ {
+ if (m_LocalAuthoritativeNetworkState.HasScale(i))
+ {
+ adjustedScale[i] = m_LocalAuthoritativeNetworkState.Scale[i];
+ }
+ }
+ }
}
else
{
- transform.position = adjustedPosition;
+ if (networkState.HasPositionX) { adjustedPosition.x = networkState.PositionX; }
+ if (networkState.HasPositionY) { adjustedPosition.y = networkState.PositionY; }
+ if (networkState.HasPositionZ) { adjustedPosition.z = networkState.PositionZ; }
+ if (networkState.HasScaleX) { adjustedScale.x = networkState.ScaleX; }
+ if (networkState.HasScaleY) { adjustedScale.y = networkState.ScaleY; }
+ if (networkState.HasScaleZ) { adjustedScale.z = networkState.ScaleZ; }
+ }
+
+ // Non-interpolated rotation
+ if (SynchronizeRotation)
+ {
+ if (networkState.QuaternionSync && networkState.HasRotAngleChange)
+ {
+ adjustedRotation = networkState.Rotation;
+ }
+ else
+ {
+ if (networkState.HasRotAngleX) { adjustedRotAngles.x = networkState.RotAngleX; }
+ if (networkState.HasRotAngleY) { adjustedRotAngles.y = networkState.RotAngleY; }
+ if (networkState.HasRotAngleZ) { adjustedRotAngles.z = networkState.RotAngleZ; }
+ adjustedRotation.eulerAngles = adjustedRotAngles;
+ }
}
}
- // Apply the new rotation if it has changed or we are interpolating and synchronizing rotation
- if (networkState.HasRotAngleChange || (useInterpolatedValue && SynchronizeRotation))
+ // Apply the position if we are synchronizing position
+ if (SynchronizePosition)
{
+ // Update our current position if it changed or we are interpolating
+ if (networkState.HasPositionChange || Interpolate)
+ {
+ m_CurrentPosition = adjustedPosition;
+ }
if (InLocalSpace)
{
- transform.localRotation = Quaternion.Euler(adjustedRotAngles);
+ transform.localPosition = m_CurrentPosition;
}
else
{
- transform.rotation = Quaternion.Euler(adjustedRotAngles);
+ transform.position = m_CurrentPosition;
}
}
- // Apply the new scale if it has changed or we are interpolating and synchronizing scale
- if (networkState.HasScaleChange || (useInterpolatedValue && SynchronizeScale))
+ // Apply the rotation if we are synchronizing rotation
+ if (SynchronizeRotation)
{
- transform.localScale = adjustedScale;
+ // Update our current rotation if it changed or we are interpolating
+ if (networkState.HasRotAngleChange || Interpolate)
+ {
+ m_CurrentRotation = adjustedRotation;
+ }
+ if (InLocalSpace)
+ {
+ transform.localRotation = m_CurrentRotation;
+ }
+ else
+ {
+ transform.rotation = m_CurrentRotation;
+ }
+ }
+
+ // Apply the scale if we are synchronizing scale
+ if (SynchronizeScale)
+ {
+ // Update our current scale if it changed or we are interpolating
+ if (networkState.HasScaleChange || Interpolate)
+ {
+ m_CurrentScale = adjustedScale;
+ }
+ transform.localScale = m_CurrentScale;
}
}
///
- /// Only non-authoritative instances should invoke this
+ /// Handles applying the full authoritative state (i.e. teleporting)
///
- private void AddInterpolatedState(NetworkTransformState newState)
+ ///
+ /// Only non-authoritative instances should invoke this
+ ///
+ private void ApplyTeleportingState(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;
-
- // When there is a change in interpolation or if teleporting, we reset
- if ((newState.InLocalSpace != InLocalSpace) || newState.IsTeleportingNextFrame)
+ if (!newState.IsTeleportingNextFrame)
{
- InLocalSpace = newState.InLocalSpace;
- var currentScale = transform.localScale;
+ return;
+ }
- // we should clear our float interpolators
- foreach (var interpolator in m_AllFloatInterpolators)
+ var sentTime = newState.SentTime;
+ var currentPosition = GetSpaceRelativePosition();
+ var currentRotation = GetSpaceRelativeRotation();
+ var currentEulerAngles = currentRotation.eulerAngles;
+ var currentScale = transform.localScale;
+
+ var isSynchronization = newState.IsSynchronizing;
+
+ // Clear all interpolators
+ m_ScaleInterpolator.Clear();
+ m_PositionInterpolator.Clear();
+ m_RotationInterpolator.Clear();
+
+ if (newState.HasPositionChange)
+ {
+ if (!UseHalfFloatPrecision)
{
- interpolator.Clear();
+ // Adjust based on which axis changed
+ if (newState.HasPositionX)
+ {
+ currentPosition.x = newState.PositionX;
+ }
+
+ if (newState.HasPositionY)
+ {
+ currentPosition.y = newState.PositionY;
+ }
+
+ if (newState.HasPositionZ)
+ {
+ currentPosition.z = newState.PositionZ;
+ }
+ UpdatePositionInterpolator(currentPosition, sentTime, true);
+ }
+ else
+ {
+ // With delta position teleport updates or synchronization, we create a new instance and provide the current network tick.
+ m_HalfPositionState = new NetworkDeltaPosition(newState.CurrentPosition, newState.NetworkTick, math.bool3(SyncPositionX, SyncPositionY, SyncPositionZ));
+
+ // When first synchronizing we determine if we need to apply the current delta position
+ // offset or not. This is specific to owner authoritative mode on the owner side only
+ if (isSynchronization)
+ {
+ if (ShouldSynchronizeHalfFloat(NetworkManager.LocalClientId))
+ {
+ m_HalfPositionState.HalfVector3.Axis = newState.NetworkDeltaPosition.HalfVector3.Axis;
+ m_HalfPositionState.DeltaPosition = newState.DeltaPosition;
+ currentPosition = m_HalfPositionState.ToVector3(newState.NetworkTick);
+ }
+ else
+ {
+ currentPosition = newState.CurrentPosition;
+ }
+ // Before the state is applied add a log entry if AddLogEntry is assigned
+ AddLogEntry(ref newState, NetworkObject.OwnerClientId, true);
+ }
+ else
+ {
+ // If we are just teleporting, then we already created a new NetworkDeltaPosition value.
+ // set the current position to the state's current position
+ currentPosition = newState.CurrentPosition;
+ }
+
+ if (Interpolate)
+ {
+ UpdatePositionInterpolator(currentPosition, sentTime, true);
+ }
+
}
- // 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_CurrentPosition = currentPosition;
// Apply the position
if (newState.InLocalSpace)
@@ -829,202 +2019,388 @@ namespace Unity.Netcode.Components
{
transform.position = currentPosition;
}
+ }
- // Adjust based on which axis changed
- if (newState.HasScaleX)
+ if (newState.HasScaleChange)
+ {
+ if (UseHalfFloatPrecision)
{
- m_ScaleXInterpolator.ResetTo(newState.ScaleX, sentTime);
- currentScale.x = newState.ScaleX;
+ currentScale = newState.Scale;
+ m_CurrentScale = currentScale;
+ }
+ else
+ {
+ // Adjust based on which axis changed
+ if (newState.HasScaleX)
+ {
+ currentScale.x = newState.ScaleX;
+ }
+
+ if (newState.HasScaleY)
+ {
+ currentScale.y = newState.ScaleY;
+ }
+
+ if (newState.HasScaleZ)
+ {
+ currentScale.z = newState.ScaleZ;
+ }
+
}
- 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;
- }
+ m_CurrentScale = currentScale;
+ m_ScaleInterpolator.ResetTo(currentScale, sentTime);
// Apply the adjusted scale
transform.localScale = currentScale;
+ }
- // Adjust based on which axis changed
- if (newState.HasRotAngleX)
+ if (newState.HasRotAngleChange)
+ {
+ if (newState.QuaternionSync)
{
- currentEulerAngles.x = newState.RotAngleX;
+ currentRotation = newState.Rotation;
+ }
+ else
+ {
+ // 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;
+ }
+ currentRotation.eulerAngles = currentEulerAngles;
}
- 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_CurrentRotation = currentRotation;
m_RotationInterpolator.ResetTo(currentRotation, sentTime);
+
+ if (InLocalSpace)
+ {
+ transform.localRotation = currentRotation;
+ }
+ else
+ {
+ transform.rotation = currentRotation;
+ }
+ }
+
+ // Add log after to applying the update if AddLogEntry is defined
+ if (isSynchronization)
+ {
+ AddLogEntry(ref newState, NetworkObject.OwnerClientId);
+ }
+ }
+
+ ///
+ /// Adds the new state's values to their respective interpolator
+ ///
+ ///
+ /// Only non-authoritative instances should invoke this
+ ///
+ private void UpdateState(NetworkTransformState oldState, NetworkTransformState newState)
+ {
+ // Set the transforms's synchronization modes
+ InLocalSpace = newState.InLocalSpace;
+ Interpolate = newState.UseInterpolation;
+ UseQuaternionSynchronization = newState.QuaternionSync;
+ UseQuaternionCompression = newState.QuaternionCompression;
+ UseHalfFloatPrecision = newState.UseHalfFloatPrecision;
+ if (SlerpPosition != newState.UsePositionSlerp)
+ {
+ SlerpPosition = newState.UsePositionSlerp;
+ UpdatePositionSlerp();
+ }
+
+ m_LocalAuthoritativeNetworkState = newState;
+ if (m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame)
+ {
+ ApplyTeleportingState(m_LocalAuthoritativeNetworkState);
+ return;
+ }
+
+ var sentTime = newState.SentTime;
+ var currentRotation = GetSpaceRelativeRotation();
+ var currentEulerAngles = currentRotation.eulerAngles;
+
+ // Only if using half float precision and our position had changed last update then
+ if (UseHalfFloatPrecision && m_LocalAuthoritativeNetworkState.HasPositionChange)
+ {
+ // assure our local NetworkDeltaPosition state is updated
+ m_HalfPositionState.HalfVector3.Axis = m_LocalAuthoritativeNetworkState.NetworkDeltaPosition.HalfVector3.Axis;
+ // and update our current position
+ m_LocalAuthoritativeNetworkState.CurrentPosition = m_HalfPositionState.ToVector3(newState.NetworkTick);
+ }
+
+ if (!Interpolate)
+ {
return;
}
// Apply axial changes from the new state
- if (newState.HasPositionX)
+ // Either apply the delta position target position or the current state's delta position
+ // depending upon whether UsePositionDeltaCompression is enabled
+
+ if (m_LocalAuthoritativeNetworkState.HasPositionChange)
{
- m_PositionXInterpolator.AddMeasurement(newState.PositionX, sentTime);
+ if (m_LocalAuthoritativeNetworkState.UseHalfFloatPrecision)
+ {
+ UpdatePositionInterpolator(m_LocalAuthoritativeNetworkState.CurrentPosition, sentTime);
+ }
+ else
+ {
+ var currentPosition = GetSpaceRelativePosition();
+ if (m_LocalAuthoritativeNetworkState.HasPositionX)
+ {
+ currentPosition.x = m_LocalAuthoritativeNetworkState.PositionX;
+ }
+
+ if (m_LocalAuthoritativeNetworkState.HasPositionY)
+ {
+ currentPosition.y = m_LocalAuthoritativeNetworkState.PositionY;
+ }
+
+ if (m_LocalAuthoritativeNetworkState.HasPositionZ)
+ {
+ currentPosition.z = m_LocalAuthoritativeNetworkState.PositionZ;
+ }
+ UpdatePositionInterpolator(currentPosition, sentTime);
+ }
}
- if (newState.HasPositionY)
+ if (m_LocalAuthoritativeNetworkState.HasScaleChange)
{
- m_PositionYInterpolator.AddMeasurement(newState.PositionY, sentTime);
- }
+ var currentScale = transform.localScale;
+ if (UseHalfFloatPrecision)
+ {
+ for (int i = 0; i < 3; i++)
+ {
+ if (m_LocalAuthoritativeNetworkState.HasScale(i))
+ {
+ currentScale[i] = m_LocalAuthoritativeNetworkState.Scale[i];
+ }
+ }
+ }
+ else
+ {
+ if (m_LocalAuthoritativeNetworkState.HasScaleX)
+ {
+ currentScale.x = m_LocalAuthoritativeNetworkState.ScaleX;
+ }
- if (newState.HasPositionZ)
- {
- m_PositionZInterpolator.AddMeasurement(newState.PositionZ, sentTime);
- }
+ if (m_LocalAuthoritativeNetworkState.HasScaleY)
+ {
+ currentScale.y = m_LocalAuthoritativeNetworkState.ScaleY;
+ }
- if (newState.HasScaleX)
- {
- m_ScaleXInterpolator.AddMeasurement(newState.ScaleX, sentTime);
- }
-
- if (newState.HasScaleY)
- {
- m_ScaleYInterpolator.AddMeasurement(newState.ScaleY, sentTime);
- }
-
- if (newState.HasScaleZ)
- {
- m_ScaleZInterpolator.AddMeasurement(newState.ScaleZ, sentTime);
+ if (m_LocalAuthoritativeNetworkState.HasScaleZ)
+ {
+ currentScale.z = m_LocalAuthoritativeNetworkState.ScaleZ;
+ }
+ }
+ m_ScaleInterpolator.AddMeasurement(currentScale, 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 (m_LocalAuthoritativeNetworkState.HasRotAngleChange)
{
- if (newState.HasRotAngleX)
+ if (m_LocalAuthoritativeNetworkState.QuaternionSync)
{
- currentEulerAngles.x = newState.RotAngleX;
+ currentRotation = m_LocalAuthoritativeNetworkState.Rotation;
}
-
- if (newState.HasRotAngleY)
+ else
{
- currentEulerAngles.y = newState.RotAngleY;
- }
+ // Adjust based on which axis changed
+ if (m_LocalAuthoritativeNetworkState.HasRotAngleX)
+ {
+ currentEulerAngles.x = m_LocalAuthoritativeNetworkState.RotAngleX;
+ }
- if (newState.HasRotAngleZ)
- {
- currentEulerAngles.z = newState.RotAngleZ;
- }
+ if (m_LocalAuthoritativeNetworkState.HasRotAngleY)
+ {
+ currentEulerAngles.y = m_LocalAuthoritativeNetworkState.RotAngleY;
+ }
- currentRotation.eulerAngles = currentEulerAngles;
+ if (m_LocalAuthoritativeNetworkState.HasRotAngleZ)
+ {
+ currentEulerAngles.z = m_LocalAuthoritativeNetworkState.RotAngleZ;
+ }
+ currentRotation.eulerAngles = currentEulerAngles;
+ }
m_RotationInterpolator.AddMeasurement(currentRotation, sentTime);
}
}
+ ///
+ /// Invoked on the non-authoritative side when the NetworkTransformState has been updated
+ ///
+ /// the previous
+ /// the new
+ protected virtual void OnNetworkTransformStateUpdated(ref NetworkTransformState oldState, ref NetworkTransformState newState)
+ {
+
+ }
+
///
/// Only non-authoritative instances should invoke this method
///
private void OnNetworkStateChanged(NetworkTransformState oldState, NetworkTransformState newState)
{
- if (!NetworkObject.IsSpawned)
+ if (!NetworkObject.IsSpawned || CanCommitToTransform)
{
return;
}
- if (CanCommitToTransform)
- {
- // we're the authority, we ignore incoming changes
- return;
- }
+ // Get the time when this new state was sent
+ newState.SentTime = new NetworkTime(NetworkManager.NetworkConfig.TickRate, newState.NetworkTick).Time;
- if (Interpolate)
- {
- // Add measurements for the new state's deltas
- AddInterpolatedState(newState);
- }
+ // Update the state
+ UpdateState(oldState, newState);
+
+ // Provide notifications when the state has been updated
+ OnNetworkTransformStateUpdated(ref oldState, ref newState);
}
///
/// Will set the maximum interpolation boundary for the interpolators of this instance.
- /// This value roughly translates to the maximum value of 't' in and
- /// for all transform elements being monitored by
- /// (i.e. Position, Rotation, and Scale)
+ /// This value roughly translates to the maximum value of 't' in and
+ /// for all transform elements being monitored by
+ /// (i.e. Position, Scale, and Rotation)
///
/// Maximum time boundary that can be used in a frame when interpolating between two values
public void SetMaxInterpolationBound(float maxInterpolationBound)
{
- m_PositionXInterpolator.MaxInterpolationBound = maxInterpolationBound;
- m_PositionYInterpolator.MaxInterpolationBound = maxInterpolationBound;
- m_PositionZInterpolator.MaxInterpolationBound = maxInterpolationBound;
m_RotationInterpolator.MaxInterpolationBound = maxInterpolationBound;
- m_ScaleXInterpolator.MaxInterpolationBound = maxInterpolationBound;
- m_ScaleYInterpolator.MaxInterpolationBound = maxInterpolationBound;
- m_ScaleZInterpolator.MaxInterpolationBound = maxInterpolationBound;
+ m_PositionInterpolator.MaxInterpolationBound = maxInterpolationBound;
+ m_ScaleInterpolator.MaxInterpolationBound = maxInterpolationBound;
}
///
/// Create interpolators when first instantiated to avoid memory allocations if the
/// associated NetworkObject persists (i.e. despawned but not destroyed or pools)
///
- private void Awake()
+ protected virtual void Awake()
{
// Rotation is a single Quaternion since each Euler axis will affect the quaternion's final value
m_RotationInterpolator = new BufferedLinearInterpolatorQuaternion();
+ m_PositionInterpolator = new BufferedLinearInterpolatorVector3();
+ m_ScaleInterpolator = new BufferedLinearInterpolatorVector3();
+ }
- // All other interpolators are BufferedLinearInterpolatorFloats
- m_PositionXInterpolator = new BufferedLinearInterpolatorFloat();
- m_PositionYInterpolator = new BufferedLinearInterpolatorFloat();
- m_PositionZInterpolator = new BufferedLinearInterpolatorFloat();
- m_ScaleXInterpolator = new BufferedLinearInterpolatorFloat();
- m_ScaleYInterpolator = new BufferedLinearInterpolatorFloat();
- m_ScaleZInterpolator = new BufferedLinearInterpolatorFloat();
-
- // Used to quickly iteration over the BufferedLinearInterpolatorFloat
- // instances
- if (m_AllFloatInterpolators.Count == 0)
+ ///
+ /// Checks for changes in the axis to synchronize. If one or more did change it
+ /// then determines if the axis were enabled and if the delta between the last known
+ /// delta position and the current position for the axis exceeds the adjustment range
+ /// before it is collapsed into the base position.
+ /// If it does exceed the adjustment range, then we have to teleport the object so
+ /// a full position synchronization takes place and the NetworkDeltaPosition is
+ /// reset with the updated base position that it then will generating a new delta position from.
+ ///
+ ///
+ /// This only happens if a user disables an axis, continues to update the disabled axis,
+ /// and then later enables the axis. (which will not be a recommended best practice)
+ ///
+ private void AxisChangedDeltaPositionCheck()
+ {
+ if (UseHalfFloatPrecision && SynchronizePosition)
{
- m_AllFloatInterpolators.Add(m_PositionXInterpolator);
- m_AllFloatInterpolators.Add(m_PositionYInterpolator);
- m_AllFloatInterpolators.Add(m_PositionZInterpolator);
- m_AllFloatInterpolators.Add(m_ScaleXInterpolator);
- m_AllFloatInterpolators.Add(m_ScaleYInterpolator);
- m_AllFloatInterpolators.Add(m_ScaleZInterpolator);
+ var synAxis = m_HalfPositionState.HalfVector3.AxisToSynchronize;
+ if (SyncPositionX != synAxis.x || SyncPositionY != synAxis.y || SyncPositionZ != synAxis.z)
+ {
+ var positionState = m_HalfPositionState.GetFullPosition();
+ var relativePosition = GetSpaceRelativePosition();
+ bool needsToTeleport = false;
+ // Only if the synchronization of an axis is turned on do we need to
+ // check if a teleport is required due to the delta from the last known
+ // to the currently known axis value exceeds MaxDeltaBeforeAdjustment.
+ if (SyncPositionX && SyncPositionX != synAxis.x)
+ {
+ needsToTeleport = Mathf.Abs(relativePosition.x - positionState.x) >= NetworkDeltaPosition.MaxDeltaBeforeAdjustment;
+ }
+ if (SyncPositionY && SyncPositionY != synAxis.y)
+ {
+ needsToTeleport = Mathf.Abs(relativePosition.y - positionState.y) >= NetworkDeltaPosition.MaxDeltaBeforeAdjustment;
+ }
+ if (SyncPositionZ && SyncPositionZ != synAxis.z)
+ {
+ needsToTeleport = Mathf.Abs(relativePosition.z - positionState.z) >= NetworkDeltaPosition.MaxDeltaBeforeAdjustment;
+ }
+ // If needed, force a teleport as the delta is outside of the valid delta boundary
+ m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = needsToTeleport;
+ }
+ }
+ }
+
+ ///
+ /// Called by authority to check for deltas and update non-authoritative instances
+ /// if any are found.
+ ///
+ internal void OnUpdateAuthoritativeState(ref Transform transformSource)
+ {
+ // If our replicated state is not dirty and our local authority state is dirty, clear it.
+ if (!ReplicatedNetworkState.IsDirty() && m_LocalAuthoritativeNetworkState.IsDirty && !m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame)
+ {
+ // Now clear our bitset and prepare for next network tick state update
+ m_LocalAuthoritativeNetworkState.ClearBitSetForNextTick();
+ }
+
+ AxisChangedDeltaPositionCheck();
+
+ TryCommitTransform(ref transformSource);
+ }
+
+ ///
+ /// Authority subscribes to network tick events and will invoke
+ /// each network tick.
+ ///
+ private void NetworkTickSystem_Tick()
+ {
+ // As long as we are still authority
+ if (CanCommitToTransform)
+ {
+ // Update any changes to the transform
+ var transformSource = transform;
+ OnUpdateAuthoritativeState(ref transformSource);
+ }
+ else
+ {
+ // If we are no longer authority, unsubscribe to the tick event
+ if (NetworkManager != null && NetworkManager.NetworkTickSystem != null)
+ {
+ NetworkManager.NetworkTickSystem.Tick -= NetworkTickSystem_Tick;
+ }
}
}
///
public override void OnNetworkSpawn()
{
+ // NOTE: Legacy and no longer used (candidates for deprecation)
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)
{
- var currentPosition = InLocalSpace ? transform.localPosition : transform.position;
- var currentRotation = InLocalSpace ? transform.localRotation : transform.rotation;
+ var currentPosition = GetSpaceRelativePosition();
+ var currentRotation = GetSpaceRelativeRotation();
// Teleport to current position
SetStateInternal(currentPosition, currentRotation, transform.localScale, true);
-
- // Force the state update to be sent
- TryCommitTransform(transform, m_CachedNetworkManager.LocalTime.Time);
}
}
@@ -1032,32 +2408,61 @@ namespace Unity.Netcode.Components
public override void OnNetworkDespawn()
{
ReplicatedNetworkState.OnValueChanged -= OnNetworkStateChanged;
+ CanCommitToTransform = false;
+ if (NetworkManager != null && NetworkManager.NetworkTickSystem != null)
+ {
+ NetworkManager.NetworkTickSystem.Tick -= NetworkTickSystem_Tick;
+ }
}
///
public override void OnDestroy()
{
+ if (NetworkManager != null && NetworkManager.NetworkTickSystem != null)
+ {
+ NetworkManager.NetworkTickSystem.Tick -= NetworkTickSystem_Tick;
+ }
+ CanCommitToTransform = false;
base.OnDestroy();
m_ReplicatedNetworkStateServer.Dispose();
m_ReplicatedNetworkStateOwner.Dispose();
+
}
///
public override void OnGainedOwnership()
{
- Initialize();
+ // Only initialize if we gained ownership
+ if (OwnerClientId == NetworkManager.LocalClientId)
+ {
+ Initialize();
+ }
}
///
public override void OnLostOwnership()
{
- Initialize();
+ // Only initialize if we are not authority and lost
+ // ownership
+ if (OwnerClientId != NetworkManager.LocalClientId)
+ {
+ Initialize();
+ }
+ }
+
+ ///
+ /// Invoked when first spawned and when ownership changes.
+ ///
+ /// the replicated
+ protected virtual void OnInitialize(ref NetworkVariable replicatedState)
+ {
+
}
///
/// Initializes NetworkTransform when spawned and ownership changes.
///
- private void Initialize()
+ protected void Initialize()
{
if (!IsSpawned)
{
@@ -1066,19 +2471,36 @@ namespace Unity.Netcode.Components
CanCommitToTransform = IsServerAuthoritative() ? IsServer : IsOwner;
var replicatedState = ReplicatedNetworkState;
- m_LocalAuthoritativeNetworkState = replicatedState.Value;
+ var currentPosition = GetSpaceRelativePosition();
if (CanCommitToTransform)
{
- replicatedState.OnValueChanged -= OnNetworkStateChanged;
+ if (UseHalfFloatPrecision)
+ {
+ m_HalfPositionState = new NetworkDeltaPosition(currentPosition, NetworkManager.NetworkTickSystem.ServerTime.Tick, math.bool3(SyncPositionX, SyncPositionY, SyncPositionZ));
+ }
+
+ // Authority only updates once per network tick
+ NetworkManager.NetworkTickSystem.Tick -= NetworkTickSystem_Tick;
+ NetworkManager.NetworkTickSystem.Tick += NetworkTickSystem_Tick;
}
else
{
+ // Sanity check to assure we only subscribe to OnValueChanged once
+ replicatedState.OnValueChanged -= OnNetworkStateChanged;
replicatedState.OnValueChanged += OnNetworkStateChanged;
- // In case we are late joining
+ // Assure we no longer subscribe to the tick event
+ NetworkManager.NetworkTickSystem.Tick -= NetworkTickSystem_Tick;
+
ResetInterpolatedStateToCurrentAuthoritativeState();
+ m_CurrentPosition = GetSpaceRelativePosition();
+ m_CurrentScale = transform.localScale;
+ m_CurrentRotation = GetSpaceRelativeRotation();
+
}
+
+ OnInitialize(ref replicatedState);
}
///
@@ -1098,23 +2520,28 @@ namespace Unity.Netcode.Components
{
if (!IsSpawned)
{
+ NetworkLog.LogError($"Cannot commit transform when not spawned!");
return;
}
- // Only the server or owner can invoke this method
- if (!IsOwner && !m_CachedIsServer)
+ // Only the server or the owner is allowed to commit a transform
+ if (!IsServer && !IsOwner)
{
- throw new Exception("Non-owner client instance cannot set the state of the NetworkTransform!");
+ var errorMessage = gameObject != NetworkObject.gameObject ?
+ $"Non-authority instance of {NetworkObject.gameObject.name} is trying to commit a transform on {gameObject.name}!" :
+ $"Non-authority instance of {NetworkObject.gameObject.name} is trying to commit a transform!";
+ NetworkLog.LogError(errorMessage);
+ return;
}
- Vector3 pos = posIn == null ? InLocalSpace ? transform.localPosition : transform.position : posIn.Value;
- Quaternion rot = rotIn == null ? InLocalSpace ? transform.localRotation : transform.rotation : rotIn.Value;
+ Vector3 pos = posIn == null ? GetSpaceRelativePosition() : posIn.Value;
+ Quaternion rot = rotIn == null ? GetSpaceRelativeRotation() : rotIn.Value;
Vector3 scale = scaleIn == null ? transform.localScale : scaleIn.Value;
if (!CanCommitToTransform)
{
// Preserving the ability for owner authoritative mode to accept state changes from server
- if (m_CachedIsServer)
+ if (IsServer)
{
m_ClientIds[0] = OwnerClientId;
m_ClientRpcParams.Send.TargetClientIds = m_ClientIds;
@@ -1148,8 +2575,8 @@ namespace Unity.Netcode.Components
}
transform.localScale = scale;
m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = shouldTeleport;
-
- TryCommitTransform(transform, m_CachedNetworkManager.LocalTime.Time);
+ var transformToCommit = transform;
+ TryCommitTransform(ref transformToCommit);
}
///
@@ -1182,63 +2609,55 @@ namespace Unity.Netcode.Components
SetStateInternal(pos, rot, scale, shouldTeleport);
}
- ///
- /// Will update the authoritative transform state if any deltas are detected.
- /// This will also reset the m_LocalAuthoritativeNetworkState if it is still dirty
- /// but the replicated network state is not.
- ///
- /// transform to be updated
- private void UpdateAuthoritativeState(Transform transformSource)
- {
- // If our replicated state is not dirty and our local authority state is dirty, clear it.
- if (!ReplicatedNetworkState.IsDirty() && m_LocalAuthoritativeNetworkState.IsDirty)
- {
- m_LastSentState = m_LocalAuthoritativeNetworkState;
- // Now clear our bitset and prepare for next network tick state update
- m_LocalAuthoritativeNetworkState.ClearBitSetForNextTick();
- }
-
- TryCommitTransform(transformSource, m_CachedNetworkManager.LocalTime.Time);
- }
-
///
///
/// 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.
+ /// - Non-authority always invokes this base class method.
///
protected virtual void Update()
{
- if (!IsSpawned)
+ // If not spawned or this instance has authority, exit early
+ if (!IsSpawned || CanCommitToTransform)
{
return;
}
- // If we are authority, update the authoritative state
- if (CanCommitToTransform)
+ // Non-Authority
+ if (Interpolate)
{
- UpdateAuthoritativeState(transform);
- }
- else // Non-Authority
- {
- if (Interpolate)
- {
- 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);
- }
+ var serverTime = NetworkManager.ServerTime;
+ var cachedDeltaTime = NetworkManager.RealTimeProvider.DeltaTime;
+ var cachedServerTime = serverTime.Time;
+ // TODO: Investigate Further
+ // With owner authoritative mode, non-authority clients can lag behind
+ // by more than 1 tick period of time. The current "solution" for now
+ // is to make their cachedRenderTime run 2 ticks behind.
+ var ticksAgo = !IsServerAuthoritative() && !IsServer ? 2 : 1;
+ var cachedRenderTime = serverTime.TimeTicksAgo(ticksAgo).Time;
+ // Now only update the interpolators for the portions of the transform being synchronized
+ if (SynchronizePosition)
+ {
+ m_PositionInterpolator.Update(cachedDeltaTime, cachedRenderTime, cachedServerTime);
+ }
+
+ if (SynchronizeRotation)
+ {
+ // When using half precision Lerp towards the target rotation.
+ // When using full precision Slerp towards the target rotation.
+ ///
+ m_RotationInterpolator.IsSlerp = !UseHalfFloatPrecision;
m_RotationInterpolator.Update(cachedDeltaTime, cachedRenderTime, cachedServerTime);
}
- // Apply the current authoritative state
- ApplyAuthoritativeState();
+ if (SynchronizeScale)
+ {
+ m_ScaleInterpolator.Update(cachedDeltaTime, cachedRenderTime, cachedServerTime);
+ }
}
+
+ // Apply the current authoritative state
+ ApplyAuthoritativeState();
}
///
@@ -1269,11 +2688,20 @@ namespace Unity.Netcode.Components
}
///
- /// Used by to determines if this is server or owner authoritative.
+ /// Method to determine if this instance is owner or server authoritative.
///
- internal bool IsServerAuthoritative()
+ ///
+ /// Used by to determines if this is server or owner authoritative.
+ ///
+ /// or
+ public bool IsServerAuthoritative()
{
return OnIsServerAuthoritative();
}
}
+
+ internal interface INetworkTransformLogStateEntry
+ {
+ void AddLogEntry(NetworkTransform.NetworkTransformState networkTransformState, ulong targetClient, bool preUpdate = false);
+ }
}
diff --git a/Components/QuaternionCompressor.cs b/Components/QuaternionCompressor.cs
new file mode 100644
index 0000000..0cdeb13
--- /dev/null
+++ b/Components/QuaternionCompressor.cs
@@ -0,0 +1,123 @@
+using System.Runtime.CompilerServices;
+using UnityEngine;
+
+namespace Unity.Netcode
+{
+ ///
+ /// A Smallest Three Quaternion Compressor Implementation
+ ///
+ ///
+ /// Explanation of why "The smallest three":
+ /// Since a normalized Quaternion's unit value is 1.0f:
+ /// x*x + y*y + z*z + w*w = M*M (where M is the magnitude of the vector)
+ /// If w was the largest value and the quaternion is normalized:
+ /// M = 1.0f (which M * M would still yield 1.0f)
+ /// w*w = M*M - (x*x + y*y + z*z) or Mathf.Sqrt(1.0f - (x*x + y*y + z*z))
+ /// w = Math.Sqrt(1.0f - (x*x + y*y + z*z))
+ /// Using the largest the number avoids potential loss of precision in the smallest three values.
+ ///
+ public static class QuaternionCompressor
+ {
+ private const ushort k_PrecisionMask = (1 << 9) - 1;
+
+ // Square root of 2 over 2 (Mathf.Sqrt(2.0f) / 2.0f == 1.0f / Mathf.Sqrt(2.0f))
+ // This provides encoding the smallest three components into a (+/-) Mathf.Sqrt(2.0f) / 2.0f range
+ private const float k_SqrtTwoOverTwoEncoding = 0.70710678118654752440084436210485f;
+
+ // We can further improve the encoding compression by dividing k_SqrtTwoOverTwo into 1.0f and multiplying that
+ // by the precision mask (minor reduction of runtime calculations)
+ private const float k_CompressionEcodingMask = (1.0f / k_SqrtTwoOverTwoEncoding) * k_PrecisionMask;
+
+ // Used to shift the negative bit to the 10th bit position when compressing and encoding
+ private const ushort k_ShiftNegativeBit = 9;
+
+ // We can do the same for our decoding and decompression by dividing k_PrecisionMask into 1.0 and multiplying
+ // that by k_SqrtTwoOverTwo (minor reduction of runtime calculations)
+ private const float k_DcompressionDecodingMask = (1.0f / k_PrecisionMask) * k_SqrtTwoOverTwoEncoding;
+
+ // The sign bit position (10th bit) used when decompressing and decoding
+ private const ushort k_NegShortBit = 0x200;
+
+ // Negative bit set values
+ private const ushort k_True = 1;
+ private const ushort k_False = 0;
+
+ // Used to store the absolute value of the 4 quaternion elements
+ private static Quaternion s_QuatAbsValues = Quaternion.identity;
+
+ ///
+ /// Compresses a Quaternion into an unsigned integer
+ ///
+ /// the to be compressed
+ /// the compressed as an unsigned integer
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static uint CompressQuaternion(ref Quaternion quaternion)
+ {
+ // Store off the absolute value for each Quaternion element
+ s_QuatAbsValues[0] = Mathf.Abs(quaternion[0]);
+ s_QuatAbsValues[1] = Mathf.Abs(quaternion[1]);
+ s_QuatAbsValues[2] = Mathf.Abs(quaternion[2]);
+ s_QuatAbsValues[3] = Mathf.Abs(quaternion[3]);
+
+ // Get the largest element value of the quaternion to know what the remaining "Smallest Three" values are
+ var quatMax = Mathf.Max(s_QuatAbsValues[0], s_QuatAbsValues[1], s_QuatAbsValues[2], s_QuatAbsValues[3]);
+
+ // Find the index of the largest element so we can skip that element while compressing and decompressing
+ var indexToSkip = (ushort)(s_QuatAbsValues[0] == quatMax ? 0 : s_QuatAbsValues[1] == quatMax ? 1 : s_QuatAbsValues[2] == quatMax ? 2 : 3);
+
+ // Get the sign of the largest element which is all that is needed when calculating the sum of squares of a normalized quaternion.
+
+ var quatMaxSign = (quaternion[indexToSkip] < 0 ? k_True : k_False);
+
+ // Start with the index to skip which will be shifted to the highest two bits
+ var compressed = (uint)indexToSkip;
+
+ // Step 1: Start with the first element
+ var currentIndex = 0;
+
+ // Step 2: If we are on the index to skip preserve the current compressed value, otherwise proceed to step 3 and 4
+ // Step 3: Get the sign of the element we are processing. If it is the not the same as the largest value's sign bit then we set the bit
+ // Step 4: Get the compressed and encoded value by multiplying the absolute value of the current element by k_CompressionEcodingMask and round that result up
+ compressed = currentIndex != indexToSkip ? (compressed << 10) | (uint)((quaternion[currentIndex] < 0 ? k_True : k_False) != quatMaxSign ? k_True : k_False) << k_ShiftNegativeBit | (ushort)Mathf.Round(k_CompressionEcodingMask * s_QuatAbsValues[currentIndex]) : compressed;
+ currentIndex++;
+ // Repeat the last 3 steps for the remaining elements
+ compressed = currentIndex != indexToSkip ? (compressed << 10) | (uint)((quaternion[currentIndex] < 0 ? k_True : k_False) != quatMaxSign ? k_True : k_False) << k_ShiftNegativeBit | (ushort)Mathf.Round(k_CompressionEcodingMask * s_QuatAbsValues[currentIndex]) : compressed;
+ currentIndex++;
+ compressed = currentIndex != indexToSkip ? (compressed << 10) | (uint)((quaternion[currentIndex] < 0 ? k_True : k_False) != quatMaxSign ? k_True : k_False) << k_ShiftNegativeBit | (ushort)Mathf.Round(k_CompressionEcodingMask * s_QuatAbsValues[currentIndex]) : compressed;
+ currentIndex++;
+ compressed = currentIndex != indexToSkip ? (compressed << 10) | (uint)((quaternion[currentIndex] < 0 ? k_True : k_False) != quatMaxSign ? k_True : k_False) << k_ShiftNegativeBit | (ushort)Mathf.Round(k_CompressionEcodingMask * s_QuatAbsValues[currentIndex]) : compressed;
+
+ // Return the compress quaternion
+ return compressed;
+ }
+
+ ///
+ /// Decompress a compressed quaternion
+ ///
+ /// quaternion to store the decompressed values within
+ /// the compressed quaternion
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void DecompressQuaternion(ref Quaternion quaternion, uint compressed)
+ {
+ // Get the last two bits for the index to skip (0-3)
+ var indexToSkip = (int)(compressed >> 30);
+
+ // Reverse out the values while skipping over the largest value index
+ var sumOfSquaredMagnitudes = 0.0f;
+ for (int i = 3; i >= 0; --i)
+ {
+ if (i == indexToSkip)
+ {
+ continue;
+ }
+ // Check the negative bit and multiply that result with the decompressed and decoded value
+ quaternion[i] = ((compressed & k_NegShortBit) > 0 ? -1.0f : 1.0f) * ((compressed & k_PrecisionMask) * k_DcompressionDecodingMask);
+ sumOfSquaredMagnitudes += quaternion[i] * quaternion[i];
+ compressed = compressed >> 10;
+ }
+ // Since a normalized quaternion's magnitude is 1.0f, we subtract the sum of the squared smallest three from the unit value and take
+ // the square root of the difference to find the final largest value
+ quaternion[indexToSkip] = Mathf.Sqrt(1.0f - sumOfSquaredMagnitudes);
+ }
+ }
+}
diff --git a/Components/QuaternionCompressor.cs.meta b/Components/QuaternionCompressor.cs.meta
new file mode 100644
index 0000000..dc96f54
--- /dev/null
+++ b/Components/QuaternionCompressor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: bb9d8b98d3c8bca469c8ee152353336f
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Components/com.unity.netcode.components.asmdef b/Components/com.unity.netcode.components.asmdef
index 6a9c4c4..3505da5 100644
--- a/Components/com.unity.netcode.components.asmdef
+++ b/Components/com.unity.netcode.components.asmdef
@@ -3,7 +3,8 @@
"rootNamespace": "Unity.Netcode.Components",
"references": [
"Unity.Netcode.Runtime",
- "Unity.Collections"
+ "Unity.Collections",
+ "Unity.Mathematics"
],
"allowUnsafeCode": true,
"versionDefines": [
diff --git a/Editor/CodeGen/INetworkMessageILPP.cs b/Editor/CodeGen/INetworkMessageILPP.cs
index 9a1e8ac..10da1b0 100644
--- a/Editor/CodeGen/INetworkMessageILPP.cs
+++ b/Editor/CodeGen/INetworkMessageILPP.cs
@@ -1,7 +1,7 @@
using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
-using System.Collections.Generic;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Cecil.Rocks;
diff --git a/Editor/CodeGen/INetworkSerializableILPP.cs b/Editor/CodeGen/INetworkSerializableILPP.cs
index 22b2f14..abfc9a2 100644
--- a/Editor/CodeGen/INetworkSerializableILPP.cs
+++ b/Editor/CodeGen/INetworkSerializableILPP.cs
@@ -1,7 +1,7 @@
using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
-using System.Collections.Generic;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Unity.CompilationPipeline.Common.Diagnostics;
diff --git a/Editor/CodeGen/NetworkBehaviourILPP.cs b/Editor/CodeGen/NetworkBehaviourILPP.cs
index 4c1fe20..b57cfa4 100644
--- a/Editor/CodeGen/NetworkBehaviourILPP.cs
+++ b/Editor/CodeGen/NetworkBehaviourILPP.cs
@@ -1,7 +1,7 @@
using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
-using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Mono.Cecil;
using Mono.Cecil.Cil;
@@ -9,9 +9,9 @@ using Mono.Cecil.Rocks;
using Unity.CompilationPipeline.Common.Diagnostics;
using Unity.CompilationPipeline.Common.ILPostProcessing;
using UnityEngine;
+using ILPPInterface = Unity.CompilationPipeline.Common.ILPostProcessing.ILPostProcessor;
using MethodAttributes = Mono.Cecil.MethodAttributes;
using ParameterAttributes = Mono.Cecil.ParameterAttributes;
-using ILPPInterface = Unity.CompilationPipeline.Common.ILPostProcessing.ILPostProcessor;
namespace Unity.Netcode.Editor.CodeGen
{
@@ -837,6 +837,58 @@ namespace Unity.Netcode.Editor.CodeGen
GetAllFieldsAndResolveGenerics(resolved, ref fieldTypes, genericParams);
}
+ private void GetAllBaseTypesAndResolveGenerics(TypeDefinition type, ref List baseTypes, Dictionary genericParameters)
+ {
+
+ if (type == null || type.BaseType == null || type.BaseType.Name == "Object")
+ {
+ return;
+ }
+
+ var baseType = type.BaseType;
+
+ var genericParams = new Dictionary();
+
+ if (baseType.IsGenericInstance)
+ {
+ var genericType = (GenericInstanceType)baseType;
+ var newGenericType = new GenericInstanceType(baseType.Resolve());
+ for (var i = 0; i < genericType.GenericArguments.Count; ++i)
+ {
+ var argument = genericType.GenericArguments[i];
+
+ if (genericParameters != null && genericParameters.ContainsKey(argument.Name))
+ {
+ newGenericType.GenericArguments.Add(genericParameters[argument.Name]);
+ genericParams[baseType.Resolve().GenericParameters[newGenericType.GenericArguments.Count - 1].Name] = genericParameters[argument.Name];
+ }
+ else
+ {
+ newGenericType.GenericArguments.Add(argument);
+ }
+ }
+ baseTypes.Add(newGenericType);
+ }
+ else
+ {
+ baseTypes.Add(baseType);
+ }
+
+ var resolved = type.BaseType.Resolve();
+ if (type.BaseType.IsGenericInstance)
+ {
+ var genericType = (GenericInstanceType)type.BaseType;
+ for (var i = 0; i < genericType.GenericArguments.Count; ++i)
+ {
+ if (!genericParams.ContainsKey(resolved.GenericParameters[i].Name))
+ {
+ genericParams[resolved.GenericParameters[i].Name] = genericType.GenericArguments[i];
+ }
+ }
+ }
+ GetAllBaseTypesAndResolveGenerics(resolved, ref baseTypes, genericParams);
+ }
+
private void ProcessNetworkBehaviour(TypeDefinition typeDefinition, string[] assemblyDefines)
{
var rpcHandlers = new List<(uint RpcMethodId, MethodDefinition RpcHandler)>();
@@ -898,6 +950,34 @@ namespace Unity.Netcode.Editor.CodeGen
}
}
}
+ {
+ var baseTypes = new List();
+
+ var genericParams = new Dictionary();
+ var resolved = type.Resolve();
+ if (type.IsGenericInstance)
+ {
+ var genericType = (GenericInstanceType)type;
+ for (var i = 0; i < genericType.GenericArguments.Count; ++i)
+ {
+ genericParams[resolved.GenericParameters[i].Name] = genericType.GenericArguments[i];
+ }
+ }
+
+ GetAllBaseTypesAndResolveGenerics(type.Resolve(), ref baseTypes, genericParams);
+ foreach (var baseType in baseTypes)
+ {
+ if (baseType.Resolve().Name == typeof(NetworkVariable<>).Name || baseType.Resolve().Name == typeof(NetworkList<>).Name)
+ {
+ var genericInstanceType = (GenericInstanceType)baseType;
+ var wrappedType = genericInstanceType.GenericArguments[0];
+ if (!m_WrappedNetworkVariableTypes.Contains(wrappedType))
+ {
+ m_WrappedNetworkVariableTypes.Add(wrappedType);
+ }
+ }
+ }
+ }
}
}
diff --git a/Editor/NetworkBehaviourEditor.cs b/Editor/NetworkBehaviourEditor.cs
index 9c40d25..6a34649 100644
--- a/Editor/NetworkBehaviourEditor.cs
+++ b/Editor/NetworkBehaviourEditor.cs
@@ -1,9 +1,9 @@
using System;
using System.Collections.Generic;
using System.Reflection;
-using UnityEngine;
-using UnityEditor;
using Unity.Netcode.Editor.Configuration;
+using UnityEditor;
+using UnityEngine;
namespace Unity.Netcode.Editor
{
@@ -135,23 +135,23 @@ namespace Unity.Netcode.Editor
}
else if (type == typeof(uint))
{
- val = (uint)EditorGUILayout.LongField(variableName, (long)((uint)val));
+ val = (uint)EditorGUILayout.LongField(variableName, (uint)val);
}
else if (type == typeof(short))
{
- val = (short)EditorGUILayout.IntField(variableName, (int)((short)val));
+ val = (short)EditorGUILayout.IntField(variableName, (short)val);
}
else if (type == typeof(ushort))
{
- val = (ushort)EditorGUILayout.IntField(variableName, (int)((ushort)val));
+ val = (ushort)EditorGUILayout.IntField(variableName, (ushort)val);
}
else if (type == typeof(sbyte))
{
- val = (sbyte)EditorGUILayout.IntField(variableName, (int)((sbyte)val));
+ val = (sbyte)EditorGUILayout.IntField(variableName, (sbyte)val);
}
else if (type == typeof(byte))
{
- val = (byte)EditorGUILayout.IntField(variableName, (int)((byte)val));
+ val = (byte)EditorGUILayout.IntField(variableName, (byte)val);
}
else if (type == typeof(long))
{
@@ -161,6 +161,10 @@ namespace Unity.Netcode.Editor
{
val = (ulong)EditorGUILayout.LongField(variableName, (long)((ulong)val));
}
+ else if (type == typeof(float))
+ {
+ val = EditorGUILayout.FloatField(variableName, (float)((float)val));
+ }
else if (type == typeof(bool))
{
val = EditorGUILayout.Toggle(variableName, (bool)val);
diff --git a/Editor/NetworkManagerEditor.cs b/Editor/NetworkManagerEditor.cs
index bcf8365..be5e6a3 100644
--- a/Editor/NetworkManagerEditor.cs
+++ b/Editor/NetworkManagerEditor.cs
@@ -1,9 +1,9 @@
using System;
using System.Collections.Generic;
using System.IO;
+using Unity.Netcode.Editor.Configuration;
using UnityEditor;
using UnityEngine;
-using Unity.Netcode.Editor.Configuration;
namespace Unity.Netcode.Editor
{
@@ -231,13 +231,7 @@ namespace Unity.Netcode.Editor
{
ReloadTransports();
- var transportComponent = m_NetworkManager.gameObject.GetComponent(m_TransportTypes[selection - 1]);
-
- if (transportComponent == null)
- {
- transportComponent = m_NetworkManager.gameObject.AddComponent(m_TransportTypes[selection - 1]);
- }
-
+ var transportComponent = m_NetworkManager.gameObject.GetComponent(m_TransportTypes[selection - 1]) ?? m_NetworkManager.gameObject.AddComponent(m_TransportTypes[selection - 1]);
m_NetworkTransportProperty.objectReferenceValue = transportComponent;
Repaint();
@@ -355,15 +349,19 @@ namespace Unity.Netcode.Editor
if (s_CenteredWordWrappedLabelStyle == null)
{
- s_CenteredWordWrappedLabelStyle = new GUIStyle(GUI.skin.label);
- s_CenteredWordWrappedLabelStyle.wordWrap = true;
- s_CenteredWordWrappedLabelStyle.alignment = TextAnchor.MiddleLeft;
+ s_CenteredWordWrappedLabelStyle = new GUIStyle(GUI.skin.label)
+ {
+ wordWrap = true,
+ alignment = TextAnchor.MiddleLeft
+ };
}
if (s_HelpBoxStyle == null)
{
- s_HelpBoxStyle = new GUIStyle(EditorStyles.helpBox);
- s_HelpBoxStyle.padding = new RectOffset(10, 10, 10, 10);
+ s_HelpBoxStyle = new GUIStyle(EditorStyles.helpBox)
+ {
+ padding = new RectOffset(10, 10, 10, 10)
+ };
}
var openDocsButtonStyle = GUI.skin.button;
diff --git a/Editor/NetworkManagerHelper.cs b/Editor/NetworkManagerHelper.cs
index 6fd6103..19643d4 100644
--- a/Editor/NetworkManagerHelper.cs
+++ b/Editor/NetworkManagerHelper.cs
@@ -1,9 +1,9 @@
using System.Collections.Generic;
using System.Linq;
using Unity.Netcode.Editor.Configuration;
+using UnityEditor;
using UnityEngine;
using UnityEngine.SceneManagement;
-using UnityEditor;
namespace Unity.Netcode.Editor
{
diff --git a/Editor/NetworkObjectEditor.cs b/Editor/NetworkObjectEditor.cs
index c63f9fc..1b582f8 100644
--- a/Editor/NetworkObjectEditor.cs
+++ b/Editor/NetworkObjectEditor.cs
@@ -1,6 +1,6 @@
using System.Collections.Generic;
-using UnityEngine;
using UnityEditor;
+using UnityEngine;
namespace Unity.Netcode.Editor
{
diff --git a/Editor/NetworkTransformEditor.cs b/Editor/NetworkTransformEditor.cs
index 6ba27ea..6a5be78 100644
--- a/Editor/NetworkTransformEditor.cs
+++ b/Editor/NetworkTransformEditor.cs
@@ -1,6 +1,6 @@
+using Unity.Netcode.Components;
using UnityEditor;
using UnityEngine;
-using Unity.Netcode.Components;
namespace Unity.Netcode.Editor
{
@@ -25,6 +25,11 @@ namespace Unity.Netcode.Editor
private SerializedProperty m_InLocalSpaceProperty;
private SerializedProperty m_InterpolateProperty;
+ private SerializedProperty m_UseQuaternionSynchronization;
+ private SerializedProperty m_UseQuaternionCompression;
+ private SerializedProperty m_UseHalfFloatPrecision;
+ private SerializedProperty m_SlerpPosition;
+
private static int s_ToggleOffset = 45;
private static float s_MaxRowWidth = EditorGUIUtility.labelWidth + EditorGUIUtility.fieldWidth + 5;
private static GUIContent s_PositionLabel = EditorGUIUtility.TrTextContent("Position");
@@ -48,6 +53,10 @@ namespace Unity.Netcode.Editor
m_ScaleThresholdProperty = serializedObject.FindProperty(nameof(NetworkTransform.ScaleThreshold));
m_InLocalSpaceProperty = serializedObject.FindProperty(nameof(NetworkTransform.InLocalSpace));
m_InterpolateProperty = serializedObject.FindProperty(nameof(NetworkTransform.Interpolate));
+ m_UseQuaternionSynchronization = serializedObject.FindProperty(nameof(NetworkTransform.UseQuaternionSynchronization));
+ m_UseQuaternionCompression = serializedObject.FindProperty(nameof(NetworkTransform.UseQuaternionCompression));
+ m_UseHalfFloatPrecision = serializedObject.FindProperty(nameof(NetworkTransform.UseHalfFloatPrecision));
+ m_SlerpPosition = serializedObject.FindProperty(nameof(NetworkTransform.SlerpPosition));
}
///
@@ -71,6 +80,8 @@ namespace Unity.Netcode.Editor
GUILayout.EndHorizontal();
}
+
+ if (!m_UseQuaternionSynchronization.boolValue)
{
GUILayout.BeginHorizontal();
@@ -88,6 +99,13 @@ namespace Unity.Netcode.Editor
GUILayout.EndHorizontal();
}
+ else
+ {
+ m_SyncRotationXProperty.boolValue = true;
+ m_SyncRotationYProperty.boolValue = true;
+ m_SyncRotationZProperty.boolValue = true;
+ }
+
{
GUILayout.BeginHorizontal();
@@ -116,6 +134,17 @@ namespace Unity.Netcode.Editor
EditorGUILayout.LabelField("Configurations", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(m_InLocalSpaceProperty);
EditorGUILayout.PropertyField(m_InterpolateProperty);
+ EditorGUILayout.PropertyField(m_SlerpPosition);
+ EditorGUILayout.PropertyField(m_UseQuaternionSynchronization);
+ if (m_UseQuaternionSynchronization.boolValue)
+ {
+ EditorGUILayout.PropertyField(m_UseQuaternionCompression);
+ }
+ else
+ {
+ m_UseQuaternionCompression.boolValue = false;
+ }
+ EditorGUILayout.PropertyField(m_UseHalfFloatPrecision);
#if COM_UNITY_MODULES_PHYSICS
// if rigidbody is present but network rigidbody is not present
diff --git a/Runtime/AssemblyInfo.cs b/Runtime/AssemblyInfo.cs
index 134b912..82e95f5 100644
--- a/Runtime/AssemblyInfo.cs
+++ b/Runtime/AssemblyInfo.cs
@@ -1,5 +1,5 @@
using System.Runtime.CompilerServices;
-
+[assembly: InternalsVisibleTo("Unity.Netcode.Components")]
#if UNITY_EDITOR
[assembly: InternalsVisibleTo("Unity.Netcode.Editor")]
[assembly: InternalsVisibleTo("Unity.Netcode.Editor.CodeGen")]
diff --git a/Runtime/Configuration/NetworkConfig.cs b/Runtime/Configuration/NetworkConfig.cs
index 23ff460..c6454af 100644
--- a/Runtime/Configuration/NetworkConfig.cs
+++ b/Runtime/Configuration/NetworkConfig.cs
@@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
-using UnityEngine;
using System.Linq;
using Unity.Collections;
+using UnityEngine;
using UnityEngine.Serialization;
namespace Unity.Netcode
@@ -208,6 +208,14 @@ namespace Unity.Netcode
private ulong? m_ConfigHash = null;
+ ///
+ /// Clears out the configuration hash value generated for a specific network session
+ ///
+ internal void ClearConfigHash()
+ {
+ m_ConfigHash = null;
+ }
+
///
/// Gets a SHA256 hash of parts of the NetworkConfig instance
///
@@ -273,8 +281,6 @@ namespace Unity.Netcode
Prefabs.Initialize();
}
- #region Legacy Network Prefab List
-
[NonSerialized]
private bool m_DidWarnOldPrefabList = false;
@@ -334,7 +340,5 @@ namespace Unity.Netcode
[FormerlySerializedAs("NetworkPrefabs")]
[SerializeField]
internal List OldPrefabList;
-
- #endregion
}
}
diff --git a/Runtime/Configuration/NetworkPrefabs.cs b/Runtime/Configuration/NetworkPrefabs.cs
index 6c2d526..1dae3ce 100644
--- a/Runtime/Configuration/NetworkPrefabs.cs
+++ b/Runtime/Configuration/NetworkPrefabs.cs
@@ -52,12 +52,23 @@ namespace Unity.Netcode
}
~NetworkPrefabs()
+ {
+ Shutdown();
+ }
+
+ ///
+ /// Deregister from add and remove events
+ /// Clear the list
+ ///
+ internal void Shutdown()
{
foreach (var list in NetworkPrefabsLists)
{
list.OnAdd -= AddTriggeredByNetworkPrefabList;
list.OnRemove -= RemoveTriggeredByNetworkPrefabList;
}
+
+ NetworkPrefabsLists.Clear();
}
///
diff --git a/Runtime/Core/ComponentFactory.cs b/Runtime/Core/ComponentFactory.cs
index 6ed6380..6bb8172 100644
--- a/Runtime/Core/ComponentFactory.cs
+++ b/Runtime/Core/ComponentFactory.cs
@@ -52,6 +52,8 @@ namespace Unity.Netcode
public static void SetDefaults()
{
SetDefault(networkManager => new DeferredMessageManager(networkManager));
+
+ SetDefault(networkManager => new RealTimeProvider());
}
private static void SetDefault(CreateObjectDelegate creator)
diff --git a/Runtime/Core/NetworkBehaviour.cs b/Runtime/Core/NetworkBehaviour.cs
index b532a78..6efb88c 100644
--- a/Runtime/Core/NetworkBehaviour.cs
+++ b/Runtime/Core/NetworkBehaviour.cs
@@ -1,9 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using UnityEngine;
using System.Reflection;
using Unity.Collections;
+using UnityEngine;
namespace Unity.Netcode
{
@@ -82,7 +82,7 @@ namespace Unity.Netcode
var context = new NetworkContext
{
SenderId = NetworkManager.ServerClientId,
- Timestamp = Time.realtimeSinceStartup,
+ Timestamp = NetworkManager.RealTimeProvider.RealTimeSinceStartup,
SystemOwner = NetworkManager,
// header information isn't valid since it's not a real message.
// RpcMessage doesn't access this stuff so it's just left empty.
@@ -219,7 +219,7 @@ namespace Unity.Netcode
var context = new NetworkContext
{
SenderId = NetworkManager.ServerClientId,
- Timestamp = Time.realtimeSinceStartup,
+ Timestamp = NetworkManager.RealTimeProvider.RealTimeSinceStartup,
SystemOwner = NetworkManager,
// header information isn't valid since it's not a real message.
// RpcMessage doesn't access this stuff so it's just left empty.
@@ -570,13 +570,10 @@ namespace Unity.Netcode
if (list == null)
{
list = new List();
- list.AddRange(type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
- }
- else
- {
- list.AddRange(type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
}
+ list.AddRange(type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
+
if (type.BaseType != null && type.BaseType != typeof(NetworkBehaviour))
{
return GetFieldInfoForTypeRecursive(type.BaseType, list);
@@ -600,13 +597,7 @@ namespace Unity.Netcode
var fieldType = sortedFields[i].FieldType;
if (fieldType.IsSubclassOf(typeof(NetworkVariableBase)))
{
- var instance = (NetworkVariableBase)sortedFields[i].GetValue(this);
-
- if (instance == null)
- {
- throw new Exception($"{GetType().FullName}.{sortedFields[i].Name} cannot be null. All {nameof(NetworkVariableBase)} instances must be initialized.");
- }
-
+ var instance = (NetworkVariableBase)sortedFields[i].GetValue(this) ?? throw new Exception($"{GetType().FullName}.{sortedFields[i].Name} cannot be null. All {nameof(NetworkVariableBase)} instances must be initialized.");
instance.Initialize(this);
var instanceNameProperty = fieldType.GetProperty(nameof(NetworkVariableBase.Name));
@@ -899,11 +890,23 @@ namespace Unity.Netcode
/// Either BufferSerializerReader or BufferSerializerWriter, depending whether the serializer
/// is in read mode or write mode.
///
+ /// the relative client identifier being synchronized
protected virtual void OnSynchronize(ref BufferSerializer serializer) where T : IReaderWriter
{
}
+ ///
+ /// The relative client identifier targeted for the serialization of this instance.
+ ///
+ ///
+ /// This value will be set prior to being invoked.
+ /// For writing (server-side), this is useful to know which client will receive the serialized data.
+ /// For reading (client-side), this will be the .
+ /// When synchronization of this instance is complete, this value will be reset to 0
+ ///
+ protected ulong m_TargetIdBeingSynchronized { get; private set; }
+
///
/// Internal method that determines if a NetworkBehaviour has additional synchronization data to
/// be synchronized when first instantiated prior to its associated NetworkObject being spawned.
@@ -913,8 +916,9 @@ namespace Unity.Netcode
/// synchronize any remaining NetworkBehaviours.
///
/// true if it wrote synchronization data and false if it did not
- internal bool Synchronize(ref BufferSerializer serializer) where T : IReaderWriter
+ internal bool Synchronize(ref BufferSerializer serializer, ulong targetClientId = 0) where T : IReaderWriter
{
+ m_TargetIdBeingSynchronized = targetClientId;
if (serializer.IsWriter)
{
// Get the writer to handle seeking and determining how many bytes were written
@@ -949,6 +953,8 @@ namespace Unity.Netcode
}
var finalPosition = writer.Position;
+ // Reset before exiting
+ m_TargetIdBeingSynchronized = default;
// If we wrote nothing then skip writing anything for this NetworkBehaviour
if (finalPosition == positionBeforeSynchronize || threwException)
{
@@ -1002,6 +1008,9 @@ namespace Unity.Netcode
synchronizationError = true;
}
+ // Reset before exiting
+ m_TargetIdBeingSynchronized = default;
+
// Skip over the entry if deserialization fails
if (synchronizationError)
{
diff --git a/Runtime/Core/NetworkManager.cs b/Runtime/Core/NetworkManager.cs
index 4cc8ba7..2ed0928 100644
--- a/Runtime/Core/NetworkManager.cs
+++ b/Runtime/Core/NetworkManager.cs
@@ -298,6 +298,8 @@ namespace Unity.Netcode
internal IDeferredMessageManager DeferredMessageManager { get; private set; }
+ internal IRealTimeProvider RealTimeProvider { get; private set; }
+
///
/// Gets the CustomMessagingManager for this NetworkManager
///
@@ -449,10 +451,28 @@ namespace Unity.Netcode
public event Action OnClientDisconnectCallback = null;
///
- /// The callback to invoke once the server is ready
+ /// This callback is invoked when the local server is started and listening for incoming connections.
///
public event Action OnServerStarted = null;
+ ///
+ /// The callback to invoke once the local client is ready
+ ///
+ public event Action OnClientStarted = null;
+
+ ///
+ /// This callback is invoked once the local server is stopped.
+ ///
+ /// The first parameter of this event will be set to when stopping a host instance and when stopping a server instance.
+ public event Action OnServerStopped = null;
+
+ ///
+ /// The callback to invoke once the local client stops
+ ///
+ /// The parameter states whether the client was running in host mode
+ /// The first parameter of this event will be set to when stopping the host client and when stopping a standard client instance.
+ public event Action OnClientStopped = null;
+
///
/// The callback to invoke if the fails.
///
@@ -735,6 +755,8 @@ namespace Unity.Netcode
DeferredMessageManager = ComponentFactory.Create(this);
+ RealTimeProvider = ComponentFactory.Create(this);
+
CustomMessagingManager = new CustomMessagingManager(this);
SceneManager = new NetworkSceneManager(this);
@@ -908,6 +930,7 @@ namespace Unity.Netcode
IsClient = true;
IsListening = true;
+ OnClientStarted?.Invoke();
return true;
}
@@ -989,13 +1012,14 @@ namespace Unity.Netcode
SpawnManager.ServerSpawnSceneObjectsOnStartSweep();
+ OnServerStarted?.Invoke();
+ OnClientStarted?.Invoke();
+
// This assures that any in-scene placed NetworkObject is spawned and
// any associated NetworkBehaviours' netcode related properties are
// set prior to invoking OnClientConnected.
InvokeOnClientConnectedCallback(LocalClientId);
- OnServerStarted?.Invoke();
-
return true;
}
@@ -1108,13 +1132,13 @@ namespace Unity.Netcode
return isParented;
}
- static internal string GenerateNestedNetworkManagerMessage(Transform transform)
+ internal static string GenerateNestedNetworkManagerMessage(Transform transform)
{
return $"{transform.name} is nested under {transform.root.name}. NetworkManager cannot be nested.\n";
}
#if UNITY_EDITOR
- static internal INetworkManagerHelper NetworkManagerHelper;
+ internal static INetworkManagerHelper NetworkManagerHelper;
///
/// Interface for NetworkManagerHelper
///
@@ -1195,13 +1219,12 @@ namespace Unity.Netcode
NetworkLog.LogInfo(nameof(ShutdownInternal));
}
- if (IsServer)
+ bool wasServer = IsServer;
+ bool wasClient = IsClient;
+ if (wasServer)
{
// make sure all messages are flushed before transport disconnect clients
- if (MessagingSystem != null)
- {
- MessagingSystem.ProcessSendQueues();
- }
+ MessagingSystem?.ProcessSendQueues();
var disconnectedIds = new HashSet();
@@ -1237,10 +1260,22 @@ namespace Unity.Netcode
}
}
+ // Unregister network updates before trying to disconnect the client
+ this.UnregisterAllNetworkUpdates();
+
if (IsClient && IsListening)
{
// Client only, send disconnect to server
- NetworkConfig.NetworkTransport.DisconnectLocalClient();
+ // If transport throws and exception, log the exception and
+ // continue the shutdown sequence (or forever be shutting down)
+ try
+ {
+ NetworkConfig.NetworkTransport.DisconnectLocalClient();
+ }
+ catch (Exception ex)
+ {
+ Debug.LogException(ex);
+ }
}
IsConnectedClient = false;
@@ -1261,8 +1296,6 @@ namespace Unity.Netcode
IsServer = false;
IsClient = false;
- this.UnregisterAllNetworkUpdates();
-
if (NetworkTickSystem != null)
{
NetworkTickSystem.Tick -= OnNetworkManagerTick;
@@ -1280,10 +1313,7 @@ namespace Unity.Netcode
NetworkConfig.NetworkTransport.OnTransportEvent -= HandleRawTransportPoll;
}
- if (DeferredMessageManager != null)
- {
- DeferredMessageManager.CleanupAllTriggers();
- }
+ DeferredMessageManager?.CleanupAllTriggers();
if (SceneManager != null)
{
@@ -1318,6 +1348,22 @@ namespace Unity.Netcode
m_StopProcessingMessages = false;
ClearClients();
+
+ if (wasClient)
+ {
+ OnClientStopped?.Invoke(wasServer);
+ }
+ if (wasServer)
+ {
+ OnServerStopped?.Invoke(wasClient);
+ }
+
+ // This cleans up the internal prefabs list
+ NetworkConfig?.Prefabs.Shutdown();
+
+ // Reset the configuration hash for next session in the event
+ // that the prefab list changes
+ NetworkConfig?.ClearConfigHash();
}
///
@@ -1417,7 +1463,7 @@ namespace Unity.Netcode
}
// Only update RTT here, server time is updated by time sync messages
- var reset = NetworkTimeSystem.Advance(Time.unscaledDeltaTime);
+ var reset = NetworkTimeSystem.Advance(RealTimeProvider.UnscaledDeltaTime);
if (reset)
{
NetworkTickSystem.Reset(NetworkTimeSystem.LocalTime, NetworkTimeSystem.ServerTime);
@@ -1426,7 +1472,7 @@ namespace Unity.Netcode
if (IsServer == false)
{
- NetworkTimeSystem.Sync(NetworkTimeSystem.LastSyncedServerTimeSec + Time.unscaledDeltaTime, NetworkConfig.NetworkTransport.GetCurrentRtt(ServerClientId) / 1000d);
+ NetworkTimeSystem.Sync(NetworkTimeSystem.LastSyncedServerTimeSec + RealTimeProvider.UnscaledDeltaTime, NetworkConfig.NetworkTransport.GetCurrentRtt(ServerClientId) / 1000d);
}
}
@@ -1435,6 +1481,10 @@ namespace Unity.Netcode
if (!m_ShuttingDown || !m_StopProcessingMessages)
{
+ // This should be invoked just prior to the MessagingSystem
+ // processes its outbound queue.
+ SceneManager.CheckForAndSendNetworkObjectSceneChanged();
+
MessagingSystem.ProcessSendQueues();
NetworkMetrics.UpdateNetworkObjectsCount(SpawnManager.SpawnedObjects.Count);
NetworkMetrics.UpdateConnectionsCount((IsServer) ? ConnectedClients.Count : 1);
@@ -1486,10 +1536,9 @@ namespace Unity.Netcode
// we should always force the rebuilding of the NetworkConfig hash value
ConfigHash = NetworkConfig.GetConfig(false),
ShouldSendConnectionData = NetworkConfig.ConnectionApproval,
- ConnectionData = NetworkConfig.ConnectionData
+ ConnectionData = NetworkConfig.ConnectionData,
+ MessageVersions = new NativeArray(MessagingSystem.MessageHandlers.Length, Allocator.Temp)
};
-
- message.MessageVersions = new NativeArray(MessagingSystem.MessageHandlers.Length, Allocator.Temp);
for (int index = 0; index < MessagingSystem.MessageHandlers.Length; index++)
{
if (MessagingSystem.MessageTypes[index] != null)
@@ -1509,7 +1558,7 @@ namespace Unity.Netcode
private IEnumerator ApprovalTimeout(ulong clientId)
{
- var timeStarted = IsServer ? LocalTime.TimeAsFloat : Time.realtimeSinceStartup;
+ var timeStarted = IsServer ? LocalTime.TimeAsFloat : RealTimeProvider.RealTimeSinceStartup;
var timedOut = false;
var connectionApproved = false;
var connectionNotApproved = false;
@@ -1519,7 +1568,7 @@ namespace Unity.Netcode
{
yield return null;
// Check if we timed out
- timedOut = timeoutMarker < (IsServer ? LocalTime.TimeAsFloat : Time.realtimeSinceStartup);
+ timedOut = timeoutMarker < (IsServer ? LocalTime.TimeAsFloat : RealTimeProvider.RealTimeSinceStartup);
if (IsServer)
{
@@ -1861,8 +1910,10 @@ namespace Unity.Netcode
if (!string.IsNullOrEmpty(reason))
{
- var disconnectReason = new DisconnectReasonMessage();
- disconnectReason.Reason = reason;
+ var disconnectReason = new DisconnectReasonMessage
+ {
+ Reason = reason
+ };
SendMessage(ref disconnectReason, NetworkDelivery.Reliable, clientId);
}
MessagingSystem.ProcessSendQueues();
@@ -2011,15 +2062,19 @@ namespace Unity.Netcode
if (response.CreatePlayerObject)
{
- var playerPrefabHash = response.PlayerPrefabHash ?? NetworkConfig.PlayerPrefab.GetComponent().GlobalObjectIdHash;
+ var prefabNetworkObject = NetworkConfig.PlayerPrefab.GetComponent();
+ var playerPrefabHash = response.PlayerPrefabHash ?? prefabNetworkObject.GlobalObjectIdHash;
// Generate a SceneObject for the player object to spawn
+ // Note: This is only to create the local NetworkObject,
+ // many of the serialized properties of the player prefab
+ // will be set when instantiated.
var sceneObject = new NetworkObject.SceneObject
{
OwnerClientId = ownerClientId,
IsPlayerObject = true,
IsSceneObject = false,
- HasTransform = true,
+ HasTransform = prefabNetworkObject.SynchronizeTransform,
Hash = playerPrefabHash,
TargetClientId = ownerClientId,
Transform = new NetworkObject.SceneObject.TransformData
@@ -2105,8 +2160,10 @@ namespace Unity.Netcode
{
if (!string.IsNullOrEmpty(response.Reason))
{
- var disconnectReason = new DisconnectReasonMessage();
- disconnectReason.Reason = response.Reason;
+ var disconnectReason = new DisconnectReasonMessage
+ {
+ Reason = response.Reason
+ };
SendMessage(ref disconnectReason, NetworkDelivery.Reliable, ownerClientId);
MessagingSystem.ProcessSendQueues();
diff --git a/Runtime/Core/NetworkObject.cs b/Runtime/Core/NetworkObject.cs
index 1e52e52..74a8080 100644
--- a/Runtime/Core/NetworkObject.cs
+++ b/Runtime/Core/NetworkObject.cs
@@ -17,6 +17,28 @@ namespace Unity.Netcode
[SerializeField]
internal uint GlobalObjectIdHash;
+ ///
+ /// Gets the Prefab Hash Id of this object if the object is registerd as a prefab otherwise it returns 0
+ ///
+ [HideInInspector]
+ public uint PrefabIdHash
+ {
+ get
+ {
+ foreach (var prefab in NetworkManager.NetworkConfig.Prefabs.Prefabs)
+ {
+ if (prefab.Prefab == gameObject)
+ {
+ return GlobalObjectIdHash;
+ }
+ }
+
+ return 0;
+ }
+ }
+
+ private bool m_IsPrefab;
+
#if UNITY_EDITOR
private void OnValidate()
{
@@ -75,6 +97,18 @@ namespace Unity.Netcode
///
public bool IsPlayerObject { get; internal set; }
+ ///
+ /// Determines if the associated NetworkObject's transform will get
+ /// synchronized when spawned.
+ ///
+ ///
+ /// For things like in-scene placed NetworkObjects that have no visual
+ /// components can help reduce the instance's initial synchronization
+ /// bandwidth cost. This can also be useful for UI elements that have
+ /// a predetermined fixed position.
+ ///
+ public bool SynchronizeTransform = true;
+
///
/// Gets if the object is the personal clients player object
///
@@ -105,6 +139,55 @@ namespace Unity.Netcode
///
public bool DestroyWithScene { get; set; }
+ ///
+ /// When set to true and the active scene is changed, this will automatically migrate the
+ /// into the new active scene on both the server and client instances.
+ ///
+ ///
+ /// - This only applies to dynamically spawned s.
+ /// - This only works when using integrated scene management ().
+ ///
+ /// If there are more than one scenes loaded and the currently active scene is unloaded, then typically
+ /// the will automatically assign a new active scene. Similar to
+ /// being set to , this prevents any from being destroyed
+ /// with the unloaded active scene by migrating it into the automatically assigned active scene.
+ /// Additionally, this is can be useful in some seamless scene streaming implementations.
+ /// Note:
+ /// Only having set to true will *not* synchronize clients when
+ /// changing a 's scene via .
+ /// To synchronize clients of a 's scene being changed via ,
+ /// make sure is enabled (it is by default).
+ ///
+ public bool ActiveSceneSynchronization;
+
+ ///
+ /// When enabled (the default), if a is migrated to a different scene (active or not)
+ /// via on the server side all client
+ /// instances will be synchronized and the migrated into the newly assigned scene.
+ /// The updated scene migration will get synchronized with late joining clients as well.
+ ///
+ ///
+ /// - This only applies to dynamically spawned s.
+ /// - This only works when using integrated scene management ().
+ /// Note:
+ /// You can have both and enabled.
+ /// The primary difference between the two is that only synchronizes clients
+ /// when the server migrates a to a new scene. If the scene is unloaded and
+ /// is and is and the scene is not the currently
+ /// active scene, then the will be destroyed.
+ ///
+ public bool SceneMigrationSynchronization = true;
+
+ ///
+ /// Notifies when the NetworkObject is migrated into a new scene
+ ///
+ ///
+ /// - or (or both) need to be enabled
+ /// - This only applies to dynamically spawned s.
+ /// - This only works when using integrated scene management ().
+ ///
+ public Action OnMigratedToNewScene;
+
///
/// Delegate type for checking visibility
///
@@ -188,6 +271,11 @@ namespace Unity.Netcode
///
internal int SceneOriginHandle = 0;
+ ///
+ /// The server-side scene origin handle
+ ///
+ internal int NetworkSceneHandle = 0;
+
private Scene m_SceneOrigin;
///
/// The scene where the NetworkObject was first instantiated
@@ -265,6 +353,15 @@ namespace Unity.Netcode
throw new VisibilityChangeException("The object is already visible");
}
+ if (CheckObjectVisibility != null && !CheckObjectVisibility(clientId))
+ {
+ if (NetworkManager.LogLevel <= LogLevel.Normal)
+ {
+ NetworkLog.LogWarning($"[NetworkShow] Trying to make {nameof(NetworkObject)} {gameObject.name} visible to client ({clientId}) but {nameof(CheckObjectVisibility)} returned false!");
+ }
+ return;
+ }
+
NetworkManager.MarkObjectForShowingTo(this, clientId);
Observers.Add(clientId);
}
@@ -578,6 +675,22 @@ namespace Unity.Netcode
private Transform m_CachedParent; // What is our last set parent Transform reference?
private bool m_CachedWorldPositionStays = true; // Used to preserve the world position stays parameter passed in TrySetParent
+ ///
+ /// Returns the last known cached WorldPositionStays value for this NetworkObject
+ ///
+ ///
+ /// When parenting NetworkObjects, the optional WorldPositionStays value is cached and synchronized with clients.
+ /// This method provides access to the instance relative cached value.
+ ///
+ ///
+ ///
+ ///
+ /// or
+ public bool WorldPositionStays()
+ {
+ return m_CachedWorldPositionStays;
+ }
+
internal void SetCachedParent(Transform parentTransform)
{
m_CachedParent = parentTransform;
@@ -1118,6 +1231,18 @@ namespace Unity.Netcode
set => ByteUtility.SetBit(ref m_BitField, 5, value);
}
+ ///
+ /// Even though the server sends notifications for NetworkObjects that get
+ /// destroyed when a scene is unloaded, we want to synchronize this so
+ /// the client side can use it as part of a filter for automatically migrating
+ /// to the current active scene when its scene is unloaded. (only for dynamically spawned)
+ ///
+ public bool DestroyWithScene
+ {
+ get => ByteUtility.GetBit(m_BitField, 6);
+ set => ByteUtility.SetBit(ref m_BitField, 6, value);
+ }
+
//If(Metadata.HasParent)
public ulong ParentObjectId;
@@ -1160,7 +1285,7 @@ namespace Unity.Netcode
var writeSize = 0;
writeSize += HasTransform ? FastBufferWriter.GetWriteSize() : 0;
- writeSize += IsSceneObject ? FastBufferWriter.GetWriteSize() : 0;
+ writeSize += FastBufferWriter.GetWriteSize();
if (!writer.TryBeginWrite(writeSize))
{
@@ -1172,14 +1297,9 @@ namespace Unity.Netcode
writer.WriteValue(Transform);
}
- // In-Scene NetworkObjects are uniquely identified NetworkPrefabs defined by their
- // NetworkSceneHandle and GlobalObjectIdHash. Client-side NetworkSceneManagers use
- // this to locate their local instance of the in-scene placed NetworkObject instance.
- // Only written for in-scene placed NetworkObjects.
- if (IsSceneObject)
- {
- writer.WriteValue(OwnerObject.GetSceneOriginHandle());
- }
+ // The NetworkSceneHandle is the server-side relative
+ // scene handle that the NetworkObject resides in.
+ writer.WriteValue(OwnerObject.GetSceneOriginHandle());
// Synchronize NetworkVariables and NetworkBehaviours
var bufferSerializer = new BufferSerializer(new BufferSerializerWriter(writer));
@@ -1205,7 +1325,7 @@ namespace Unity.Netcode
var readSize = 0;
readSize += HasTransform ? FastBufferWriter.GetWriteSize() : 0;
- readSize += IsSceneObject ? FastBufferWriter.GetWriteSize() : 0;
+ readSize += FastBufferWriter.GetWriteSize();
// Try to begin reading the remaining bytes
if (!reader.TryBeginRead(readSize))
@@ -1218,14 +1338,9 @@ namespace Unity.Netcode
reader.ReadValue(out Transform);
}
- // In-Scene NetworkObjects are uniquely identified NetworkPrefabs defined by their
- // NetworkSceneHandle and GlobalObjectIdHash. Client-side NetworkSceneManagers use
- // this to locate their local instance of the in-scene placed NetworkObject instance.
- // Only read for in-scene placed NetworkObjects
- if (IsSceneObject)
- {
- reader.ReadValue(out NetworkSceneHandle);
- }
+ // The NetworkSceneHandle is the server-side relative
+ // scene handle that the NetworkObject resides in.
+ reader.ReadValue(out NetworkSceneHandle);
}
}
@@ -1265,7 +1380,7 @@ namespace Unity.Netcode
var synchronizationCount = (byte)0;
foreach (var childBehaviour in ChildNetworkBehaviours)
{
- if (childBehaviour.Synchronize(ref serializer))
+ if (childBehaviour.Synchronize(ref serializer, targetClientId))
{
synchronizationCount++;
}
@@ -1304,7 +1419,7 @@ namespace Unity.Netcode
{
serializer.SerializeValue(ref networkBehaviourId);
var networkBehaviour = GetNetworkBehaviourAtOrderIndex(networkBehaviourId);
- networkBehaviour.Synchronize(ref serializer);
+ networkBehaviour.Synchronize(ref serializer, targetClientId);
}
}
}
@@ -1317,6 +1432,7 @@ namespace Unity.Netcode
OwnerClientId = OwnerClientId,
IsPlayerObject = IsPlayerObject,
IsSceneObject = IsSceneObject ?? true,
+ DestroyWithScene = DestroyWithScene,
Hash = HostCheckForGlobalObjectIdHashOverride(),
OwnerObject = this,
TargetClientId = targetClientId
@@ -1352,7 +1468,7 @@ namespace Unity.Netcode
if (IncludeTransformWhenSpawning == null || IncludeTransformWhenSpawning(OwnerClientId))
{
- obj.HasTransform = true;
+ obj.HasTransform = SynchronizeTransform;
// We start with the default AutoObjectParentSync values to determine which transform space we will
// be synchronizing clients with.
@@ -1435,11 +1551,126 @@ namespace Unity.Netcode
networkObject.SynchronizeNetworkBehaviours(ref bufferSerializer, networkManager.LocalClientId);
// Spawn the NetworkObject
- networkManager.SpawnManager.SpawnNetworkObjectLocally(networkObject, sceneObject, false);
+ networkManager.SpawnManager.SpawnNetworkObjectLocally(networkObject, sceneObject, sceneObject.DestroyWithScene);
return networkObject;
}
+ ///
+ /// Subscribes to changes in the currently active scene
+ ///
+ ///
+ /// Only for dynamically spawned NetworkObjects
+ ///
+ internal void SubscribeToActiveSceneForSynch()
+ {
+ if (ActiveSceneSynchronization)
+ {
+ if (IsSceneObject.HasValue && !IsSceneObject.Value)
+ {
+ // Just in case it is a recycled NetworkObject, unsubscribe first
+ SceneManager.activeSceneChanged -= CurrentlyActiveSceneChanged;
+ SceneManager.activeSceneChanged += CurrentlyActiveSceneChanged;
+ }
+ }
+ }
+
+ ///
+ /// If AutoSynchActiveScene is enabled, then this is the callback that handles updating
+ /// a NetworkObject's scene information.
+ ///
+ private void CurrentlyActiveSceneChanged(Scene current, Scene next)
+ {
+ // Early exit if there is no NetworkManager assigned, the NetworkManager is shutting down, the NetworkObject
+ // is not spawned, or an in-scene placed NetworkObject
+ if (NetworkManager == null || NetworkManager.ShutdownInProgress || !IsSpawned || IsSceneObject != false)
+ {
+ return;
+ }
+ // This check is here in the event a user wants to disable this for some reason but also wants
+ // the NetworkObject to synchronize to changes in the currently active scene at some later time.
+ if (ActiveSceneSynchronization)
+ {
+ // Only dynamically spawned NetworkObjects that are not already in the newly assigned active scene will migrate
+ // and update their scene handles
+ if (IsSceneObject.HasValue && !IsSceneObject.Value && gameObject.scene != next && gameObject.transform.parent == null)
+ {
+ SceneManager.MoveGameObjectToScene(gameObject, next);
+ SceneChangedUpdate(next);
+ }
+ }
+ }
+
+ ///
+ /// Handles updating the NetworkObject's tracked scene handles
+ ///
+ internal void SceneChangedUpdate(Scene scene, bool notify = false)
+ {
+ // Avoiding edge case scenarios, if no NetworkSceneManager exit early
+ if (NetworkManager.SceneManager == null)
+ {
+ return;
+ }
+
+ SceneOriginHandle = scene.handle;
+ // Clients need to update the NetworkSceneHandle
+ if (!NetworkManager.IsServer && NetworkManager.SceneManager.ClientSceneHandleToServerSceneHandle.ContainsKey(SceneOriginHandle))
+ {
+ NetworkSceneHandle = NetworkManager.SceneManager.ClientSceneHandleToServerSceneHandle[SceneOriginHandle];
+ }
+ else if (NetworkManager.IsServer)
+ {
+ // Since the server is the source of truth for the NetworkSceneHandle,
+ // the NetworkSceneHandle is the same as the SceneOriginHandle.
+ NetworkSceneHandle = SceneOriginHandle;
+ }
+ else // Otherwise, the client did not find the client to server scene handle
+ if (NetworkManager.LogLevel == LogLevel.Developer)
+ {
+ // There could be a scenario where a user has some client-local scene loaded that they migrate the NetworkObject
+ // into, but that scenario seemed very edge case and under most instances a user should be notified that this
+ // server - client scene handle mismatch has occurred. It also seemed pertinent to make the message replicate to
+ // the server-side too.
+ NetworkLog.LogWarningServer($"[Client-{NetworkManager.LocalClientId}][{gameObject.name}] Server - " +
+ $"client scene mismatch detected! Client-side scene handle ({SceneOriginHandle}) for scene ({gameObject.scene.name})" +
+ $"has no associated server side (network) scene handle!");
+ }
+ OnMigratedToNewScene?.Invoke();
+
+ // Only the server side will notify clients of non-parented NetworkObject scene changes
+ if (NetworkManager.IsServer && notify && transform.parent == null)
+ {
+ NetworkManager.SceneManager.NotifyNetworkObjectSceneChanged(this);
+ }
+ }
+
+ ///
+ /// Update
+ /// Detects if a NetworkObject's scene has changed for both server and client instances
+ ///
+ ///
+ /// About In-Scene Placed NetworkObjects:
+ /// Since the same scene can be loaded more than once and in-scene placed NetworkObjects GlobalObjectIdHash
+ /// values are only unique to the scene asset itself (and not per scene instance loaded), we will not be able
+ /// to add this same functionality to in-scene placed NetworkObjects until we have a way to generate
+ /// per-NetworkObject-instance unique GlobalObjectIdHash values for in-scene placed NetworkObjects.
+ ///
+ private void Update()
+ {
+ // Early exit if SceneMigrationSynchronization is disabled, there is no NetworkManager assigned,
+ // the NetworkManager is shutting down, the NetworkObject is not spawned, it is an in-scene placed
+ // NetworkObject, or the GameObject's current scene handle is the same as the SceneOriginHandle
+ if (!SceneMigrationSynchronization || NetworkManager == null || NetworkManager.ShutdownInProgress || !IsSpawned
+ || IsSceneObject != false || gameObject.scene.handle == SceneOriginHandle)
+ {
+ return;
+ }
+
+ // Otherwise, this has to be a dynamically spawned NetworkObject that has been
+ // migrated to a new scene.
+ SceneChangedUpdate(gameObject.scene, true);
+ }
+
///
/// Only applies to Host mode.
/// Will return the registered source NetworkPrefab's GlobalObjectIdHash if one exists.
diff --git a/Runtime/Core/NetworkUpdateLoop.cs b/Runtime/Core/NetworkUpdateLoop.cs
index 81473ed..cd47065 100644
--- a/Runtime/Core/NetworkUpdateLoop.cs
+++ b/Runtime/Core/NetworkUpdateLoop.cs
@@ -167,7 +167,7 @@ namespace Unity.Netcode
///
public static NetworkUpdateStage UpdateStage;
- private static void RunNetworkUpdateStage(NetworkUpdateStage updateStage)
+ internal static void RunNetworkUpdateStage(NetworkUpdateStage updateStage)
{
UpdateStage = updateStage;
diff --git a/Runtime/Hashing/XXHash.cs b/Runtime/Hashing/XXHash.cs
index 615736c..392e200 100644
--- a/Runtime/Hashing/XXHash.cs
+++ b/Runtime/Hashing/XXHash.cs
@@ -1,6 +1,6 @@
using System;
-using System.Text;
using System.Runtime.CompilerServices;
+using System.Text;
namespace Unity.Netcode
{
diff --git a/Runtime/Messaging/CustomMessageManager.cs b/Runtime/Messaging/CustomMessageManager.cs
index 573d645..c1140da 100644
--- a/Runtime/Messaging/CustomMessageManager.cs
+++ b/Runtime/Messaging/CustomMessageManager.cs
@@ -151,14 +151,18 @@ namespace Unity.Netcode
// We dont know what size to use. Try every (more collision prone)
if (m_NamedMessageHandlers32.TryGetValue(hash, out HandleNamedMessageDelegate messageHandler32))
{
+ // handler can remove itself, cache the name for metrics
+ string messageName = m_MessageHandlerNameLookup32[hash];
messageHandler32(sender, reader);
- m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, m_MessageHandlerNameLookup32[hash], bytesCount);
+ m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, messageName, bytesCount);
}
if (m_NamedMessageHandlers64.TryGetValue(hash, out HandleNamedMessageDelegate messageHandler64))
{
+ // handler can remove itself, cache the name for metrics
+ string messageName = m_MessageHandlerNameLookup64[hash];
messageHandler64(sender, reader);
- m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, m_MessageHandlerNameLookup64[hash], bytesCount);
+ m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, messageName, bytesCount);
}
}
else
@@ -169,15 +173,19 @@ namespace Unity.Netcode
case HashSize.VarIntFourBytes:
if (m_NamedMessageHandlers32.TryGetValue(hash, out HandleNamedMessageDelegate messageHandler32))
{
+ // handler can remove itself, cache the name for metrics
+ string messageName = m_MessageHandlerNameLookup32[hash];
messageHandler32(sender, reader);
- m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, m_MessageHandlerNameLookup32[hash], bytesCount);
+ m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, messageName, bytesCount);
}
break;
case HashSize.VarIntEightBytes:
if (m_NamedMessageHandlers64.TryGetValue(hash, out HandleNamedMessageDelegate messageHandler64))
{
+ // handler can remove itself, cache the name for metrics
+ string messageName = m_MessageHandlerNameLookup64[hash];
messageHandler64(sender, reader);
- m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, m_MessageHandlerNameLookup64[hash], bytesCount);
+ m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, messageName, bytesCount);
}
break;
}
diff --git a/Runtime/Messaging/DeferredMessageManager.cs b/Runtime/Messaging/DeferredMessageManager.cs
index 8ee919b..8c48909 100644
--- a/Runtime/Messaging/DeferredMessageManager.cs
+++ b/Runtime/Messaging/DeferredMessageManager.cs
@@ -1,6 +1,5 @@
using System.Collections.Generic;
using Unity.Collections;
-using Time = UnityEngine.Time;
namespace Unity.Netcode
{
@@ -49,7 +48,7 @@ namespace Unity.Netcode
{
triggerInfo = new TriggerInfo
{
- Expiry = Time.realtimeSinceStartup + m_NetworkManager.NetworkConfig.SpawnTimeout,
+ Expiry = m_NetworkManager.RealTimeProvider.RealTimeSinceStartup + m_NetworkManager.NetworkConfig.SpawnTimeout,
TriggerData = new NativeList(Allocator.Persistent)
};
triggers[key] = triggerInfo;
@@ -77,7 +76,7 @@ namespace Unity.Netcode
int index = 0;
foreach (var kvp2 in kvp.Value)
{
- if (kvp2.Value.Expiry < Time.realtimeSinceStartup)
+ if (kvp2.Value.Expiry < m_NetworkManager.RealTimeProvider.RealTimeSinceStartup)
{
staleKeys[index++] = kvp2.Key;
PurgeTrigger(kvp.Key, kvp2.Key, kvp2.Value);
diff --git a/Runtime/Messaging/DisconnectReasonMessage.cs b/Runtime/Messaging/DisconnectReasonMessage.cs
index eb5d39a..7ea6284 100644
--- a/Runtime/Messaging/DisconnectReasonMessage.cs
+++ b/Runtime/Messaging/DisconnectReasonMessage.cs
@@ -8,11 +8,7 @@ namespace Unity.Netcode
public void Serialize(FastBufferWriter writer, int targetVersion)
{
- string reasonSent = Reason;
- if (reasonSent == null)
- {
- reasonSent = string.Empty;
- }
+ string reasonSent = Reason ?? string.Empty;
// Since we don't send a ConnectionApprovedMessage, the version for this message is encded with the message
// itself. However, note that we HAVE received a ConnectionRequestMessage, so we DO have a valid targetVersion
diff --git a/Runtime/Messaging/Messages/RpcMessages.cs b/Runtime/Messaging/Messages/RpcMessages.cs
index 21889c0..dd6b199 100644
--- a/Runtime/Messaging/Messages/RpcMessages.cs
+++ b/Runtime/Messaging/Messages/RpcMessages.cs
@@ -1,6 +1,6 @@
using System;
-using UnityEngine;
using Unity.Collections;
+using UnityEngine;
namespace Unity.Netcode
{
diff --git a/Runtime/Messaging/MessagingSystem.cs b/Runtime/Messaging/MessagingSystem.cs
index b384d3b..87f02ce 100644
--- a/Runtime/Messaging/MessagingSystem.cs
+++ b/Runtime/Messaging/MessagingSystem.cs
@@ -365,37 +365,35 @@ namespace Unity.Netcode
public void HandleMessage(in MessageHeader header, FastBufferReader reader, ulong senderId, float timestamp, int serializedHeaderSize)
{
- if (header.MessageType >= m_HighMessageType)
- {
- Debug.LogWarning($"Received a message with invalid message type value {header.MessageType}");
- reader.Dispose();
- return;
- }
- var context = new NetworkContext
- {
- SystemOwner = m_Owner,
- SenderId = senderId,
- Timestamp = timestamp,
- Header = header,
- SerializedHeaderSize = serializedHeaderSize,
- MessageSize = header.MessageSize,
- };
-
- var type = m_ReverseTypeMap[header.MessageType];
- if (!CanReceive(senderId, type, reader, ref context))
- {
- reader.Dispose();
- return;
- }
-
- for (var hookIdx = 0; hookIdx < m_Hooks.Count; ++hookIdx)
- {
- m_Hooks[hookIdx].OnBeforeReceiveMessage(senderId, type, reader.Length + FastBufferWriter.GetWriteSize());
- }
-
- var handler = m_MessageHandlers[header.MessageType];
using (reader)
{
+ if (header.MessageType >= m_HighMessageType)
+ {
+ Debug.LogWarning($"Received a message with invalid message type value {header.MessageType}");
+ return;
+ }
+ var context = new NetworkContext
+ {
+ SystemOwner = m_Owner,
+ SenderId = senderId,
+ Timestamp = timestamp,
+ Header = header,
+ SerializedHeaderSize = serializedHeaderSize,
+ MessageSize = header.MessageSize,
+ };
+
+ var type = m_ReverseTypeMap[header.MessageType];
+ if (!CanReceive(senderId, type, reader, ref context))
+ {
+ return;
+ }
+
+ var handler = m_MessageHandlers[header.MessageType];
+ for (var hookIdx = 0; hookIdx < m_Hooks.Count; ++hookIdx)
+ {
+ m_Hooks[hookIdx].OnBeforeReceiveMessage(senderId, type, reader.Length + FastBufferWriter.GetWriteSize());
+ }
+
// 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
@@ -420,10 +418,10 @@ namespace Unity.Netcode
Debug.LogException(e);
}
}
- }
- for (var hookIdx = 0; hookIdx < m_Hooks.Count; ++hookIdx)
- {
- m_Hooks[hookIdx].OnAfterReceiveMessage(senderId, type, reader.Length + FastBufferWriter.GetWriteSize());
+ for (var hookIdx = 0; hookIdx < m_Hooks.Count; ++hookIdx)
+ {
+ m_Hooks[hookIdx].OnAfterReceiveMessage(senderId, type, reader.Length + FastBufferWriter.GetWriteSize());
+ }
}
}
diff --git a/Runtime/NetworkVariable/NetworkVariable.cs b/Runtime/NetworkVariable/NetworkVariable.cs
index 5f62721..538d6f3 100644
--- a/Runtime/NetworkVariable/NetworkVariable.cs
+++ b/Runtime/NetworkVariable/NetworkVariable.cs
@@ -1,5 +1,5 @@
-using UnityEngine;
using System;
+using UnityEngine;
namespace Unity.Netcode
{
diff --git a/Runtime/SceneManagement/DefaultSceneManagerHandler.cs b/Runtime/SceneManagement/DefaultSceneManagerHandler.cs
new file mode 100644
index 0000000..5599eb7
--- /dev/null
+++ b/Runtime/SceneManagement/DefaultSceneManagerHandler.cs
@@ -0,0 +1,377 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEngine.SceneManagement;
+
+
+namespace Unity.Netcode
+{
+ ///
+ /// The default SceneManagerHandler that interfaces between the SceneManager and NetworkSceneManager
+ ///
+ internal class DefaultSceneManagerHandler : ISceneManagerHandler
+ {
+ private Scene m_InvalidScene = new Scene();
+
+ internal struct SceneEntry
+ {
+ public bool IsAssigned;
+ public Scene Scene;
+ }
+
+ internal Dictionary> SceneNameToSceneHandles = new Dictionary>();
+
+ public AsyncOperation LoadSceneAsync(string sceneName, LoadSceneMode loadSceneMode, SceneEventProgress sceneEventProgress)
+ {
+ var operation = SceneManager.LoadSceneAsync(sceneName, loadSceneMode);
+ sceneEventProgress.SetAsyncOperation(operation);
+ return operation;
+ }
+
+ public AsyncOperation UnloadSceneAsync(Scene scene, SceneEventProgress sceneEventProgress)
+ {
+ var operation = SceneManager.UnloadSceneAsync(scene);
+ sceneEventProgress.SetAsyncOperation(operation);
+ return operation;
+ }
+
+ ///
+ /// Resets scene tracking
+ ///
+ public void ClearSceneTracking(NetworkManager networkManager)
+ {
+ SceneNameToSceneHandles.Clear();
+ }
+
+ ///
+ /// Stops tracking a specific scene
+ ///
+ public void StopTrackingScene(int handle, string name, NetworkManager networkManager)
+ {
+ if (SceneNameToSceneHandles.ContainsKey(name))
+ {
+ if (SceneNameToSceneHandles[name].ContainsKey(handle))
+ {
+ SceneNameToSceneHandles[name].Remove(handle);
+ if (SceneNameToSceneHandles[name].Count == 0)
+ {
+ SceneNameToSceneHandles.Remove(name);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Starts tracking a specific scene
+ ///
+ public void StartTrackingScene(Scene scene, bool assigned, NetworkManager networkManager)
+ {
+ if (!SceneNameToSceneHandles.ContainsKey(scene.name))
+ {
+ SceneNameToSceneHandles.Add(scene.name, new Dictionary());
+ }
+
+ if (!SceneNameToSceneHandles[scene.name].ContainsKey(scene.handle))
+ {
+ var sceneEntry = new SceneEntry()
+ {
+ IsAssigned = true,
+ Scene = scene
+ };
+ SceneNameToSceneHandles[scene.name].Add(scene.handle, sceneEntry);
+ }
+ else
+ {
+ throw new Exception($"[Duplicate Handle] Scene {scene.name} already has scene handle {scene.handle} registered!");
+ }
+ }
+
+ ///
+ /// Determines if there is an existing scene loaded that matches the scene name but has not been assigned
+ ///
+ public bool DoesSceneHaveUnassignedEntry(string sceneName, NetworkManager networkManager)
+ {
+ var scenesWithSceneName = new List();
+
+ // Get all loaded scenes with the same name
+ for (int i = 0; i < SceneManager.sceneCount; i++)
+ {
+ var scene = SceneManager.GetSceneAt(i);
+ if (scene.name == sceneName)
+ {
+ scenesWithSceneName.Add(scene);
+ }
+ }
+
+ // If there are no scenes of this name loaded then we have no loaded scenes
+ // to use
+ if (scenesWithSceneName.Count == 0)
+ {
+ return false;
+ }
+
+ // If we have 1 or more scenes with the name and we have no entries, then we do have
+ // a scene to use
+ if (scenesWithSceneName.Count > 0 && !SceneNameToSceneHandles.ContainsKey(sceneName))
+ {
+ return true;
+ }
+
+ // Determine if any of the loaded scenes has been used for synchronizing
+ foreach (var scene in scenesWithSceneName)
+ {
+ // If we don't have the handle, then we can use that scene
+ if (!SceneNameToSceneHandles[scene.name].ContainsKey(scene.handle))
+ {
+ return true;
+ }
+
+ // If we have an entry, but it is not yet assigned (i.e. preloaded)
+ // then we can use that.
+ if (!SceneNameToSceneHandles[scene.name][scene.handle].IsAssigned)
+ {
+ return true;
+ }
+ }
+ // If none were found, then we have no available scene (which most likely means one will get loaded)
+ return false;
+ }
+
+ ///
+ /// This will find any scene entry that hasn't been used/assigned, set the entry to assigned, and
+ /// return the associated scene. If none are found it returns an invalid scene.
+ ///
+ public Scene GetSceneFromLoadedScenes(string sceneName, NetworkManager networkManager)
+ {
+ if (SceneNameToSceneHandles.ContainsKey(sceneName))
+ {
+ foreach (var sceneHandleEntry in SceneNameToSceneHandles[sceneName])
+ {
+ if (!sceneHandleEntry.Value.IsAssigned)
+ {
+ var sceneEntry = sceneHandleEntry.Value;
+ sceneEntry.IsAssigned = true;
+ SceneNameToSceneHandles[sceneName][sceneHandleEntry.Key] = sceneEntry;
+ return sceneEntry.Scene;
+ }
+ }
+ }
+ // If we found nothing return an invalid scene
+ return m_InvalidScene;
+ }
+
+ ///
+ /// Only invoked is client synchronization is additive, this will generate the scene tracking table
+ /// in order to re-use the same scenes the server is synchronizing instead of having to unload the
+ /// scenes and reload them when synchronizing (i.e. client disconnects due to external reason, the
+ /// same application instance is still running, the same scenes are still loaded on the client, and
+ /// upon reconnecting the client doesn't have to unload the scenes and then reload them)
+ ///
+ public void PopulateLoadedScenes(ref Dictionary scenesLoaded, NetworkManager networkManager)
+ {
+ SceneNameToSceneHandles.Clear();
+ var sceneCount = SceneManager.sceneCount;
+ for (int i = 0; i < sceneCount; i++)
+ {
+ var scene = SceneManager.GetSceneAt(i);
+ if (!SceneNameToSceneHandles.ContainsKey(scene.name))
+ {
+ SceneNameToSceneHandles.Add(scene.name, new Dictionary());
+ }
+
+ if (!SceneNameToSceneHandles[scene.name].ContainsKey(scene.handle))
+ {
+ var sceneEntry = new SceneEntry()
+ {
+ IsAssigned = false,
+ Scene = scene
+ };
+ SceneNameToSceneHandles[scene.name].Add(scene.handle, sceneEntry);
+ if (!scenesLoaded.ContainsKey(scene.handle))
+ {
+ scenesLoaded.Add(scene.handle, scene);
+ }
+ }
+ else
+ {
+ throw new Exception($"[Duplicate Handle] Scene {scene.name} already has scene handle {scene.handle} registered!");
+ }
+ }
+ }
+
+ private List m_ScenesToUnload = new List();
+
+ ///
+ /// Unloads any scenes that have not been assigned.
+ ///
+ ///
+ public void UnloadUnassignedScenes(NetworkManager networkManager = null)
+ {
+ var sceneManager = networkManager.SceneManager;
+ SceneManager.sceneUnloaded += SceneManager_SceneUnloaded;
+ foreach (var sceneEntry in SceneNameToSceneHandles)
+ {
+ var scenHandleEntries = SceneNameToSceneHandles[sceneEntry.Key];
+ foreach (var sceneHandleEntry in scenHandleEntries)
+ {
+ if (!sceneHandleEntry.Value.IsAssigned)
+ {
+ if (sceneManager.VerifySceneBeforeUnloading == null || sceneManager.VerifySceneBeforeUnloading.Invoke(sceneHandleEntry.Value.Scene))
+ {
+ m_ScenesToUnload.Add(sceneHandleEntry.Value.Scene);
+ }
+ }
+ }
+ }
+ foreach (var sceneToUnload in m_ScenesToUnload)
+ {
+ SceneManager.UnloadSceneAsync(sceneToUnload);
+ }
+ }
+
+ private void SceneManager_SceneUnloaded(Scene scene)
+ {
+ if (SceneNameToSceneHandles.ContainsKey(scene.name))
+ {
+ if (SceneNameToSceneHandles[scene.name].ContainsKey(scene.handle))
+ {
+ SceneNameToSceneHandles[scene.name].Remove(scene.handle);
+ }
+ if (SceneNameToSceneHandles[scene.name].Count == 0)
+ {
+ SceneNameToSceneHandles.Remove(scene.name);
+ }
+ m_ScenesToUnload.Remove(scene);
+ if (m_ScenesToUnload.Count == 0)
+ {
+ SceneManager.sceneUnloaded -= SceneManager_SceneUnloaded;
+ }
+ }
+ }
+
+ ///
+ /// Handles determining if a client should attempt to load a scene during synchronization.
+ ///
+ /// name of the scene to be loaded
+ /// when in client synchronization mode single, this determines if the scene is the primary active scene
+ /// the current client synchronization mode
+ /// instance
+ ///
+ public bool ClientShouldPassThrough(string sceneName, bool isPrimaryScene, LoadSceneMode clientSynchronizationMode, NetworkManager networkManager)
+ {
+ var shouldPassThrough = clientSynchronizationMode == LoadSceneMode.Single ? false : DoesSceneHaveUnassignedEntry(sceneName, networkManager);
+ var activeScene = SceneManager.GetActiveScene();
+
+ // If shouldPassThrough is not yet true and the scene to be loaded is the currently active scene
+ if (!shouldPassThrough && sceneName == activeScene.name)
+ {
+ // In additive mode we always pass through, but in LoadSceneMode.Single we only pass through if the currently active scene
+ // is the primary scene to be loaded
+ if (clientSynchronizationMode == LoadSceneMode.Additive || (isPrimaryScene && clientSynchronizationMode == LoadSceneMode.Single))
+ {
+ // don't try to reload this scene and pass through to post load processing.
+ shouldPassThrough = true;
+ }
+ }
+ return shouldPassThrough;
+ }
+
+ ///
+ /// Handles migrating dynamically spawned NetworkObjects to the DDOL when a scene is unloaded
+ ///
+ /// relative instance
+ /// scene being unloaded
+ public void MoveObjectsFromSceneToDontDestroyOnLoad(ref NetworkManager networkManager, Scene scene)
+ {
+ bool isActiveScene = scene == SceneManager.GetActiveScene();
+ // Create a local copy of the spawned objects list since the spawn manager will adjust the list as objects
+ // are despawned.
+ var localSpawnedObjectsHashSet = new HashSet(networkManager.SpawnManager.SpawnedObjectsList);
+ foreach (var networkObject in localSpawnedObjectsHashSet)
+ {
+ if (networkObject == null || (networkObject != null && networkObject.gameObject.scene.handle != scene.handle))
+ {
+ continue;
+ }
+
+ // Only NetworkObjects marked to not be destroyed with the scene and are not already in the DDOL are preserved
+ if (!networkObject.DestroyWithScene && networkObject.gameObject.scene != networkManager.SceneManager.DontDestroyOnLoadScene)
+ {
+ // Only move dynamically spawned NetworkObjects with no parent as the children will follow
+ if (networkObject.gameObject.transform.parent == null && networkObject.IsSceneObject != null && !networkObject.IsSceneObject.Value)
+ {
+ UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject);
+ }
+ }
+ else if (networkManager.IsServer)
+ {
+ networkObject.Despawn();
+ }
+ else // We are a client, migrate the object into the DDOL temporarily until it receives the destroy command from the server
+ {
+ UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject);
+ }
+ }
+ }
+
+ ///
+ /// Sets the client synchronization mode which impacts whether both the server or client take into consideration scenes loaded before
+ /// starting the .
+ ///
+ ///
+ /// : Does not take preloaded scenes into consideration
+ /// : Does take preloaded scenes into consideration
+ ///
+ /// relative instance
+ /// or
+ public void SetClientSynchronizationMode(ref NetworkManager networkManager, LoadSceneMode mode)
+ {
+ var sceneManager = networkManager.SceneManager;
+ // Don't let client's set this value
+ if (!networkManager.IsServer)
+ {
+ if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
+ {
+ NetworkLog.LogWarning("Clients should not set this value as it is automatically synchronized with the server's setting!");
+ }
+ return;
+ }
+ else // Warn users if they are changing this after there are clients already connected and synchronized
+ if (networkManager.ConnectedClientsIds.Count > (networkManager.IsServer ? 0 : 1) && sceneManager.ClientSynchronizationMode != mode)
+ {
+ if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
+ {
+ NetworkLog.LogWarning("Server is changing client synchronization mode after clients have been synchronized! It is recommended to do this before clients are connected!");
+ }
+ }
+
+ // For additive client synchronization, we take into consideration scenes
+ // already loaded.
+ if (mode == LoadSceneMode.Additive)
+ {
+ for (int i = 0; i < SceneManager.sceneCount; i++)
+ {
+ var scene = SceneManager.GetSceneAt(i);
+
+ // If using scene verification
+ if (sceneManager.VerifySceneBeforeLoading != null)
+ {
+ // Determine if we should take this scene into consideration
+ if (!sceneManager.VerifySceneBeforeLoading.Invoke(scene.buildIndex, scene.name, LoadSceneMode.Additive))
+ {
+ continue;
+ }
+ }
+
+ // If the scene is not already in the ScenesLoaded list, then add it
+ if (!sceneManager.ScenesLoaded.ContainsKey(scene.handle))
+ {
+ sceneManager.ScenesLoaded.Add(scene.handle, scene);
+ }
+ }
+ }
+ // Set the client synchronization mode
+ sceneManager.ClientSynchronizationMode = mode;
+ }
+ }
+}
diff --git a/Runtime/SceneManagement/DefaultSceneManagerHandler.cs.meta b/Runtime/SceneManagement/DefaultSceneManagerHandler.cs.meta
new file mode 100644
index 0000000..a89ce83
--- /dev/null
+++ b/Runtime/SceneManagement/DefaultSceneManagerHandler.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 8c18076bb9734cf4ea7297f85b7729be
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Runtime/SceneManagement/ISceneManagerHandler.cs b/Runtime/SceneManagement/ISceneManagerHandler.cs
index 8b5b7e7..240a5c9 100644
--- a/Runtime/SceneManagement/ISceneManagerHandler.cs
+++ b/Runtime/SceneManagement/ISceneManagerHandler.cs
@@ -1,3 +1,4 @@
+using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
@@ -12,5 +13,24 @@ namespace Unity.Netcode
AsyncOperation LoadSceneAsync(string sceneName, LoadSceneMode loadSceneMode, SceneEventProgress sceneEventProgress);
AsyncOperation UnloadSceneAsync(Scene scene, SceneEventProgress sceneEventProgress);
+
+ void PopulateLoadedScenes(ref Dictionary scenesLoaded, NetworkManager networkManager = null);
+ Scene GetSceneFromLoadedScenes(string sceneName, NetworkManager networkManager = null);
+
+ bool DoesSceneHaveUnassignedEntry(string sceneName, NetworkManager networkManager = null);
+
+ void StopTrackingScene(int handle, string name, NetworkManager networkManager = null);
+
+ void StartTrackingScene(Scene scene, bool assigned, NetworkManager networkManager = null);
+
+ void ClearSceneTracking(NetworkManager networkManager = null);
+
+ void UnloadUnassignedScenes(NetworkManager networkManager = null);
+
+ void MoveObjectsFromSceneToDontDestroyOnLoad(ref NetworkManager networkManager, Scene scene);
+
+ void SetClientSynchronizationMode(ref NetworkManager networkManager, LoadSceneMode mode);
+
+ bool ClientShouldPassThrough(string sceneName, bool isPrimaryScene, LoadSceneMode clientSynchronizationMode, NetworkManager networkManager);
}
}
diff --git a/Runtime/SceneManagement/NetworkSceneManager.cs b/Runtime/SceneManagement/NetworkSceneManager.cs
index b538903..c9731aa 100644
--- a/Runtime/SceneManagement/NetworkSceneManager.cs
+++ b/Runtime/SceneManagement/NetworkSceneManager.cs
@@ -1,5 +1,5 @@
-using System.Collections.Generic;
using System;
+using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.SceneManagement;
@@ -10,16 +10,16 @@ namespace Unity.Netcode
///
/// Used for local notifications of various scene events. The of
/// delegate type uses this class to provide
- /// scene event status.
- /// Note: This is only when is enabled.
- /// *** Do not start new scene events within scene event notification callbacks.
- /// See also:
+ /// scene event status.
+ /// Note: This is only when is enabled.
+ /// *** Do not start new scene events within scene event notification callbacks.
+ /// See also:
///
///
public class SceneEvent
{
///
- /// The returned by
+ /// The returned by
/// This is set for the following s:
///
///
@@ -34,7 +34,7 @@ namespace Unity.Netcode
public SceneEventType SceneEventType;
///
- /// If applicable, this reflects the type of scene loading or unloading that is occurring.
+ /// If applicable, this reflects the type of scene loading or unloading that is occurring.
/// This is set for the following s:
///
///
@@ -48,7 +48,7 @@ namespace Unity.Netcode
public LoadSceneMode LoadSceneMode;
///
- /// This will be set to the scene name that the event pertains to.
+ /// This will be set to the scene name that the event pertains to.
/// This is set for the following s:
///
///
@@ -62,7 +62,7 @@ namespace Unity.Netcode
public string SceneName;
///
- /// When a scene is loaded, the Scene structure is returned.
+ /// When a scene is loaded, the Scene structure is returned.
/// This is set for the following s:
///
///
@@ -71,11 +71,11 @@ namespace Unity.Netcode
public Scene Scene;
///
- /// The client identifier can vary depending upon the following conditions:
+ /// The client identifier can vary depending upon the following conditions:
///
/// - s that always set the
/// to the local client identifier, are initiated (and processed locally) by the
- /// server-host, and sent to all clients to be processed.
+ /// server-host, and sent to all clients to be processed.
///
///
///
@@ -104,7 +104,7 @@ namespace Unity.Netcode
public ulong ClientId;
///
- /// List of clients that completed a loading or unloading event.
+ /// List of clients that completed a loading or unloading event.
/// This is set for the following s:
///
///
@@ -114,7 +114,7 @@ namespace Unity.Netcode
public List ClientsThatCompleted;
///
- /// List of clients that timed out during a loading or unloading event.
+ /// List of clients that timed out during a loading or unloading event.
/// This is set for the following s:
///
///
@@ -143,20 +143,20 @@ namespace Unity.Netcode
private bool m_IsSceneEventActive = false;
///
- /// The delegate callback definition for scene event notifications.
- /// See also:
- ///
+ /// The delegate callback definition for scene event notifications.
+ /// See also:
+ ///
///
///
///
public delegate void SceneEventDelegate(SceneEvent sceneEvent);
///
- /// Subscribe to this event to receive all notifications.
- /// For more details review over and .
- /// Alternate Single Event Type Notification Registration Options
+ /// Subscribe to this event to receive all notifications.
+ /// For more details review over and .
+ /// Alternate Single Event Type Notification Registration Options
/// To receive only a specific event type notification or a limited set of notifications you can alternately subscribe to
- /// each notification type individually via the following events:
+ /// each notification type individually via the following events:
///
/// - Invoked only when a event is being processed
/// - Invoked only when an event is being processed
@@ -167,13 +167,13 @@ namespace Unity.Netcode
/// - Invoked only when an event is being processed
/// - Invoked only when a event is being processed
///
- /// Note: Do not start new scene events within NetworkSceneManager scene event notification callbacks.
+ /// Note: Do not start new scene events within NetworkSceneManager scene event notification callbacks.
///
public event SceneEventDelegate OnSceneEvent;
///
- /// Delegate declaration for the OnLoad event.
- /// See also:
+ /// Delegate declaration for the OnLoad event.
+ /// See also:
/// for more information
///
/// the client that is processing this event (the server will receive all of these events for every client and itself)
@@ -183,8 +183,8 @@ namespace Unity.Netcode
public delegate void OnLoadDelegateHandler(ulong clientId, string sceneName, LoadSceneMode loadSceneMode, AsyncOperation asyncOperation);
///
- /// Delegate declaration for the OnUnload event.
- /// See also:
+ /// Delegate declaration for the OnUnload event.
+ /// See also:
/// for more information
///
/// the client that is processing this event (the server will receive all of these events for every client and itself)
@@ -193,17 +193,17 @@ namespace Unity.Netcode
public delegate void OnUnloadDelegateHandler(ulong clientId, string sceneName, AsyncOperation asyncOperation);
///
- /// Delegate declaration for the OnSynchronize event.
- /// See also:
+ /// Delegate declaration for the OnSynchronize event.
+ /// See also:
/// for more information
///
/// the client that is processing this event (the server will receive all of these events for every client and itself)
public delegate void OnSynchronizeDelegateHandler(ulong clientId);
///
- /// Delegate declaration for the OnLoadEventCompleted and OnUnloadEventCompleted events.
- /// See also:
- ///
+ /// Delegate declaration for the OnLoadEventCompleted and OnUnloadEventCompleted events.
+ /// See also:
+ ///
///
///
/// scene pertaining to this event
@@ -213,8 +213,8 @@ namespace Unity.Netcode
public delegate void OnEventCompletedDelegateHandler(string sceneName, LoadSceneMode loadSceneMode, List clientsCompleted, List clientsTimedOut);
///
- /// Delegate declaration for the OnLoadComplete event.
- /// See also:
+ /// Delegate declaration for the OnLoadComplete event.
+ /// See also:
/// for more information
///
/// the client that is processing this event (the server will receive all of these events for every client and itself)
@@ -223,8 +223,8 @@ namespace Unity.Netcode
public delegate void OnLoadCompleteDelegateHandler(ulong clientId, string sceneName, LoadSceneMode loadSceneMode);
///
- /// Delegate declaration for the OnUnloadComplete event.
- /// See also:
+ /// Delegate declaration for the OnUnloadComplete event.
+ /// See also:
/// for more information
///
/// the client that is processing this event (the server will receive all of these events for every client and itself)
@@ -232,34 +232,34 @@ namespace Unity.Netcode
public delegate void OnUnloadCompleteDelegateHandler(ulong clientId, string sceneName);
///
- /// Delegate declaration for the OnSynchronizeComplete event.
- /// See also:
+ /// Delegate declaration for the OnSynchronizeComplete event.
+ /// See also:
/// for more information
///
/// the client that completed this event
public delegate void OnSynchronizeCompleteDelegateHandler(ulong clientId);
///
- /// Invoked when a event is started by the server.
- /// Note: The server and connected client(s) will always receive this notification.
- /// *** Do not start new scene events within scene event notification callbacks.
+ /// Invoked when a event is started by the server.
+ /// Note: The server and connected client(s) will always receive this notification.
+ /// *** Do not start new scene events within scene event notification callbacks.
///
public event OnLoadDelegateHandler OnLoad;
///
- /// Invoked when a event is started by the server.
- /// Note: The server and connected client(s) will always receive this notification.
- /// *** Do not start new scene events within scene event notification callbacks.
+ /// Invoked when a event is started by the server.
+ /// Note: The server and connected client(s) will always receive this notification.
+ /// *** Do not start new scene events within scene event notification callbacks.
///
public event OnUnloadDelegateHandler OnUnload;
///
/// Invoked when a event is started by the server
/// after a client is approved for connection in order to synchronize the client with the currently loaded
- /// scenes and NetworkObjects. This event signifies the beginning of the synchronization event.
+ /// scenes and NetworkObjects. This event signifies the beginning of the synchronization event.
/// Note: The server and connected client(s) will always receive this notification.
- /// This event is generated on a per newly connected and approved client basis.
- /// *** Do not start new scene events within scene event notification callbacks.
+ /// This event is generated on a per newly connected and approved client basis.
+ /// *** Do not start new scene events within scene event notification callbacks.
///
public event OnSynchronizeDelegateHandler OnSynchronize;
@@ -267,9 +267,9 @@ namespace Unity.Netcode
/// Invoked when a event is generated by the server.
/// This event signifies the end of an existing event as it pertains
/// to all clients connected when the event was started. This event signifies that all clients (and server) have
- /// finished the event.
- /// Note: this is useful to know when all clients have loaded the same scene (single or additive mode)
- /// *** Do not start new scene events within scene event notification callbacks.
+ /// finished the event.
+ /// Note: this is useful to know when all clients have loaded the same scene (single or additive mode)
+ /// *** Do not start new scene events within scene event notification callbacks.
///
public event OnEventCompletedDelegateHandler OnLoadEventCompleted;
@@ -277,35 +277,35 @@ namespace Unity.Netcode
/// Invoked when a event is generated by the server.
/// This event signifies the end of an existing event as it pertains
/// to all clients connected when the event was started. This event signifies that all clients (and server) have
- /// finished the event.
+ /// finished the event.
/// Note: this is useful to know when all clients have unloaded a specific scene. The will
- /// always be for this event.
- /// *** Do not start new scene events within scene event notification callbacks.
+ /// always be for this event.
+ /// *** Do not start new scene events within scene event notification callbacks.
///
public event OnEventCompletedDelegateHandler OnUnloadEventCompleted;
///
- /// Invoked when a event is generated by a client or server.
+ /// Invoked when a event is generated by a client or server.
/// Note: The server receives this message from all clients (including itself).
- /// Each client receives their own notification sent to the server.
- /// *** Do not start new scene events within scene event notification callbacks.
+ /// Each client receives their own notification sent to the server.
+ /// *** Do not start new scene events within scene event notification callbacks.
///
public event OnLoadCompleteDelegateHandler OnLoadComplete;
///
- /// Invoked when a event is generated by a client or server.
+ /// Invoked when a event is generated by a client or server.
/// Note: The server receives this message from all clients (including itself).
- /// Each client receives their own notification sent to the server.
- /// *** Do not start new scene events within scene event notification callbacks.
+ /// Each client receives their own notification sent to the server.
+ /// *** Do not start new scene events within scene event notification callbacks.
///
public event OnUnloadCompleteDelegateHandler OnUnloadComplete;
///
- /// Invoked when a event is generated by a client.
+ /// Invoked when a event is generated by a client.
/// Note: The server receives this message from the client, but will never generate this event for itself.
/// Each client receives their own notification sent to the server. This is useful to know that a client has
/// completed the entire connection sequence, loaded all scenes, and synchronized all NetworkObjects.
- /// *** Do not start new scene events within scene event notification callbacks.
+ /// *** Do not start new scene events within scene event notification callbacks.
///
public event OnSynchronizeCompleteDelegateHandler OnSynchronizeComplete;
@@ -322,37 +322,79 @@ namespace Unity.Netcode
///
/// Delegate handler defined by that is invoked before the
- /// server or client loads a scene during an active netcode game session.
- /// Client Side: In order for clients to be notified of this condition you must assign the delegate handler.
- /// Server Side: will return .
+ /// server or client loads a scene during an active netcode game session.
///
+ ///
+ /// Client Side: In order for clients to be notified of this condition you must assign the delegate handler.
+ /// Server Side: will return .
+ ///
public VerifySceneBeforeLoadingDelegateHandler VerifySceneBeforeLoading;
+ ///
+ /// Delegate declaration for the handler that provides
+ /// an additional level of scene unloading validation to assure the scene being unloaded should
+ /// be unloaded.
+ ///
+ /// The scene to be unloaded
+ /// true (valid) or false (not valid)
+ public delegate bool VerifySceneBeforeUnloadingDelegateHandler(Scene scene);
+
+ ///
+ /// Client Side Only:
+ /// Delegate handler defined by that is only invoked when the client
+ /// is finished synchronizing and when is set to .
+ ///
+ public VerifySceneBeforeUnloadingDelegateHandler VerifySceneBeforeUnloading;
+
+ ///
+ /// When enabled and is , any scenes not synchronized with
+ /// the server will be unloaded unless returns true. This provides more granular control over
+ /// which already loaded client-side scenes not synchronized with the server should be unloaded.
+ ///
+ ///
+ /// If the delegate callback is not set then any scene loaded on the just synchronized client
+ /// will be unloaded.
+ /// One scenario is a synchronized client is disconnected for unexpected reasons and attempts to reconnect to the same network session
+ /// but still has all scenes that were loaded through server synchronization (initially or through scene events). However, during the
+ /// client disconnection period the server unloads one (or more) of the scenes loaded and as such the reconnecting client could still
+ /// have the now unloaded scenes still loaded. Enabling this flag coupled with assignment of the assignment of the
+ /// delegate callback provides you with the ability to keep scenes loaded by the client (i.e. UI etc) while discarding any artifact
+ /// scenes that no longer need to be loaded.
+ ///
+ public bool PostSynchronizationSceneUnloading;
+
+ private bool m_ActiveSceneSynchronizationEnabled;
+ ///
+ /// When enabled, the server or host will synchronize clients with changes to the currently active scene
+ ///
+ public bool ActiveSceneSynchronizationEnabled
+ {
+ get
+ {
+ return m_ActiveSceneSynchronizationEnabled;
+ }
+ set
+ {
+ if (m_ActiveSceneSynchronizationEnabled != value)
+ {
+ m_ActiveSceneSynchronizationEnabled = value;
+ if (m_ActiveSceneSynchronizationEnabled)
+ {
+ SceneManager.activeSceneChanged += SceneManager_ActiveSceneChanged;
+ }
+ else
+ {
+ SceneManager.activeSceneChanged -= SceneManager_ActiveSceneChanged;
+ }
+ }
+ }
+ }
+
///
/// The SceneManagerHandler implementation
///
internal ISceneManagerHandler SceneManagerHandler = new DefaultSceneManagerHandler();
- ///
- /// The default SceneManagerHandler that interfaces between the SceneManager and NetworkSceneManager
- ///
- private class DefaultSceneManagerHandler : ISceneManagerHandler
- {
- public AsyncOperation LoadSceneAsync(string sceneName, LoadSceneMode loadSceneMode, SceneEventProgress sceneEventProgress)
- {
- var operation = SceneManager.LoadSceneAsync(sceneName, loadSceneMode);
- sceneEventProgress.SetAsyncOperation(operation);
- return operation;
- }
-
- public AsyncOperation UnloadSceneAsync(Scene scene, SceneEventProgress sceneEventProgress)
- {
- var operation = SceneManager.UnloadSceneAsync(scene);
- sceneEventProgress.SetAsyncOperation(operation);
- return operation;
- }
- }
-
internal readonly Dictionary SceneEventProgressTracking = new Dictionary();
///
@@ -385,6 +427,77 @@ namespace Unity.Netcode
/// instances with client unique scene instances
///
internal Dictionary ServerSceneHandleToClientSceneHandle = new Dictionary();
+ internal Dictionary ClientSceneHandleToServerSceneHandle = new Dictionary();
+
+ ///
+ /// Add the client to server (and vice versa) scene handle lookup.
+ /// Add the client-side handle to scene entry in the HandleToScene table.
+ /// If it fails (i.e. already added) it returns false.
+ ///
+ internal bool UpdateServerClientSceneHandle(int serverHandle, int clientHandle, Scene localScene)
+ {
+ if (!ServerSceneHandleToClientSceneHandle.ContainsKey(serverHandle))
+ {
+ ServerSceneHandleToClientSceneHandle.Add(serverHandle, clientHandle);
+ }
+ else
+ {
+ return false;
+ }
+
+ if (!ClientSceneHandleToServerSceneHandle.ContainsKey(clientHandle))
+ {
+ ClientSceneHandleToServerSceneHandle.Add(clientHandle, serverHandle);
+ }
+ else
+ {
+ return false;
+ }
+
+ // It is "Ok" if this already has an entry
+ if (!ScenesLoaded.ContainsKey(clientHandle))
+ {
+ ScenesLoaded.Add(clientHandle, localScene);
+ }
+
+ return true;
+ }
+
+ ///
+ /// Removes the client to server (and vice versa) scene handles.
+ /// If it fails (i.e. already removed) it returns false.
+ ///
+ internal bool RemoveServerClientSceneHandle(int serverHandle, int clientHandle)
+ {
+ if (ServerSceneHandleToClientSceneHandle.ContainsKey(serverHandle))
+ {
+ ServerSceneHandleToClientSceneHandle.Remove(serverHandle);
+ }
+ else
+ {
+ return false;
+ }
+
+ if (ClientSceneHandleToServerSceneHandle.ContainsKey(clientHandle))
+ {
+ ClientSceneHandleToServerSceneHandle.Remove(clientHandle);
+ }
+ else
+ {
+ return false;
+ }
+
+ if (ScenesLoaded.ContainsKey(clientHandle))
+ {
+ ScenesLoaded.Remove(clientHandle);
+ }
+ else
+ {
+ return false;
+ }
+
+ return true;
+ }
///
/// Hash to build index lookup table
@@ -413,16 +526,22 @@ namespace Unity.Netcode
private NetworkManager m_NetworkManager { get; }
+ // Keep track of this scene until the NetworkSceneManager is destroyed.
internal Scene DontDestroyOnLoadScene;
///
- /// LoadSceneMode.Single: All currently loaded scenes on the client will be unloaded and
- /// the server's currently active scene will be loaded in single mode on the client
- /// unless it was already loaded.
+ /// This setting changes how clients handle scene loading when initially synchronizing with the server.
+ /// See:
+ ///
+ ///
+ /// LoadSceneMode.Single: All currently loaded scenes on the client will be unloaded and the
+ /// server's currently active scene will be loaded in single mode on the client unless it was already
+ /// loaded.
/// LoadSceneMode.Additive: All currently loaded scenes are left as they are and any newly loaded
/// scenes will be loaded additively. Users need to determine which scenes are valid to load via the
- /// method.
- ///
+ /// and, if is
+ /// set, callback(s).
+ ///
public LoadSceneMode ClientSynchronizationMode { get; internal set; }
///
@@ -435,11 +554,12 @@ namespace Unity.Netcode
///
public void Dispose()
{
+ // Always assure we no longer listen to scene changes when disposed.
+ SceneManager.activeSceneChanged -= SceneManager_ActiveSceneChanged;
SceneUnloadEventHandler.Shutdown();
-
foreach (var keypair in SceneEventDataStore)
{
- if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
+ if (NetworkLog.CurrentLogLevel == LogLevel.Developer)
{
NetworkLog.LogInfo($"{nameof(SceneEventDataStore)} is disposing {nameof(SceneEventData.SceneEventId)} '{keypair.Key}'.");
}
@@ -493,6 +613,11 @@ namespace Unity.Netcode
///
internal void GenerateScenesInBuild()
{
+ // TODO 2023: We could support addressable or asset bundle scenes by
+ // adding a method that would allow users to add scenes to this.
+ // The method would be server-side only and require an additional SceneEventType
+ // that would be used to notify clients of the added scene. This might need
+ // to include information about the addressable or asset bundle (i.e. address to load assets)
HashToBuildIndex.Clear();
BuildIndexToHash.Clear();
for (int i = 0; i < SceneManager.sceneCountInBuildSettings; i++)
@@ -581,18 +706,22 @@ namespace Unity.Netcode
}
///
- /// This will change how clients are initially synchronized.
- /// LoadSceneMode.Single: All currently loaded scenes on the client will be unloaded and
- /// the server's currently active scene will be loaded in single mode on the client
- /// unless it was already loaded.
+ /// This setting changes how clients handle scene loading when initially synchronizing with the server.
+ /// The server or host should set this value as clients will automatically be synchronized with the server (or host) side.
+ ///
+ /// LoadSceneMode.Single: All currently loaded scenes on the client will be unloaded and the
+ /// server's currently active scene will be loaded in single mode on the client unless it was already
+ /// loaded.
/// LoadSceneMode.Additive: All currently loaded scenes are left as they are and any newly loaded
/// scenes will be loaded additively. Users need to determine which scenes are valid to load via the
- /// method.
- ///
+ /// and, if is
+ /// set, callback(s).
+ ///
/// for initial client synchronization
public void SetClientSynchronizationMode(LoadSceneMode mode)
{
- ClientSynchronizationMode = mode;
+ var networkManager = m_NetworkManager;
+ SceneManagerHandler.SetClientSynchronizationMode(ref networkManager, mode);
}
///
@@ -605,13 +734,46 @@ namespace Unity.Netcode
m_NetworkManager = networkManager;
SceneEventDataStore = new Dictionary();
+ // Generates the scene name to hash value
GenerateScenesInBuild();
// Since NetworkManager is now always migrated to the DDOL we will use this to get the DDOL scene
DontDestroyOnLoadScene = networkManager.gameObject.scene;
- ServerSceneHandleToClientSceneHandle.Add(DontDestroyOnLoadScene.handle, DontDestroyOnLoadScene.handle);
- ScenesLoaded.Add(DontDestroyOnLoadScene.handle, DontDestroyOnLoadScene);
+ // Add to the server to client scene handle table
+ UpdateServerClientSceneHandle(DontDestroyOnLoadScene.handle, DontDestroyOnLoadScene.handle, DontDestroyOnLoadScene);
+ }
+
+ ///
+ /// Synchronizes clients when the currently active scene is changed
+ ///
+ private void SceneManager_ActiveSceneChanged(Scene current, Scene next)
+ {
+ // If no clients are connected, then don't worry about notifications
+ if (!(m_NetworkManager.ConnectedClientsIds.Count > (m_NetworkManager.IsHost ? 1 : 0)))
+ {
+ return;
+ }
+
+ // Don't notify if a scene event is in progress
+ foreach (var sceneEventEntry in SceneEventProgressTracking)
+ {
+ if (!sceneEventEntry.Value.HasTimedOut() && sceneEventEntry.Value.Status == SceneEventProgressStatus.Started)
+ {
+ return;
+ }
+ }
+
+ // If the scene's build index is in the hash table
+ if (BuildIndexToHash.ContainsKey(next.buildIndex))
+ {
+ // Notify clients of the change in active scene
+ var sceneEvent = BeginSceneEvent();
+ sceneEvent.SceneEventType = SceneEventType.ActiveSceneChanged;
+ sceneEvent.ActiveSceneHash = BuildIndexToHash[next.buildIndex];
+ SendSceneEventData(sceneEvent.SceneEventId, m_NetworkManager.ConnectedClientsIds.Where(c => c != NetworkManager.ServerClientId).ToArray());
+ EndSceneEvent(sceneEvent.SceneEventId);
+ }
}
///
@@ -629,7 +791,7 @@ namespace Unity.Netcode
var sceneIndex = SceneUtility.GetBuildIndexByScenePath(sceneName);
if (VerifySceneBeforeLoading != null)
{
- validated = VerifySceneBeforeLoading.Invoke((int)sceneIndex, sceneName, loadSceneMode);
+ validated = VerifySceneBeforeLoading.Invoke(sceneIndex, sceneName, loadSceneMode);
}
if (!validated && !m_DisableValidationWarningMessages)
{
@@ -675,11 +837,11 @@ namespace Unity.Netcode
if (!ScenesLoaded.ContainsKey(sceneLoaded.handle))
{
ScenesLoaded.Add(sceneLoaded.handle, sceneLoaded);
+ SceneManagerHandler.StartTrackingScene(sceneLoaded, true, m_NetworkManager);
return sceneLoaded;
}
}
}
-
throw new Exception($"Failed to find any loaded scene named {sceneName}!");
}
}
@@ -788,7 +950,7 @@ namespace Unity.Netcode
///
/// the scene to be unloaded
///
- private SceneEventProgress ValidateSceneEventUnLoading(Scene scene)
+ private SceneEventProgress ValidateSceneEventUnloading(Scene scene)
{
if (!m_NetworkManager.IsServer)
{
@@ -798,9 +960,9 @@ namespace Unity.Netcode
if (!m_NetworkManager.NetworkConfig.EnableSceneManagement)
{
//Log message about enabling SceneManagement
- throw new Exception($"{nameof(NetworkConfig.EnableSceneManagement)} flag is not enabled in the {nameof(NetworkManager)}'s {nameof(NetworkConfig)}. " +
- $"Please set {nameof(NetworkConfig.EnableSceneManagement)} flag to true before calling " +
- $"{nameof(NetworkSceneManager.LoadScene)} or {nameof(NetworkSceneManager.UnloadScene)}.");
+ throw new Exception(
+ $"{nameof(NetworkConfig.EnableSceneManagement)} flag is not enabled in the {nameof(NetworkManager)}'s {nameof(NetworkConfig)}. " +
+ $"Please set {nameof(NetworkConfig.EnableSceneManagement)} flag to true before calling {nameof(LoadScene)} or {nameof(UnloadScene)}.");
}
if (!scene.isLoaded)
@@ -826,9 +988,9 @@ namespace Unity.Netcode
if (!m_NetworkManager.NetworkConfig.EnableSceneManagement)
{
//Log message about enabling SceneManagement
- throw new Exception($"{nameof(NetworkConfig.EnableSceneManagement)} flag is not enabled in the {nameof(NetworkManager)}'s {nameof(NetworkConfig)}. " +
- $"Please set {nameof(NetworkConfig.EnableSceneManagement)} flag to true before calling " +
- $"{nameof(NetworkSceneManager.LoadScene)} or {nameof(NetworkSceneManager.UnloadScene)}.");
+ throw new Exception(
+ $"{nameof(NetworkConfig.EnableSceneManagement)} flag is not enabled in the {nameof(NetworkManager)}'s {nameof(NetworkConfig)}. " +
+ $"Please set {nameof(NetworkConfig.EnableSceneManagement)} flag to true before calling {nameof(LoadScene)} or {nameof(UnloadScene)}.");
}
return ValidateSceneEvent(sceneName);
@@ -939,7 +1101,7 @@ namespace Unity.Netcode
return SceneEventProgressStatus.SceneNotLoaded;
}
- var sceneEventProgress = ValidateSceneEventUnLoading(scene);
+ var sceneEventProgress = ValidateSceneEventUnloading(scene);
if (sceneEventProgress.Status != SceneEventProgressStatus.Started)
{
return sceneEventProgress.Status;
@@ -950,6 +1112,13 @@ namespace Unity.Netcode
Debug.LogError($"{nameof(UnloadScene)} internal error! {sceneName} with handle {scene.handle} is not within the internal scenes loaded dictionary!");
return SceneEventProgressStatus.InternalNetcodeError;
}
+
+ // Any NetworkObjects marked to not be destroyed with a scene and reside within the scene about to be unloaded
+ // should be migrated temporarily into the DDOL, once the scene is unloaded they will be migrated into the
+ // currently active scene.
+ var networkManager = m_NetworkManager;
+ SceneManagerHandler.MoveObjectsFromSceneToDontDestroyOnLoad(ref networkManager, scene);
+
var sceneEventData = BeginSceneEvent();
sceneEventData.SceneEventProgressId = sceneEventProgress.Guid;
sceneEventData.SceneEventType = SceneEventType.Unload;
@@ -964,7 +1133,6 @@ namespace Unity.Netcode
sceneEventProgress.SceneEventId = sceneEventData.SceneEventId;
sceneEventProgress.OnSceneEventCompleted = OnSceneUnloaded;
var sceneUnload = SceneManagerHandler.UnloadSceneAsync(scene, sceneEventProgress);
-
// Notify local server that a scene is going to be unloaded
OnSceneEvent?.Invoke(new SceneEvent()
{
@@ -1006,16 +1174,30 @@ namespace Unity.Netcode
throw new Exception($"Client failed to unload scene {sceneName} " +
$"because the client scene handle {sceneHandle} was not found in ScenesLoaded!");
}
- m_IsSceneEventActive = true;
- var sceneEventProgress = new SceneEventProgress(m_NetworkManager);
- sceneEventProgress.SceneEventId = sceneEventData.SceneEventId;
- sceneEventProgress.OnSceneEventCompleted = OnSceneUnloaded;
- var sceneUnload = SceneManagerHandler.UnloadSceneAsync(ScenesLoaded[sceneHandle], sceneEventProgress);
- ScenesLoaded.Remove(sceneHandle);
+ var scene = ScenesLoaded[sceneHandle];
+ // Any NetworkObjects marked to not be destroyed with a scene and reside within the scene about to be unloaded
+ // should be migrated temporarily into the DDOL, once the scene is unloaded they will be migrated into the
+ // currently active scene.
+ var networkManager = m_NetworkManager;
+ SceneManagerHandler.MoveObjectsFromSceneToDontDestroyOnLoad(ref networkManager, scene);
+
+ m_IsSceneEventActive = true;
+ var sceneEventProgress = new SceneEventProgress(m_NetworkManager)
+ {
+ SceneEventId = sceneEventData.SceneEventId,
+ OnSceneEventCompleted = OnSceneUnloaded
+ };
+ var sceneUnload = SceneManagerHandler.UnloadSceneAsync(scene, sceneEventProgress);
+
+ SceneManagerHandler.StopTrackingScene(sceneHandle, sceneName, m_NetworkManager);
// Remove our server to scene handle lookup
- ServerSceneHandleToClientSceneHandle.Remove(sceneEventData.SceneHandle);
+ if (!RemoveServerClientSceneHandle(sceneEventData.SceneHandle, sceneHandle))
+ {
+ // If the exact same handle exists then there are problems with using handles
+ throw new Exception($"Failed to remove server scene handle ({sceneEventData.SceneHandle}) or client scene handle({sceneHandle})! Happened during scene unload for {sceneName}.");
+ }
// Notify the local client that a scene is going to be unloaded
OnSceneEvent?.Invoke(new SceneEvent()
@@ -1042,6 +1224,9 @@ namespace Unity.Netcode
return;
}
+ // Migrate the NetworkObjects marked to not be destroyed with the scene into the currently active scene
+ MoveObjectsFromDontDestroyOnLoadToScene(SceneManager.GetActiveScene());
+
var sceneEventData = SceneEventDataStore[sceneEventId];
// First thing we do, if we are a server, is to send the unload scene event.
if (m_NetworkManager.IsServer)
@@ -1103,15 +1288,18 @@ namespace Unity.Netcode
// Validate the scene as well as ignore the DDOL (which will have a negative buildIndex)
if (currentActiveScene.name != keyHandleEntry.Value.name && keyHandleEntry.Value.buildIndex >= 0)
{
- var sceneEventProgress = new SceneEventProgress(m_NetworkManager);
- sceneEventProgress.SceneEventId = sceneEventId;
- sceneEventProgress.OnSceneEventCompleted = EmptySceneUnloadedOperation;
+ var sceneEventProgress = new SceneEventProgress(m_NetworkManager)
+ {
+ SceneEventId = sceneEventId,
+ OnSceneEventCompleted = EmptySceneUnloadedOperation
+ };
var sceneUnload = SceneManagerHandler.UnloadSceneAsync(keyHandleEntry.Value, sceneEventProgress);
SceneUnloadEventHandler.RegisterScene(this, keyHandleEntry.Value, LoadSceneMode.Additive, sceneUnload);
}
}
// clear out our scenes loaded list
ScenesLoaded.Clear();
+ SceneManagerHandler.ClearSceneTracking(m_NetworkManager);
}
///
@@ -1339,9 +1527,11 @@ namespace Unity.Netcode
SceneUnloadEventHandler.RegisterScene(this, SceneManager.GetActiveScene(), LoadSceneMode.Single);
}
- var sceneEventProgress = new SceneEventProgress(m_NetworkManager);
- sceneEventProgress.SceneEventId = sceneEventId;
- sceneEventProgress.OnSceneEventCompleted = OnSceneLoaded;
+ var sceneEventProgress = new SceneEventProgress(m_NetworkManager)
+ {
+ SceneEventId = sceneEventId,
+ OnSceneEventCompleted = OnSceneLoaded
+ };
var sceneLoad = SceneManagerHandler.LoadSceneAsync(sceneName, sceneEventData.LoadSceneMode, sceneEventProgress);
OnSceneEvent?.Invoke(new SceneEvent()
@@ -1403,11 +1593,7 @@ namespace Unity.Netcode
else
{
// For the client, we make a server scene handle to client scene handle look up table
- if (!ServerSceneHandleToClientSceneHandle.ContainsKey(sceneEventData.SceneHandle))
- {
- ServerSceneHandleToClientSceneHandle.Add(sceneEventData.SceneHandle, nextScene.handle);
- }
- else
+ if (!UpdateServerClientSceneHandle(sceneEventData.SceneHandle, nextScene.handle, nextScene))
{
// If the exact same handle exists then there are problems with using handles
throw new Exception($"Server Scene Handle ({sceneEventData.SceneHandle}) already exist! Happened during scene load of {nextScene.name} with Client Handle ({nextScene.handle})");
@@ -1530,12 +1716,16 @@ namespace Unity.Netcode
m_NetworkManager.SpawnManager.UpdateObservedNetworkObjects(clientId);
var sceneEventData = BeginSceneEvent();
-
+ sceneEventData.ClientSynchronizationMode = ClientSynchronizationMode;
sceneEventData.InitializeForSynch();
sceneEventData.TargetClientId = clientId;
sceneEventData.LoadSceneMode = ClientSynchronizationMode;
var activeScene = SceneManager.GetActiveScene();
sceneEventData.SceneEventType = SceneEventType.Synchronize;
+ if (BuildIndexToHash.ContainsKey(activeScene.buildIndex))
+ {
+ sceneEventData.ActiveSceneHash = BuildIndexToHash[activeScene.buildIndex];
+ }
// Organize how (and when) we serialize our NetworkObjects
for (int i = 0; i < SceneManager.sceneCount; i++)
@@ -1549,6 +1739,11 @@ namespace Unity.Netcode
continue;
}
+ if (scene == DontDestroyOnLoadScene)
+ {
+ continue;
+ }
+
var sceneHash = SceneHashFromNameOrPath(scene.path);
// This would depend upon whether we are additive or not
@@ -1620,9 +1815,6 @@ namespace Unity.Netcode
});
OnSynchronize?.Invoke(m_NetworkManager.LocalClientId);
-
- // Clear the in-scene placed NetworkObjects when we load the first scene in our synchronization process
- ScenePlacedObjects.Clear();
}
// Always check to see if the scene needs to be validated
@@ -1636,23 +1828,23 @@ namespace Unity.Netcode
return;
}
- var shouldPassThrough = false;
var sceneLoad = (AsyncOperation)null;
- // Check to see if the client already has loaded the scene to be loaded
- if (sceneName == activeScene.name)
- {
- // If the client is already in the same scene, then pass through and
- // don't try to reload it.
- shouldPassThrough = true;
- }
+ // Determines if the client has the scene to be loaded already loaded, if so will return true and the client will skip loading this scene
+ // For ClientSynchronizationMode LoadSceneMode.Single, we pass in whether the scene being loaded is the first/primary active scene and if it is already loaded
+ // it should pass through to post load processing (ClientLoadedSynchronization).
+ // For ClientSynchronizationMode LoadSceneMode.Additive, if the scene is already loaded or the active scene is the scene to be loaded (does not require it to
+ // be the initial primary scene) then go ahead and pass through to post load processing (ClientLoadedSynchronization).
+ var shouldPassThrough = SceneManagerHandler.ClientShouldPassThrough(sceneName, sceneHash == sceneEventData.SceneHash, ClientSynchronizationMode, m_NetworkManager);
if (!shouldPassThrough)
{
// If not, then load the scene
- var sceneEventProgress = new SceneEventProgress(m_NetworkManager);
- sceneEventProgress.SceneEventId = sceneEventId;
- sceneEventProgress.OnSceneEventCompleted = ClientLoadedSynchronization;
+ var sceneEventProgress = new SceneEventProgress(m_NetworkManager)
+ {
+ SceneEventId = sceneEventId,
+ OnSceneEventCompleted = ClientLoadedSynchronization
+ };
sceneLoad = SceneManagerHandler.LoadSceneAsync(sceneName, loadSceneMode, sceneEventProgress);
// Notify local client that a scene load has begun
@@ -1683,7 +1875,11 @@ namespace Unity.Netcode
{
var sceneEventData = SceneEventDataStore[sceneEventId];
var sceneName = SceneNameFromHash(sceneEventData.ClientSceneHash);
- var nextScene = GetAndAddNewlyLoadedSceneByName(sceneName);
+ var nextScene = SceneManagerHandler.GetSceneFromLoadedScenes(sceneName, m_NetworkManager);
+ if (!nextScene.IsValid())
+ {
+ nextScene = GetAndAddNewlyLoadedSceneByName(sceneName);
+ }
if (!nextScene.isLoaded || !nextScene.IsValid())
{
@@ -1698,11 +1894,8 @@ namespace Unity.Netcode
SceneManager.SetActiveScene(nextScene);
}
- if (!ServerSceneHandleToClientSceneHandle.ContainsKey(sceneEventData.NetworkSceneHandle))
- {
- ServerSceneHandleToClientSceneHandle.Add(sceneEventData.NetworkSceneHandle, nextScene.handle);
- }
- else
+ // For the client, we make a server scene handle to client scene handle look up table
+ if (!UpdateServerClientSceneHandle(sceneEventData.NetworkSceneHandle, nextScene.handle, nextScene))
{
// If the exact same handle exists then there are problems with using handles
throw new Exception($"Server Scene Handle ({sceneEventData.SceneHandle}) already exist! Happened during scene load of {nextScene.name} with Client Handle ({nextScene.handle})");
@@ -1744,6 +1937,48 @@ namespace Unity.Netcode
HandleClientSceneEvent(sceneEventId);
}
+ ///
+ /// Makes sure that client-side instantiated dynamically spawned NetworkObjects are migrated
+ /// into the same scene (if not already) as they are on the server-side during the initial
+ /// client connection synchronization process.
+ ///
+ private void SynchronizeNetworkObjectScene()
+ {
+ foreach (var networkObject in m_NetworkManager.SpawnManager.SpawnedObjectsList)
+ {
+ // This is only done for dynamically spawned NetworkObjects
+ // Theoretically, a server could have NetworkObjects in a server-side only scene, if the client doesn't have that scene loaded
+ // then skip it (it will reside in the currently active scene in this scenario on the client-side)
+ if (networkObject.IsSceneObject.Value == false && ServerSceneHandleToClientSceneHandle.ContainsKey(networkObject.NetworkSceneHandle))
+ {
+ networkObject.SceneOriginHandle = ServerSceneHandleToClientSceneHandle[networkObject.NetworkSceneHandle];
+
+
+
+ // If the NetworkObject does not have a parent and is not in the same scene as it is on the server side, then find the right scene
+ // and move it to that scene.
+ if (networkObject.gameObject.scene.handle != networkObject.SceneOriginHandle && networkObject.transform.parent == null)
+ {
+ if (ScenesLoaded.ContainsKey(networkObject.SceneOriginHandle))
+ {
+ var scene = ScenesLoaded[networkObject.SceneOriginHandle];
+ if (scene == DontDestroyOnLoadScene)
+ {
+ Debug.Log($"{networkObject.gameObject.name} migrating into DDOL!");
+ }
+
+ SceneManager.MoveGameObjectToScene(networkObject.gameObject, scene);
+ }
+ else if (m_NetworkManager.LogLevel <= LogLevel.Normal)
+ {
+ NetworkLog.LogWarningServer($"[Client-{m_NetworkManager.LocalClientId}][{networkObject.gameObject.name}] Server - " +
+ $"client scene mismatch detected! Client-side has no scene loaded with handle ({networkObject.SceneOriginHandle})!");
+ }
+ }
+ }
+ }
+ }
+
///
/// Client Side:
/// Handles incoming Scene_Event messages for clients
@@ -1754,6 +1989,23 @@ namespace Unity.Netcode
var sceneEventData = SceneEventDataStore[sceneEventId];
switch (sceneEventData.SceneEventType)
{
+ case SceneEventType.ActiveSceneChanged:
+ {
+ if (HashToBuildIndex.ContainsKey(sceneEventData.ActiveSceneHash))
+ {
+ var scene = SceneManager.GetSceneByBuildIndex(HashToBuildIndex[sceneEventData.ActiveSceneHash]);
+ if (scene.isLoaded)
+ {
+ SceneManager.SetActiveScene(scene);
+ }
+ }
+ break;
+ }
+ case SceneEventType.ObjectSceneChanged:
+ {
+ MigrateNetworkObjectsIntoScenes();
+ break;
+ }
case SceneEventType.Load:
{
OnClientSceneLoadingEvent(sceneEventId);
@@ -1777,6 +2029,19 @@ namespace Unity.Netcode
// Synchronize the NetworkObjects for this scene
sceneEventData.SynchronizeSceneNetworkObjects(m_NetworkManager);
+ // If needed, set the currently active scene
+ if (HashToBuildIndex.ContainsKey(sceneEventData.ActiveSceneHash))
+ {
+ var targetActiveScene = SceneManager.GetSceneByBuildIndex(HashToBuildIndex[sceneEventData.ActiveSceneHash]);
+ if (targetActiveScene.isLoaded && targetActiveScene.handle != SceneManager.GetActiveScene().handle)
+ {
+ SceneManager.SetActiveScene(targetActiveScene);
+ }
+ }
+
+ // If needed, migrate dynamically spawned NetworkObjects to the same scene as on the server side
+ SynchronizeNetworkObjectScene();
+
sceneEventData.SceneEventType = SceneEventType.SynchronizeComplete;
SendSceneEventData(sceneEventId, new ulong[] { NetworkManager.ServerClientId });
@@ -1793,6 +2058,19 @@ namespace Unity.Netcode
ClientId = m_NetworkManager.LocalClientId, // Client sent this to the server
});
+ // Process any SceneEventType.ObjectSceneChanged messages that
+ // were deferred while synchronizing and migrate the associated
+ // NetworkObjects to their newly assigned scenes.
+ sceneEventData.ProcessDeferredObjectSceneChangedEvents();
+
+ // Only if PostSynchronizationSceneUnloading is set and we are running in client synchronization
+ // mode additive do we unload any remaining scene that was not synchronized (otherwise any loaded
+ // scene not synchronized by the server will remain loaded)
+ if (PostSynchronizationSceneUnloading && ClientSynchronizationMode == LoadSceneMode.Additive)
+ {
+ SceneManagerHandler.UnloadUnassignedScenes(m_NetworkManager);
+ }
+
OnSynchronizeComplete?.Invoke(m_NetworkManager.LocalClientId);
EndSceneEvent(sceneEventId);
@@ -1907,9 +2185,12 @@ namespace Unity.Netcode
OnSynchronizeComplete?.Invoke(clientId);
- // We now can call the client connected callback on the server at this time
- // This assures the client is fully synchronized with all loaded scenes and
- // NetworkObjects
+ // At this time the client is fully synchronized with all loaded scenes and
+ // NetworkObjects and should be considered "fully connected". Send the
+ // notification that the client is connected.
+ // TODO 2023: We should have a better name for this or have multiple states the
+ // client progresses through (the name and associated legacy behavior/expected state
+ // of the client was persisted since MLAPI)
m_NetworkManager.InvokeOnClientConnectedCallback(clientId);
// Check to see if the client needs to resynchronize and before sending the message make sure the client is still connected to avoid
@@ -1955,6 +2236,23 @@ namespace Unity.Netcode
if (sceneEventData.IsSceneEventClientSide())
{
+ // If the client is being synchronized for the first time do some initialization
+ if (sceneEventData.SceneEventType == SceneEventType.Synchronize)
+ {
+ ScenePlacedObjects.Clear();
+ // Set the server's configured client synchronization mode on the client side
+ ClientSynchronizationMode = sceneEventData.ClientSynchronizationMode;
+
+ // Only if ClientSynchronizationMode is Additive and the client receives a synchronize scene event
+ if (ClientSynchronizationMode == LoadSceneMode.Additive)
+ {
+ // Check for scenes already loaded and create a table of scenes already loaded (SceneEntries) that will be
+ // used if the server is synchronizing the same scenes (i.e. if a matching scene is already loaded on the
+ // client side, then that scene will be used as opposed to loading another scene). This allows for clients
+ // to reconnect to a network session without having to unload all of the scenes and reload all of the scenes.
+ SceneManagerHandler.PopulateLoadedScenes(ref ScenesLoaded, m_NetworkManager);
+ }
+ }
HandleClientSceneEvent(sceneEventData.SceneEventId);
}
else
@@ -1964,7 +2262,7 @@ namespace Unity.Netcode
}
else
{
- Debug.LogError($"{nameof(NetworkSceneManager.HandleSceneEvent)} was invoked but {nameof(NetworkManager)} reference was null!");
+ Debug.LogError($"{nameof(HandleSceneEvent)} was invoked but {nameof(NetworkManager)} reference was null!");
}
}
@@ -1974,26 +2272,28 @@ namespace Unity.Netcode
///
internal void MoveObjectsToDontDestroyOnLoad()
{
- // Move ALL NetworkObjects marked to persist scene transitions into the DDOL scene
- var objectsToKeep = new HashSet(m_NetworkManager.SpawnManager.SpawnedObjectsList);
- foreach (var sobj in objectsToKeep)
+ // Create a local copy of the spawned objects list since the spawn manager will adjust the list as objects
+ // are despawned.
+ var localSpawnedObjectsHashSet = new HashSet(m_NetworkManager.SpawnManager.SpawnedObjectsList);
+ foreach (var networkObject in localSpawnedObjectsHashSet)
{
- if (sobj == null)
+ if (networkObject == null || (networkObject != null && networkObject.gameObject.scene == DontDestroyOnLoadScene))
{
continue;
}
- if (!sobj.DestroyWithScene || sobj.gameObject.scene == DontDestroyOnLoadScene)
+ // Only NetworkObjects marked to not be destroyed with the scene
+ if (!networkObject.DestroyWithScene)
{
- // Only move dynamically spawned network objects with no parent as child objects will follow
- if (sobj.gameObject.transform.parent == null && sobj.IsSceneObject != null && !sobj.IsSceneObject.Value)
+ // Only move dynamically spawned NetworkObjects with no parent as the children will follow
+ if (networkObject.gameObject.transform.parent == null && networkObject.IsSceneObject != null && !networkObject.IsSceneObject.Value)
{
- UnityEngine.Object.DontDestroyOnLoad(sobj.gameObject);
+ UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject);
}
}
else if (m_NetworkManager.IsServer)
{
- sobj.Despawn();
+ networkObject.Despawn();
}
}
}
@@ -2028,9 +2328,10 @@ namespace Unity.Netcode
foreach (var networkObjectInstance in networkObjects)
{
var globalObjectIdHash = networkObjectInstance.GlobalObjectIdHash;
- var sceneHandle = networkObjectInstance.GetSceneOriginHandle();
+ var sceneHandle = networkObjectInstance.gameObject.scene.handle;
// We check to make sure the NetworkManager instance is the same one to be "NetcodeIntegrationTestHelpers" compatible and filter the list on a per scene basis (for additive scenes)
- if (networkObjectInstance.IsSceneObject != false && networkObjectInstance.NetworkManager == m_NetworkManager && sceneHandle == sceneToFilterBy.handle)
+ if (networkObjectInstance.IsSceneObject != false && (networkObjectInstance.NetworkManager == m_NetworkManager ||
+ networkObjectInstance.NetworkManagerOwner == null) && sceneHandle == sceneToFilterBy.handle)
{
if (!ScenePlacedObjects.ContainsKey(globalObjectIdHash))
{
@@ -2057,26 +2358,139 @@ namespace Unity.Netcode
/// scene to move the NetworkObjects to
internal void MoveObjectsFromDontDestroyOnLoadToScene(Scene scene)
{
- // Move ALL NetworkObjects to the temp scene
- var objectsToKeep = m_NetworkManager.SpawnManager.SpawnedObjectsList;
-
- foreach (var sobj in objectsToKeep)
+ foreach (var networkObject in m_NetworkManager.SpawnManager.SpawnedObjectsList)
{
- if (sobj == null)
+ if (networkObject == null)
{
continue;
}
// If it is in the DDOL then
- if (sobj.gameObject.scene == DontDestroyOnLoadScene)
+ if (networkObject.gameObject.scene == DontDestroyOnLoadScene && !networkObject.DestroyWithScene)
{
// only move dynamically spawned network objects, with no parent as child objects will follow,
// back into the currently active scene
- if (sobj.gameObject.transform.parent == null && sobj.IsSceneObject != null && !sobj.IsSceneObject.Value)
+ if (networkObject.gameObject.transform.parent == null && networkObject.IsSceneObject != null && !networkObject.IsSceneObject.Value)
{
- SceneManager.MoveGameObjectToScene(sobj.gameObject, scene);
+ SceneManager.MoveGameObjectToScene(networkObject.gameObject, scene);
}
}
}
}
+
+ ///
+ /// Holds a list of scene handles (server-side relative) and NetworkObjects migrated into it
+ /// during the current frame.
+ ///
+ internal Dictionary> ObjectsMigratedIntoNewScene = new Dictionary>();
+
+ ///
+ /// Handles notifying clients when a NetworkObject has been migrated into a new scene
+ ///
+ internal void NotifyNetworkObjectSceneChanged(NetworkObject networkObject)
+ {
+ // Really, this should never happen but in case it does
+ if (!m_NetworkManager.IsServer)
+ {
+ if (m_NetworkManager.LogLevel == LogLevel.Developer)
+ {
+ NetworkLog.LogErrorServer("[Please Report This Error][NotifyNetworkObjectSceneChanged] A client is trying to notify of an object's scene change!");
+ }
+ return;
+ }
+
+ // Ignore in-scene placed NetworkObjects
+ if (networkObject.IsSceneObject != false)
+ {
+ // Really, this should ever happen but in case it does
+ if (m_NetworkManager.LogLevel == LogLevel.Developer)
+ {
+ NetworkLog.LogErrorServer("[Please Report This Error][NotifyNetworkObjectSceneChanged] Trying to notify in-scene placed object scene change!");
+ }
+ return;
+ }
+
+ // Ignore if the scene is the currently active scene and the NetworkObject is auto synchronizing/migrating
+ // to the currently active scene.
+ if (networkObject.gameObject.scene == SceneManager.GetActiveScene() && networkObject.ActiveSceneSynchronization)
+ {
+ return;
+ }
+
+ // Don't notify if a scene event is in progress
+ // Note: This does not apply to SceneEventType.Synchronize since synchronization isn't a global connected client event.
+ foreach (var sceneEventEntry in SceneEventProgressTracking)
+ {
+ if (!sceneEventEntry.Value.HasTimedOut() && sceneEventEntry.Value.Status == SceneEventProgressStatus.Started)
+ {
+ return;
+ }
+ }
+
+ // Otherwise, add the NetworkObject into the list of NetworkObjects who's scene has changed
+ if (!ObjectsMigratedIntoNewScene.ContainsKey(networkObject.gameObject.scene.handle))
+ {
+ ObjectsMigratedIntoNewScene.Add(networkObject.gameObject.scene.handle, new List());
+ }
+ ObjectsMigratedIntoNewScene[networkObject.gameObject.scene.handle].Add(networkObject);
+ }
+
+ ///
+ /// Invoked by clients when processing a event
+ /// or invoked by when a client finishes
+ /// synchronization.
+ ///
+ internal void MigrateNetworkObjectsIntoScenes()
+ {
+ try
+ {
+ foreach (var sceneEntry in ObjectsMigratedIntoNewScene)
+ {
+ if (ServerSceneHandleToClientSceneHandle.ContainsKey(sceneEntry.Key))
+ {
+ var clientSceneHandle = ServerSceneHandleToClientSceneHandle[sceneEntry.Key];
+ if (ScenesLoaded.ContainsKey(ServerSceneHandleToClientSceneHandle[sceneEntry.Key]))
+ {
+ var scene = ScenesLoaded[clientSceneHandle];
+ foreach (var networkObject in sceneEntry.Value)
+ {
+ SceneManager.MoveGameObjectToScene(networkObject.gameObject, scene);
+ }
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ NetworkLog.LogErrorServer($"{ex.Message}\n Stack Trace:\n {ex.StackTrace}");
+ }
+
+ // Clear out the list once complete
+ ObjectsMigratedIntoNewScene.Clear();
+ }
+
+ ///
+ /// Should be invoked during PostLateUpdate just prior to the
+ /// MessagingSystem processes its outbound message queue.
+ ///
+ internal void CheckForAndSendNetworkObjectSceneChanged()
+ {
+ // Early exit if not the server or there is nothing pending
+ if (!m_NetworkManager.IsServer || ObjectsMigratedIntoNewScene.Count == 0)
+ {
+ return;
+ }
+ var sceneEvent = BeginSceneEvent();
+ sceneEvent.SceneEventType = SceneEventType.ObjectSceneChanged;
+ SendSceneEventData(sceneEvent.SceneEventId, m_NetworkManager.ConnectedClientsIds.Where(c => c != NetworkManager.ServerClientId).ToArray());
+ EndSceneEvent(sceneEvent.SceneEventId);
+ }
+
+ // Used to handle client-side scene migration messages received while
+ // a client is synchronizing
+ internal struct DeferredObjectsMovedEvent
+ {
+ internal Dictionary> ObjectsMigratedTable;
+ }
+ internal List DeferredObjectsMovedEvents = new List();
}
}
diff --git a/Runtime/SceneManagement/SceneEventData.cs b/Runtime/SceneManagement/SceneEventData.cs
index f4c9927..351e49d 100644
--- a/Runtime/SceneManagement/SceneEventData.cs
+++ b/Runtime/SceneManagement/SceneEventData.cs
@@ -1,5 +1,5 @@
-using System.Collections.Generic;
using System;
+using System.Collections.Generic;
using System.Linq;
using Unity.Collections;
using UnityEngine.SceneManagement;
@@ -80,6 +80,16 @@ namespace Unity.Netcode
/// Event Notification: Both server and client receive a local notification.
///
SynchronizeComplete,
+ ///
+ /// Synchronizes clients when the active scene has changed
+ /// See:
+ ///
+ ActiveSceneChanged,
+ ///
+ /// Synchronizes clients when one or more NetworkObjects are migrated into a new scene
+ /// See:
+ ///
+ ObjectSceneChanged,
}
///
@@ -94,7 +104,7 @@ namespace Unity.Netcode
internal ForceNetworkSerializeByMemcpy SceneEventProgressId;
internal uint SceneEventId;
-
+ internal uint ActiveSceneHash;
internal uint SceneHash;
internal int SceneHandle;
@@ -139,6 +149,8 @@ namespace Unity.Netcode
internal Queue ScenesToSynchronize;
internal Queue SceneHandlesToSynchronize;
+ internal LoadSceneMode ClientSynchronizationMode;
+
///
/// Server Side:
@@ -315,6 +327,8 @@ namespace Unity.Netcode
case SceneEventType.ReSynchronize:
case SceneEventType.LoadEventCompleted:
case SceneEventType.UnloadEventCompleted:
+ case SceneEventType.ActiveSceneChanged:
+ case SceneEventType.ObjectSceneChanged:
{
return true;
}
@@ -384,6 +398,18 @@ namespace Unity.Netcode
// Write the scene event type
writer.WriteValueSafe(SceneEventType);
+ if (SceneEventType == SceneEventType.ActiveSceneChanged)
+ {
+ writer.WriteValueSafe(ActiveSceneHash);
+ return;
+ }
+
+ if (SceneEventType == SceneEventType.ObjectSceneChanged)
+ {
+ SerializeObjectsMovedIntoNewScene(writer);
+ return;
+ }
+
// Write the scene loading mode
writer.WriteValueSafe((byte)LoadSceneMode);
@@ -392,6 +418,10 @@ namespace Unity.Netcode
{
writer.WriteValueSafe(SceneEventProgressId);
}
+ else
+ {
+ writer.WriteValueSafe(ClientSynchronizationMode);
+ }
// Write the scene index and handle
writer.WriteValueSafe(SceneHash);
@@ -401,6 +431,7 @@ namespace Unity.Netcode
{
case SceneEventType.Synchronize:
{
+ writer.WriteValueSafe(ActiveSceneHash);
WriteSceneSynchronizationData(writer);
break;
}
@@ -445,7 +476,7 @@ namespace Unity.Netcode
// Size Place Holder -- Start
// !!NOTE!!: Since this is a placeholder to be set after we know how much we have written,
// for stream offset purposes this MUST not be a packed value!
- writer.WriteValueSafe((int)0);
+ writer.WriteValueSafe(0);
int totalBytes = 0;
// Write the number of NetworkObjects we are serializing
@@ -458,7 +489,7 @@ namespace Unity.Netcode
var sceneObject = m_NetworkObjectsSync[i].GetMessageSceneObject(TargetClientId);
sceneObject.Serialize(writer);
var noStop = writer.Position;
- totalBytes += (int)(noStop - noStart);
+ totalBytes += noStop - noStart;
}
// Write the number of despawned in-scene placed NetworkObjects
@@ -470,7 +501,7 @@ namespace Unity.Netcode
writer.WriteValueSafe(m_DespawnedInSceneObjectsSync[i].GetSceneOriginHandle());
writer.WriteValueSafe(m_DespawnedInSceneObjectsSync[i].GlobalObjectIdHash);
var noStop = writer.Position;
- totalBytes += (int)(noStop - noStart);
+ totalBytes += noStop - noStart;
}
// Size Place Holder -- End
@@ -536,6 +567,26 @@ namespace Unity.Netcode
internal void Deserialize(FastBufferReader reader)
{
reader.ReadValueSafe(out SceneEventType);
+ if (SceneEventType == SceneEventType.ActiveSceneChanged)
+ {
+ reader.ReadValueSafe(out ActiveSceneHash);
+ return;
+ }
+
+ if (SceneEventType == SceneEventType.ObjectSceneChanged)
+ {
+ // Defer these scene event types if a client hasn't finished synchronizing
+ if (!m_NetworkManager.IsConnectedClient)
+ {
+ DeferObjectsMovedIntoNewScene(reader);
+ }
+ else
+ {
+ DeserializeObjectsMovedIntoNewScene(reader);
+ }
+ return;
+ }
+
reader.ReadValueSafe(out byte loadSceneMode);
LoadSceneMode = (LoadSceneMode)loadSceneMode;
@@ -543,6 +594,10 @@ namespace Unity.Netcode
{
reader.ReadValueSafe(out SceneEventProgressId);
}
+ else
+ {
+ reader.ReadValueSafe(out ClientSynchronizationMode);
+ }
reader.ReadValueSafe(out SceneHash);
reader.ReadValueSafe(out SceneHandle);
@@ -551,6 +606,7 @@ namespace Unity.Netcode
{
case SceneEventType.Synchronize:
{
+ reader.ReadValueSafe(out ActiveSceneHash);
CopySceneSynchronizationData(reader);
break;
}
@@ -939,6 +995,143 @@ namespace Unity.Netcode
}
}
+ ///
+ /// Serialize scene handles and associated NetworkObjects that were migrated
+ /// into a new scene.
+ ///
+ private void SerializeObjectsMovedIntoNewScene(FastBufferWriter writer)
+ {
+ var sceneManager = m_NetworkManager.SceneManager;
+ // Write the number of scene handles
+ writer.WriteValueSafe(sceneManager.ObjectsMigratedIntoNewScene.Count);
+ foreach (var sceneHandleObjects in sceneManager.ObjectsMigratedIntoNewScene)
+ {
+ // Write the scene handle
+ writer.WriteValueSafe(sceneHandleObjects.Key);
+ // Write the number of NetworkObjectIds to expect
+ writer.WriteValueSafe(sceneHandleObjects.Value.Count);
+ foreach (var networkObject in sceneHandleObjects.Value)
+ {
+ writer.WriteValueSafe(networkObject.NetworkObjectId);
+ }
+ }
+ // Once we are done, clear the table
+ sceneManager.ObjectsMigratedIntoNewScene.Clear();
+ }
+
+ ///
+ /// Deserialize scene handles and associated NetworkObjects that need to
+ /// be migrated into a new scene.
+ ///
+ private void DeserializeObjectsMovedIntoNewScene(FastBufferReader reader)
+ {
+ var sceneManager = m_NetworkManager.SceneManager;
+ var spawnManager = m_NetworkManager.SpawnManager;
+ // Just always assure this has no entries
+ sceneManager.ObjectsMigratedIntoNewScene.Clear();
+ var numberOfScenes = 0;
+ var sceneHandle = 0;
+ var objectCount = 0;
+ var networkObjectId = (ulong)0;
+ reader.ReadValueSafe(out numberOfScenes);
+ for (int i = 0; i < numberOfScenes; i++)
+ {
+ reader.ReadValueSafe(out sceneHandle);
+ sceneManager.ObjectsMigratedIntoNewScene.Add(sceneHandle, new List());
+ reader.ReadValueSafe(out objectCount);
+ for (int j = 0; j < objectCount; j++)
+ {
+ reader.ReadValueSafe(out networkObjectId);
+ if (!spawnManager.SpawnedObjects.ContainsKey(networkObjectId))
+ {
+ NetworkLog.LogError($"[Object Scene Migration] Trying to synchronize NetworkObjectId ({networkObjectId}) but it was not spawned or no longer exists!!");
+ continue;
+ }
+ // Add NetworkObject scene migration to ObjectsMigratedIntoNewScene dictionary that is processed
+ //
+ sceneManager.ObjectsMigratedIntoNewScene[sceneHandle].Add(spawnManager.SpawnedObjects[networkObjectId]);
+ }
+ }
+ }
+
+
+ ///
+ /// While a client is synchronizing ObjectSceneChanged messages could be received.
+ /// This defers any ObjectSceneChanged message processing to occur after the client
+ /// has completed synchronization to assure the associated NetworkObjects being
+ /// migrated to a new scene are instantiated and spawned.
+ ///
+ private void DeferObjectsMovedIntoNewScene(FastBufferReader reader)
+ {
+ var sceneManager = m_NetworkManager.SceneManager;
+ var spawnManager = m_NetworkManager.SpawnManager;
+ var numberOfScenes = 0;
+ var sceneHandle = 0;
+ var objectCount = 0;
+ var networkObjectId = (ulong)0;
+
+ var deferredObjectsMovedEvent = new NetworkSceneManager.DeferredObjectsMovedEvent()
+ {
+ ObjectsMigratedTable = new Dictionary>()
+ };
+
+ reader.ReadValueSafe(out numberOfScenes);
+ for (int i = 0; i < numberOfScenes; i++)
+ {
+ reader.ReadValueSafe(out sceneHandle);
+ deferredObjectsMovedEvent.ObjectsMigratedTable.Add(sceneHandle, new List());
+ reader.ReadValueSafe(out objectCount);
+ for (int j = 0; j < objectCount; j++)
+ {
+ reader.ReadValueSafe(out networkObjectId);
+ deferredObjectsMovedEvent.ObjectsMigratedTable[sceneHandle].Add(networkObjectId);
+ }
+ }
+ sceneManager.DeferredObjectsMovedEvents.Add(deferredObjectsMovedEvent);
+ }
+
+ internal void ProcessDeferredObjectSceneChangedEvents()
+ {
+ var sceneManager = m_NetworkManager.SceneManager;
+ var spawnManager = m_NetworkManager.SpawnManager;
+ if (sceneManager.DeferredObjectsMovedEvents.Count == 0)
+ {
+ return;
+ }
+ foreach (var objectsMovedEvent in sceneManager.DeferredObjectsMovedEvents)
+ {
+ foreach (var keyEntry in objectsMovedEvent.ObjectsMigratedTable)
+ {
+ if (!sceneManager.ObjectsMigratedIntoNewScene.ContainsKey(keyEntry.Key))
+ {
+ sceneManager.ObjectsMigratedIntoNewScene.Add(keyEntry.Key, new List());
+ }
+ foreach (var objectId in keyEntry.Value)
+ {
+ if (!spawnManager.SpawnedObjects.ContainsKey(objectId))
+ {
+ NetworkLog.LogWarning($"[Deferred][Object Scene Migration] Trying to synchronize NetworkObjectId ({objectId}) but it was not spawned or no longer exists!");
+ continue;
+ }
+ var networkObject = spawnManager.SpawnedObjects[objectId];
+ if (!sceneManager.ObjectsMigratedIntoNewScene[keyEntry.Key].Contains(networkObject))
+ {
+ sceneManager.ObjectsMigratedIntoNewScene[keyEntry.Key].Add(networkObject);
+ }
+ }
+ }
+ objectsMovedEvent.ObjectsMigratedTable.Clear();
+ }
+
+ sceneManager.DeferredObjectsMovedEvents.Clear();
+
+ // If there are any pending objects to migrate, then migrate them
+ if (sceneManager.ObjectsMigratedIntoNewScene.Count > 0)
+ {
+ sceneManager.MigrateNetworkObjectsIntoScenes();
+ }
+ }
+
///
/// Used to release the pooled network buffer
///
diff --git a/Runtime/SceneManagement/SceneEventProgress.cs b/Runtime/SceneManagement/SceneEventProgress.cs
index 6ab998c..a20ee25 100644
--- a/Runtime/SceneManagement/SceneEventProgress.cs
+++ b/Runtime/SceneManagement/SceneEventProgress.cs
@@ -83,7 +83,7 @@ namespace Unity.Netcode
///
internal bool HasTimedOut()
{
- return WhenSceneEventHasTimedOut <= Time.realtimeSinceStartup;
+ return WhenSceneEventHasTimedOut <= m_NetworkManager.RealTimeProvider.RealTimeSinceStartup;
}
///
@@ -164,7 +164,7 @@ namespace Unity.Netcode
ClientsProcessingSceneEvent.Add(connectedClientId, false);
}
- WhenSceneEventHasTimedOut = Time.realtimeSinceStartup + networkManager.NetworkConfig.LoadSceneTimeOut;
+ WhenSceneEventHasTimedOut = networkManager.RealTimeProvider.RealTimeSinceStartup + networkManager.NetworkConfig.LoadSceneTimeOut;
m_TimeOutCoroutine = m_NetworkManager.StartCoroutine(TimeOutSceneEventProgress());
}
}
diff --git a/Runtime/Serialization/FastBufferReader.cs b/Runtime/Serialization/FastBufferReader.cs
index 180311c..2f62a65 100644
--- a/Runtime/Serialization/FastBufferReader.cs
+++ b/Runtime/Serialization/FastBufferReader.cs
@@ -1299,8 +1299,10 @@ namespace Unity.Netcode
where T : unmanaged, INativeList, IUTF8Bytes
{
ReadUnmanaged(out int length);
- value = new T();
- value.Length = length;
+ value = new T
+ {
+ Length = length
+ };
ReadBytes(value.GetUnsafePtr(), length);
}
@@ -1319,8 +1321,10 @@ namespace Unity.Netcode
where T : unmanaged, INativeList, IUTF8Bytes
{
ReadUnmanagedSafe(out int length);
- value = new T();
- value.Length = length;
+ value = new T
+ {
+ Length = length
+ };
ReadBytesSafe(value.GetUnsafePtr(), length);
}
diff --git a/Runtime/Serialization/NetworkObjectReference.cs b/Runtime/Serialization/NetworkObjectReference.cs
index 2634035..dc91044 100644
--- a/Runtime/Serialization/NetworkObjectReference.cs
+++ b/Runtime/Serialization/NetworkObjectReference.cs
@@ -54,13 +54,7 @@ namespace Unity.Netcode
throw new ArgumentNullException(nameof(gameObject));
}
- var networkObject = gameObject.GetComponent();
-
- if (networkObject == null)
- {
- throw new ArgumentException($"Cannot create {nameof(NetworkObjectReference)} from {nameof(GameObject)} without a {nameof(NetworkObject)} component.");
- }
-
+ var networkObject = gameObject.GetComponent() ?? throw new ArgumentException($"Cannot create {nameof(NetworkObjectReference)} from {nameof(GameObject)} without a {nameof(NetworkObject)} component.");
if (networkObject.IsSpawned == false)
{
throw new ArgumentException($"{nameof(NetworkObjectReference)} can only be created from spawned {nameof(NetworkObject)}s.");
@@ -90,7 +84,7 @@ namespace Unity.Netcode
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static NetworkObject Resolve(NetworkObjectReference networkObjectRef, NetworkManager networkManager = null)
{
- networkManager = networkManager != null ? networkManager : NetworkManager.Singleton;
+ networkManager = networkManager ?? NetworkManager.Singleton;
networkManager.SpawnManager.SpawnedObjects.TryGetValue(networkObjectRef.m_NetworkObjectId, out NetworkObject networkObject);
return networkObject;
diff --git a/Runtime/Spawning/NetworkSpawnManager.cs b/Runtime/Spawning/NetworkSpawnManager.cs
index 131489a..2a9c341 100644
--- a/Runtime/Spawning/NetworkSpawnManager.cs
+++ b/Runtime/Spawning/NetworkSpawnManager.cs
@@ -154,7 +154,7 @@ namespace Unity.Netcode
internal ulong GetNetworkObjectId()
{
- if (ReleasedNetworkObjectIds.Count > 0 && NetworkManager.NetworkConfig.RecycleNetworkIds && (Time.unscaledTime - ReleasedNetworkObjectIds.Peek().ReleaseTime) >= NetworkManager.NetworkConfig.NetworkIdRecycleDelay)
+ if (ReleasedNetworkObjectIds.Count > 0 && NetworkManager.NetworkConfig.RecycleNetworkIds && (NetworkManager.RealTimeProvider.UnscaledTime - ReleasedNetworkObjectIds.Peek().ReleaseTime) >= NetworkManager.NetworkConfig.NetworkIdRecycleDelay)
{
return ReleasedNetworkObjectIds.Dequeue().NetworkId;
}
@@ -405,6 +405,9 @@ namespace Unity.Netcode
if (networkObject != null)
{
+ networkObject.DestroyWithScene = sceneObject.DestroyWithScene;
+ networkObject.NetworkSceneHandle = sceneObject.NetworkSceneHandle;
+
// SPECIAL CASE FOR IN-SCENE PLACED: (only when the parent has a NetworkObject)
// This is a special case scenario where a late joining client has joined and loaded one or
// more scenes that contain nested in-scene placed NetworkObject children yet the server's
@@ -610,6 +613,12 @@ namespace Unity.Netcode
}
childObject.IsSceneObject = sceneObject;
}
+
+ // Only dynamically spawned NetworkObjects are allowed
+ if (!sceneObject)
+ {
+ networkObject.SubscribeToActiveSceneForSynch();
+ }
}
internal void SendSpawnCallForObject(ulong clientId, NetworkObject networkObject)
@@ -848,7 +857,7 @@ namespace Unity.Netcode
ReleasedNetworkObjectIds.Enqueue(new ReleasedNetworkId()
{
NetworkId = networkObject.NetworkObjectId,
- ReleaseTime = Time.unscaledTime
+ ReleaseTime = NetworkManager.RealTimeProvider.UnscaledTime
});
}
diff --git a/Runtime/Timing/IRealTimeProvider.cs b/Runtime/Timing/IRealTimeProvider.cs
new file mode 100644
index 0000000..07740c8
--- /dev/null
+++ b/Runtime/Timing/IRealTimeProvider.cs
@@ -0,0 +1,10 @@
+namespace Unity.Netcode
+{
+ internal interface IRealTimeProvider
+ {
+ float RealTimeSinceStartup { get; }
+ float UnscaledTime { get; }
+ float UnscaledDeltaTime { get; }
+ float DeltaTime { get; }
+ }
+}
diff --git a/Runtime/Timing/IRealTimeProvider.cs.meta b/Runtime/Timing/IRealTimeProvider.cs.meta
new file mode 100644
index 0000000..f05e1f6
--- /dev/null
+++ b/Runtime/Timing/IRealTimeProvider.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 73bdda41e36846e893fd14dbd6de9978
+timeCreated: 1679413210
\ No newline at end of file
diff --git a/Runtime/Timing/RealTimeProvider.cs b/Runtime/Timing/RealTimeProvider.cs
new file mode 100644
index 0000000..840180d
--- /dev/null
+++ b/Runtime/Timing/RealTimeProvider.cs
@@ -0,0 +1,12 @@
+using UnityEngine;
+
+namespace Unity.Netcode
+{
+ internal class RealTimeProvider : IRealTimeProvider
+ {
+ public float RealTimeSinceStartup => Time.realtimeSinceStartup;
+ public float UnscaledTime => Time.unscaledTime;
+ public float UnscaledDeltaTime => Time.unscaledDeltaTime;
+ public float DeltaTime => Time.deltaTime;
+ }
+}
diff --git a/Runtime/Timing/RealTimeProvider.cs.meta b/Runtime/Timing/RealTimeProvider.cs.meta
new file mode 100644
index 0000000..678d3d2
--- /dev/null
+++ b/Runtime/Timing/RealTimeProvider.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 5aa5470767d64d8e89ac69ff52a8c404
+timeCreated: 1679413182
\ No newline at end of file
diff --git a/Runtime/Transports/UNET/UNetTransport.cs b/Runtime/Transports/UNET/UNetTransport.cs
index 7afb7b3..c03f415 100644
--- a/Runtime/Transports/UNET/UNetTransport.cs
+++ b/Runtime/Transports/UNET/UNetTransport.cs
@@ -149,7 +149,7 @@ namespace Unity.Netcode.Transports.UNET
var eventType = UnityEngine.Networking.NetworkTransport.Receive(out int hostId, out int connectionId, out _, m_MessageBuffer, m_MessageBuffer.Length, out int receivedSize, out byte error);
clientId = GetNetcodeClientId((byte)hostId, (ushort)connectionId, false);
- receiveTime = Time.realtimeSinceStartup;
+ receiveTime = NetworkManager.RealTimeProvider.RealTimeSinceStartup;
var networkError = (NetworkError)error;
if (networkError == NetworkError.MessageToLong)
@@ -214,7 +214,7 @@ namespace Unity.Netcode.Transports.UNET
{
GetUNetConnectionDetails(clientId, out byte hostId, out ushort connectionId);
- UnityEngine.Networking.NetworkTransport.Disconnect((int)hostId, (int)connectionId, out byte error);
+ UnityEngine.Networking.NetworkTransport.Disconnect(hostId, connectionId, out byte error);
}
public override void DisconnectLocalClient()
@@ -226,7 +226,7 @@ namespace Unity.Netcode.Transports.UNET
{
GetUNetConnectionDetails(clientId, out byte hostId, out ushort connectionId);
- return (ulong)UnityEngine.Networking.NetworkTransport.GetCurrentRTT((int)hostId, (int)connectionId, out byte error);
+ return (ulong)UnityEngine.Networking.NetworkTransport.GetCurrentRTT(hostId, connectionId, out byte error);
}
public override void Shutdown()
diff --git a/Runtime/Transports/UTP/UnityTransport.cs b/Runtime/Transports/UTP/UnityTransport.cs
index 410db46..c40376d 100644
--- a/Runtime/Transports/UTP/UnityTransport.cs
+++ b/Runtime/Transports/UTP/UnityTransport.cs
@@ -450,6 +450,8 @@ namespace Unity.Netcode.Transports.UTP
internal NetworkManager NetworkManager;
+ private IRealTimeProvider m_RealTimeProvider;
+
///
/// SendQueue dictionary is used to batch events instead of sending them immediately.
///
@@ -763,6 +765,10 @@ namespace Unity.Netcode.Transports.UTP
// Send as many batched messages from the queue as possible.
private void SendBatchedMessages(SendTarget sendTarget, BatchedSendQueue queue)
{
+ if (!m_Driver.IsCreated)
+ {
+ return;
+ }
new SendBatchedMessagesJob
{
Driver = m_Driver.ToConcurrent(),
@@ -784,7 +790,7 @@ namespace Unity.Netcode.Transports.UTP
InvokeOnTransportEvent(NetcodeNetworkEvent.Connect,
ParseClientId(connection),
default,
- Time.realtimeSinceStartup);
+ m_RealTimeProvider.RealTimeSinceStartup);
return true;
@@ -819,7 +825,7 @@ namespace Unity.Netcode.Transports.UTP
break;
}
- InvokeOnTransportEvent(NetcodeNetworkEvent.Data, clientId, message, Time.realtimeSinceStartup);
+ InvokeOnTransportEvent(NetcodeNetworkEvent.Data, clientId, message, m_RealTimeProvider.RealTimeSinceStartup);
}
}
@@ -835,7 +841,7 @@ namespace Unity.Netcode.Transports.UTP
InvokeOnTransportEvent(NetcodeNetworkEvent.Connect,
clientId,
default,
- Time.realtimeSinceStartup);
+ m_RealTimeProvider.RealTimeSinceStartup);
m_State = State.Connected;
return true;
@@ -863,7 +869,7 @@ namespace Unity.Netcode.Transports.UTP
InvokeOnTransportEvent(NetcodeNetworkEvent.Disconnect,
clientId,
default,
- Time.realtimeSinceStartup);
+ m_RealTimeProvider.RealTimeSinceStartup);
return true;
}
@@ -893,7 +899,7 @@ namespace Unity.Netcode.Transports.UTP
Debug.LogError("Transport failure! Relay allocation needs to be recreated, and NetworkManager restarted. " +
"Use NetworkManager.OnTransportFailure to be notified of such events programmatically.");
- InvokeOnTransportEvent(NetcodeNetworkEvent.TransportFailure, 0, default, Time.realtimeSinceStartup);
+ InvokeOnTransportEvent(NetcodeNetworkEvent.TransportFailure, 0, default, m_RealTimeProvider.RealTimeSinceStartup);
return;
}
@@ -1116,7 +1122,7 @@ namespace Unity.Netcode.Transports.UTP
InvokeOnTransportEvent(NetcodeNetworkEvent.Disconnect,
m_ServerClientId,
default,
- Time.realtimeSinceStartup);
+ m_RealTimeProvider.RealTimeSinceStartup);
}
}
}
@@ -1179,6 +1185,8 @@ namespace Unity.Netcode.Transports.UTP
NetworkManager = networkManager;
+ m_RealTimeProvider = NetworkManager ? NetworkManager.RealTimeProvider : new RealTimeProvider();
+
m_NetworkSettings = new NetworkSettings(Allocator.Persistent);
// If the user sends a message of exactly m_MaxPayloadSize in length, we need to
@@ -1203,7 +1211,7 @@ namespace Unity.Netcode.Transports.UTP
///
/// The clientId this event is for
/// The incoming data payload
- /// The time the event was received, as reported by Time.realtimeSinceStartup.
+ /// The time the event was received, as reported by m_RealTimeProvider.RealTimeSinceStartup.
/// Returns the event type
public override NetcodeNetworkEvent PollEvent(out ulong clientId, out ArraySegment payload, out float receiveTime)
{
@@ -1276,7 +1284,7 @@ namespace Unity.Netcode.Transports.UTP
InvokeOnTransportEvent(NetcodeNetworkEvent.Disconnect,
clientId,
default(ArraySegment),
- Time.realtimeSinceStartup);
+ m_RealTimeProvider.RealTimeSinceStartup);
}
}
else
@@ -1424,6 +1432,10 @@ namespace Unity.Netcode.Transports.UTP
private string m_ClientCaCertificate;
/// Set the server parameters for encryption.
+ ///
+ /// The public certificate and private key are expected to be in the PEM format, including
+ /// the begin/end markers like -----BEGIN CERTIFICATE-----.
+ ///
/// Public certificate for the server (PEM format).
/// Private key for the server (PEM format).
public void SetServerSecrets(string serverCertificate, string serverPrivateKey)
@@ -1434,9 +1446,15 @@ namespace Unity.Netcode.Transports.UTP
/// Set the client parameters for encryption.
///
+ ///
/// If the CA certificate is not provided, validation will be done against the OS/browser
/// certificate store. This is what you'd want if using certificates from a known provider.
/// For self-signed certificates, the CA certificate needs to be provided.
+ ///
+ ///
+ /// The CA certificate (if provided) is expected to be in the PEM format, including the
+ /// begin/end markers like -----BEGIN CERTIFICATE-----.
+ ///
///
/// Common name of the server (typically hostname).
/// CA certificate used to validate the server's authenticity.
diff --git a/Runtime/com.unity.netcode.runtime.asmdef b/Runtime/com.unity.netcode.runtime.asmdef
index 334966a..43433ab 100644
--- a/Runtime/com.unity.netcode.runtime.asmdef
+++ b/Runtime/com.unity.netcode.runtime.asmdef
@@ -12,7 +12,8 @@
"Unity.Multiplayer.Tools.NetworkSolutionInterface",
"Unity.Networking.Transport",
"Unity.Collections",
- "Unity.Burst"
+ "Unity.Burst",
+ "Unity.Mathematics"
],
"allowUnsafeCode": true,
"versionDefines": [
diff --git a/TestHelpers/Runtime/IntegrationTestSceneHandler.cs b/TestHelpers/Runtime/IntegrationTestSceneHandler.cs
index 80e09c8..c5b5595 100644
--- a/TestHelpers/Runtime/IntegrationTestSceneHandler.cs
+++ b/TestHelpers/Runtime/IntegrationTestSceneHandler.cs
@@ -15,6 +15,16 @@ namespace Unity.Netcode.TestHelpers.Runtime
///
internal class IntegrationTestSceneHandler : ISceneManagerHandler, IDisposable
{
+ private Scene m_InvalidScene = new Scene();
+
+ internal struct SceneEntry
+ {
+ public bool IsAssigned;
+ public Scene Scene;
+ }
+
+ internal static Dictionary>> SceneNameToSceneHandles = new Dictionary>>();
+
// All IntegrationTestSceneHandler instances register their associated NetworkManager
internal static List NetworkManagers = new List();
@@ -96,7 +106,7 @@ namespace Unity.Netcode.TestHelpers.Runtime
/// Processes scene loading jobs
///
/// job to process
- static internal IEnumerator ProcessLoadingSceneJob(QueuedSceneJob queuedSceneJob)
+ internal static IEnumerator ProcessLoadingSceneJob(QueuedSceneJob queuedSceneJob)
{
var itegrationTestSceneHandler = queuedSceneJob.IntegrationTestSceneHandler;
while (!itegrationTestSceneHandler.OnCanClientsLoad())
@@ -170,7 +180,7 @@ namespace Unity.Netcode.TestHelpers.Runtime
/// Processes scene unloading jobs
///
/// job to process
- static internal IEnumerator ProcessUnloadingSceneJob(QueuedSceneJob queuedSceneJob)
+ internal static IEnumerator ProcessUnloadingSceneJob(QueuedSceneJob queuedSceneJob)
{
var itegrationTestSceneHandler = queuedSceneJob.IntegrationTestSceneHandler;
while (!itegrationTestSceneHandler.OnCanClientsUnload())
@@ -213,7 +223,7 @@ namespace Unity.Netcode.TestHelpers.Runtime
/// Processes all jobs within the queue.
/// When all jobs are finished, the coroutine stops.
///
- static internal IEnumerator JobQueueProcessor()
+ internal static IEnumerator JobQueueProcessor()
{
while (QueuedSceneJobs.Count != 0)
{
@@ -267,8 +277,8 @@ namespace Unity.Netcode.TestHelpers.Runtime
{
if (m_ServerSceneBeingLoaded == scene.name)
{
- ProcessInSceneObjects(scene, NetworkManager);
SceneManager.sceneLoaded -= Sever_SceneLoaded;
+ ProcessInSceneObjects(scene, NetworkManager);
}
}
@@ -330,6 +340,7 @@ namespace Unity.Netcode.TestHelpers.Runtime
{
continue;
}
+
if (networkManager.SceneManager.ScenesLoaded.ContainsKey(sceneLoaded.handle))
{
if (NetworkManager.LogLevel == LogLevel.Developer)
@@ -347,7 +358,12 @@ namespace Unity.Netcode.TestHelpers.Runtime
{
NetworkLog.LogInfo($"{NetworkManager.name} adding {sceneLoaded.name} with a handle of {sceneLoaded.handle} to its ScenesLoaded.");
}
+ if (DoesANetworkManagerHoldThisScene(sceneLoaded))
+ {
+ continue;
+ }
NetworkManager.SceneManager.ScenesLoaded.Add(sceneLoaded.handle, sceneLoaded);
+ StartTrackingScene(sceneLoaded, true, NetworkManager);
return sceneLoaded;
}
}
@@ -365,6 +381,521 @@ namespace Unity.Netcode.TestHelpers.Runtime
return true;
}
+ public void ClearSceneTracking(NetworkManager networkManager)
+ {
+ SceneNameToSceneHandles.Clear();
+ }
+
+ public void StopTrackingScene(int handle, string name, NetworkManager networkManager)
+ {
+ if (!SceneNameToSceneHandles.ContainsKey(networkManager))
+ {
+ return;
+ }
+
+ if (SceneNameToSceneHandles[networkManager].ContainsKey(name))
+ {
+ if (SceneNameToSceneHandles[networkManager][name].ContainsKey(handle))
+ {
+ SceneNameToSceneHandles[networkManager][name].Remove(handle);
+ if (SceneNameToSceneHandles[networkManager][name].Count == 0)
+ {
+ SceneNameToSceneHandles[networkManager].Remove(name);
+ }
+ }
+ }
+ }
+
+ public void StartTrackingScene(Scene scene, bool assigned, NetworkManager networkManager)
+ {
+ if (!SceneNameToSceneHandles.ContainsKey(networkManager))
+ {
+ SceneNameToSceneHandles.Add(networkManager, new Dictionary>());
+ }
+
+ if (!SceneNameToSceneHandles[networkManager].ContainsKey(scene.name))
+ {
+ SceneNameToSceneHandles[networkManager].Add(scene.name, new Dictionary());
+ }
+
+ if (!SceneNameToSceneHandles[networkManager][scene.name].ContainsKey(scene.handle))
+ {
+ var sceneEntry = new SceneEntry()
+ {
+ IsAssigned = true,
+ Scene = scene
+ };
+ SceneNameToSceneHandles[networkManager][scene.name].Add(scene.handle, sceneEntry);
+ }
+ }
+
+ private bool DoesANetworkManagerHoldThisScene(Scene scene)
+ {
+ foreach (var netManEntry in SceneNameToSceneHandles)
+ {
+ if (!netManEntry.Value.ContainsKey(scene.name))
+ {
+ continue;
+ }
+ // The other NetworkManager only has to have an entry to
+ // disqualify this scene instance
+ if (netManEntry.Value[scene.name].ContainsKey(scene.handle))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public bool DoesSceneHaveUnassignedEntry(string sceneName, NetworkManager networkManager)
+ {
+ var scenesWithSceneName = new List();
+ var scenesAssigned = new List();
+ for (int i = 0; i < SceneManager.sceneCount; i++)
+ {
+ var scene = SceneManager.GetSceneAt(i);
+ if (scene.name == sceneName)
+ {
+ scenesWithSceneName.Add(scene);
+ }
+ }
+
+ // Check for other NetworkManager instances already having been assigned this scene
+ foreach (var netManEntry in SceneNameToSceneHandles)
+ {
+ // Ignore this NetworkManager instance at this stage
+ if (netManEntry.Key == networkManager)
+ {
+ continue;
+ }
+
+ foreach (var scene in scenesWithSceneName)
+ {
+ if (!netManEntry.Value.ContainsKey(scene.name))
+ {
+ continue;
+ }
+ // The other NetworkManager only has to have an entry to
+ // disqualify this scene instance
+ if (netManEntry.Value[scene.name].ContainsKey(scene.handle))
+ {
+ scenesAssigned.Add(scene);
+ }
+ }
+ }
+
+ // Remove all of the assigned scenes from the list of scenes with the
+ // passed in scene name.
+ foreach (var assignedScene in scenesAssigned)
+ {
+ if (scenesWithSceneName.Contains(assignedScene))
+ {
+ scenesWithSceneName.Remove(assignedScene);
+ }
+ }
+
+ // If all currently loaded scenes with the scene name are taken
+ // then we return false
+ if (scenesWithSceneName.Count == 0)
+ {
+ return false;
+ }
+
+ // If we made it here, then no other NetworkManager is tracking this scene
+ // and if we don't have an entry for this NetworkManager then we can use any
+ // of the remaining scenes loaded with that name.
+ if (!SceneNameToSceneHandles.ContainsKey(networkManager))
+ {
+ return true;
+ }
+
+ // If we don't yet have a scene name in this NetworkManager's lookup table,
+ // then we can use any of the remaining availabel scenes with that scene name
+ if (!SceneNameToSceneHandles[networkManager].ContainsKey(sceneName))
+ {
+ return true;
+ }
+
+ foreach (var scene in scenesWithSceneName)
+ {
+ // If we don't have an entry for this scene handle (with the scene name) then we
+ // can use that scene
+ if (!SceneNameToSceneHandles[networkManager][scene.name].ContainsKey(scene.handle))
+ {
+ return true;
+ }
+
+ // This entry is not assigned, then we can use the associated scene
+ if (!SceneNameToSceneHandles[networkManager][scene.name][scene.handle].IsAssigned)
+ {
+ return true;
+ }
+ }
+
+ // None of the scenes with the same scene name can be used
+ return false;
+ }
+
+ public Scene GetSceneFromLoadedScenes(string sceneName, NetworkManager networkManager)
+ {
+
+ if (!SceneNameToSceneHandles.ContainsKey(networkManager))
+ {
+ return m_InvalidScene;
+ }
+ if (SceneNameToSceneHandles[networkManager].ContainsKey(sceneName))
+ {
+ foreach (var sceneHandleEntry in SceneNameToSceneHandles[networkManager][sceneName])
+ {
+ if (!sceneHandleEntry.Value.IsAssigned)
+ {
+ var sceneEntry = sceneHandleEntry.Value;
+ sceneEntry.IsAssigned = true;
+ SceneNameToSceneHandles[networkManager][sceneName][sceneHandleEntry.Key] = sceneEntry;
+ return sceneEntry.Scene;
+ }
+ }
+ }
+ // This is tricky since NetworkManager instances share the same scene hierarchy during integration tests.
+ // TODO 2023: Determine if there is a better way to associate the active scene for client NetworkManager instances.
+ var activeScene = SceneManager.GetActiveScene();
+
+ if (sceneName == activeScene.name && networkManager.SceneManager.ClientSynchronizationMode == LoadSceneMode.Additive)
+ {
+ // For now, just return the current active scene
+ // Note: Clients will not be able to synchronize in-scene placed NetworkObjects in an integration test for
+ // scenes loaded that have in-scene placed NetworkObjects prior to the clients joining (i.e. there will only
+ // ever be one instance of the active scene). To test in-scene placed NetworkObjects and make an integration
+ // test loaded scene be the active scene, don't set scene as an active scene on the server side until all
+ // clients have connected and loaded the scene.
+ return activeScene;
+ }
+ // If we found nothing return an invalid scene
+ return m_InvalidScene;
+ }
+
+ public void PopulateLoadedScenes(ref Dictionary scenesLoaded, NetworkManager networkManager)
+ {
+ if (!SceneNameToSceneHandles.ContainsKey(networkManager))
+ {
+ SceneNameToSceneHandles.Add(networkManager, new Dictionary>());
+ }
+
+ var sceneCount = SceneManager.sceneCount;
+ for (int i = 0; i < sceneCount; i++)
+ {
+ var scene = SceneManager.GetSceneAt(i);
+ // Ignore scenes that belong to other NetworkManager instances
+
+ if (DoesANetworkManagerHoldThisScene(scene))
+ {
+ continue;
+ }
+
+ if (!DoesSceneHaveUnassignedEntry(scene.name, networkManager))
+ {
+ continue;
+ }
+
+ if (!SceneNameToSceneHandles[networkManager].ContainsKey(scene.name))
+ {
+ SceneNameToSceneHandles[networkManager].Add(scene.name, new Dictionary());
+ }
+
+ if (!SceneNameToSceneHandles[networkManager][scene.name].ContainsKey(scene.handle))
+ {
+ var sceneEntry = new SceneEntry()
+ {
+ IsAssigned = false,
+ Scene = scene
+ };
+ SceneNameToSceneHandles[networkManager][scene.name].Add(scene.handle, sceneEntry);
+ if (!scenesLoaded.ContainsKey(scene.handle))
+ {
+ scenesLoaded.Add(scene.handle, scene);
+ }
+ }
+ else
+ {
+ throw new Exception($"[{networkManager.LocalClient.PlayerObject.name}][Duplicate Handle] Scene {scene.name} already has scene handle {scene.handle} registered!");
+ }
+ }
+ }
+
+ private Dictionary m_ScenesToUnload = new Dictionary();
+
+ ///
+ /// Handles unloading any scenes that might remain on a client that
+ /// need to be unloaded.
+ ///
+ ///
+ public void UnloadUnassignedScenes(NetworkManager networkManager = null)
+ {
+ if (!SceneNameToSceneHandles.ContainsKey(networkManager))
+ {
+ return;
+ }
+ var relativeSceneNameToSceneHandles = SceneNameToSceneHandles[networkManager];
+ var sceneManager = networkManager.SceneManager;
+ SceneManager.sceneUnloaded += SceneManager_SceneUnloaded;
+
+ foreach (var sceneEntry in relativeSceneNameToSceneHandles)
+ {
+ var scenHandleEntries = relativeSceneNameToSceneHandles[sceneEntry.Key];
+ foreach (var sceneHandleEntry in scenHandleEntries)
+ {
+ if (!sceneHandleEntry.Value.IsAssigned)
+ {
+ if (sceneManager.VerifySceneBeforeUnloading == null || sceneManager.VerifySceneBeforeUnloading.Invoke(sceneHandleEntry.Value.Scene))
+ {
+ m_ScenesToUnload.Add(sceneHandleEntry.Value.Scene, networkManager);
+ }
+ }
+ }
+ }
+
+ foreach (var sceneToUnload in m_ScenesToUnload)
+ {
+ SceneManager.UnloadSceneAsync(sceneToUnload.Key);
+ }
+ }
+
+ ///
+ /// Removes the scene entry from the scene name to scene handle table
+ ///
+ private void SceneManager_SceneUnloaded(Scene scene)
+ {
+ if (m_ScenesToUnload.ContainsKey(scene))
+ {
+ var networkManager = m_ScenesToUnload[scene];
+ var relativeSceneNameToSceneHandles = SceneNameToSceneHandles[networkManager];
+ if (relativeSceneNameToSceneHandles.ContainsKey(scene.name))
+ {
+ var scenHandleEntries = relativeSceneNameToSceneHandles[scene.name];
+ if (scenHandleEntries.ContainsKey(scene.handle))
+ {
+ scenHandleEntries.Remove(scene.handle);
+ if (scenHandleEntries.Count == 0)
+ {
+ relativeSceneNameToSceneHandles.Remove(scene.name);
+ }
+ m_ScenesToUnload.Remove(scene);
+ if (m_ScenesToUnload.Count == 0)
+ {
+ SceneManager.sceneUnloaded -= SceneManager_SceneUnloaded;
+ }
+ }
+ }
+ }
+ }
+
+ ///
+ /// Integration test version that handles migrating dynamically spawned NetworkObjects to
+ /// the DDOL when a scene is unloaded
+ ///
+ /// relative instance
+ /// scene being unloaded
+ public void MoveObjectsFromSceneToDontDestroyOnLoad(ref NetworkManager networkManager, Scene scene)
+ {
+ // Create a local copy of the spawned objects list since the spawn manager will adjust the list as objects
+ // are despawned.
+#if UNITY_2023_1_OR_NEWER
+ var networkObjects = Object.FindObjectsByType(FindObjectsSortMode.InstanceID).Where((c) => c.IsSpawned);
+#else
+ var networkObjects = Object.FindObjectsOfType().Where((c) => c.IsSpawned);
+#endif
+ foreach (var networkObject in networkObjects)
+ {
+ if (networkObject == null || (networkObject != null && networkObject.gameObject.scene.handle != scene.handle))
+ {
+ if (networkObject != null)
+ {
+ VerboseDebug($"[MoveObjects from {scene.name} | {scene.handle}] Ignoring {networkObject.gameObject.name} because it isn't in scene {networkObject.gameObject.scene.name} ");
+ }
+ continue;
+ }
+
+ bool skipPrefab = false;
+
+ foreach (var networkPrefab in networkManager.NetworkConfig.Prefabs.Prefabs)
+ {
+ if (networkPrefab.Prefab == null)
+ {
+ continue;
+ }
+ if (networkObject == networkPrefab.Prefab.GetComponent())
+ {
+ skipPrefab = true;
+ break;
+ }
+ }
+ if (skipPrefab)
+ {
+ continue;
+ }
+
+ // Only NetworkObjects marked to not be destroyed with the scene and are not already in the DDOL are preserved
+ if (!networkObject.DestroyWithScene && networkObject.gameObject.scene != networkManager.SceneManager.DontDestroyOnLoadScene)
+ {
+ // Only move dynamically spawned NetworkObjects with no parent as the children will follow
+ if (networkObject.gameObject.transform.parent == null && networkObject.IsSceneObject != null && !networkObject.IsSceneObject.Value)
+ {
+ VerboseDebug($"[MoveObjects from {scene.name} | {scene.handle}] Moving {networkObject.gameObject.name} because it is in scene {networkObject.gameObject.scene.name} with DWS = {networkObject.DestroyWithScene}.");
+ Object.DontDestroyOnLoad(networkObject.gameObject);
+ }
+ }
+ else if (networkManager.IsServer)
+ {
+ if (networkObject.NetworkManager == networkManager)
+ {
+ VerboseDebug($"[MoveObjects from {scene.name} | {scene.handle}] Destroying {networkObject.gameObject.name} because it is in scene {networkObject.gameObject.scene.name} with DWS = {networkObject.DestroyWithScene}.");
+ networkObject.Despawn();
+ }
+ else //For integration testing purposes, migrate remaining into DDOL
+ {
+ VerboseDebug($"[MoveObjects from {scene.name} | {scene.handle}] Temporarily migrating {networkObject.gameObject.name} into DDOL to await server destroy message.");
+ Object.DontDestroyOnLoad(networkObject.gameObject);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Sets the client synchronization mode which impacts whether both the server or client take into consideration scenes loaded before
+ /// starting the .
+ ///
+ ///
+ /// : Does not take preloaded scenes into consideration
+ /// : Does take preloaded scenes into consideration
+ ///
+ /// relative instance
+ /// or
+ public void SetClientSynchronizationMode(ref NetworkManager networkManager, LoadSceneMode mode)
+ {
+
+ var sceneManager = networkManager.SceneManager;
+
+ // Don't let client's set this value
+ if (!networkManager.IsServer)
+ {
+ if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
+ {
+ NetworkLog.LogWarning("Clients should not set this value as it is automatically synchronized with the server's setting!");
+ }
+ return;
+ }
+ else if (networkManager.ConnectedClientsIds.Count > (networkManager.IsHost ? 1 : 0) && sceneManager.ClientSynchronizationMode != mode)
+ {
+ if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
+ {
+ NetworkLog.LogWarning("Server is changing client synchronization mode after clients have been synchronized! It is recommended to do this before clients are connected!");
+ }
+ }
+
+
+
+ // For additive client synchronization, we take into consideration scenes
+ // already loaded.
+ if (mode == LoadSceneMode.Additive)
+ {
+ if (networkManager.IsServer)
+ {
+ sceneManager.OnSceneEvent -= SceneManager_OnSceneEvent;
+ sceneManager.OnSceneEvent += SceneManager_OnSceneEvent;
+ }
+
+ if (!SceneNameToSceneHandles.ContainsKey(networkManager))
+ {
+ SceneNameToSceneHandles.Add(networkManager, new Dictionary>());
+ }
+
+ var networkManagerScenes = SceneNameToSceneHandles[networkManager];
+
+ for (int i = 0; i < SceneManager.sceneCount; i++)
+ {
+ var scene = SceneManager.GetSceneAt(i);
+
+ // Ignore scenes that belong to other NetworkManager instances
+ if (!DoesSceneHaveUnassignedEntry(scene.name, networkManager))
+ {
+ continue;
+ }
+
+ // If using scene verification
+ if (sceneManager.VerifySceneBeforeLoading != null)
+ {
+ // Determine if we should take this scene into consideration
+ if (!sceneManager.VerifySceneBeforeLoading.Invoke(scene.buildIndex, scene.name, LoadSceneMode.Additive))
+ {
+ continue;
+ }
+ }
+
+ // If the scene is not already in the ScenesLoaded list, then add it
+ if (!sceneManager.ScenesLoaded.ContainsKey(scene.handle))
+ {
+ StartTrackingScene(scene, true, networkManager);
+ sceneManager.ScenesLoaded.Add(scene.handle, scene);
+ }
+ }
+ }
+ // Set the client synchronization mode
+ sceneManager.ClientSynchronizationMode = mode;
+ }
+
+ ///
+ /// During integration testing, if the server loads a scene then
+ /// we want to start tracking it.
+ ///
+ ///
+ private void SceneManager_OnSceneEvent(SceneEvent sceneEvent)
+ {
+ // Filter for server only scene events
+ if (!NetworkManager.IsServer || sceneEvent.ClientId != NetworkManager.ServerClientId)
+ {
+ return;
+ }
+
+ switch (sceneEvent.SceneEventType)
+ {
+ case SceneEventType.LoadComplete:
+ {
+ StartTrackingScene(sceneEvent.Scene, true, NetworkManager);
+ break;
+ }
+ }
+ }
+
+ ///
+ /// Handles determining if a client should attempt to load a scene during synchronization.
+ ///
+ /// name of the scene to be loaded
+ /// when in client synchronization mode single, this determines if the scene is the primary active scene
+ /// the current client synchronization mode
+ /// relative instance
+ ///
+ public bool ClientShouldPassThrough(string sceneName, bool isPrimaryScene, LoadSceneMode clientSynchronizationMode, NetworkManager networkManager)
+ {
+ var shouldPassThrough = clientSynchronizationMode == LoadSceneMode.Single ? false : DoesSceneHaveUnassignedEntry(sceneName, networkManager);
+ var activeScene = SceneManager.GetActiveScene();
+
+ // If shouldPassThrough is not yet true and the scene to be loaded is the currently active scene
+ if (!shouldPassThrough && sceneName == activeScene.name)
+ {
+ // In additive client synchronization mode we always pass through.
+ // Unlike the default behavior(i.e. DefaultSceneManagerHandler), for integration testing we always return false
+ // if it is the active scene and the client synchronization mode is LoadSceneMode.Single because the client should
+ // load the active scene additively for this NetworkManager instance (i.e. can't have multiple active scenes).
+ if (clientSynchronizationMode == LoadSceneMode.Additive)
+ {
+ // don't try to reload this scene and pass through to post load processing.
+ shouldPassThrough = true;
+ }
+ }
+ return shouldPassThrough;
+ }
+
///
/// Constructor now must take NetworkManager
///
@@ -410,7 +941,11 @@ namespace Unity.Netcode.TestHelpers.Runtime
}
}
QueuedSceneJobs.Clear();
- Object.Destroy(CoroutineRunner.gameObject);
+ if (CoroutineRunner != null && CoroutineRunner.gameObject != null)
+ {
+ Object.Destroy(CoroutineRunner.gameObject);
+ }
+
}
}
}
diff --git a/TestHelpers/Runtime/IntegrationTestWithApproximation.cs b/TestHelpers/Runtime/IntegrationTestWithApproximation.cs
new file mode 100644
index 0000000..462fd74
--- /dev/null
+++ b/TestHelpers/Runtime/IntegrationTestWithApproximation.cs
@@ -0,0 +1,79 @@
+using System.Runtime.CompilerServices;
+using UnityEngine;
+
+
+namespace Unity.Netcode.TestHelpers.Runtime
+{
+ public abstract class IntegrationTestWithApproximation : NetcodeIntegrationTest
+ {
+ private const float k_AproximateDeltaVariance = 0.01f;
+
+ protected virtual float GetDeltaVarianceThreshold()
+ {
+ return k_AproximateDeltaVariance;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected float EulerDelta(float a, float b)
+ {
+ return Mathf.DeltaAngle(a, b);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected Vector3 EulerDelta(Vector3 a, Vector3 b)
+ {
+ return new Vector3(Mathf.DeltaAngle(a.x, b.x), Mathf.DeltaAngle(a.y, b.y), Mathf.DeltaAngle(a.z, b.z));
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected bool ApproximatelyEuler(float a, float b)
+ {
+ return Mathf.Abs(EulerDelta(a, b)) <= GetDeltaVarianceThreshold();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected bool Approximately(float a, float b)
+ {
+ return Mathf.Abs(a - b) <= GetDeltaVarianceThreshold();
+ }
+
+ protected bool Approximately(Vector2 a, Vector2 b)
+ {
+ var deltaVariance = GetDeltaVarianceThreshold();
+ return Mathf.Abs(a.x - b.x) <= deltaVariance &&
+ Mathf.Abs(a.y - b.y) <= deltaVariance;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected bool Approximately(Vector3 a, Vector3 b)
+ {
+ var deltaVariance = GetDeltaVarianceThreshold();
+ return Mathf.Abs(a.x - b.x) <= deltaVariance &&
+ Mathf.Abs(a.y - b.y) <= deltaVariance &&
+ Mathf.Abs(a.z - b.z) <= deltaVariance;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected bool Approximately(Quaternion a, Quaternion b)
+ {
+ var deltaVariance = GetDeltaVarianceThreshold();
+ return Mathf.Abs(a.x - b.x) <= deltaVariance &&
+ Mathf.Abs(a.y - b.y) <= deltaVariance &&
+ Mathf.Abs(a.z - b.z) <= deltaVariance &&
+ Mathf.Abs(a.w - b.w) <= deltaVariance;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected bool ApproximatelyEuler(Vector3 a, Vector3 b)
+ {
+ return ApproximatelyEuler(a.x, b.x) && ApproximatelyEuler(a.y, b.y) && ApproximatelyEuler(a.z, b.z);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected Vector3 GetRandomVector3(float min, float max)
+ {
+ return new Vector3(Random.Range(min, max), Random.Range(min, max), Random.Range(min, max));
+ }
+
+ }
+}
diff --git a/TestHelpers/Runtime/IntegrationTestWithApproximation.cs.meta b/TestHelpers/Runtime/IntegrationTestWithApproximation.cs.meta
new file mode 100644
index 0000000..af1adfd
--- /dev/null
+++ b/TestHelpers/Runtime/IntegrationTestWithApproximation.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 50a3b194bb5b8714d883dafd911db1ba
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/TestHelpers/Runtime/MockTimeProvider.cs b/TestHelpers/Runtime/MockTimeProvider.cs
new file mode 100644
index 0000000..f97b5dc
--- /dev/null
+++ b/TestHelpers/Runtime/MockTimeProvider.cs
@@ -0,0 +1,30 @@
+namespace Unity.Netcode.TestHelpers.Runtime
+{
+ public class MockTimeProvider : IRealTimeProvider
+ {
+ public float RealTimeSinceStartup => (float)s_DoubleRealTime;
+ public float UnscaledTime => (float)s_DoubleRealTime;
+ public float UnscaledDeltaTime => (float)s_DoubleDelta;
+ public float DeltaTime => (float)s_DoubleDelta;
+
+ public static float StaticRealTimeSinceStartup => (float)s_DoubleRealTime;
+ public static float StaticUnscaledTime => (float)s_DoubleRealTime;
+ public static float StaticUnscaledDeltaTime => (float)s_DoubleDelta;
+ public static float StaticDeltaTime => (float)s_DoubleDelta;
+
+ private static double s_DoubleRealTime = 0;
+ private static double s_DoubleDelta = 0;
+
+ public static void TimeTravel(double amountOfTimeTraveled)
+ {
+ s_DoubleDelta = amountOfTimeTraveled;
+ s_DoubleRealTime += amountOfTimeTraveled;
+ }
+
+ public static void Reset()
+ {
+ s_DoubleDelta = 0;
+ s_DoubleRealTime = 0;
+ }
+ }
+}
diff --git a/TestHelpers/Runtime/MockTimeProvider.cs.meta b/TestHelpers/Runtime/MockTimeProvider.cs.meta
new file mode 100644
index 0000000..a5e871c
--- /dev/null
+++ b/TestHelpers/Runtime/MockTimeProvider.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: bcc9a7faadea4b8ebeb041ee6e395a92
+timeCreated: 1679414015
\ No newline at end of file
diff --git a/TestHelpers/Runtime/MockTransport.cs b/TestHelpers/Runtime/MockTransport.cs
new file mode 100644
index 0000000..1bfc130
--- /dev/null
+++ b/TestHelpers/Runtime/MockTransport.cs
@@ -0,0 +1,89 @@
+using System;
+using System.Collections.Generic;
+
+namespace Unity.Netcode.TestHelpers.Runtime
+{
+ internal class MockTransport : NetworkTransport
+ {
+ private struct MessageData
+ {
+ public ulong FromClientId;
+ public ArraySegment Payload;
+ public NetworkEvent Event;
+ }
+
+ private static Dictionary> s_MessageQueue = new Dictionary>();
+
+ public override ulong ServerClientId { get; } = 0;
+
+ public static ulong HighTransportId = 0;
+ public ulong TransportId = 0;
+
+ public NetworkManager NetworkManager;
+
+ public override void Send(ulong clientId, ArraySegment payload, NetworkDelivery networkDelivery)
+ {
+ var copy = new byte[payload.Array.Length];
+ Array.Copy(payload.Array, copy, payload.Array.Length);
+ s_MessageQueue[clientId].Enqueue(new MessageData { FromClientId = TransportId, Payload = new ArraySegment(copy, payload.Offset, payload.Count), Event = NetworkEvent.Data });
+ }
+
+ public override NetworkEvent PollEvent(out ulong clientId, out ArraySegment payload, out float receiveTime)
+ {
+ if (s_MessageQueue[TransportId].Count > 0)
+ {
+ var data = s_MessageQueue[TransportId].Dequeue();
+ clientId = data.FromClientId;
+ payload = data.Payload;
+ receiveTime = NetworkManager.RealTimeProvider.RealTimeSinceStartup;
+ if (NetworkManager.IsServer && data.Event == NetworkEvent.Connect)
+ {
+ s_MessageQueue[data.FromClientId].Enqueue(new MessageData { Event = NetworkEvent.Connect, FromClientId = ServerClientId, Payload = new ArraySegment() });
+ }
+ return data.Event;
+ }
+ clientId = 0;
+ payload = new ArraySegment();
+ receiveTime = 0;
+ return NetworkEvent.Nothing;
+ }
+
+ public override bool StartClient()
+ {
+ TransportId = ++HighTransportId;
+ s_MessageQueue[TransportId] = new Queue();
+ s_MessageQueue[ServerClientId].Enqueue(new MessageData { Event = NetworkEvent.Connect, FromClientId = TransportId, Payload = new ArraySegment() });
+ return true;
+ }
+
+ public override bool StartServer()
+ {
+ s_MessageQueue[ServerClientId] = new Queue();
+ return true;
+ }
+
+ public override void DisconnectRemoteClient(ulong clientId)
+ {
+ s_MessageQueue[clientId].Enqueue(new MessageData { Event = NetworkEvent.Disconnect, FromClientId = TransportId, Payload = new ArraySegment() });
+ }
+
+ public override void DisconnectLocalClient()
+ {
+ s_MessageQueue[ServerClientId].Enqueue(new MessageData { Event = NetworkEvent.Disconnect, FromClientId = TransportId, Payload = new ArraySegment() });
+ }
+
+ public override ulong GetCurrentRtt(ulong clientId)
+ {
+ return 0;
+ }
+
+ public override void Shutdown()
+ {
+ }
+
+ public override void Initialize(NetworkManager networkManager = null)
+ {
+ NetworkManager = networkManager;
+ }
+ }
+}
diff --git a/TestHelpers/Runtime/MockTransport.cs.meta b/TestHelpers/Runtime/MockTransport.cs.meta
new file mode 100644
index 0000000..0f81ac8
--- /dev/null
+++ b/TestHelpers/Runtime/MockTransport.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 335908e9a37f428ba087acf00563c7be
+timeCreated: 1679415868
\ No newline at end of file
diff --git a/TestHelpers/Runtime/NetcodeIntegrationTest.cs b/TestHelpers/Runtime/NetcodeIntegrationTest.cs
index cce5d2f..87c678b 100644
--- a/TestHelpers/Runtime/NetcodeIntegrationTest.cs
+++ b/TestHelpers/Runtime/NetcodeIntegrationTest.cs
@@ -2,12 +2,14 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
+using System.Reflection;
+using System.Runtime.CompilerServices;
using NUnit.Framework;
+using Unity.Netcode.RuntimeTests;
+using Unity.Netcode.Transports.UTP;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.TestTools;
-using System.Runtime.CompilerServices;
-using Unity.Netcode.RuntimeTests;
using Object = UnityEngine.Object;
namespace Unity.Netcode.TestHelpers.Runtime
@@ -22,6 +24,7 @@ namespace Unity.Netcode.TestHelpers.Runtime
/// determine how clients will load scenes
///
internal static bool IsRunning { get; private set; }
+
protected static TimeoutHelper s_GlobalTimeoutHelper = new TimeoutHelper(8.0f);
protected static WaitForSecondsRealtime s_DefaultWaitForTick = new WaitForSecondsRealtime(1.0f / k_DefaultTickRate);
@@ -44,6 +47,7 @@ namespace Unity.Netcode.TestHelpers.Runtime
{
s_GlobalNetworkObjects.Add(networkObject.NetworkManager.LocalClientId, new Dictionary());
}
+
if (s_GlobalNetworkObjects[networkObject.NetworkManager.LocalClientId].ContainsKey(networkObject.NetworkObjectId))
{
if (s_GlobalNetworkObjects[networkObject.NetworkManager.LocalClientId] == null)
@@ -100,9 +104,9 @@ namespace Unity.Netcode.TestHelpers.Runtime
public enum NetworkManagerInstatiationMode
{
- PerTest, // This will create and destroy new NetworkManagers for each test within a child derived class
- AllTests, // This will create one set of NetworkManagers used for all tests within a child derived class (destroyed once all tests are finished)
- DoNotCreate // This will not create any NetworkManagers, it is up to the derived class to manage.
+ PerTest, // This will create and destroy new NetworkManagers for each test within a child derived class
+ AllTests, // This will create one set of NetworkManagers used for all tests within a child derived class (destroyed once all tests are finished)
+ DoNotCreate // This will not create any NetworkManagers, it is up to the derived class to manage.
}
public enum HostOrServer
@@ -143,6 +147,75 @@ namespace Unity.Netcode.TestHelpers.Runtime
///
protected bool m_BypassConnectionTimeout { get; set; }
+ ///
+ /// Enables "Time Travel" within the test, which swaps the time provider for the SDK from Unity's
+ /// class to , and also swaps the transport implementation
+ /// from to .
+ ///
+ /// This enables five important things that help with both performance and determinism of tests that involve a
+ /// lot of time and waiting:
+ /// 1) It allows time to move in a completely deterministic way (testing that something happens after n seconds,
+ /// the test will always move exactly n seconds with no chance of any variability in the timing),
+ /// 2) It allows skipping periods of time without actually waiting that amount of time, while still simulating
+ /// SDK frames as if that time were passing,
+ /// 3) It dissociates the SDK's update loop from Unity's update loop, allowing us to simulate SDK frame updates
+ /// without waiting for Unity to process things like physics, animation, and rendering that aren't relevant to
+ /// the test,
+ /// 4) It dissociates the SDK's messaging system from the networking hardware, meaning there's no delay between
+ /// a message being sent and it being received, allowing us to deterministically rely on the message being
+ /// received within specific time frames for the test, and
+ /// 5) It allows tests to be written without the use of coroutines, which not only improves the test's runtime,
+ /// but also results in easier-to-read callstacks and removes the possibility for an assertion to result in the
+ /// test hanging.
+ ///
+ /// When time travel is enabled, the following methods become available:
+ ///
+ /// : Simulates a specific number of frames passing over a specific time period
+ /// : Skips forward to the next tick, siumlating at the current application frame rate
+ /// : Simulates frames at the application frame rate until the given condition is true
+ /// : Simulates frames at the application frame rate until the required message is received
+ /// : Simulates frames at the application frame rate until the required messages are received
+ /// : Starts a server and client and allows them to connect via simulated frames
+ /// : Creates a client and waits for it to connect via simulated frames
+ /// Simulates frames at the application frame rate until the given clients are connected
+ /// : Stops a client and simulates frames until it's fully disconnected.
+ ///
+ /// When time travel is enabled, will automatically use these in its methods
+ /// when doing things like automatically connecting clients during SetUp.
+ ///
+ /// Additionally, the following methods replace their non-time-travel equivalents with variants that are not coroutines:
+ /// - called when server and clients are started
+ /// - called when server and clients are connected
+ ///
+ /// Note that all of the non-time travel functions can still be used even when time travel is enabled - this is
+ /// sometimes needed for, e.g., testing NetworkAnimator, where the unity update loop needs to run to process animations.
+ /// However, it's VERY important to note here that, because the SDK will not be operating based on real-world time
+ /// but based on the frozen time that's locked in from MockTimeProvider, actions that pass 10 seconds apart by
+ /// real-world clock time will be perceived by the SDK as having happened simultaneously if you don't call
+ /// to cover the equivalent time span in the mock time provider.
+ /// (Calling instead of
+ /// will move time forward without simulating any frames, which, in the case where real-world time has passed,
+ /// is likely more desirable). In most cases, this desynch won't affect anything, but it is worth noting that
+ /// it happens just in case a tested system depends on both the unity update loop happening *and* time moving forward.
+ ///
+ protected virtual bool m_EnableTimeTravel => false;
+
+ ///
+ /// If this is false, SetUp will call OnInlineSetUp instead of OnSetUp.
+ /// This is a performance advantage when not using the coroutine functionality, as a coroutine that
+ /// has no yield instructions in it will nonetheless still result in delaying the continuation of the
+ /// method that called it for a full frame after it returns.
+ ///
+ protected virtual bool m_SetupIsACoroutine => true;
+
+ ///
+ /// If this is false, TearDown will call OnInlineTearDown instead of OnTearDown.
+ /// This is a performance advantage when not using the coroutine functionality, as a coroutine that
+ /// has no yield instructions in it will nonetheless still result in delaying the continuation of the
+ /// method that called it for a full frame after it returns.
+ ///
+ protected virtual bool m_TearDownIsACoroutine => true;
+
///
/// Used to display the various integration test
/// stages and can be used to log verbose information
@@ -216,20 +289,54 @@ namespace Unity.Netcode.TestHelpers.Runtime
yield return null;
}
+ ///
+ /// Called before creating and starting the server and clients
+ /// Note: For and
+ /// mode integration tests.
+ /// For those two modes, if you want to have access to the server or client
+ /// s then override .
+ /// and
+ ///
+ protected virtual void OnInlineSetup()
+ {
+ }
+
[UnitySetUp]
public IEnumerator SetUp()
{
VerboseDebug($"Entering {nameof(SetUp)}");
NetcodeLogAssert = new NetcodeLogAssert();
- yield return OnSetup();
+ if (m_SetupIsACoroutine)
+ {
+ yield return OnSetup();
+ }
+ else
+ {
+ OnInlineSetup();
+ }
+
+ if (m_EnableTimeTravel)
+ {
+ MockTimeProvider.Reset();
+ ComponentFactory.Register(manager => new MockTimeProvider());
+ }
+
if (m_NetworkManagerInstatiationMode == NetworkManagerInstatiationMode.AllTests && m_ServerNetworkManager == null ||
m_NetworkManagerInstatiationMode == NetworkManagerInstatiationMode.PerTest)
{
CreateServerAndClients();
- yield return StartServerAndClients();
+ if (m_EnableTimeTravel)
+ {
+ StartServerAndClientsWithTimeTravel();
+ }
+ else
+ {
+ yield return StartServerAndClients();
+ }
}
+
VerboseDebug($"Exiting {nameof(SetUp)}");
}
@@ -294,6 +401,7 @@ namespace Unity.Netcode.TestHelpers.Runtime
{
clientNetworkManagersList.Remove(networkManager);
}
+
m_ClientNetworkManagers = clientNetworkManagersList.ToArray();
m_NumberOfClients = clientNetworkManagersList.Count;
}
@@ -304,7 +412,6 @@ namespace Unity.Netcode.TestHelpers.Runtime
///
protected virtual void OnNewClientCreated(NetworkManager networkManager)
{
-
}
///
@@ -322,7 +429,6 @@ namespace Unity.Netcode.TestHelpers.Runtime
///
protected virtual void OnNewClientStartedAndConnected(NetworkManager networkManager)
{
-
}
///
@@ -331,7 +437,7 @@ namespace Unity.Netcode.TestHelpers.Runtime
///
protected IEnumerator CreateAndStartNewClient()
{
- var networkManager = NetcodeIntegrationTestHelpers.CreateNewClient(m_ClientNetworkManagers.Length);
+ var networkManager = NetcodeIntegrationTestHelpers.CreateNewClient(m_ClientNetworkManagers.Length, m_EnableTimeTravel);
networkManager.NetworkConfig.PlayerPrefab = m_PlayerPrefab;
// Notification that the new client (NetworkManager) has been created
@@ -356,13 +462,53 @@ namespace Unity.Netcode.TestHelpers.Runtime
if (s_GlobalTimeoutHelper.TimedOut)
{
AddRemoveNetworkManager(networkManager, false);
- Object.Destroy(networkManager.gameObject);
+ Object.DestroyImmediate(networkManager.gameObject);
}
+
AssertOnTimeout($"{nameof(CreateAndStartNewClient)} timed out waiting for the new client to be connected!");
ClientNetworkManagerPostStart(networkManager);
VerboseDebug($"[{networkManager.name}] Created and connected!");
}
+ ///
+ /// This will create, start, and connect a new client while in the middle of an
+ /// integration test.
+ ///
+ protected void CreateAndStartNewClientWithTimeTravel()
+ {
+ var networkManager = NetcodeIntegrationTestHelpers.CreateNewClient(m_ClientNetworkManagers.Length, m_EnableTimeTravel);
+ networkManager.NetworkConfig.PlayerPrefab = m_PlayerPrefab;
+
+ // Notification that the new client (NetworkManager) has been created
+ // in the event any modifications need to be made before starting the client
+ OnNewClientCreated(networkManager);
+
+ NetcodeIntegrationTestHelpers.StartOneClient(networkManager);
+
+ if (LogAllMessages)
+ {
+ networkManager.MessagingSystem.Hook(new DebugNetworkHooks());
+ }
+
+ AddRemoveNetworkManager(networkManager, true);
+
+ OnNewClientStarted(networkManager);
+
+ // Wait for the new client to connect
+ var connected = WaitForClientsConnectedOrTimeOutWithTimeTravel();
+
+ OnNewClientStartedAndConnected(networkManager);
+ if (!connected)
+ {
+ AddRemoveNetworkManager(networkManager, false);
+ Object.DestroyImmediate(networkManager.gameObject);
+ }
+
+ Assert.IsTrue(connected, $"{nameof(CreateAndStartNewClient)} timed out waiting for the new client to be connected!");
+ ClientNetworkManagerPostStart(networkManager);
+ VerboseDebug($"[{networkManager.name}] Created and connected!");
+ }
+
///
/// This will stop a client while in the middle of an integration test
///
@@ -373,6 +519,16 @@ namespace Unity.Netcode.TestHelpers.Runtime
yield return WaitForConditionOrTimeOut(() => !networkManager.IsConnectedClient);
}
+ ///
+ /// This will stop a client while in the middle of an integration test
+ ///
+ protected void StopOneClientWithTimeTravel(NetworkManager networkManager, bool destroy = false)
+ {
+ NetcodeIntegrationTestHelpers.StopOneClient(networkManager, destroy);
+ AddRemoveNetworkManager(networkManager, false);
+ Assert.True(WaitForConditionOrTimeOutWithTimeTravel(() => !networkManager.IsConnectedClient));
+ }
+
///
/// Creates the server and clients
///
@@ -383,8 +539,13 @@ namespace Unity.Netcode.TestHelpers.Runtime
CreatePlayerPrefab();
+ if (m_EnableTimeTravel)
+ {
+ m_TargetFrameRate = -1;
+ }
+
// Create multiple NetworkManager instances
- if (!NetcodeIntegrationTestHelpers.Create(numberOfClients, out NetworkManager server, out NetworkManager[] clients, m_TargetFrameRate, m_CreateServerFirst))
+ if (!NetcodeIntegrationTestHelpers.Create(numberOfClients, out NetworkManager server, out NetworkManager[] clients, m_TargetFrameRate, m_CreateServerFirst, m_EnableTimeTravel))
{
Debug.LogError("Failed to create instances");
Assert.Fail("Failed to create instances");
@@ -431,6 +592,14 @@ namespace Unity.Netcode.TestHelpers.Runtime
yield return null;
}
+ ///
+ /// Invoked after the server and clients have started.
+ /// Note: No connection verification has been done at this point
+ ///
+ protected virtual void OnTimeTravelStartedServerAndClients()
+ {
+ }
+
///
/// Invoked after the server and clients have started and verified
/// their connections with each other.
@@ -440,6 +609,14 @@ namespace Unity.Netcode.TestHelpers.Runtime
yield return null;
}
+ ///
+ /// Invoked after the server and clients have started and verified
+ /// their connections with each other.
+ ///
+ protected virtual void OnTimeTravelServerAndClientsConnected()
+ {
+ }
+
private void ClientNetworkManagerPostStart(NetworkManager networkManager)
{
networkManager.name = $"NetworkManager - Client - {networkManager.LocalClientId}";
@@ -466,6 +643,7 @@ namespace Unity.Netcode.TestHelpers.Runtime
{
m_PlayerNetworkObjects.Add(playerNetworkObject.NetworkManager.LocalClientId, new Dictionary());
}
+
if (!m_PlayerNetworkObjects[playerNetworkObject.NetworkManager.LocalClientId].ContainsKey(networkManager.LocalClientId))
{
m_PlayerNetworkObjects[playerNetworkObject.NetworkManager.LocalClientId].Add(networkManager.LocalClientId, playerNetworkObject);
@@ -495,6 +673,7 @@ namespace Unity.Netcode.TestHelpers.Runtime
{
ClientNetworkManagerPostStart(networkManager);
}
+
if (m_UseHost)
{
#if UNITY_2023_1_OR_NEWER
@@ -509,6 +688,7 @@ namespace Unity.Netcode.TestHelpers.Runtime
{
m_PlayerNetworkObjects.Add(playerNetworkObject.NetworkManager.LocalClientId, new Dictionary());
}
+
if (!m_PlayerNetworkObjects[playerNetworkObject.NetworkManager.LocalClientId].ContainsKey(m_ServerNetworkManager.LocalClientId))
{
m_PlayerNetworkObjects[playerNetworkObject.NetworkManager.LocalClientId].Add(m_ServerNetworkManager.LocalClientId, playerNetworkObject);
@@ -570,6 +750,7 @@ namespace Unity.Netcode.TestHelpers.Runtime
{
m_PlayerNetworkObjects.Add(playerNetworkObject.NetworkManager.LocalClientId, new Dictionary());
}
+
m_PlayerNetworkObjects[playerNetworkObject.NetworkManager.LocalClientId].Add(m_ServerNetworkManager.LocalClientId, playerNetworkObject);
}
}
@@ -585,6 +766,73 @@ namespace Unity.Netcode.TestHelpers.Runtime
}
}
+ ///
+ /// This starts the server and clients as long as
+ /// returns true.
+ ///
+ protected void StartServerAndClientsWithTimeTravel()
+ {
+ if (CanStartServerAndClients())
+ {
+ VerboseDebug($"Entering {nameof(StartServerAndClientsWithTimeTravel)}");
+
+ // Start the instances and pass in our SceneManagerInitialization action that is invoked immediately after host-server
+ // is started and after each client is started.
+ if (!NetcodeIntegrationTestHelpers.Start(m_UseHost, m_ServerNetworkManager, m_ClientNetworkManagers))
+ {
+ Debug.LogError("Failed to start instances");
+ Assert.Fail("Failed to start instances");
+ }
+
+ if (LogAllMessages)
+ {
+ EnableMessageLogging();
+ }
+
+ RegisterSceneManagerHandler();
+
+ // Notification that the server and clients have been started
+ OnTimeTravelStartedServerAndClients();
+
+ // When true, we skip everything else (most likely a connection oriented test)
+ if (!m_BypassConnectionTimeout)
+ {
+ // Wait for all clients to connect
+ WaitForClientsConnectedOrTimeOutWithTimeTravel();
+
+ AssertOnTimeout($"{nameof(StartServerAndClients)} timed out waiting for all clients to be connected!");
+
+ if (m_UseHost || m_ServerNetworkManager.IsHost)
+ {
+#if UNITY_2023_1_OR_NEWER
+ // Add the server player instance to all m_ClientSidePlayerNetworkObjects entries
+ var serverPlayerClones = Object.FindObjectsByType(FindObjectsSortMode.None).Where((c) => c.IsPlayerObject && c.OwnerClientId == m_ServerNetworkManager.LocalClientId);
+#else
+ // Add the server player instance to all m_ClientSidePlayerNetworkObjects entries
+ var serverPlayerClones = Object.FindObjectsOfType().Where((c) => c.IsPlayerObject && c.OwnerClientId == m_ServerNetworkManager.LocalClientId);
+#endif
+ foreach (var playerNetworkObject in serverPlayerClones)
+ {
+ if (!m_PlayerNetworkObjects.ContainsKey(playerNetworkObject.NetworkManager.LocalClientId))
+ {
+ m_PlayerNetworkObjects.Add(playerNetworkObject.NetworkManager.LocalClientId, new Dictionary());
+ }
+
+ m_PlayerNetworkObjects[playerNetworkObject.NetworkManager.LocalClientId].Add(m_ServerNetworkManager.LocalClientId, playerNetworkObject);
+ }
+ }
+
+ ClientNetworkManagerPostStartInit();
+
+ // Notification that at this time the server and client(s) are instantiated,
+ // started, and connected on both sides.
+ OnTimeTravelServerAndClientsConnected();
+
+ VerboseDebug($"Exiting {nameof(StartServerAndClients)}");
+ }
+ }
+ }
+
///
/// Override this method to control when clients
/// can fake-load a scene.
@@ -660,12 +908,15 @@ namespace Unity.Netcode.TestHelpers.Runtime
m_PlayerNetworkObjects.Clear();
s_GlobalNetworkObjects.Clear();
}
- catch (Exception e) { throw e; }
+ catch (Exception e)
+ {
+ throw e;
+ }
finally
{
if (m_PlayerPrefab != null)
{
- Object.Destroy(m_PlayerPrefab);
+ Object.DestroyImmediate(m_PlayerPrefab);
m_PlayerPrefab = null;
}
}
@@ -689,17 +940,34 @@ namespace Unity.Netcode.TestHelpers.Runtime
yield return null;
}
+ protected virtual void OnInlineTearDown()
+ {
+ }
+
[UnityTearDown]
public IEnumerator TearDown()
{
+ IntegrationTestSceneHandler.SceneNameToSceneHandles.Clear();
VerboseDebug($"Entering {nameof(TearDown)}");
- yield return OnTearDown();
+ if (m_TearDownIsACoroutine)
+ {
+ yield return OnTearDown();
+ }
+ else
+ {
+ OnInlineTearDown();
+ }
if (m_NetworkManagerInstatiationMode == NetworkManagerInstatiationMode.PerTest)
{
ShutdownAndCleanUp();
}
+ if (m_EnableTimeTravel)
+ {
+ ComponentFactory.Deregister();
+ }
+
VerboseDebug($"Exiting {nameof(TearDown)}");
LogWaitForMessages();
NetcodeLogAssert.Dispose();
@@ -773,6 +1041,7 @@ namespace Unity.Netcode.TestHelpers.Runtime
{
continue;
}
+
if (CanDestroyNetworkObject(networkObject))
{
networkObject.NetworkManagerOwner = m_ServerNetworkManager;
@@ -831,10 +1100,49 @@ namespace Unity.Netcode.TestHelpers.Runtime
// Otherwise wait for 1 tick interval
yield return s_DefaultWaitForTick;
}
+
// Stop checking for a timeout
timeOutHelper.Stop();
}
+
+ ///
+ /// Waits for the function condition to return true or it will time out. Uses time travel to simulate this
+ /// for the given number of frames, simulating delta times at the application frame rate.
+ ///
+ public bool WaitForConditionOrTimeOutWithTimeTravel(Func checkForCondition, int maxTries = 60)
+ {
+ if (checkForCondition == null)
+ {
+ throw new ArgumentNullException($"checkForCondition cannot be null!");
+ }
+
+ if (!m_EnableTimeTravel)
+ {
+ throw new ArgumentException($"Time travel must be enabled to use {nameof(WaitForConditionOrTimeOutWithTimeTravel)}!");
+ }
+
+ var frameRate = Application.targetFrameRate;
+ if (frameRate <= 0)
+ {
+ frameRate = 60;
+ }
+
+ var updateInterval = 1f / frameRate;
+ for (var i = 0; i < maxTries; ++i)
+ {
+ // Simulate a frame passing on all network managers
+ TimeTravel(updateInterval, 1);
+ // Update and check to see if the condition has been met
+ if (checkForCondition.Invoke())
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
///
/// This version accepts an IConditionalPredicate implementation to provide
/// more flexibility for checking complex conditional cases.
@@ -857,6 +1165,29 @@ namespace Unity.Netcode.TestHelpers.Runtime
conditionalPredicate.Finished(timeOutHelper.TimedOut);
}
+ ///
+ /// This version accepts an IConditionalPredicate implementation to provide
+ /// more flexibility for checking complex conditional cases. Uses time travel to simulate this
+ /// for the given number of frames, simulating delta times at the application frame rate.
+ ///
+ public bool WaitForConditionOrTimeOutWithTimeTravel(IConditionalPredicate conditionalPredicate, int maxTries = 60)
+ {
+ if (conditionalPredicate == null)
+ {
+ throw new ArgumentNullException($"checkForCondition cannot be null!");
+ }
+
+ if (!m_EnableTimeTravel)
+ {
+ throw new ArgumentException($"Time travel must be enabled to use {nameof(WaitForConditionOrTimeOutWithTimeTravel)}!");
+ }
+
+ conditionalPredicate.Started();
+ var success = WaitForConditionOrTimeOutWithTimeTravel(conditionalPredicate.HasConditionBeenReached, maxTries);
+ conditionalPredicate.Finished(!success);
+ return success;
+ }
+
///
/// Validates that all remote clients (i.e. non-server) detect they are connected
/// to the server and that the server reflects the appropriate number of clients
@@ -869,7 +1200,23 @@ namespace Unity.Netcode.TestHelpers.Runtime
var serverClientCount = m_ServerNetworkManager.IsHost ? remoteClientCount + 1 : remoteClientCount;
yield return WaitForConditionOrTimeOut(() => clientsToCheck.Where((c) => c.IsConnectedClient).Count() == remoteClientCount &&
- m_ServerNetworkManager.ConnectedClients.Count == serverClientCount);
+ m_ServerNetworkManager.ConnectedClients.Count == serverClientCount);
+ }
+
+ ///
+ /// Validates that all remote clients (i.e. non-server) detect they are connected
+ /// to the server and that the server reflects the appropriate number of clients
+ /// have connected or it will time out. Uses time travel to simulate this
+ /// for the given number of frames, simulating delta times at the application frame rate.
+ ///
+ /// An array of clients to be checked
+ protected bool WaitForClientsConnectedOrTimeOutWithTimeTravel(NetworkManager[] clientsToCheck)
+ {
+ var remoteClientCount = clientsToCheck.Length;
+ var serverClientCount = m_ServerNetworkManager.IsHost ? remoteClientCount + 1 : remoteClientCount;
+
+ return WaitForConditionOrTimeOutWithTimeTravel(() => clientsToCheck.Where((c) => c.IsConnectedClient).Count() == remoteClientCount &&
+ m_ServerNetworkManager.ConnectedClients.Count == serverClientCount);
}
///
@@ -881,6 +1228,16 @@ namespace Unity.Netcode.TestHelpers.Runtime
yield return WaitForClientsConnectedOrTimeOut(m_ClientNetworkManagers);
}
+ ///
+ /// Overloaded method that just passes in all clients to
+ /// Uses time travel to simulate this
+ /// for the given number of frames, simulating delta times at the application frame rate.
+ ///
+ protected bool WaitForClientsConnectedOrTimeOutWithTimeTravel()
+ {
+ return WaitForClientsConnectedOrTimeOutWithTimeTravel(m_ClientNetworkManagers);
+ }
+
internal IEnumerator WaitForMessageReceived(List wiatForReceivedBy, ReceiptType type = ReceiptType.Handled) where T : INetworkMessage
{
// Build our message hook entries tables so we can determine if all clients received spawn or ownership messages
@@ -891,17 +1248,18 @@ namespace Unity.Netcode.TestHelpers.Runtime
messageHook.AssignMessageType();
messageHookEntriesForSpawn.Add(messageHook);
}
+
// Used to determine if all clients received the CreateObjectMessage
var hooks = new MessageHooksConditional(messageHookEntriesForSpawn);
yield return WaitForConditionOrTimeOut(hooks);
Assert.False(s_GlobalTimeoutHelper.TimedOut);
}
- internal IEnumerator WaitForMessagesReceived(List messagesInOrder, List wiatForReceivedBy, ReceiptType type = ReceiptType.Handled)
+ internal IEnumerator WaitForMessagesReceived(List messagesInOrder, List waitForReceivedBy, ReceiptType type = ReceiptType.Handled)
{
// Build our message hook entries tables so we can determine if all clients received spawn or ownership messages
var messageHookEntriesForSpawn = new List();
- foreach (var clientNetworkManager in wiatForReceivedBy)
+ foreach (var clientNetworkManager in waitForReceivedBy)
{
foreach (var message in messagesInOrder)
{
@@ -910,12 +1268,49 @@ namespace Unity.Netcode.TestHelpers.Runtime
messageHookEntriesForSpawn.Add(messageHook);
}
}
+
// Used to determine if all clients received the CreateObjectMessage
var hooks = new MessageHooksConditional(messageHookEntriesForSpawn);
yield return WaitForConditionOrTimeOut(hooks);
Assert.False(s_GlobalTimeoutHelper.TimedOut);
}
+
+ internal void WaitForMessageReceivedWithTimeTravel(List waitForReceivedBy, ReceiptType type = ReceiptType.Handled) where T : INetworkMessage
+ {
+ // Build our message hook entries tables so we can determine if all clients received spawn or ownership messages
+ var messageHookEntriesForSpawn = new List();
+ foreach (var clientNetworkManager in waitForReceivedBy)
+ {
+ var messageHook = new MessageHookEntry(clientNetworkManager, type);
+ messageHook.AssignMessageType();
+ messageHookEntriesForSpawn.Add(messageHook);
+ }
+
+ // Used to determine if all clients received the CreateObjectMessage
+ var hooks = new MessageHooksConditional(messageHookEntriesForSpawn);
+ Assert.True(WaitForConditionOrTimeOutWithTimeTravel(hooks));
+ }
+
+ internal void WaitForMessagesReceivedWithTimeTravel(List messagesInOrder, List waitForReceivedBy, ReceiptType type = ReceiptType.Handled)
+ {
+ // Build our message hook entries tables so we can determine if all clients received spawn or ownership messages
+ var messageHookEntriesForSpawn = new List();
+ foreach (var clientNetworkManager in waitForReceivedBy)
+ {
+ foreach (var message in messagesInOrder)
+ {
+ var messageHook = new MessageHookEntry(clientNetworkManager, type);
+ messageHook.AssignMessageType(message);
+ messageHookEntriesForSpawn.Add(messageHook);
+ }
+ }
+
+ // Used to determine if all clients received the CreateObjectMessage
+ var hooks = new MessageHooksConditional(messageHookEntriesForSpawn);
+ Assert.True(WaitForConditionOrTimeOutWithTimeTravel(hooks));
+ }
+
///
/// Creates a basic NetworkObject test prefab, assigns it to a new
/// NetworkPrefab entry, and then adds it to the server and client(s)
@@ -926,7 +1321,7 @@ namespace Unity.Netcode.TestHelpers.Runtime
protected GameObject CreateNetworkObjectPrefab(string baseName)
{
var prefabCreateAssertError = $"You can only invoke this method during {nameof(OnServerAndClientsCreated)} " +
- $"but before {nameof(OnStartedServerAndClients)}!";
+ $"but before {nameof(OnStartedServerAndClients)}!";
Assert.IsNotNull(m_ServerNetworkManager, prefabCreateAssertError);
Assert.IsFalse(m_ServerNetworkManager.IsListening, prefabCreateAssertError);
@@ -1000,6 +1395,7 @@ namespace Unity.Netcode.TestHelpers.Runtime
{
gameObjectsSpawned.Add(SpawnObject(prefabNetworkObject, owner, destroyWithScene));
}
+
return gameObjectsSpawned;
}
@@ -1008,7 +1404,6 @@ namespace Unity.Netcode.TestHelpers.Runtime
///
public NetcodeIntegrationTest()
{
-
}
///
@@ -1038,7 +1433,7 @@ namespace Unity.Netcode.TestHelpers.Runtime
///
protected void AssertOnTimeout(string timeOutErrorMessage, TimeoutHelper assignedTimeoutHelper = null)
{
- var timeoutHelper = assignedTimeoutHelper != null ? assignedTimeoutHelper : s_GlobalTimeoutHelper;
+ var timeoutHelper = assignedTimeoutHelper ?? s_GlobalTimeoutHelper;
Assert.False(timeoutHelper.TimedOut, timeOutErrorMessage);
}
@@ -1054,6 +1449,7 @@ namespace Unity.Netcode.TestHelpers.Runtime
{
continue;
}
+
VerboseDebug($"Unloading scene {scene.name}-{scene.handle}");
var asyncOperation = SceneManager.UnloadSceneAsync(scene);
}
@@ -1093,6 +1489,7 @@ namespace Unity.Netcode.TestHelpers.Runtime
}
}
}
+
m_WaitForLog.Append($"[NetworkManager-{networkManager.LocalClientId}][WaitForTicks-End] Waited for ({networkManager.NetworkTickSystem.LocalTime.Tick - tickStart}) network ticks and ({frameCount}) frames to pass.\n");
yield break;
}
@@ -1114,5 +1511,83 @@ namespace Unity.Netcode.TestHelpers.Runtime
m_WaitForLog.Append($"[NetworkManager-{networkManager.LocalClientId}][WaitForTicks] TickRate ({networkManager.NetworkConfig.TickRate}) | Tick Wait ({count}) | TargetFrameRate ({Application.targetFrameRate}) | Target Frames ({framesPerTick * count})\n");
yield return WaitForTickAndFrames(networkManager, count, totalFrameCount);
}
+
+ ///
+ /// Simulate a number of frames passing over a specific amount of time.
+ /// The delta time simulated for each frame will be evenly divided as time/numFrames
+ /// This will only simulate the netcode update loop, as well as update events on
+ /// NetworkBehaviour instances, and will not simulate any Unity update processes (physics, etc)
+ ///
+ ///
+ ///
+ protected static void TimeTravel(double amountOfTimeInSeconds, int numFramesToSimulate)
+ {
+ var interval = amountOfTimeInSeconds / numFramesToSimulate;
+ for (var i = 0; i < numFramesToSimulate; ++i)
+ {
+ MockTimeProvider.TimeTravel(interval);
+ SimulateOneFrame();
+ }
+ }
+
+ ///
+ /// Helper function to time travel exactly one tick's worth of time at the current frame and tick rates.
+ ///
+ public static void TimeTravelToNextTick()
+ {
+ var timePassed = 1.0f / k_DefaultTickRate;
+ var frameRate = Application.targetFrameRate;
+ if (frameRate <= 0)
+ {
+ frameRate = 60;
+ }
+
+ var frames = Math.Max((int)(timePassed / frameRate), 1);
+ TimeTravel(timePassed, frames);
+ }
+
+ ///
+ /// Simulates one SDK frame. This can be used even without TimeTravel, though it's of somewhat less use
+ /// without TimeTravel, as, without the mock transport, it will likely not provide enough time for any
+ /// sent messages to be received even if called dozens of times.
+ ///
+ public static void SimulateOneFrame()
+ {
+ foreach (NetworkUpdateStage stage in Enum.GetValues(typeof(NetworkUpdateStage)))
+ {
+ NetworkUpdateLoop.RunNetworkUpdateStage(stage);
+ string methodName = string.Empty;
+ switch (stage)
+ {
+ case NetworkUpdateStage.FixedUpdate:
+ methodName = "FixedUpdate"; // mapping NetworkUpdateStage.FixedUpdate to MonoBehaviour.FixedUpdate
+ break;
+ case NetworkUpdateStage.Update:
+ methodName = "Update"; // mapping NetworkUpdateStage.Update to MonoBehaviour.Update
+ break;
+ case NetworkUpdateStage.PreLateUpdate:
+ methodName = "LateUpdate"; // mapping NetworkUpdateStage.PreLateUpdate to MonoBehaviour.LateUpdate
+ break;
+ }
+
+ if (!string.IsNullOrEmpty(methodName))
+ {
+#if UNITY_2023_1_OR_NEWER
+ foreach (var behaviour in Object.FindObjectsByType(FindObjectsSortMode.InstanceID))
+#else
+ foreach (var behaviour in Object.FindObjectsOfType())
+#endif
+ {
+ var method = behaviour.GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance);
+ if (method == null)
+ {
+ method = behaviour.GetType().GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance);
+ }
+
+ method?.Invoke(behaviour, new object[] { });
+ }
+ }
+ }
+ }
}
}
diff --git a/TestHelpers/Runtime/NetcodeIntegrationTestHelpers.cs b/TestHelpers/Runtime/NetcodeIntegrationTestHelpers.cs
index b360d56..35adac2 100644
--- a/TestHelpers/Runtime/NetcodeIntegrationTestHelpers.cs
+++ b/TestHelpers/Runtime/NetcodeIntegrationTestHelpers.cs
@@ -186,7 +186,16 @@ namespace Unity.Netcode.TestHelpers.Runtime
networkManager.NetworkConfig.NetworkTransport = unityTransport;
}
- public static NetworkManager CreateServer()
+ private static void AddMockTransport(NetworkManager networkManager)
+ {
+ // Create transport
+ var mockTransport = networkManager.gameObject.AddComponent();
+ // Set the NetworkConfig
+ networkManager.NetworkConfig ??= new NetworkConfig();
+ networkManager.NetworkConfig.NetworkTransport = mockTransport;
+ }
+
+ public static NetworkManager CreateServer(bool mockTransport = false)
{
// Create gameObject
var go = new GameObject("NetworkManager - Server");
@@ -194,7 +203,14 @@ namespace Unity.Netcode.TestHelpers.Runtime
// Create networkManager component
var server = go.AddComponent();
NetworkManagerInstances.Insert(0, server);
- AddUnityTransport(server);
+ if (mockTransport)
+ {
+ AddMockTransport(server);
+ }
+ else
+ {
+ AddUnityTransport(server);
+ }
return server;
}
@@ -206,20 +222,20 @@ namespace Unity.Netcode.TestHelpers.Runtime
/// The clients NetworkManagers
/// The targetFrameRate of the Unity engine to use while the multi instance helper is running. Will be reset on shutdown.
/// This determines if the server or clients will be instantiated first (defaults to server first)
- public static bool Create(int clientCount, out NetworkManager server, out NetworkManager[] clients, int targetFrameRate = 60, bool serverFirst = true)
+ public static bool Create(int clientCount, out NetworkManager server, out NetworkManager[] clients, int targetFrameRate = 60, bool serverFirst = true, bool useMockTransport = false)
{
s_NetworkManagerInstances = new List();
server = null;
if (serverFirst)
{
- server = CreateServer();
+ server = CreateServer(useMockTransport);
}
- CreateNewClients(clientCount, out clients);
+ CreateNewClients(clientCount, out clients, useMockTransport);
if (!serverFirst)
{
- server = CreateServer();
+ server = CreateServer(useMockTransport);
}
s_OriginalTargetFrameRate = Application.targetFrameRate;
@@ -228,13 +244,20 @@ namespace Unity.Netcode.TestHelpers.Runtime
return true;
}
- internal static NetworkManager CreateNewClient(int identifier)
+ internal static NetworkManager CreateNewClient(int identifier, bool mockTransport = false)
{
// Create gameObject
var go = new GameObject("NetworkManager - Client - " + identifier);
// Create networkManager component
var networkManager = go.AddComponent();
- AddUnityTransport(networkManager);
+ if (mockTransport)
+ {
+ AddMockTransport(networkManager);
+ }
+ else
+ {
+ AddUnityTransport(networkManager);
+ }
return networkManager;
}
@@ -244,13 +267,13 @@ namespace Unity.Netcode.TestHelpers.Runtime
///
/// The amount of clients
///
- public static bool CreateNewClients(int clientCount, out NetworkManager[] clients)
+ public static bool CreateNewClients(int clientCount, out NetworkManager[] clients, bool useMockTransport = false)
{
clients = new NetworkManager[clientCount];
for (int i = 0; i < clientCount; i++)
{
// Create networkManager component
- clients[i] = CreateNewClient(i);
+ clients[i] = CreateNewClient(i, useMockTransport);
}
NetworkManagerInstances.AddRange(clients);
@@ -314,7 +337,7 @@ namespace Unity.Netcode.TestHelpers.Runtime
{
if (networkManager.gameObject != null)
{
- Object.Destroy(networkManager.gameObject);
+ Object.DestroyImmediate(networkManager.gameObject);
}
}
@@ -339,6 +362,12 @@ namespace Unity.Netcode.TestHelpers.Runtime
return true;
}
+ private static bool VerifySceneIsValidForClientsToUnload(Scene scene)
+ {
+ // Unless specifically set, we always return false
+ return false;
+ }
+
///
/// This registers scene validation callback for the server to prevent it from telling connecting
/// clients to synchronize (i.e. load) the test runner scene. This will also register the test runner
@@ -351,10 +380,21 @@ namespace Unity.Netcode.TestHelpers.Runtime
if (networkManager.IsServer && networkManager.SceneManager.VerifySceneBeforeLoading == null)
{
networkManager.SceneManager.VerifySceneBeforeLoading = VerifySceneIsValidForClientsToLoad;
+
// If a unit/integration test does not handle this on their own, then Ignore the validation warning
networkManager.SceneManager.DisableValidationWarnings(true);
}
+ // For testing purposes, all clients always set the VerifySceneBeforeUnloading callback and enabled
+ // PostSynchronizationSceneUnloading. Where tests that expect clients to unload scenes should override
+ // the callback and return true for the scenes the client(s) is/are allowed to unload.
+ if (!networkManager.IsServer && networkManager.SceneManager.VerifySceneBeforeUnloading == null)
+ {
+ networkManager.SceneManager.VerifySceneBeforeUnloading = VerifySceneIsValidForClientsToUnload;
+ networkManager.SceneManager.PostSynchronizationSceneUnloading = true;
+ }
+
+
// Register the test runner scene so it will be able to synchronize NetworkObjects without logging a
// warning about using the currently active scene
var scene = SceneManager.GetActiveScene();
@@ -494,8 +534,10 @@ namespace Unity.Netcode.TestHelpers.Runtime
Assert.IsNotNull(server, prefabCreateAssertError);
Assert.IsFalse(server.IsListening, prefabCreateAssertError);
- var gameObject = new GameObject();
- gameObject.name = baseName;
+ var gameObject = new GameObject
+ {
+ name = baseName
+ };
var networkObject = gameObject.AddComponent();
networkObject.NetworkManagerOwner = server;
MakeNetworkObjectTestPrefab(networkObject);
@@ -715,6 +757,40 @@ namespace Unity.Netcode.TestHelpers.Runtime
}
}
+ ///
+ /// Gets a NetworkObject instance as it's represented by a certain peer.
+ ///
+ /// The predicate used to filter for your target NetworkObject
+ /// The representation to get the object from
+ /// The result
+ /// Whether or not to fail if no object is found and result is null
+ /// The max frames to wait for
+ public static void GetNetworkObjectByRepresentationWithTimeTravel(Func predicate, NetworkManager representation, ResultWrapper result, bool failIfNull = true, int maxTries = 60)
+ {
+ if (result == null)
+ {
+ throw new ArgumentNullException("Result cannot be null");
+ }
+
+ if (predicate == null)
+ {
+ throw new ArgumentNullException("Predicate cannot be null");
+ }
+
+ var tries = 0;
+ while (++tries < maxTries && !representation.SpawnManager.SpawnedObjects.Any(x => predicate(x.Value)))
+ {
+ NetcodeIntegrationTest.SimulateOneFrame();
+ }
+
+ result.Result = representation.SpawnManager.SpawnedObjects.FirstOrDefault(x => predicate(x.Value)).Value;
+
+ if (failIfNull && result.Result == null)
+ {
+ Assert.Fail("NetworkObject could not be found");
+ }
+ }
+
///
/// Waits for a predicate condition to be met
///
diff --git a/TestHelpers/Runtime/NetworkManagerHelper.cs b/TestHelpers/Runtime/NetworkManagerHelper.cs
index a45af70..23aad06 100644
--- a/TestHelpers/Runtime/NetworkManagerHelper.cs
+++ b/TestHelpers/Runtime/NetworkManagerHelper.cs
@@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
-using UnityEngine;
using NUnit.Framework;
using Unity.Netcode.Transports.UTP;
+using UnityEngine;
namespace Unity.Netcode.TestHelpers.Runtime
{
diff --git a/TestHelpers/Runtime/TimeoutHelper.cs b/TestHelpers/Runtime/TimeoutHelper.cs
index f5d2f70..1de7802 100644
--- a/TestHelpers/Runtime/TimeoutHelper.cs
+++ b/TestHelpers/Runtime/TimeoutHelper.cs
@@ -8,30 +8,67 @@ namespace Unity.Netcode.TestHelpers.Runtime
///
public class TimeoutHelper
{
- private const float k_DefaultTimeOutWaitPeriod = 2.0f;
+ protected const float k_DefaultTimeOutWaitPeriod = 2.0f;
+
private float m_MaximumTimeBeforeTimeOut;
private float m_TimeOutPeriod;
- private bool m_IsStarted;
+ protected bool m_IsStarted { get; private set; }
public bool TimedOut { get; internal set; }
+ private float m_TimeStarted;
+ private float m_TimeStopped;
+
+ public float GetTimeElapsed()
+ {
+ if (m_IsStarted)
+ {
+ return Time.realtimeSinceStartup - m_TimeStarted;
+ }
+ else
+ {
+ return m_TimeStopped - m_TimeStarted;
+ }
+ }
+
+ protected virtual void OnStart()
+ {
+ }
+
public void Start()
{
+ m_TimeStopped = 0.0f;
+ m_TimeStarted = Time.realtimeSinceStartup;
m_MaximumTimeBeforeTimeOut = Time.realtimeSinceStartup + m_TimeOutPeriod;
m_IsStarted = true;
TimedOut = false;
+ OnStart();
+ }
+
+ protected virtual void OnStop()
+ {
}
public void Stop()
{
+ if (m_TimeStopped == 0.0f)
+ {
+ m_TimeStopped = Time.realtimeSinceStartup;
+ }
TimedOut = HasTimedOut();
m_IsStarted = false;
+ OnStop();
+ }
+
+ protected virtual bool OnHasTimedOut()
+ {
+ return m_IsStarted ? m_MaximumTimeBeforeTimeOut < Time.realtimeSinceStartup : TimedOut;
}
public bool HasTimedOut()
{
- return m_IsStarted ? m_MaximumTimeBeforeTimeOut < Time.realtimeSinceStartup : TimedOut;
+ return OnHasTimedOut();
}
public TimeoutHelper(float timeOutPeriod = k_DefaultTimeOutWaitPeriod)
@@ -39,4 +76,70 @@ namespace Unity.Netcode.TestHelpers.Runtime
m_TimeOutPeriod = timeOutPeriod;
}
}
+
+ ///
+ /// This can be used in place of TimeoutHelper if you suspect a test is having
+ /// issues on a system where the frame rate is running slow than expected and
+ /// allowing a certain number of frame updates is required.
+ ///
+ public class TimeoutFrameCountHelper : TimeoutHelper
+ {
+ private const uint k_DefaultTickRate = 30;
+
+ private float m_TotalFramesToWait;
+ private int m_StartFrameCount;
+ private int m_EndFrameCount;
+ private bool m_ReachedFrameCount;
+
+ public int GetFrameCount()
+ {
+ if (m_IsStarted)
+ {
+ return Time.frameCount - m_StartFrameCount;
+ }
+ else
+ {
+ return m_EndFrameCount - m_StartFrameCount;
+ }
+ }
+
+ protected override void OnStop()
+ {
+ if (m_EndFrameCount == 0)
+ {
+ m_EndFrameCount = Time.frameCount;
+ }
+ base.OnStop();
+ }
+
+ protected override bool OnHasTimedOut()
+ {
+ var currentFrameCountDelta = Time.frameCount - m_StartFrameCount;
+ if (m_IsStarted)
+ {
+ m_ReachedFrameCount = currentFrameCountDelta >= m_TotalFramesToWait;
+ }
+ // Only time out if we have both exceeded the time period and the expected number of frames has reached the expected number of frames
+ // (this handles the scenario where some systems are running a much lower frame rate)
+ return m_ReachedFrameCount && base.OnHasTimedOut();
+ }
+
+ protected override void OnStart()
+ {
+ m_EndFrameCount = 0;
+ m_StartFrameCount = Time.frameCount;
+ base.OnStart();
+ }
+
+ public TimeoutFrameCountHelper(float timeOutPeriod = k_DefaultTimeOutWaitPeriod, uint tickRate = k_DefaultTickRate) : base(timeOutPeriod)
+ {
+ // Calculate the expected number of frame updates that should occur during the tick count wait period
+ var frameFrequency = 1.0f / (Application.targetFrameRate >= 60 && Application.targetFrameRate <= 100 ? Application.targetFrameRate : 60.0f);
+ var tickFrequency = 1.0f / tickRate;
+ var framesPerTick = tickFrequency / frameFrequency;
+ var totalExpectedTicks = timeOutPeriod / tickFrequency;
+
+ m_TotalFramesToWait = framesPerTick * totalExpectedTicks;
+ }
+ }
}
diff --git a/Tests/Editor/DisconnectMessageTests.cs b/Tests/Editor/DisconnectMessageTests.cs
index 58f5502..644ed70 100644
--- a/Tests/Editor/DisconnectMessageTests.cs
+++ b/Tests/Editor/DisconnectMessageTests.cs
@@ -10,8 +10,10 @@ namespace Unity.Netcode.EditorTests
{
var networkContext = new NetworkContext();
var writer = new FastBufferWriter(20, Allocator.Temp, 20);
- var msg = new DisconnectReasonMessage();
- msg.Reason = string.Empty;
+ var msg = new DisconnectReasonMessage
+ {
+ Reason = string.Empty
+ };
msg.Serialize(writer, msg.Version);
var fbr = new FastBufferReader(writer, Allocator.Temp);
@@ -26,8 +28,10 @@ namespace Unity.Netcode.EditorTests
{
var networkContext = new NetworkContext();
var writer = new FastBufferWriter(20, Allocator.Temp, 20);
- var msg = new DisconnectReasonMessage();
- msg.Reason = "Foo";
+ var msg = new DisconnectReasonMessage
+ {
+ Reason = "Foo"
+ };
msg.Serialize(writer, msg.Version);
var fbr = new FastBufferReader(writer, Allocator.Temp);
@@ -42,8 +46,10 @@ namespace Unity.Netcode.EditorTests
{
var networkContext = new NetworkContext();
var writer = new FastBufferWriter(20, Allocator.Temp, 20);
- var msg = new DisconnectReasonMessage();
- msg.Reason = "ThisStringIsWayLongerThanTwentyBytes";
+ var msg = new DisconnectReasonMessage
+ {
+ Reason = "ThisStringIsWayLongerThanTwentyBytes"
+ };
msg.Serialize(writer, msg.Version);
var fbr = new FastBufferReader(writer, Allocator.Temp);
diff --git a/Tests/Editor/Messaging/MessageRegistrationTests.cs b/Tests/Editor/Messaging/MessageRegistrationTests.cs
index 2f9d58c..c218bdf 100644
--- a/Tests/Editor/Messaging/MessageRegistrationTests.cs
+++ b/Tests/Editor/Messaging/MessageRegistrationTests.cs
@@ -222,10 +222,11 @@ namespace Unity.Netcode.EditorTests
{
var listMessages = new List();
- var messageWithHandler = new MessagingSystem.MessageWithHandler();
-
- messageWithHandler.MessageType = typeof(zzzLateLexicographicNetworkMessage);
- messageWithHandler.GetVersion = MessagingSystem.CreateMessageAndGetVersion;
+ var messageWithHandler = new MessagingSystem.MessageWithHandler
+ {
+ MessageType = typeof(zzzLateLexicographicNetworkMessage),
+ GetVersion = MessagingSystem.CreateMessageAndGetVersion
+ };
listMessages.Add(messageWithHandler);
messageWithHandler.MessageType = typeof(ConnectionRequestMessage);
diff --git a/Tests/Editor/Messaging/MessageVersioningTests.cs b/Tests/Editor/Messaging/MessageVersioningTests.cs
index cbc7109..06ac3c6 100644
--- a/Tests/Editor/Messaging/MessageVersioningTests.cs
+++ b/Tests/Editor/Messaging/MessageVersioningTests.cs
@@ -149,7 +149,7 @@ namespace Unity.Netcode.EditorTests
var v1 = new VersionedTestMessage_v1();
v1.Deserialize(reader, ref context, receivedMessageVersion);
A = v1.A;
- D = (float)v1.D;
+ D = v1.D;
E = k_DefaultE;
Upgraded = true;
return true;
diff --git a/Tests/Editor/NetworkManagerConfigurationTests.cs b/Tests/Editor/NetworkManagerConfigurationTests.cs
index 5529e4d..b0b2fd4 100644
--- a/Tests/Editor/NetworkManagerConfigurationTests.cs
+++ b/Tests/Editor/NetworkManagerConfigurationTests.cs
@@ -1,9 +1,9 @@
using System.Collections.Generic;
using NUnit.Framework;
-using UnityEngine;
using Unity.Netcode.Editor;
using Unity.Netcode.Transports.UTP;
using UnityEditor.SceneManagement;
+using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.TestTools;
@@ -130,9 +130,11 @@ namespace Unity.Netcode.EditorTests
overridingTargetPrefab.GlobalObjectIdHash = 3;
sourcePrefabToOverride.GlobalObjectIdHash = 4;
- networkConfig.OldPrefabList = new List();
- networkConfig.OldPrefabList.Add(new NetworkPrefab { Prefab = regularPrefab.gameObject });
- networkConfig.OldPrefabList.Add(new NetworkPrefab { Prefab = overriddenPrefab.gameObject, Override = NetworkPrefabOverride.Prefab, OverridingTargetPrefab = overridingTargetPrefab.gameObject, SourcePrefabToOverride = sourcePrefabToOverride.gameObject, SourceHashToOverride = 123456 });
+ networkConfig.OldPrefabList = new List
+ {
+ new NetworkPrefab { Prefab = regularPrefab.gameObject },
+ new NetworkPrefab { Prefab = overriddenPrefab.gameObject, Override = NetworkPrefabOverride.Prefab, OverridingTargetPrefab = overridingTargetPrefab.gameObject, SourcePrefabToOverride = sourcePrefabToOverride.gameObject, SourceHashToOverride = 123456 }
+ };
networkConfig.InitializePrefabs();
@@ -159,15 +161,20 @@ namespace Unity.Netcode.EditorTests
// Setup
var networkManagerObject = new GameObject(nameof(NestedNetworkObjectPrefabCheck));
var networkManager = networkManagerObject.AddComponent();
- networkManager.NetworkConfig = new NetworkConfig();
- networkManager.NetworkConfig.NetworkTransport = networkManager.gameObject.AddComponent();
+ networkManager.NetworkConfig = new NetworkConfig
+ {
+ NetworkTransport = networkManager.gameObject.AddComponent()
+ };
var networkManagerObject2 = new GameObject(nameof(NestedNetworkObjectPrefabCheck));
var networkManager2 = networkManagerObject2.AddComponent();
- networkManager2.NetworkConfig = new NetworkConfig();
- networkManager2.NetworkConfig.NetworkTransport = networkManager.gameObject.AddComponent();
+ networkManager2.NetworkConfig = new NetworkConfig
+ {
+ NetworkTransport = networkManager.gameObject.AddComponent()
+ };
var object1 = new GameObject("Object 1").AddComponent();
+
var object2 = new GameObject("Object 2").AddComponent();
var object3 = new GameObject("Object 3").AddComponent();
@@ -205,13 +212,17 @@ namespace Unity.Netcode.EditorTests
// Setup
var networkManagerObject = new GameObject(nameof(NestedNetworkObjectPrefabCheck));
var networkManager = networkManagerObject.AddComponent();
- networkManager.NetworkConfig = new NetworkConfig();
- networkManager.NetworkConfig.NetworkTransport = networkManager.gameObject.AddComponent();
+ networkManager.NetworkConfig = new NetworkConfig
+ {
+ NetworkTransport = networkManager.gameObject.AddComponent()
+ };
var networkManagerObject2 = new GameObject(nameof(NestedNetworkObjectPrefabCheck));
var networkManager2 = networkManagerObject2.AddComponent();
- networkManager2.NetworkConfig = new NetworkConfig();
- networkManager2.NetworkConfig.NetworkTransport = networkManager.gameObject.AddComponent();
+ networkManager2.NetworkConfig = new NetworkConfig
+ {
+ NetworkTransport = networkManager.gameObject.AddComponent()
+ };
var object1 = new GameObject("Object 1").AddComponent();
var object2 = new GameObject("Object 2").AddComponent();
@@ -251,13 +262,17 @@ namespace Unity.Netcode.EditorTests
// Setup
var networkManagerObject = new GameObject(nameof(NestedNetworkObjectPrefabCheck));
var networkManager = networkManagerObject.AddComponent();
- networkManager.NetworkConfig = new NetworkConfig();
- networkManager.NetworkConfig.NetworkTransport = networkManager.gameObject.AddComponent();
+ networkManager.NetworkConfig = new NetworkConfig
+ {
+ NetworkTransport = networkManager.gameObject.AddComponent()
+ };
var networkManagerObject2 = new GameObject(nameof(NestedNetworkObjectPrefabCheck));
var networkManager2 = networkManagerObject2.AddComponent();
- networkManager2.NetworkConfig = new NetworkConfig();
- networkManager2.NetworkConfig.NetworkTransport = networkManager.gameObject.AddComponent();
+ networkManager2.NetworkConfig = new NetworkConfig
+ {
+ NetworkTransport = networkManager.gameObject.AddComponent()
+ };
var object1 = new GameObject("Object 1").AddComponent();
var object2 = new GameObject("Object 2").AddComponent();
diff --git a/Tests/Editor/NetworkPrefabProcessorTests.cs b/Tests/Editor/NetworkPrefabProcessorTests.cs
index 9af7fe4..275a83d 100644
--- a/Tests/Editor/NetworkPrefabProcessorTests.cs
+++ b/Tests/Editor/NetworkPrefabProcessorTests.cs
@@ -1,7 +1,7 @@
using NUnit.Framework;
-using UnityEngine;
using Unity.Netcode.Editor.Configuration;
using UnityEditor;
+using UnityEngine;
namespace Unity.Netcode.EditorTests
{
diff --git a/Tests/Editor/Serialization/BaseFastBufferReaderWriterTest.cs b/Tests/Editor/Serialization/BaseFastBufferReaderWriterTest.cs
index 21c3e57..0f0229b 100644
--- a/Tests/Editor/Serialization/BaseFastBufferReaderWriterTest.cs
+++ b/Tests/Editor/Serialization/BaseFastBufferReaderWriterTest.cs
@@ -7,8 +7,6 @@ namespace Unity.Netcode.EditorTests
{
public abstract class BaseFastBufferReaderWriterTest
{
-
- #region Test Types
protected enum ByteEnum : byte
{
A,
@@ -78,7 +76,6 @@ namespace Unity.Netcode.EditorTests
WriteDirect,
WriteSafe
}
- #endregion
protected abstract void RunTypeTest(T valueToTest) where T : unmanaged;
@@ -88,7 +85,6 @@ namespace Unity.Netcode.EditorTests
protected abstract void RunTypeArrayTestSafe(T[] valueToTest) where T : unmanaged;
- #region Helpers
protected TestStruct GetTestStruct()
{
var random = new Random();
@@ -98,7 +94,7 @@ namespace Unity.Netcode.EditorTests
A = (byte)random.Next(),
B = (short)random.Next(),
C = (ushort)random.Next(),
- D = (int)random.Next(),
+ D = random.Next(),
E = (uint)random.Next(),
F = ((long)random.Next() << 32) + random.Next(),
G = ((ulong)random.Next() << 32) + (ulong)random.Next(),
@@ -111,9 +107,6 @@ namespace Unity.Netcode.EditorTests
return testStruct;
}
- #endregion
-
-
private void RunTestWithWriteType(T val, WriteType wt, FastBufferWriter.ForPrimitives _ = default) where T : unmanaged
{
switch (wt)
@@ -149,7 +142,7 @@ namespace Unity.Netcode.EditorTests
}
else if (testType == typeof(int))
{
- RunTestWithWriteType((int)random.Next(), writeType);
+ RunTestWithWriteType(random.Next(), writeType);
}
else if (testType == typeof(uint))
{
@@ -354,10 +347,10 @@ namespace Unity.Netcode.EditorTests
else if (testType == typeof(long))
{
RunTypeTestLocal(new[]{
- ((long)random.Next() << 32) + (long)random.Next(),
- ((long)random.Next() << 32) + (long)random.Next(),
- ((long)random.Next() << 32) + (long)random.Next(),
- ((long)random.Next() << 32) + (long)random.Next()
+ ((long)random.Next() << 32) + random.Next(),
+ ((long)random.Next() << 32) + random.Next(),
+ ((long)random.Next() << 32) + random.Next(),
+ ((long)random.Next() << 32) + random.Next()
}, writeType);
}
else if (testType == typeof(ulong))
diff --git a/Tests/Editor/Serialization/BytePackerTests.cs b/Tests/Editor/Serialization/BytePackerTests.cs
index 3c072b4..99e8582 100644
--- a/Tests/Editor/Serialization/BytePackerTests.cs
+++ b/Tests/Editor/Serialization/BytePackerTests.cs
@@ -10,8 +10,6 @@ namespace Unity.Netcode.EditorTests
{
public class BytePackerTests
{
- #region Test Types
-
private enum ByteEnum : byte
{
A,
@@ -74,8 +72,6 @@ namespace Unity.Netcode.EditorTests
WriteAsObject
}
- #endregion
-
private unsafe void VerifyBytewiseEquality(T value, T otherValue) where T : unmanaged
{
byte* asBytePointer = (byte*)&value;
diff --git a/Tests/Editor/Serialization/FastBufferReaderTests.cs b/Tests/Editor/Serialization/FastBufferReaderTests.cs
index b6d0b01..e1dcbba 100644
--- a/Tests/Editor/Serialization/FastBufferReaderTests.cs
+++ b/Tests/Editor/Serialization/FastBufferReaderTests.cs
@@ -9,7 +9,6 @@ namespace Unity.Netcode.EditorTests
{
public class FastBufferReaderTests : BaseFastBufferReaderWriterTest
{
- #region Common Checks
private void WriteCheckBytes(FastBufferWriter writer, int writeSize, string failMessage = "")
{
Assert.IsTrue(writer.TryBeginWrite(2), "Writer denied write permission");
@@ -230,9 +229,7 @@ namespace Unity.Netcode.EditorTests
method.Invoke(reader, args);
value = (T[])args[0];
}
- #endregion
- #region Generic Checks
protected override unsafe void RunTypeTest(T valueToTest)
{
var writeSize = FastBufferWriter.GetWriteSize(valueToTest);
@@ -343,9 +340,6 @@ namespace Unity.Netcode.EditorTests
}
}
- #endregion
-
- #region Tests
[Test]
public void GivenFastBufferWriterContainingValue_WhenReadingUnmanagedType_ValueMatchesWhatWasWritten(
[Values(typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), typeof(int), typeof(uint),
@@ -1220,7 +1214,5 @@ namespace Unity.Netcode.EditorTests
Assert.AreEqual(reader.Handle->AllowedReadMark, 25);
}
}
-
- #endregion
}
}
diff --git a/Tests/Editor/Serialization/FastBufferWriterTests.cs b/Tests/Editor/Serialization/FastBufferWriterTests.cs
index 653bb67..ac924c9 100644
--- a/Tests/Editor/Serialization/FastBufferWriterTests.cs
+++ b/Tests/Editor/Serialization/FastBufferWriterTests.cs
@@ -9,9 +9,6 @@ namespace Unity.Netcode.EditorTests
{
public class FastBufferWriterTests : BaseFastBufferReaderWriterTest
{
-
- #region Common Checks
-
private void WriteCheckBytes(FastBufferWriter writer, int writeSize, string failMessage = "")
{
Assert.IsTrue(writer.TryBeginWrite(2), "Writer denied write permission");
@@ -66,10 +63,6 @@ namespace Unity.Netcode.EditorTests
VerifyTypedEquality(valueToTest, writer.GetUnsafePtr());
}
- #endregion
-
- #region Generic Checks
-
private void RunMethod(string methodName, FastBufferWriter writer, in T value) where T : unmanaged
{
MethodInfo method = typeof(FastBufferWriter).GetMethod(methodName, new[] { typeof(T).MakeByRefType() });
@@ -248,11 +241,8 @@ namespace Unity.Netcode.EditorTests
VerifyCheckBytes(underlyingArray, writeSize);
}
}
- #endregion
-
- #region Tests
- [Test, Description("Tests ")]
+ [Test, Description("Tests")]
public void WhenWritingUnmanagedType_ValueIsWrittenCorrectly(
[Values(typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), typeof(int), typeof(uint),
typeof(long), typeof(ulong), typeof(bool), typeof(char), typeof(float), typeof(double),
@@ -1317,6 +1307,5 @@ namespace Unity.Netcode.EditorTests
Assert.AreEqual(writer.Handle->AllowedWriteMark, 25);
}
}
- #endregion
}
}
diff --git a/Tests/Editor/Transports/BatchedReceiveQueueTests.cs b/Tests/Editor/Transports/BatchedReceiveQueueTests.cs
index 4bbc5be..5470f21 100644
--- a/Tests/Editor/Transports/BatchedReceiveQueueTests.cs
+++ b/Tests/Editor/Transports/BatchedReceiveQueueTests.cs
@@ -28,7 +28,7 @@ namespace Unity.Netcode.EditorTests
var writer = new DataStreamWriter(data);
writer.WriteInt(1);
- writer.WriteByte((byte)42);
+ writer.WriteByte(42);
var reader = new DataStreamReader(data);
var q = new BatchedReceiveQueue(reader);
@@ -52,9 +52,9 @@ namespace Unity.Netcode.EditorTests
var writer = new DataStreamWriter(data);
writer.WriteInt(1);
- writer.WriteByte((byte)42);
+ writer.WriteByte(42);
writer.WriteInt(1);
- writer.WriteByte((byte)142);
+ writer.WriteByte(142);
var reader = new DataStreamReader(data);
var q = new BatchedReceiveQueue(reader);
@@ -132,7 +132,7 @@ namespace Unity.Netcode.EditorTests
var writer = new DataStreamWriter(data);
writer.WriteInt(1);
- writer.WriteByte((byte)42);
+ writer.WriteByte(42);
var reader = new DataStreamReader(data);
var q = new BatchedReceiveQueue(reader);
@@ -168,7 +168,7 @@ namespace Unity.Netcode.EditorTests
var writer = new DataStreamWriter(data);
writer.WriteInt(1);
- writer.WriteByte((byte)42);
+ writer.WriteByte(42);
var reader = new DataStreamReader(data);
var q = new BatchedReceiveQueue(reader);
diff --git a/Tests/Editor/com.unity.netcode.editortests.asmdef b/Tests/Editor/com.unity.netcode.editortests.asmdef
index 7d2b613..56d2377 100644
--- a/Tests/Editor/com.unity.netcode.editortests.asmdef
+++ b/Tests/Editor/com.unity.netcode.editortests.asmdef
@@ -10,7 +10,8 @@
"Unity.Multiplayer.NetStats",
"Unity.Multiplayer.Tools.MetricTypes",
"Unity.Multiplayer.Tools.NetStats",
- "Unity.Networking.Transport"
+ "Unity.Networking.Transport",
+ "Unity.Mathematics"
],
"optionalUnityReferences": [
"TestAssemblies"
diff --git a/Tests/Runtime/AddNetworkPrefabTests.cs b/Tests/Runtime/AddNetworkPrefabTests.cs
index 57f1791..c00126e 100644
--- a/Tests/Runtime/AddNetworkPrefabTests.cs
+++ b/Tests/Runtime/AddNetworkPrefabTests.cs
@@ -1,8 +1,8 @@
using System.Collections;
using NUnit.Framework;
+using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
using UnityEngine.TestTools;
-using Unity.Netcode.TestHelpers.Runtime;
using Object = UnityEngine.Object;
namespace Unity.Netcode.RuntimeTests
diff --git a/Tests/Runtime/ClientOnlyConnectionTests.cs b/Tests/Runtime/ClientOnlyConnectionTests.cs
index cef0899..1e1be9e 100644
--- a/Tests/Runtime/ClientOnlyConnectionTests.cs
+++ b/Tests/Runtime/ClientOnlyConnectionTests.cs
@@ -1,9 +1,9 @@
using System.Collections;
using NUnit.Framework;
-using UnityEngine;
-using UnityEngine.TestTools;
using Unity.Netcode.TestHelpers.Runtime;
using Unity.Netcode.Transports.UTP;
+using UnityEngine;
+using UnityEngine.TestTools;
namespace Unity.Netcode.RuntimeTests
{
diff --git a/Tests/Runtime/Components/BufferDataValidationComponent.cs b/Tests/Runtime/Components/BufferDataValidationComponent.cs
index b9e1ac8..3912448 100644
--- a/Tests/Runtime/Components/BufferDataValidationComponent.cs
+++ b/Tests/Runtime/Components/BufferDataValidationComponent.cs
@@ -1,6 +1,6 @@
using System.Collections.Generic;
-using UnityEngine;
using Unity.Netcode.TestHelpers.Runtime;
+using UnityEngine;
namespace Unity.Netcode.RuntimeTests
{
diff --git a/Tests/Runtime/Components/NetworkVariableTestComponent.cs b/Tests/Runtime/Components/NetworkVariableTestComponent.cs
index 5f1788f..9e3c686 100644
--- a/Tests/Runtime/Components/NetworkVariableTestComponent.cs
+++ b/Tests/Runtime/Components/NetworkVariableTestComponent.cs
@@ -1,8 +1,8 @@
using System;
using NUnit.Framework;
using Unity.Collections;
-using UnityEngine;
using Unity.Netcode.TestHelpers.Runtime;
+using UnityEngine;
namespace Unity.Netcode.RuntimeTests
{
@@ -255,14 +255,14 @@ namespace Unity.Netcode.RuntimeTests
// NetworkVariable Value Type Constructor Test Coverage
m_NetworkVariableBool = new NetworkVariable(true);
- m_NetworkVariableByte = new NetworkVariable((byte)0);
+ m_NetworkVariableByte = new NetworkVariable(0);
m_NetworkVariableColor = new NetworkVariable(new Color(1, 1, 1, 1));
m_NetworkVariableColor32 = new NetworkVariable(new Color32(1, 1, 1, 1));
m_NetworkVariableDouble = new NetworkVariable(1.0);
m_NetworkVariableFloat = new NetworkVariable(1.0f);
m_NetworkVariableInt = new NetworkVariable(1);
m_NetworkVariableLong = new NetworkVariable(1);
- m_NetworkVariableSByte = new NetworkVariable((sbyte)0);
+ m_NetworkVariableSByte = new NetworkVariable(0);
m_NetworkVariableQuaternion = new NetworkVariable(Quaternion.identity);
m_NetworkVariableShort = new NetworkVariable(256);
m_NetworkVariableVector4 = new NetworkVariable(new Vector4(1, 1, 1, 1));
diff --git a/Tests/Runtime/ConnectionApproval.cs b/Tests/Runtime/ConnectionApproval.cs
index 4e7ec95..bd14cfa 100644
--- a/Tests/Runtime/ConnectionApproval.cs
+++ b/Tests/Runtime/ConnectionApproval.cs
@@ -1,10 +1,10 @@
using System;
-using System.Text;
using System.Collections;
-using UnityEngine;
-using UnityEngine.TestTools;
+using System.Text;
using NUnit.Framework;
using Unity.Netcode.TestHelpers.Runtime;
+using UnityEngine;
+using UnityEngine.TestTools;
namespace Unity.Netcode.RuntimeTests
{
@@ -66,9 +66,11 @@ namespace Unity.Netcode.RuntimeTests
[Test]
public void VerifyUniqueNetworkConfigPerRequest()
{
- var networkConfig = new NetworkConfig();
- networkConfig.EnableSceneManagement = true;
- networkConfig.TickRate = 30;
+ var networkConfig = new NetworkConfig
+ {
+ EnableSceneManagement = true,
+ TickRate = 30
+ };
var currentHash = networkConfig.GetConfig();
networkConfig.EnableSceneManagement = false;
networkConfig.TickRate = 60;
diff --git a/Tests/Runtime/DeferredMessagingTests.cs b/Tests/Runtime/DeferredMessagingTests.cs
index 919195f..95f0161 100644
--- a/Tests/Runtime/DeferredMessagingTests.cs
+++ b/Tests/Runtime/DeferredMessagingTests.cs
@@ -1,11 +1,10 @@
using System;
-using System.Collections;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
+using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
using UnityEngine.TestTools;
-using Unity.Netcode.TestHelpers.Runtime;
using Object = UnityEngine.Object;
namespace Unity.Netcode.RuntimeTests
@@ -194,7 +193,11 @@ namespace Unity.Netcode.RuntimeTests
private int m_NumberOfClientsToLateJoin = 2;
- protected override IEnumerator OnSetup()
+ protected override bool m_EnableTimeTravel => true;
+ protected override bool m_SetupIsACoroutine => false;
+ protected override bool m_TearDownIsACoroutine => false;
+
+ protected override void OnInlineSetup()
{
DeferredMessageTestRpcAndNetworkVariableComponent.ClientInstances.Clear();
DeferredMessageTestRpcComponent.ClientInstances.Clear();
@@ -205,15 +208,13 @@ namespace Unity.Netcode.RuntimeTests
// Replace the IDeferredMessageManager component with our test one in the component factory
ComponentFactory.Register(networkManager => new TestDeferredMessageManager(networkManager));
- yield return null;
}
- protected override IEnumerator OnTearDown()
+ protected override void OnInlineTearDown()
{
// Revert the IDeferredMessageManager component to its default (DeferredMessageManager)
ComponentFactory.Deregister();
m_ClientSpawnCatchers.Clear();
- yield return null;
}
protected override void OnServerAndClientsCreated()
@@ -255,12 +256,12 @@ namespace Unity.Netcode.RuntimeTests
base.OnNewClientCreated(networkManager);
}
- private IEnumerator SpawnClients(bool clearTestDeferredMessageManagerCallFlags = true)
+ private void SpawnClients(bool clearTestDeferredMessageManagerCallFlags = true)
{
for (int i = 0; i < m_NumberOfClientsToLateJoin; i++)
{
// Create and join client
- yield return CreateAndStartNewClient();
+ CreateAndStartNewClientWithTimeTravel();
}
if (clearTestDeferredMessageManagerCallFlags)
@@ -308,16 +309,15 @@ namespace Unity.Netcode.RuntimeTests
m_ClientSpawnCatchers.Clear();
}
- protected override IEnumerator OnServerAndClientsConnected()
+ protected override void OnTimeTravelServerAndClientsConnected()
{
// Clear out these values from whatever might have set them during the initial startup.
ClearTestDeferredMessageManagerCallFlags();
- yield return null;
}
- private IEnumerator WaitForClientsToCatchSpawns(int count = 1)
+ private void WaitForClientsToCatchSpawns(int count = 1)
{
- yield return WaitForConditionOrTimeOut(() =>
+ Assert.IsTrue(WaitForConditionOrTimeOutWithTimeTravel(() =>
{
foreach (var catcher in m_ClientSpawnCatchers)
{
@@ -328,7 +328,7 @@ namespace Unity.Netcode.RuntimeTests
}
return true;
- });
+ }));
}
private void ClearTestDeferredMessageManagerCallFlags()
@@ -348,28 +348,28 @@ namespace Unity.Netcode.RuntimeTests
Assert.AreEqual(0, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnAddPrefab));
}
- private IEnumerator WaitForAllClientsToReceive() where T : INetworkMessage
+ private void WaitForAllClientsToReceive() where T : INetworkMessage
{
- yield return WaitForMessageReceived(m_ClientNetworkManagers.ToList(), ReceiptType.Received);
+ WaitForMessageReceivedWithTimeTravel(m_ClientNetworkManagers.ToList(), ReceiptType.Received);
}
- private IEnumerator WaitForAllClientsToReceive()
+ private void WaitForAllClientsToReceive()
where TFirstMessage : INetworkMessage
where TSecondMessage : INetworkMessage
{
- yield return WaitForMessagesReceived(new List
+ WaitForMessagesReceivedWithTimeTravel(new List
{
typeof(TFirstMessage),
typeof(TSecondMessage)
}, m_ClientNetworkManagers.ToList(), ReceiptType.Received);
}
- private IEnumerator WaitForAllClientsToReceive()
+ private void WaitForAllClientsToReceive()
where TFirstMessage : INetworkMessage
where TSecondMessage : INetworkMessage
where TThirdMessage : INetworkMessage
{
- yield return WaitForMessagesReceived(new List
+ WaitForMessagesReceivedWithTimeTravel(new List
{
typeof(TFirstMessage),
typeof(TSecondMessage),
@@ -377,13 +377,13 @@ namespace Unity.Netcode.RuntimeTests
}, m_ClientNetworkManagers.ToList(), ReceiptType.Received);
}
- private IEnumerator WaitForAllClientsToReceive()
+ private void WaitForAllClientsToReceive()
where TFirstMessage : INetworkMessage
where TSecondMessage : INetworkMessage
where TThirdMessage : INetworkMessage
where TFourthMessage : INetworkMessage
{
- yield return WaitForMessagesReceived(new List
+ WaitForMessagesReceivedWithTimeTravel(new List
{
typeof(TFirstMessage),
typeof(TSecondMessage),
@@ -392,19 +392,19 @@ namespace Unity.Netcode.RuntimeTests
}, m_ClientNetworkManagers.ToList(), ReceiptType.Received);
}
- [UnityTest]
- public IEnumerator WhenAnRpcArrivesBeforeASpawnArrives_ItIsDeferred()
+ [Test]
+ public void WhenAnRpcArrivesBeforeASpawnArrives_ItIsDeferred()
{
- yield return SpawnClients();
+ SpawnClients();
CatchSpawns();
var serverObject = Object.Instantiate(m_RpcPrefab);
serverObject.GetComponent().NetworkManagerOwner = m_ServerNetworkManager;
serverObject.GetComponent().Spawn();
- yield return WaitForClientsToCatchSpawns();
+ WaitForClientsToCatchSpawns();
serverObject.GetComponent().SendTestClientRpc();
- yield return WaitForAllClientsToReceive();
+ WaitForAllClientsToReceive();
foreach (var client in m_ClientNetworkManagers)
{
@@ -415,19 +415,19 @@ namespace Unity.Netcode.RuntimeTests
}
}
- [UnityTest]
- public IEnumerator WhenADespawnArrivesBeforeASpawnArrives_ItIsDeferred()
+ [Test]
+ public void WhenADespawnArrivesBeforeASpawnArrives_ItIsDeferred()
{
- yield return SpawnClients();
+ SpawnClients();
CatchSpawns();
var serverObject = Object.Instantiate(m_RpcPrefab);
serverObject.GetComponent().NetworkManagerOwner = m_ServerNetworkManager;
serverObject.GetComponent().Spawn();
- yield return WaitForClientsToCatchSpawns();
+ WaitForClientsToCatchSpawns();
serverObject.GetComponent().Despawn(false);
- yield return WaitForAllClientsToReceive();
+ WaitForAllClientsToReceive();
foreach (var client in m_ClientNetworkManagers)
{
@@ -438,18 +438,18 @@ namespace Unity.Netcode.RuntimeTests
}
}
- [UnityTest]
- public IEnumerator WhenAChangeOwnershipMessageArrivesBeforeASpawnArrives_ItIsDeferred()
+ [Test]
+ public void WhenAChangeOwnershipMessageArrivesBeforeASpawnArrives_ItIsDeferred()
{
- yield return SpawnClients();
+ SpawnClients();
CatchSpawns();
var serverObject = Object.Instantiate(m_RpcPrefab);
serverObject.GetComponent().NetworkManagerOwner = m_ServerNetworkManager;
serverObject.GetComponent().Spawn();
- yield return WaitForClientsToCatchSpawns();
+ WaitForClientsToCatchSpawns();
serverObject.GetComponent().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId);
- yield return WaitForAllClientsToReceive();
+ WaitForAllClientsToReceive();
foreach (var client in m_ClientNetworkManagers)
{
var manager = (TestDeferredMessageManager)client.DeferredMessageManager;
@@ -459,22 +459,22 @@ namespace Unity.Netcode.RuntimeTests
}
}
- [UnityTest]
- public IEnumerator WhenANetworkVariableDeltaMessageArrivesBeforeASpawnArrives_ItIsDeferred()
+ [Test]
+ public void WhenANetworkVariableDeltaMessageArrivesBeforeASpawnArrives_ItIsDeferred()
{
m_SkipAddingPrefabsToClient = true;
- yield return SpawnClients();
+ SpawnClients();
CatchSpawns();
var serverObject = Object.Instantiate(m_NetworkVariablePrefab);
serverObject.GetComponent().NetworkManagerOwner = m_ServerNetworkManager;
serverObject.GetComponent().Spawn();
- yield return WaitForClientsToCatchSpawns();
+ WaitForClientsToCatchSpawns();
serverObject.GetComponent().TestNetworkVariable.Value = 1;
- yield return WaitForAllClientsToReceive();
+ WaitForAllClientsToReceive();
foreach (var client in m_ClientNetworkManagers)
{
@@ -487,17 +487,17 @@ namespace Unity.Netcode.RuntimeTests
}
}
- [UnityTest]
+ [Test]
//[Ignore("Disabling this temporarily until it is migrated into new integration test.")]
- public IEnumerator WhenASpawnMessageArrivesBeforeThePrefabIsAvailable_ItIsDeferred()
+ public void WhenASpawnMessageArrivesBeforeThePrefabIsAvailable_ItIsDeferred()
{
m_SkipAddingPrefabsToClient = true;
- yield return SpawnClients();
+ SpawnClients();
var serverObject = Object.Instantiate(m_RpcPrefab);
serverObject.GetComponent().NetworkManagerOwner = m_ServerNetworkManager;
serverObject.GetComponent().Spawn();
- yield return WaitForAllClientsToReceive();
+ WaitForAllClientsToReceive();
foreach (var client in m_ClientNetworkManagers)
{
@@ -514,10 +514,10 @@ namespace Unity.Netcode.RuntimeTests
}
}
- [UnityTest]
- public IEnumerator WhenAnRpcIsDeferred_ItIsProcessedOnSpawn()
+ [Test]
+ public void WhenAnRpcIsDeferred_ItIsProcessedOnSpawn()
{
- yield return WhenAnRpcArrivesBeforeASpawnArrives_ItIsDeferred();
+ WhenAnRpcArrivesBeforeASpawnArrives_ItIsDeferred();
ReleaseSpawns();
foreach (var client in m_ClientNetworkManagers)
@@ -532,10 +532,10 @@ namespace Unity.Netcode.RuntimeTests
}
}
- [UnityTest]
- public IEnumerator WhenADespawnIsDeferred_ItIsProcessedOnSpawn()
+ [Test]
+ public void WhenADespawnIsDeferred_ItIsProcessedOnSpawn()
{
- yield return WhenADespawnArrivesBeforeASpawnArrives_ItIsDeferred();
+ WhenADespawnArrivesBeforeASpawnArrives_ItIsDeferred();
ReleaseSpawns();
foreach (var client in m_ClientNetworkManagers)
@@ -551,10 +551,10 @@ namespace Unity.Netcode.RuntimeTests
}
}
- [UnityTest]
- public IEnumerator WhenAChangeOwnershipMessageIsDeferred_ItIsProcessedOnSpawn()
+ [Test]
+ public void WhenAChangeOwnershipMessageIsDeferred_ItIsProcessedOnSpawn()
{
- yield return WhenAChangeOwnershipMessageArrivesBeforeASpawnArrives_ItIsDeferred();
+ WhenAChangeOwnershipMessageArrivesBeforeASpawnArrives_ItIsDeferred();
ReleaseSpawns();
foreach (var client in m_ClientNetworkManagers)
@@ -568,10 +568,10 @@ namespace Unity.Netcode.RuntimeTests
}
}
- [UnityTest]
- public IEnumerator WhenANetworkVariableDeltaMessageIsDeferred_ItIsProcessedOnSpawn()
+ [Test]
+ public void WhenANetworkVariableDeltaMessageIsDeferred_ItIsProcessedOnSpawn()
{
- yield return WhenANetworkVariableDeltaMessageArrivesBeforeASpawnArrives_ItIsDeferred();
+ WhenANetworkVariableDeltaMessageArrivesBeforeASpawnArrives_ItIsDeferred();
foreach (var client in m_ClientNetworkManagers)
{
@@ -592,7 +592,7 @@ namespace Unity.Netcode.RuntimeTests
}
return true;
}
- yield return WaitForConditionOrTimeOut(HaveAllClientsSpawned);
+ WaitForConditionOrTimeOutWithTimeTravel(HaveAllClientsSpawned);
foreach (var client in m_ClientNetworkManagers)
{
@@ -605,12 +605,12 @@ namespace Unity.Netcode.RuntimeTests
}
}
- [UnityTest]
- public IEnumerator WhenASpawnMessageIsDeferred_ItIsProcessedOnAddPrefab()
+ [Test]
+ public void WhenASpawnMessageIsDeferred_ItIsProcessedOnAddPrefab()
{
// This will prevent spawned clients from adding prefabs
m_SkipAddingPrefabsToClient = true;
- yield return WhenASpawnMessageArrivesBeforeThePrefabIsAvailable_ItIsDeferred();
+ WhenASpawnMessageArrivesBeforeThePrefabIsAvailable_ItIsDeferred();
// Now add the prefabs
foreach (var client in m_ClientNetworkManagers)
@@ -630,7 +630,7 @@ namespace Unity.Netcode.RuntimeTests
}
return true;
}
- yield return WaitForConditionOrTimeOut(HaveAllClientsSpawned);
+ WaitForConditionOrTimeOutWithTimeTravel(HaveAllClientsSpawned);
// Validate this test
foreach (var client in m_ClientNetworkManagers)
@@ -644,28 +644,26 @@ namespace Unity.Netcode.RuntimeTests
}
}
- protected override bool LogAllMessages => true;
-
- [UnityTest]
- public IEnumerator WhenMultipleSpawnTriggeredMessagesAreDeferred_TheyAreAllProcessedOnSpawn()
+ [Test]
+ public void WhenMultipleSpawnTriggeredMessagesAreDeferred_TheyAreAllProcessedOnSpawn()
{
m_SkipAddingPrefabsToClient = true;
- yield return SpawnClients();
+ SpawnClients();
CatchSpawns();
var serverObject = Object.Instantiate(m_RpcAndNetworkVariablePrefab);
serverObject.GetComponent().NetworkManagerOwner = m_ServerNetworkManager;
serverObject.GetComponent().Spawn();
- yield return WaitForClientsToCatchSpawns();
+ WaitForClientsToCatchSpawns();
serverObject.GetComponent().SendTestClientRpc();
serverObject.GetComponent().TestNetworkVariable.Value = 1;
- yield return WaitForAllClientsToReceive();
+ WaitForAllClientsToReceive();
serverObject.GetComponent().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId);
- yield return WaitForAllClientsToReceive();
+ WaitForAllClientsToReceive();
foreach (var client in m_ClientNetworkManagers)
{
@@ -694,8 +692,8 @@ namespace Unity.Netcode.RuntimeTests
}
return true;
}
- yield return WaitForConditionOrTimeOut(HaveAllClientsSpawned);
- yield return new WaitForSeconds(0.1f);
+ WaitForConditionOrTimeOutWithTimeTravel(HaveAllClientsSpawned);
+ TimeTravel(0.1, 1);
// Validate the spawned objects
foreach (var client in m_ClientNetworkManagers)
@@ -711,11 +709,11 @@ namespace Unity.Netcode.RuntimeTests
}
}
- [UnityTest]
- public IEnumerator WhenMultipleAddPrefabTriggeredMessagesAreDeferred_TheyAreAllProcessedOnAddNetworkPrefab()
+ [Test]
+ public void WhenMultipleAddPrefabTriggeredMessagesAreDeferred_TheyAreAllProcessedOnAddNetworkPrefab()
{
m_SkipAddingPrefabsToClient = true;
- yield return SpawnClients();
+ SpawnClients();
var serverObject = Object.Instantiate(m_RpcPrefab);
serverObject.GetComponent().NetworkManagerOwner = m_ServerNetworkManager;
serverObject.GetComponent().Spawn();
@@ -724,7 +722,7 @@ namespace Unity.Netcode.RuntimeTests
serverObject2.GetComponent().NetworkManagerOwner = m_ServerNetworkManager;
serverObject2.GetComponent().Spawn();
- yield return WaitForAllClientsToReceive();
+ WaitForAllClientsToReceive();
foreach (var client in m_ClientNetworkManagers)
{
@@ -751,7 +749,7 @@ namespace Unity.Netcode.RuntimeTests
}
return true;
}
- yield return WaitForConditionOrTimeOut(HaveAllClientsSpawned);
+ WaitForConditionOrTimeOutWithTimeTravel(HaveAllClientsSpawned);
foreach (var client in m_ClientNetworkManagers)
@@ -789,11 +787,11 @@ namespace Unity.Netcode.RuntimeTests
}
}
- [UnityTest]
- public IEnumerator WhenSpawnTriggeredMessagesAreDeferredBeforeThePrefabIsAdded_AddingThePrefabCausesThemToBeProcessed()
+ [Test]
+ public void WhenSpawnTriggeredMessagesAreDeferredBeforeThePrefabIsAdded_AddingThePrefabCausesThemToBeProcessed()
{
m_SkipAddingPrefabsToClient = true;
- yield return SpawnClients();
+ SpawnClients();
var serverObject = Object.Instantiate(m_RpcAndNetworkVariablePrefab);
serverObject.GetComponent().NetworkManagerOwner = m_ServerNetworkManager;
@@ -803,11 +801,11 @@ namespace Unity.Netcode.RuntimeTests
serverObject.GetComponent().TestNetworkVariable.Value = 1;
// TODO: Remove this if we figure out how to work around the NetworkVariableDeltaMessage.Serialized issue at line 59
// Otherwise, we have to wait for at least 1 tick for the NetworkVariableDeltaMessage to be generated before changing ownership
- yield return WaitForAllClientsToReceive();
+ WaitForAllClientsToReceive();
serverObject.GetComponent().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId);
- yield return WaitForAllClientsToReceive();
+ WaitForAllClientsToReceive();
// Validate messages are deferred and pending
foreach (var client in m_ClientNetworkManagers)
@@ -837,9 +835,9 @@ namespace Unity.Netcode.RuntimeTests
}
return true;
}
- yield return WaitForConditionOrTimeOut(HaveAllClientsSpawned);
+ WaitForConditionOrTimeOutWithTimeTravel(HaveAllClientsSpawned);
- yield return new WaitForSeconds(0.1f);
+ TimeTravel(0.1, 1);
// Validate the test
foreach (var client in m_ClientNetworkManagers)
@@ -856,11 +854,11 @@ namespace Unity.Netcode.RuntimeTests
}
}
- [UnityTest]
- public IEnumerator WhenAMessageIsDeferredForMoreThanTheConfiguredTime_ItIsRemoved([Values(1, 2, 3)] int timeout)
+ [Test]
+ public void WhenAMessageIsDeferredForMoreThanTheConfiguredTime_ItIsRemoved([Values(1, 2, 3)] int timeout)
{
m_SkipAddingPrefabsToClient = true;
- yield return SpawnClients();
+ SpawnClients();
CatchSpawns();
foreach (var client in m_ClientNetworkManagers)
{
@@ -869,7 +867,7 @@ namespace Unity.Netcode.RuntimeTests
var serverObject = Object.Instantiate(m_RpcPrefab);
serverObject.GetComponent().NetworkManagerOwner = m_ServerNetworkManager;
serverObject.GetComponent().Spawn();
- yield return WaitForClientsToCatchSpawns();
+ WaitForClientsToCatchSpawns();
var start = 0f;
@@ -879,7 +877,7 @@ namespace Unity.Netcode.RuntimeTests
{
if (start == 0)
{
- start = Time.realtimeSinceStartup;
+ start = client.RealTimeProvider.RealTimeSinceStartup;
}
};
var manager = (TestDeferredMessageManager)client.DeferredMessageManager;
@@ -888,7 +886,7 @@ namespace Unity.Netcode.RuntimeTests
serverObject.GetComponent().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId);
- yield return WaitForAllClientsToReceive();
+ WaitForAllClientsToReceive();
foreach (var unused in m_ClientNetworkManagers)
{
@@ -901,8 +899,9 @@ namespace Unity.Netcode.RuntimeTests
TestDeferredMessageManager.BeforePurgeDelegate beforePurge = (manager, key) =>
{
++purgeCount;
- var elapsed = Time.realtimeSinceStartup - start;
- Assert.GreaterOrEqual(elapsed, timeout - 0.05f);
+ var elapsed = client.RealTimeProvider.RealTimeSinceStartup - start;
+ Debug.Log(client.RealTimeProvider.GetType().FullName);
+ Assert.GreaterOrEqual(elapsed, timeout);
Assert.AreEqual(1, manager.DeferredMessageCountTotal());
Assert.AreEqual(1, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn));
Assert.AreEqual(1, manager.DeferredMessageCountForKey(IDeferredMessageManager.TriggerType.OnSpawn, key));
@@ -912,8 +911,20 @@ namespace Unity.Netcode.RuntimeTests
manager.OnBeforePurge = beforePurge;
}
- yield return new WaitForSeconds(timeout + 0.1f);
+ TimeTravel(timeout - 0.01, 1);
+ bool HaveAnyClientsPurged()
+ {
+ foreach (var client in m_ClientNetworkManagers)
+ {
+ var manager = (TestDeferredMessageManager)client.DeferredMessageManager;
+ if (manager.DeferredMessageCountTotal() == 0)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
bool HaveAllClientsPurged()
{
foreach (var client in m_ClientNetworkManagers)
@@ -927,15 +938,18 @@ namespace Unity.Netcode.RuntimeTests
return true;
}
- yield return WaitForConditionOrTimeOut(HaveAllClientsPurged);
- AssertOnTimeout("Timed out waiting for all clients to purge their deferred messages!");
+ Assert.IsFalse(HaveAnyClientsPurged());
+
+ TimeTravel(0.02, 1);
+
+ Assert.IsTrue(HaveAllClientsPurged());
}
- [UnityTest]
- public IEnumerator WhenMultipleMessagesForTheSameObjectAreDeferredForMoreThanTheConfiguredTime_TheyAreAllRemoved([Values(1, 2, 3)] int timeout)
+ [Test]
+ public void WhenMultipleMessagesForTheSameObjectAreDeferredForMoreThanTheConfiguredTime_TheyAreAllRemoved([Values(1, 2, 3)] int timeout)
{
m_SkipAddingPrefabsToClient = true;
- yield return SpawnClients();
+ SpawnClients();
CatchSpawns();
foreach (var client in m_ClientNetworkManagers)
@@ -945,7 +959,7 @@ namespace Unity.Netcode.RuntimeTests
var serverObject = Object.Instantiate(m_RpcAndNetworkVariablePrefab);
serverObject.GetComponent().NetworkManagerOwner = m_ServerNetworkManager;
serverObject.GetComponent().Spawn();
- yield return WaitForClientsToCatchSpawns();
+ WaitForClientsToCatchSpawns();
var start = 0f;
@@ -955,7 +969,7 @@ namespace Unity.Netcode.RuntimeTests
{
if (start == 0)
{
- start = Time.realtimeSinceStartup;
+ start = client.RealTimeProvider.RealTimeSinceStartup;
}
};
var manager = (TestDeferredMessageManager)client.DeferredMessageManager;
@@ -966,7 +980,7 @@ namespace Unity.Netcode.RuntimeTests
serverObject.GetComponent().TestNetworkVariable.Value = 1;
serverObject.GetComponent().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId);
- yield return WaitForMessagesReceived(
+ WaitForMessagesReceivedWithTimeTravel(
new List {typeof(ClientRpcMessage), typeof(NetworkVariableDeltaMessage), typeof(ChangeOwnershipMessage),
}, m_ClientNetworkManagers.ToList(), ReceiptType.Received);
@@ -981,8 +995,8 @@ namespace Unity.Netcode.RuntimeTests
TestDeferredMessageManager.BeforePurgeDelegate beforePurge = (manager, key) =>
{
++purgeCount;
- var elapsed = Time.realtimeSinceStartup - start;
- Assert.GreaterOrEqual(elapsed, timeout - 0.25f);
+ var elapsed = client.RealTimeProvider.RealTimeSinceStartup - start;
+ Assert.GreaterOrEqual(elapsed, timeout);
Assert.AreEqual(3, manager.DeferredMessageCountTotal());
Assert.AreEqual(3, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn));
Assert.AreEqual(3, manager.DeferredMessageCountForKey(IDeferredMessageManager.TriggerType.OnSpawn, key));
@@ -992,6 +1006,21 @@ namespace Unity.Netcode.RuntimeTests
manager.OnBeforePurge = beforePurge;
}
+ var timePassedSinceFirstStart = MockTimeProvider.StaticRealTimeSinceStartup - start;
+ TimeTravel(timeout - 0.01 - timePassedSinceFirstStart, 1);
+
+ bool HaveAnyClientsPurged()
+ {
+ foreach (var client in m_ClientNetworkManagers)
+ {
+ var manager = (TestDeferredMessageManager)client.DeferredMessageManager;
+ if (manager.DeferredMessageCountTotal() == 0)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
bool HaveAllClientsPurged()
{
foreach (var client in m_ClientNetworkManagers)
@@ -1005,15 +1034,18 @@ namespace Unity.Netcode.RuntimeTests
return true;
}
- yield return WaitForConditionOrTimeOut(HaveAllClientsPurged);
- AssertOnTimeout("Timed out waiting for all clients to purge their deferred messages!");
+ Assert.IsFalse(HaveAnyClientsPurged());
+
+ TimeTravel(0.02 + timePassedSinceFirstStart, 1);
+
+ Assert.IsTrue(HaveAllClientsPurged());
}
- [UnityTest]
- public IEnumerator WhenMultipleMessagesForDifferentObjectsAreDeferredForMoreThanTheConfiguredTime_TheyAreAllRemoved([Values(1, 2, 3)] int timeout)
+ [Test]
+ public void WhenMultipleMessagesForDifferentObjectsAreDeferredForMoreThanTheConfiguredTime_TheyAreAllRemoved([Values(1, 2, 3)] int timeout)
{
m_SkipAddingPrefabsToClient = true;
- yield return SpawnClients();
+ SpawnClients();
CatchSpawns();
foreach (var client in m_ClientNetworkManagers)
@@ -1028,7 +1060,7 @@ namespace Unity.Netcode.RuntimeTests
serverObject2.GetComponent().NetworkManagerOwner = m_ServerNetworkManager;
serverObject2.GetComponent().Spawn();
- yield return WaitForClientsToCatchSpawns(2);
+ WaitForClientsToCatchSpawns(2);
var start = 0f;
@@ -1038,7 +1070,7 @@ namespace Unity.Netcode.RuntimeTests
{
if (start == 0)
{
- start = Time.realtimeSinceStartup;
+ start = client.RealTimeProvider.RealTimeSinceStartup;
}
};
var manager = (TestDeferredMessageManager)client.DeferredMessageManager;
@@ -1053,7 +1085,7 @@ namespace Unity.Netcode.RuntimeTests
serverObject2.GetComponent().TestNetworkVariable.Value = 1;
serverObject2.GetComponent().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId);
- yield return WaitForMessagesReceived(
+ WaitForMessagesReceivedWithTimeTravel(
new List {typeof(ClientRpcMessage), typeof(NetworkVariableDeltaMessage), typeof(ChangeOwnershipMessage),typeof(ClientRpcMessage), typeof(NetworkVariableDeltaMessage), typeof(ChangeOwnershipMessage),
}, m_ClientNetworkManagers.ToList(), ReceiptType.Received);
@@ -1071,7 +1103,7 @@ namespace Unity.Netcode.RuntimeTests
TestDeferredMessageManager.BeforePurgeDelegate beforePurge = (manager, key) =>
{
++purgeCount;
- var elapsed = Time.realtimeSinceStartup - start;
+ var elapsed = client.RealTimeProvider.RealTimeSinceStartup - start;
Assert.GreaterOrEqual(elapsed, timeout - 0.25f);
Assert.AreEqual(remainingMessagesTotalThisClient, manager.DeferredMessageCountTotal());
Assert.AreEqual(remainingMessagesTotalThisClient, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn));
@@ -1082,7 +1114,7 @@ namespace Unity.Netcode.RuntimeTests
manager.OnBeforePurge = beforePurge;
}
- yield return new WaitForSeconds(timeout + 0.1f);
+ TimeTravel(timeout + 0.1f, 1);
foreach (var client in m_ClientNetworkManagers)
{
AddPrefabsToClient(client);
@@ -1095,11 +1127,11 @@ namespace Unity.Netcode.RuntimeTests
}
}
- [UnityTest]
- public IEnumerator WhenADeferredMessageIsRemoved_OtherMessagesForSameObjectAreRemoved([Values(1, 2, 3)] int timeout)
+ [Test]
+ public void WhenADeferredMessageIsRemoved_OtherMessagesForSameObjectAreRemoved([Values(1, 2, 3)] int timeout)
{
m_SkipAddingPrefabsToClient = true;
- yield return SpawnClients();
+ SpawnClients();
CatchSpawns();
foreach (var client in m_ClientNetworkManagers)
{
@@ -1108,7 +1140,7 @@ namespace Unity.Netcode.RuntimeTests
var serverObject = Object.Instantiate(m_RpcPrefab);
serverObject.GetComponent().NetworkManagerOwner = m_ServerNetworkManager;
serverObject.GetComponent().Spawn();
- yield return WaitForClientsToCatchSpawns();
+ WaitForClientsToCatchSpawns();
var start = 0f;
@@ -1118,7 +1150,7 @@ namespace Unity.Netcode.RuntimeTests
{
if (start == 0)
{
- start = Time.realtimeSinceStartup;
+ start = client.RealTimeProvider.RealTimeSinceStartup;
}
};
var manager = (TestDeferredMessageManager)client.DeferredMessageManager;
@@ -1127,9 +1159,9 @@ namespace Unity.Netcode.RuntimeTests
serverObject.GetComponent().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId);
- yield return WaitForAllClientsToReceive();
+ WaitForAllClientsToReceive();
- yield return new WaitForSeconds(timeout - 0.5f);
+ TimeTravel(timeout - 0.5f, 1);
foreach (var client in m_ClientNetworkManagers)
{
@@ -1140,7 +1172,7 @@ namespace Unity.Netcode.RuntimeTests
}
serverObject.GetComponent().ChangeOwnership(m_ServerNetworkManager.LocalClientId);
- yield return WaitForAllClientsToReceive();
+ WaitForAllClientsToReceive();
foreach (var client in m_ClientNetworkManagers)
{
@@ -1161,7 +1193,7 @@ namespace Unity.Netcode.RuntimeTests
TestDeferredMessageManager.BeforePurgeDelegate beforePurge = (manager, key) =>
{
++purgeCount;
- var elapsed = Time.realtimeSinceStartup - start;
+ var elapsed = client.RealTimeProvider.RealTimeSinceStartup - start;
Assert.GreaterOrEqual(elapsed, timeout - 0.05f);
Assert.AreEqual(2, manager.DeferredMessageCountTotal());
Assert.AreEqual(2, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn));
@@ -1177,7 +1209,7 @@ namespace Unity.Netcode.RuntimeTests
AddPrefabsToClient(client);
}
- yield return new WaitForSeconds(0.6f);
+ TimeTravel(0.6f, 1);
Assert.AreEqual(m_NumberOfClientsToLateJoin, purgeCount);
foreach (var client in m_ClientNetworkManagers)
@@ -1187,11 +1219,11 @@ namespace Unity.Netcode.RuntimeTests
}
}
- [UnityTest]
- public IEnumerator WhenADeferredMessageIsRemoved_OtherMessagesForDifferentObjectsAreNotRemoved([Values(1, 2, 3)] int timeout)
+ [Test]
+ public void WhenADeferredMessageIsRemoved_OtherMessagesForDifferentObjectsAreNotRemoved([Values(1, 2, 3)] int timeout)
{
m_SkipAddingPrefabsToClient = true;
- yield return SpawnClients();
+ SpawnClients();
CatchSpawns();
foreach (var client in m_ClientNetworkManagers)
{
@@ -1203,7 +1235,7 @@ namespace Unity.Netcode.RuntimeTests
var serverObject2 = Object.Instantiate(m_RpcPrefab);
serverObject2.GetComponent().NetworkManagerOwner = m_ServerNetworkManager;
serverObject2.GetComponent().Spawn();
- yield return WaitForClientsToCatchSpawns(2);
+ WaitForClientsToCatchSpawns(2);
var start = 0f;
@@ -1213,7 +1245,7 @@ namespace Unity.Netcode.RuntimeTests
{
if (start == 0)
{
- start = Time.realtimeSinceStartup;
+ start = client.RealTimeProvider.RealTimeSinceStartup;
}
};
var manager = (TestDeferredMessageManager)client.DeferredMessageManager;
@@ -1222,9 +1254,9 @@ namespace Unity.Netcode.RuntimeTests
serverObject.GetComponent().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId);
- yield return WaitForAllClientsToReceive();
+ WaitForAllClientsToReceive();
- yield return new WaitForSeconds(timeout - 0.5f);
+ TimeTravel(timeout - 0.5f, 1);
foreach (var client in m_ClientNetworkManagers)
{
@@ -1236,7 +1268,7 @@ namespace Unity.Netcode.RuntimeTests
}
serverObject2.GetComponent().ChangeOwnership(m_ServerNetworkManager.LocalClientId);
- yield return WaitForAllClientsToReceive();
+ WaitForAllClientsToReceive();
foreach (var client in m_ClientNetworkManagers)
{
@@ -1258,7 +1290,7 @@ namespace Unity.Netcode.RuntimeTests
TestDeferredMessageManager.BeforePurgeDelegate beforePurge = (manager, key) =>
{
++purgeCount;
- var elapsed = Time.realtimeSinceStartup - start;
+ var elapsed = client.RealTimeProvider.RealTimeSinceStartup - start;
Assert.GreaterOrEqual(elapsed, timeout - 0.05f);
Assert.AreEqual(2, manager.DeferredMessageCountTotal());
Assert.AreEqual(2, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn));
@@ -1277,7 +1309,7 @@ namespace Unity.Netcode.RuntimeTests
AddPrefabsToClient(client);
}
- yield return new WaitForSeconds(0.6f);
+ TimeTravel(0.6f, 1);
Assert.AreEqual(m_NumberOfClientsToLateJoin, purgeCount);
foreach (var client in m_ClientNetworkManagers)
diff --git a/Tests/Runtime/DisconnectTests.cs b/Tests/Runtime/DisconnectTests.cs
index aae933b..a9c25a9 100644
--- a/Tests/Runtime/DisconnectTests.cs
+++ b/Tests/Runtime/DisconnectTests.cs
@@ -1,9 +1,9 @@
using System.Collections;
using System.Linq;
using NUnit.Framework;
+using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
using UnityEngine.TestTools;
-using Unity.Netcode.TestHelpers.Runtime;
namespace Unity.Netcode.RuntimeTests
{
diff --git a/Tests/Runtime/HiddenVariableTests.cs b/Tests/Runtime/HiddenVariableTests.cs
index 3b3bf04..83d1597 100644
--- a/Tests/Runtime/HiddenVariableTests.cs
+++ b/Tests/Runtime/HiddenVariableTests.cs
@@ -1,9 +1,9 @@
using System.Collections;
using System.Collections.Generic;
using NUnit.Framework;
+using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
using UnityEngine.TestTools;
-using Unity.Netcode.TestHelpers.Runtime;
namespace Unity.Netcode.RuntimeTests
{
diff --git a/Tests/Runtime/IntegrationTestExamples.cs b/Tests/Runtime/IntegrationTestExamples.cs
index 0556a1c..7cba4fb 100644
--- a/Tests/Runtime/IntegrationTestExamples.cs
+++ b/Tests/Runtime/IntegrationTestExamples.cs
@@ -1,9 +1,9 @@
using System.Collections;
using System.Linq;
-using UnityEngine;
-using UnityEngine.TestTools;
using NUnit.Framework;
using Unity.Netcode.TestHelpers.Runtime;
+using UnityEngine;
+using UnityEngine.TestTools;
namespace Unity.Netcode.RuntimeTests
{
diff --git a/Tests/Runtime/InvalidConnectionEventsTest.cs b/Tests/Runtime/InvalidConnectionEventsTest.cs
index 6ae6c59..39e2f46 100644
--- a/Tests/Runtime/InvalidConnectionEventsTest.cs
+++ b/Tests/Runtime/InvalidConnectionEventsTest.cs
@@ -5,9 +5,9 @@ using System.Linq;
using System.Text.RegularExpressions;
using NUnit.Framework;
using Unity.Collections;
-using UnityEngine.TestTools;
using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
+using UnityEngine.TestTools;
using Debug = UnityEngine.Debug;
namespace Unity.Netcode.RuntimeTests
diff --git a/Tests/Runtime/ListChangedTest.cs b/Tests/Runtime/ListChangedTest.cs
index 8c9c3be..f88f70a 100644
--- a/Tests/Runtime/ListChangedTest.cs
+++ b/Tests/Runtime/ListChangedTest.cs
@@ -1,7 +1,7 @@
using System.Collections;
+using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
using UnityEngine.TestTools;
-using Unity.Netcode.TestHelpers.Runtime;
namespace Unity.Netcode.RuntimeTests
{
diff --git a/Tests/Runtime/Messaging/DisconnectReasonTests.cs b/Tests/Runtime/Messaging/DisconnectReasonTests.cs
index cb19fd3..ce28966 100644
--- a/Tests/Runtime/Messaging/DisconnectReasonTests.cs
+++ b/Tests/Runtime/Messaging/DisconnectReasonTests.cs
@@ -2,9 +2,9 @@ using System;
using System.Collections;
using System.Text.RegularExpressions;
using NUnit.Framework;
+using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
using UnityEngine.TestTools;
-using Unity.Netcode.TestHelpers.Runtime;
namespace Unity.Netcode.RuntimeTests
{
diff --git a/Tests/Runtime/Messaging/NamedMessageTests.cs b/Tests/Runtime/Messaging/NamedMessageTests.cs
index b96c63d..90eeb71 100644
--- a/Tests/Runtime/Messaging/NamedMessageTests.cs
+++ b/Tests/Runtime/Messaging/NamedMessageTests.cs
@@ -3,8 +3,8 @@ using System.Collections;
using System.Collections.Generic;
using NUnit.Framework;
using Unity.Collections;
-using UnityEngine.TestTools;
using Unity.Netcode.TestHelpers.Runtime;
+using UnityEngine.TestTools;
namespace Unity.Netcode.RuntimeTests
{
diff --git a/Tests/Runtime/Messaging/UnnamedMessageTests.cs b/Tests/Runtime/Messaging/UnnamedMessageTests.cs
index 5c88696..bd583cf 100644
--- a/Tests/Runtime/Messaging/UnnamedMessageTests.cs
+++ b/Tests/Runtime/Messaging/UnnamedMessageTests.cs
@@ -3,8 +3,8 @@ using System.Collections;
using System.Collections.Generic;
using NUnit.Framework;
using Unity.Collections;
-using UnityEngine.TestTools;
using Unity.Netcode.TestHelpers.Runtime;
+using UnityEngine.TestTools;
namespace Unity.Netcode.RuntimeTests
{
diff --git a/Tests/Runtime/NestedNetworkManagerTests.cs b/Tests/Runtime/NestedNetworkManagerTests.cs
index afbea13..7ae6ad2 100644
--- a/Tests/Runtime/NestedNetworkManagerTests.cs
+++ b/Tests/Runtime/NestedNetworkManagerTests.cs
@@ -1,7 +1,7 @@
-using UnityEngine;
using NUnit.Framework;
-using UnityEngine.TestTools;
using Unity.Netcode.Transports.UTP;
+using UnityEngine;
+using UnityEngine.TestTools;
using Object = UnityEngine.Object;
namespace Unity.Netcode.RuntimeTests
diff --git a/Tests/Runtime/NetworkBehaviourGenericTests.cs b/Tests/Runtime/NetworkBehaviourGenericTests.cs
index 1d8dd76..cea941a 100644
--- a/Tests/Runtime/NetworkBehaviourGenericTests.cs
+++ b/Tests/Runtime/NetworkBehaviourGenericTests.cs
@@ -1,9 +1,9 @@
using System.Collections;
-using UnityEngine;
using NUnit.Framework;
-using UnityEngine.TestTools;
-using Unity.Netcode.TestHelpers.Runtime;
using Unity.Netcode.Components;
+using Unity.Netcode.TestHelpers.Runtime;
+using UnityEngine;
+using UnityEngine.TestTools;
namespace Unity.Netcode.RuntimeTests
{
@@ -56,8 +56,10 @@ namespace Unity.Netcode.RuntimeTests
yield return StartServerAndClients();
var parentObject = new GameObject();
- var childObject = new GameObject();
- childObject.name = "ChildObject";
+ var childObject = new GameObject
+ {
+ name = "ChildObject"
+ };
childObject.transform.parent = parentObject.transform;
var parentNetworkObject = parentObject.AddComponent();
var childBehaviour = childObject.AddComponent();
diff --git a/Tests/Runtime/NetworkBehaviourUpdaterTests.cs b/Tests/Runtime/NetworkBehaviourUpdaterTests.cs
index c51a195..a233a6e 100644
--- a/Tests/Runtime/NetworkBehaviourUpdaterTests.cs
+++ b/Tests/Runtime/NetworkBehaviourUpdaterTests.cs
@@ -2,9 +2,9 @@ using System.Collections;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
+using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
using UnityEngine.TestTools;
-using Unity.Netcode.TestHelpers.Runtime;
using Object = UnityEngine.Object;
namespace Unity.Netcode.RuntimeTests
diff --git a/Tests/Runtime/NetworkManagerCustomMessageManagerTests.cs b/Tests/Runtime/NetworkManagerCustomMessageManagerTests.cs
index eb2e81b..e3255b9 100644
--- a/Tests/Runtime/NetworkManagerCustomMessageManagerTests.cs
+++ b/Tests/Runtime/NetworkManagerCustomMessageManagerTests.cs
@@ -12,12 +12,11 @@ namespace Unity.Netcode.RuntimeTests
var networkManager = gameObject.AddComponent();
var transport = gameObject.AddComponent();
- networkManager.NetworkConfig = new NetworkConfig();
-
-
-
- // Set dummy transport that does nothing
- networkManager.NetworkConfig.NetworkTransport = transport;
+ networkManager.NetworkConfig = new NetworkConfig
+ {
+ // Set dummy transport that does nothing
+ NetworkTransport = transport
+ };
CustomMessagingManager preManager = networkManager.CustomMessagingManager;
diff --git a/Tests/Runtime/NetworkManagerEventsTests.cs b/Tests/Runtime/NetworkManagerEventsTests.cs
new file mode 100644
index 0000000..3902871
--- /dev/null
+++ b/Tests/Runtime/NetworkManagerEventsTests.cs
@@ -0,0 +1,258 @@
+using System;
+using System.Collections;
+using NUnit.Framework;
+using Unity.Netcode.TestHelpers.Runtime;
+using UnityEngine;
+using UnityEngine.TestTools;
+
+namespace Unity.Netcode.RuntimeTests
+{
+ public class NetworkManagerEventsTests
+ {
+ private NetworkManager m_ClientManager;
+ private NetworkManager m_ServerManager;
+
+ [UnityTest]
+ public IEnumerator OnServerStoppedCalledWhenServerStops()
+ {
+ bool callbackInvoked = false;
+ var gameObject = new GameObject(nameof(OnServerStoppedCalledWhenServerStops));
+ m_ServerManager = gameObject.AddComponent();
+
+ // Set dummy transport that does nothing
+ var transport = gameObject.AddComponent();
+ m_ServerManager.NetworkConfig = new NetworkConfig() { NetworkTransport = transport };
+
+ Action onServerStopped = (bool wasAlsoClient) =>
+ {
+ callbackInvoked = true;
+ Assert.IsFalse(wasAlsoClient);
+ if (m_ServerManager.IsServer)
+ {
+ Assert.Fail("OnServerStopped called when the server is still active");
+ }
+ };
+
+ // Start server to cause initialization process
+ Assert.True(m_ServerManager.StartServer());
+ Assert.True(m_ServerManager.IsListening);
+
+ m_ServerManager.OnServerStopped += onServerStopped;
+ m_ServerManager.Shutdown();
+ UnityEngine.Object.DestroyImmediate(gameObject);
+
+ yield return WaitUntilManagerShutsdown();
+
+ Assert.False(m_ServerManager.IsListening);
+ Assert.True(callbackInvoked, "OnServerStopped wasn't invoked");
+ }
+
+ [UnityTest]
+ public IEnumerator OnClientStoppedCalledWhenClientStops()
+ {
+ yield return InitializeServerAndAClient();
+
+ bool callbackInvoked = false;
+ Action onClientStopped = (bool wasAlsoServer) =>
+ {
+ callbackInvoked = true;
+ Assert.IsFalse(wasAlsoServer);
+ if (m_ClientManager.IsClient)
+ {
+ Assert.Fail("onClientStopped called when the client is still active");
+ }
+ };
+
+ m_ClientManager.OnClientStopped += onClientStopped;
+ m_ClientManager.Shutdown();
+ yield return WaitUntilManagerShutsdown();
+
+ Assert.True(callbackInvoked, "OnClientStopped wasn't invoked");
+ }
+
+ [UnityTest]
+ public IEnumerator OnClientAndServerStoppedCalledWhenHostStops()
+ {
+ var gameObject = new GameObject(nameof(OnClientAndServerStoppedCalledWhenHostStops));
+ m_ServerManager = gameObject.AddComponent();
+
+ // Set dummy transport that does nothing
+ var transport = gameObject.AddComponent();
+ m_ServerManager.NetworkConfig = new NetworkConfig() { NetworkTransport = transport };
+
+ int callbacksInvoked = 0;
+ Action onClientStopped = (bool wasAlsoServer) =>
+ {
+ callbacksInvoked++;
+ Assert.IsTrue(wasAlsoServer);
+ if (m_ServerManager.IsClient)
+ {
+ Assert.Fail("onClientStopped called when the client is still active");
+ }
+ };
+
+ Action onServerStopped = (bool wasAlsoClient) =>
+ {
+ callbacksInvoked++;
+ Assert.IsTrue(wasAlsoClient);
+ if (m_ServerManager.IsServer)
+ {
+ Assert.Fail("OnServerStopped called when the server is still active");
+ }
+ };
+
+ // Start server to cause initialization process
+ Assert.True(m_ServerManager.StartHost());
+ Assert.True(m_ServerManager.IsListening);
+
+ m_ServerManager.OnServerStopped += onServerStopped;
+ m_ServerManager.OnClientStopped += onClientStopped;
+ m_ServerManager.Shutdown();
+ UnityEngine.Object.DestroyImmediate(gameObject);
+
+ yield return WaitUntilManagerShutsdown();
+
+ Assert.False(m_ServerManager.IsListening);
+ Assert.AreEqual(2, callbacksInvoked, "either OnServerStopped or OnClientStopped wasn't invoked");
+ }
+
+ [UnityTest]
+ public IEnumerator OnServerStartedCalledWhenServerStarts()
+ {
+ var gameObject = new GameObject(nameof(OnServerStartedCalledWhenServerStarts));
+ m_ServerManager = gameObject.AddComponent();
+
+ // Set dummy transport that does nothing
+ var transport = gameObject.AddComponent();
+ m_ServerManager.NetworkConfig = new NetworkConfig() { NetworkTransport = transport };
+
+ bool callbackInvoked = false;
+ Action onServerStarted = () =>
+ {
+ callbackInvoked = true;
+ if (!m_ServerManager.IsServer)
+ {
+ Assert.Fail("OnServerStarted called when the server is not active yet");
+ }
+ };
+
+ // Start server to cause initialization process
+ m_ServerManager.OnServerStarted += onServerStarted;
+
+ Assert.True(m_ServerManager.StartServer());
+ Assert.True(m_ServerManager.IsListening);
+
+ yield return WaitUntilServerBufferingIsReady();
+
+ Assert.True(callbackInvoked, "OnServerStarted wasn't invoked");
+ }
+
+ [UnityTest]
+ public IEnumerator OnClientStartedCalledWhenClientStarts()
+ {
+ bool callbackInvoked = false;
+ Action onClientStarted = () =>
+ {
+ callbackInvoked = true;
+ if (!m_ClientManager.IsClient)
+ {
+ Assert.Fail("onClientStarted called when the client is not active yet");
+ }
+ };
+
+ yield return InitializeServerAndAClient(onClientStarted);
+
+ Assert.True(callbackInvoked, "OnClientStarted wasn't invoked");
+ }
+
+ [UnityTest]
+ public IEnumerator OnClientAndServerStartedCalledWhenHostStarts()
+ {
+ var gameObject = new GameObject(nameof(OnClientAndServerStartedCalledWhenHostStarts));
+ m_ServerManager = gameObject.AddComponent();
+
+ // Set dummy transport that does nothing
+ var transport = gameObject.AddComponent();
+ m_ServerManager.NetworkConfig = new NetworkConfig() { NetworkTransport = transport };
+
+ int callbacksInvoked = 0;
+ Action onClientStarted = () =>
+ {
+ callbacksInvoked++;
+ if (!m_ServerManager.IsClient)
+ {
+ Assert.Fail("OnClientStarted called when the client is not active yet");
+ }
+ };
+
+ Action onServerStarted = () =>
+ {
+ callbacksInvoked++;
+ if (!m_ServerManager.IsServer)
+ {
+ Assert.Fail("OnServerStarted called when the server is not active yet");
+ }
+ };
+
+ m_ServerManager.OnServerStarted += onServerStarted;
+ m_ServerManager.OnClientStarted += onClientStarted;
+
+ // Start server to cause initialization process
+ Assert.True(m_ServerManager.StartHost());
+ Assert.True(m_ServerManager.IsListening);
+
+ yield return WaitUntilServerBufferingIsReady();
+ Assert.AreEqual(2, callbacksInvoked, "either OnServerStarted or OnClientStarted wasn't invoked");
+ }
+
+ private IEnumerator WaitUntilManagerShutsdown()
+ {
+ /* Need two updates to actually shut down. First one to see the transport failing, which
+ marks the NetworkManager as shutting down. Second one where actual shutdown occurs. */
+ yield return null;
+ yield return null;
+ }
+
+ private IEnumerator InitializeServerAndAClient(Action onClientStarted = null)
+ {
+ // Create multiple NetworkManager instances
+ if (!NetcodeIntegrationTestHelpers.Create(1, out m_ServerManager, out NetworkManager[] clients, 30))
+ {
+ Debug.LogError("Failed to create instances");
+ Assert.Fail("Failed to create instances");
+ }
+
+ // passing no clients on purpose to start them manually later
+ NetcodeIntegrationTestHelpers.Start(false, m_ServerManager, new NetworkManager[] { });
+
+ yield return WaitUntilServerBufferingIsReady();
+ m_ClientManager = clients[0];
+
+ if (onClientStarted != null)
+ {
+ m_ClientManager.OnClientStarted += onClientStarted;
+ }
+
+ Assert.True(m_ClientManager.StartClient());
+ NetcodeIntegrationTestHelpers.RegisterHandlers(clients[0]);
+ // Wait for connection on client side
+ yield return NetcodeIntegrationTestHelpers.WaitForClientsConnected(clients);
+ }
+
+ private IEnumerator WaitUntilServerBufferingIsReady()
+ {
+ /* wait until at least more than 2 server ticks have passed
+ Note: Waiting for more than 2 ticks on the server is due
+ to the time system applying buffering to the received time
+ in NetworkTimeSystem.Sync */
+ yield return new WaitUntil(() => m_ServerManager.NetworkTickSystem.ServerTime.Tick > 2);
+ }
+
+ [UnityTearDown]
+ public virtual IEnumerator Teardown()
+ {
+ NetcodeIntegrationTestHelpers.Destroy();
+ yield return null;
+ }
+ }
+}
diff --git a/Tests/Runtime/NetworkManagerEventsTests.cs.meta b/Tests/Runtime/NetworkManagerEventsTests.cs.meta
new file mode 100644
index 0000000..996ba3b
--- /dev/null
+++ b/Tests/Runtime/NetworkManagerEventsTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 238d8724ba5ce3947bc20f5d6c056b6e
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Tests/Runtime/NetworkManagerSceneManagerTests.cs b/Tests/Runtime/NetworkManagerSceneManagerTests.cs
index 5bb76c2..dd96132 100644
--- a/Tests/Runtime/NetworkManagerSceneManagerTests.cs
+++ b/Tests/Runtime/NetworkManagerSceneManagerTests.cs
@@ -11,9 +11,11 @@ namespace Unity.Netcode.RuntimeTests
var gameObject = new GameObject(nameof(SceneManagerAssigned));
var networkManager = gameObject.AddComponent();
var transport = gameObject.AddComponent();
- networkManager.NetworkConfig = new NetworkConfig();
- // Set dummy transport that does nothing
- networkManager.NetworkConfig.NetworkTransport = transport;
+ networkManager.NetworkConfig = new NetworkConfig
+ {
+ // Set dummy transport that does nothing
+ NetworkTransport = transport
+ };
NetworkSceneManager preManager = networkManager.SceneManager;
diff --git a/Tests/Runtime/NetworkManagerTransportTests.cs b/Tests/Runtime/NetworkManagerTransportTests.cs
index f356245..b4af8a4 100644
--- a/Tests/Runtime/NetworkManagerTransportTests.cs
+++ b/Tests/Runtime/NetworkManagerTransportTests.cs
@@ -1,8 +1,8 @@
using System;
using System.Collections;
+using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
-using NUnit.Framework;
namespace Unity.Netcode.RuntimeTests
{
diff --git a/Tests/Runtime/NetworkObject/NetworkObjectDestroyTests.cs b/Tests/Runtime/NetworkObject/NetworkObjectDestroyTests.cs
index fa45a5c..78f7a05 100644
--- a/Tests/Runtime/NetworkObject/NetworkObjectDestroyTests.cs
+++ b/Tests/Runtime/NetworkObject/NetworkObjectDestroyTests.cs
@@ -1,8 +1,8 @@
using System.Collections;
using NUnit.Framework;
+using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
using UnityEngine.TestTools;
-using Unity.Netcode.TestHelpers.Runtime;
using Object = UnityEngine.Object;
namespace Unity.Netcode.RuntimeTests
diff --git a/Tests/Runtime/NetworkObject/NetworkObjectDontDestroyWithOwnerTests.cs b/Tests/Runtime/NetworkObject/NetworkObjectDontDestroyWithOwnerTests.cs
index c7f106d..4300afd 100644
--- a/Tests/Runtime/NetworkObject/NetworkObjectDontDestroyWithOwnerTests.cs
+++ b/Tests/Runtime/NetworkObject/NetworkObjectDontDestroyWithOwnerTests.cs
@@ -1,9 +1,9 @@
using System.Collections;
using System.Linq;
using NUnit.Framework;
+using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
using UnityEngine.TestTools;
-using Unity.Netcode.TestHelpers.Runtime;
namespace Unity.Netcode.RuntimeTests
diff --git a/Tests/Runtime/NetworkObject/NetworkObjectNetworkClientOwnedObjectsTests.cs b/Tests/Runtime/NetworkObject/NetworkObjectNetworkClientOwnedObjectsTests.cs
index 1043a88..cd31ca2 100644
--- a/Tests/Runtime/NetworkObject/NetworkObjectNetworkClientOwnedObjectsTests.cs
+++ b/Tests/Runtime/NetworkObject/NetworkObjectNetworkClientOwnedObjectsTests.cs
@@ -1,9 +1,9 @@
using System.Collections;
using System.Linq;
using NUnit.Framework;
+using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
using UnityEngine.TestTools;
-using Unity.Netcode.TestHelpers.Runtime;
namespace Unity.Netcode.RuntimeTests
{
diff --git a/Tests/Runtime/NetworkObject/NetworkObjectOnNetworkDespawnTests.cs b/Tests/Runtime/NetworkObject/NetworkObjectOnNetworkDespawnTests.cs
index c3ef58e..726266c 100644
--- a/Tests/Runtime/NetworkObject/NetworkObjectOnNetworkDespawnTests.cs
+++ b/Tests/Runtime/NetworkObject/NetworkObjectOnNetworkDespawnTests.cs
@@ -1,8 +1,8 @@
using System.Collections;
using NUnit.Framework;
+using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
using UnityEngine.TestTools;
-using Unity.Netcode.TestHelpers.Runtime;
namespace Unity.Netcode.RuntimeTests
diff --git a/Tests/Runtime/NetworkObject/NetworkObjectOnSpawnTests.cs b/Tests/Runtime/NetworkObject/NetworkObjectOnSpawnTests.cs
index 1772c60..f07b196 100644
--- a/Tests/Runtime/NetworkObject/NetworkObjectOnSpawnTests.cs
+++ b/Tests/Runtime/NetworkObject/NetworkObjectOnSpawnTests.cs
@@ -1,9 +1,9 @@
using System.Collections;
using System.Collections.Generic;
using NUnit.Framework;
+using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
using UnityEngine.TestTools;
-using Unity.Netcode.TestHelpers.Runtime;
namespace Unity.Netcode.RuntimeTests
{
diff --git a/Tests/Runtime/NetworkObject/NetworkObjectOwnershipTests.cs b/Tests/Runtime/NetworkObject/NetworkObjectOwnershipTests.cs
index 9e10d16..1f4e3c7 100644
--- a/Tests/Runtime/NetworkObject/NetworkObjectOwnershipTests.cs
+++ b/Tests/Runtime/NetworkObject/NetworkObjectOwnershipTests.cs
@@ -2,9 +2,9 @@ using System.Collections;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
+using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
using UnityEngine.TestTools;
-using Unity.Netcode.TestHelpers.Runtime;
namespace Unity.Netcode.RuntimeTests
{
diff --git a/Tests/Runtime/NetworkObject/NetworkObjectPropertyTests.cs b/Tests/Runtime/NetworkObject/NetworkObjectPropertyTests.cs
new file mode 100644
index 0000000..364a7cf
--- /dev/null
+++ b/Tests/Runtime/NetworkObject/NetworkObjectPropertyTests.cs
@@ -0,0 +1,45 @@
+using NUnit.Framework;
+using Unity.Netcode.TestHelpers.Runtime;
+using UnityEngine;
+
+namespace Unity.Netcode.RuntimeTests
+{
+ ///
+ /// Tests properties of NetworkObject for proper functionality.
+ ///
+ public class NetworkObjectPropertyTests : NetcodeIntegrationTest
+ {
+ protected override int NumberOfClients => 1;
+
+ private NetworkObject m_TestPrefabNetworkObject;
+
+ protected override void OnServerAndClientsCreated()
+ {
+ // create prefab and get the NetworkObject component attached to it
+ m_TestPrefabNetworkObject = CreateNetworkObjectPrefab("TestObject").GetComponent();
+ }
+
+ ///
+ /// Tests PrefabHashId returns correctly when the NetworkObject is not a prefab.
+ ///
+ [Test]
+ public void TestPrefabHashIdPropertyNotAPrefab()
+ {
+ const uint kInvalidPrefabHashId = 0;
+
+ var gameObject = new GameObject("TestObject");
+ var networkObject = gameObject.AddComponent();
+ Assert.AreEqual(kInvalidPrefabHashId, networkObject.PrefabIdHash);
+ }
+
+ ///
+ /// Tests PrefabHashId returns correctly when the NetworkObject is a prefab.
+ ///
+ ///
+ [Test]
+ public void TestPrefabHashIdPropertyIsAPrefab()
+ {
+ Assert.AreEqual(m_TestPrefabNetworkObject.GlobalObjectIdHash, m_TestPrefabNetworkObject.PrefabIdHash);
+ }
+ }
+}
diff --git a/Tests/Runtime/NetworkObject/NetworkObjectPropertyTests.cs.meta b/Tests/Runtime/NetworkObject/NetworkObjectPropertyTests.cs.meta
new file mode 100644
index 0000000..7442554
--- /dev/null
+++ b/Tests/Runtime/NetworkObject/NetworkObjectPropertyTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: c11415470078c1846836b9a8b6f80dc2
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Tests/Runtime/NetworkObject/NetworkObjectSpawnManyObjectsTests.cs b/Tests/Runtime/NetworkObject/NetworkObjectSpawnManyObjectsTests.cs
index 956ca61..70d60ca 100644
--- a/Tests/Runtime/NetworkObject/NetworkObjectSpawnManyObjectsTests.cs
+++ b/Tests/Runtime/NetworkObject/NetworkObjectSpawnManyObjectsTests.cs
@@ -1,8 +1,8 @@
using System.Collections;
using NUnit.Framework;
+using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
using UnityEngine.TestTools;
-using Unity.Netcode.TestHelpers.Runtime;
namespace Unity.Netcode.RuntimeTests
{
diff --git a/Tests/Runtime/NetworkObject/NetworkObjectSynchronizationTests.cs b/Tests/Runtime/NetworkObject/NetworkObjectSynchronizationTests.cs
index 695dde8..3ac712e 100644
--- a/Tests/Runtime/NetworkObject/NetworkObjectSynchronizationTests.cs
+++ b/Tests/Runtime/NetworkObject/NetworkObjectSynchronizationTests.cs
@@ -1,9 +1,9 @@
using System.Collections;
using System.Collections.Generic;
-using UnityEngine;
-using UnityEngine.TestTools;
using NUnit.Framework;
using Unity.Netcode.TestHelpers.Runtime;
+using UnityEngine;
+using UnityEngine.TestTools;
using Random = UnityEngine.Random;
namespace Unity.Netcode.RuntimeTests
diff --git a/Tests/Runtime/NetworkPrefabHandlerTests.cs b/Tests/Runtime/NetworkPrefabHandlerTests.cs
index ee3359c..1e2378b 100644
--- a/Tests/Runtime/NetworkPrefabHandlerTests.cs
+++ b/Tests/Runtime/NetworkPrefabHandlerTests.cs
@@ -1,9 +1,9 @@
using System;
-using System.Linq;
using System.Collections.Generic;
-using UnityEngine;
+using System.Linq;
using NUnit.Framework;
using Unity.Netcode.TestHelpers.Runtime;
+using UnityEngine;
namespace Unity.Netcode.RuntimeTests
{
diff --git a/Tests/Runtime/NetworkShowHideTests.cs b/Tests/Runtime/NetworkShowHideTests.cs
index 0124cd4..4f239cd 100644
--- a/Tests/Runtime/NetworkShowHideTests.cs
+++ b/Tests/Runtime/NetworkShowHideTests.cs
@@ -2,9 +2,9 @@ using System.Collections;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
+using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
using UnityEngine.TestTools;
-using Unity.Netcode.TestHelpers.Runtime;
namespace Unity.Netcode.RuntimeTests
{
@@ -71,7 +71,7 @@ namespace Unity.Netcode.RuntimeTests
public NetworkList MyListSetOnSpawn;
public NetworkVariable MyOwnerReadNetworkVariable;
public NetworkList MyList;
- static public NetworkManager NetworkManagerOfInterest;
+ public static NetworkManager NetworkManagerOfInterest;
internal static int GainOwnershipCount = 0;
@@ -114,10 +114,7 @@ namespace Unity.Netcode.RuntimeTests
public void SomeRandomClientRPC()
{
Debug.Log($"RPC called {NetworkManager.LocalClientId}");
- if (ClientIdsRpcCalledOn != null)
- {
- ClientIdsRpcCalledOn.Add(NetworkManager.LocalClientId);
- }
+ ClientIdsRpcCalledOn?.Add(NetworkManager.LocalClientId);
}
public void TriggerRpc()
@@ -208,10 +205,12 @@ namespace Unity.Netcode.RuntimeTests
}
else
{
- var list = new List();
- list.Add(m_NetSpawnedObject1);
- list.Add(m_NetSpawnedObject2);
- list.Add(m_NetSpawnedObject3);
+ var list = new List
+ {
+ m_NetSpawnedObject1,
+ m_NetSpawnedObject2,
+ m_NetSpawnedObject3
+ };
if (!visibility)
{
diff --git a/Tests/Runtime/NetworkSpawnManagerTests.cs b/Tests/Runtime/NetworkSpawnManagerTests.cs
index a8a4b43..0806775 100644
--- a/Tests/Runtime/NetworkSpawnManagerTests.cs
+++ b/Tests/Runtime/NetworkSpawnManagerTests.cs
@@ -1,8 +1,8 @@
using System.Collections;
using NUnit.Framework;
+using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
using UnityEngine.TestTools;
-using Unity.Netcode.TestHelpers.Runtime;
namespace Unity.Netcode.RuntimeTests
{
diff --git a/Tests/Runtime/NetworkTransform/NetworkTransformOwnershipTests.cs b/Tests/Runtime/NetworkTransform/NetworkTransformOwnershipTests.cs
index 1dda427..6efc10b 100644
--- a/Tests/Runtime/NetworkTransform/NetworkTransformOwnershipTests.cs
+++ b/Tests/Runtime/NetworkTransform/NetworkTransformOwnershipTests.cs
@@ -2,14 +2,14 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
-using Unity.Netcode.Components;
using NUnit.Framework;
+using Unity.Netcode.Components;
+using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
using UnityEngine.TestTools;
-using Unity.Netcode.TestHelpers.Runtime;
namespace Unity.Netcode.RuntimeTests
{
- public class NetworkTransformOwnershipTests : NetcodeIntegrationTest
+ public class NetworkTransformOwnershipTests : IntegrationTestWithApproximation
{
protected override int NumberOfClients => 1;
@@ -22,20 +22,26 @@ namespace Unity.Netcode.RuntimeTests
m_ClientNetworkTransformPrefab = CreateNetworkObjectPrefab("OwnerAuthorityTest");
var clientNetworkTransform = m_ClientNetworkTransformPrefab.AddComponent();
clientNetworkTransform.Interpolate = false;
+ clientNetworkTransform.UseHalfFloatPrecision = false;
var rigidBody = m_ClientNetworkTransformPrefab.AddComponent();
rigidBody.useGravity = false;
+ // NOTE: We don't use a sphere collider for this integration test because by the time we can
+ // assure they don't collide and skew the results the NetworkObjects are already synchronized
+ // with skewed results
m_ClientNetworkTransformPrefab.AddComponent();
- m_ClientNetworkTransformPrefab.AddComponent();
m_ClientNetworkTransformPrefab.AddComponent();
m_NetworkTransformPrefab = CreateNetworkObjectPrefab("ServerAuthorityTest");
var networkTransform = m_NetworkTransformPrefab.AddComponent();
rigidBody = m_NetworkTransformPrefab.AddComponent();
rigidBody.useGravity = false;
+ // NOTE: We don't use a sphere collider for this integration test because by the time we can
+ // assure they don't collide and skew the results the NetworkObjects are already synchronized
+ // with skewed results
m_NetworkTransformPrefab.AddComponent();
- m_NetworkTransformPrefab.AddComponent();
m_NetworkTransformPrefab.AddComponent();
networkTransform.Interpolate = false;
+ networkTransform.UseHalfFloatPrecision = false;
base.OnServerAndClientsCreated();
}
@@ -78,11 +84,13 @@ namespace Unity.Netcode.RuntimeTests
var valueSetByOwner = Vector3.one * 2;
ownerInstance.transform.position = valueSetByOwner;
ownerInstance.transform.localScale = valueSetByOwner;
- var rotation = new Quaternion();
- rotation.eulerAngles = valueSetByOwner;
+ var rotation = new Quaternion
+ {
+ eulerAngles = valueSetByOwner
+ };
ownerInstance.transform.rotation = rotation;
var transformToTest = nonOwnerInstance.transform;
- yield return WaitForConditionOrTimeOut(() => transformToTest.position == valueSetByOwner && transformToTest.localScale == valueSetByOwner && transformToTest.rotation == rotation);
+ yield return WaitForConditionOrTimeOut(() => Approximately(transformToTest.position, valueSetByOwner) && Approximately(transformToTest.localScale, valueSetByOwner) && Approximately(transformToTest.rotation, rotation));
Assert.False(s_GlobalTimeoutHelper.TimedOut, $"Timed out waiting for {networkManagerNonOwner.name}'s object instance {nonOwnerInstance.name} to change its transform!\n" +
$"Expected Position: {valueSetByOwner} | Current Position: {transformToTest.position}\n" +
$"Expected Rotation: {valueSetByOwner} | Current Rotation: {transformToTest.rotation.eulerAngles}\n" +
@@ -91,7 +99,7 @@ namespace Unity.Netcode.RuntimeTests
// Verify non-owners cannot change transform values
nonOwnerInstance.transform.position = Vector3.zero;
yield return s_DefaultWaitForTick;
- Assert.True(nonOwnerInstance.transform.position == valueSetByOwner, $"{networkManagerNonOwner.name}'s object instance {nonOwnerInstance.name} was allowed to change its position! Expected: {Vector3.one} Is Currently:{nonOwnerInstance.transform.position}");
+ Assert.True(Approximately(nonOwnerInstance.transform.position, valueSetByOwner), $"{networkManagerNonOwner.name}'s object instance {nonOwnerInstance.name} was allowed to change its position! Expected: {valueSetByOwner} Is Currently:{nonOwnerInstance.transform.position}");
// Change ownership and wait for the non-owner to reflect the change
VerifyObjectIsSpawnedOnClient.ResetObjectTable();
@@ -113,13 +121,13 @@ namespace Unity.Netcode.RuntimeTests
Assert.True(nonOwnerInstance.GetComponent().isKinematic, $"{networkManagerNonOwner.name}'s object instance {nonOwnerInstance.name} is not kinematic when it should be!");
// Have the new owner change transform values and wait for those values to be applied on the non-owner side.
- valueSetByOwner = Vector3.one * 50;
+ valueSetByOwner = Vector3.one * 10;
ownerInstance.transform.position = valueSetByOwner;
ownerInstance.transform.localScale = valueSetByOwner;
rotation.eulerAngles = valueSetByOwner;
ownerInstance.transform.rotation = rotation;
transformToTest = nonOwnerInstance.transform;
- yield return WaitForConditionOrTimeOut(() => transformToTest.position == valueSetByOwner && transformToTest.localScale == valueSetByOwner && transformToTest.rotation == rotation);
+ yield return WaitForConditionOrTimeOut(() => Approximately(transformToTest.position, valueSetByOwner) && Approximately(transformToTest.localScale, valueSetByOwner) && Approximately(transformToTest.rotation, rotation));
Assert.False(s_GlobalTimeoutHelper.TimedOut, $"Timed out waiting for {networkManagerNonOwner.name}'s object instance {nonOwnerInstance.name} to change its transform!\n" +
$"Expected Position: {valueSetByOwner} | Current Position: {transformToTest.position}\n" +
$"Expected Rotation: {valueSetByOwner} | Current Rotation: {transformToTest.rotation.eulerAngles}\n" +
@@ -128,7 +136,7 @@ namespace Unity.Netcode.RuntimeTests
// The last check is to verify non-owners cannot change transform values after ownership has changed
nonOwnerInstance.transform.position = Vector3.zero;
yield return s_DefaultWaitForTick;
- Assert.True(nonOwnerInstance.transform.position == valueSetByOwner, $"{networkManagerNonOwner.name}'s object instance {nonOwnerInstance.name} was allowed to change its position! Expected: {Vector3.one} Is Currently:{nonOwnerInstance.transform.position}");
+ Assert.True(Approximately(nonOwnerInstance.transform.position, valueSetByOwner), $"{networkManagerNonOwner.name}'s object instance {nonOwnerInstance.name} was allowed to change its position! Expected: {Vector3.one} Is Currently:{nonOwnerInstance.transform.position}");
}
///
@@ -155,8 +163,10 @@ namespace Unity.Netcode.RuntimeTests
var valueSetByOwner = Vector3.one * 2;
ownerInstance.transform.position = valueSetByOwner;
ownerInstance.transform.localScale = valueSetByOwner;
- var rotation = new Quaternion();
- rotation.eulerAngles = valueSetByOwner;
+ var rotation = new Quaternion
+ {
+ eulerAngles = valueSetByOwner
+ };
ownerInstance.transform.rotation = rotation;
var transformToTest = nonOwnerInstance.transform;
yield return WaitForConditionOrTimeOut(() => transformToTest.position == valueSetByOwner && transformToTest.localScale == valueSetByOwner && transformToTest.rotation == rotation);
@@ -217,13 +227,6 @@ namespace Unity.Netcode.RuntimeTests
public override void OnNetworkSpawn()
{
- // This makes sure that the NetworkManager relative NetworkObject instances don't collide with each other
- // and skew the expected changes to the transforms
- foreach (var entry in s_NetworkManagerRelativeSpawnedObjects)
- {
- Physics.IgnoreCollision(entry.Value.GetComponent(), GetComponent());
- }
-
if (!s_NetworkManagerRelativeSpawnedObjects.ContainsKey(NetworkManager.LocalClientId))
{
s_NetworkManagerRelativeSpawnedObjects.Add(NetworkManager.LocalClientId, this);
diff --git a/Tests/Runtime/NetworkTransform/NetworkTransformStateTests.cs b/Tests/Runtime/NetworkTransform/NetworkTransformStateTests.cs
index 2c56188..ac85b19 100644
--- a/Tests/Runtime/NetworkTransform/NetworkTransformStateTests.cs
+++ b/Tests/Runtime/NetworkTransform/NetworkTransformStateTests.cs
@@ -5,8 +5,14 @@ using UnityEngine;
namespace Unity.Netcode.RuntimeTests
{
- [TestFixture(TransformSpace.World)]
- [TestFixture(TransformSpace.Local)]
+ [TestFixture(TransformSpace.World, Precision.Full, Rotation.Euler)]
+ [TestFixture(TransformSpace.World, Precision.Half, Rotation.Euler)]
+ [TestFixture(TransformSpace.Local, Precision.Full, Rotation.Euler)]
+ [TestFixture(TransformSpace.Local, Precision.Half, Rotation.Euler)]
+ [TestFixture(TransformSpace.World, Precision.Full, Rotation.Quaternion)]
+ [TestFixture(TransformSpace.World, Precision.Half, Rotation.Quaternion)]
+ [TestFixture(TransformSpace.Local, Precision.Full, Rotation.Quaternion)]
+ [TestFixture(TransformSpace.Local, Precision.Half, Rotation.Quaternion)]
public class NetworkTransformStateTests
{
public enum SyncAxis
@@ -47,17 +53,33 @@ namespace Unity.Netcode.RuntimeTests
Local
}
+ public enum Rotation
+ {
+ Euler,
+ Quaternion
+ }
+
public enum SynchronizationType
{
Delta,
Teleport
}
- private TransformSpace m_TransformSpace;
+ public enum Precision
+ {
+ Half,
+ Full
+ }
- public NetworkTransformStateTests(TransformSpace transformSpace)
+ private TransformSpace m_TransformSpace;
+ private Precision m_Precision;
+ private Rotation m_Rotation;
+
+ public NetworkTransformStateTests(TransformSpace transformSpace, Precision precision, Rotation rotation)
{
m_TransformSpace = transformSpace;
+ m_Precision = precision;
+ m_Rotation = rotation;
}
private bool WillAnAxisBeSynchronized(ref NetworkTransform networkTransform)
@@ -93,7 +115,8 @@ namespace Unity.Netcode.RuntimeTests
var initialPosition = Vector3.zero;
var initialRotAngles = Vector3.zero;
var initialScale = Vector3.one;
-
+ networkTransform.UseHalfFloatPrecision = m_Precision == Precision.Half;
+ networkTransform.UseQuaternionSynchronization = m_Rotation == Rotation.Quaternion;
networkTransform.transform.position = initialPosition;
networkTransform.transform.eulerAngles = initialRotAngles;
networkTransform.transform.localScale = initialScale;
@@ -115,6 +138,7 @@ namespace Unity.Netcode.RuntimeTests
{
InLocalSpace = inLocalSpace,
IsTeleportingNextFrame = isTeleporting,
+ NetworkDeltaPosition = new NetworkDeltaPosition(Vector3.zero, 0)
};
// Step 1: change properties, expect state to be dirty
@@ -277,6 +301,8 @@ namespace Unity.Netcode.RuntimeTests
// Step 3: disable a particular sync flag, expect state to be not dirty
// We do this last because it changes which axis will be synchronized.
{
+ // Reset the NetworkTransformState since teleporting will preserve
+ // any dirty values
networkTransformState = new NetworkTransform.NetworkTransformState
{
InLocalSpace = inLocalSpace,
@@ -309,6 +335,13 @@ namespace Unity.Netcode.RuntimeTests
}
}
+ // Reset the NetworkTransformState since teleporting will preserve
+ // any dirty values
+ networkTransformState = new NetworkTransform.NetworkTransformState
+ {
+ InLocalSpace = inLocalSpace,
+ IsTeleportingNextFrame = isTeleporting,
+ };
// SyncPositionY
if (syncPosY)
{
@@ -334,6 +367,13 @@ namespace Unity.Netcode.RuntimeTests
}
}
+ // Reset the NetworkTransformState since teleporting will preserve
+ // any dirty values
+ networkTransformState = new NetworkTransform.NetworkTransformState
+ {
+ InLocalSpace = inLocalSpace,
+ IsTeleportingNextFrame = isTeleporting,
+ };
// SyncPositionZ
if (syncPosZ)
{
@@ -359,8 +399,15 @@ namespace Unity.Netcode.RuntimeTests
}
}
- // SyncRotAngleX
- if (syncRotX)
+ // Reset the NetworkTransformState since teleporting will preserve
+ // any dirty values
+ networkTransformState = new NetworkTransform.NetworkTransformState
+ {
+ InLocalSpace = inLocalSpace,
+ IsTeleportingNextFrame = isTeleporting,
+ };
+ // SyncRotAngleX - Now test that we don't synchronize this specific axis as long as we are not using quaternion synchronization
+ if (syncRotX && m_Rotation == Rotation.Euler)
{
networkTransform.SyncRotAngleX = false;
@@ -383,8 +430,16 @@ namespace Unity.Netcode.RuntimeTests
Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform));
}
}
- // SyncRotAngleY
- if (syncRotY)
+
+ // Reset the NetworkTransformState since teleporting will preserve
+ // any dirty values
+ networkTransformState = new NetworkTransform.NetworkTransformState
+ {
+ InLocalSpace = inLocalSpace,
+ IsTeleportingNextFrame = isTeleporting,
+ };
+ // SyncRotAngleY - Now test that we don't synchronize this specific axis as long as we are not using quaternion synchronization
+ if (syncRotY && m_Rotation == Rotation.Euler)
{
networkTransform.SyncRotAngleY = false;
@@ -407,8 +462,16 @@ namespace Unity.Netcode.RuntimeTests
Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform));
}
}
- // SyncRotAngleZ
- if (syncRotZ)
+
+ // Reset the NetworkTransformState since teleporting will preserve
+ // any dirty values
+ networkTransformState = new NetworkTransform.NetworkTransformState
+ {
+ InLocalSpace = inLocalSpace,
+ IsTeleportingNextFrame = isTeleporting,
+ };
+ // SyncRotAngleZ - Now test that we don't synchronize this specific axis as long as we are not using quaternion synchronization
+ if (syncRotZ && m_Rotation == Rotation.Euler)
{
networkTransform.SyncRotAngleZ = false;
@@ -432,6 +495,13 @@ namespace Unity.Netcode.RuntimeTests
}
}
+ // Reset the NetworkTransformState since teleporting will preserve
+ // any dirty values
+ networkTransformState = new NetworkTransform.NetworkTransformState
+ {
+ InLocalSpace = inLocalSpace,
+ IsTeleportingNextFrame = isTeleporting,
+ };
// SyncScaleX
if (syncScaX)
{
@@ -456,6 +526,14 @@ namespace Unity.Netcode.RuntimeTests
Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform));
}
}
+
+ // Reset the NetworkTransformState since teleporting will preserve
+ // any dirty values
+ networkTransformState = new NetworkTransform.NetworkTransformState
+ {
+ InLocalSpace = inLocalSpace,
+ IsTeleportingNextFrame = isTeleporting,
+ };
// SyncScaleY
if (syncScaY)
{
@@ -480,6 +558,14 @@ namespace Unity.Netcode.RuntimeTests
Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform));
}
}
+
+ // Reset the NetworkTransformState since teleporting will preserve
+ // any dirty values
+ networkTransformState = new NetworkTransform.NetworkTransformState
+ {
+ InLocalSpace = inLocalSpace,
+ IsTeleportingNextFrame = isTeleporting,
+ };
// SyncScaleZ
if (syncScaZ)
{
diff --git a/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs b/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs
index 558bd48..d1adb38 100644
--- a/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs
+++ b/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs
@@ -1,10 +1,8 @@
-using System.Collections;
using System.Collections.Generic;
-using Unity.Netcode.Components;
using NUnit.Framework;
-using UnityEngine;
-using UnityEngine.TestTools;
+using Unity.Netcode.Components;
using Unity.Netcode.TestHelpers.Runtime;
+using UnityEngine;
namespace Unity.Netcode.RuntimeTests
{
@@ -16,6 +14,23 @@ namespace Unity.Netcode.RuntimeTests
public bool ServerAuthority;
public bool ReadyToReceivePositionUpdate = false;
+ public NetworkTransformState AuthorityLastSentState;
+ public bool StatePushed { get; internal set; }
+
+ protected override void OnAuthorityPushTransformState(ref NetworkTransformState networkTransformState)
+ {
+ StatePushed = true;
+ AuthorityLastSentState = networkTransformState;
+ base.OnAuthorityPushTransformState(ref networkTransformState);
+ }
+
+
+ public bool StateUpdated { get; internal set; }
+ protected override void OnNetworkTransformStateUpdated(ref NetworkTransformState oldState, ref NetworkTransformState newState)
+ {
+ StateUpdated = true;
+ base.OnNetworkTransformStateUpdated(ref oldState, ref newState);
+ }
protected override bool OnIsServerAuthoritative()
{
@@ -46,9 +61,9 @@ namespace Unity.Netcode.RuntimeTests
///
public class ChildObjectComponent : NetworkBehaviour
{
- public readonly static List Instances = new List();
+ public static readonly List Instances = new List();
public static ChildObjectComponent ServerInstance { get; internal set; }
- public readonly static Dictionary ClientInstances = new Dictionary();
+ public static readonly Dictionary ClientInstances = new Dictionary();
public static void Reset()
{
@@ -82,7 +97,7 @@ namespace Unity.Netcode.RuntimeTests
[TestFixture(HostOrServer.Server, Authority.ServerAuthority)]
[TestFixture(HostOrServer.Server, Authority.OwnerAuthority)]
- public class NetworkTransformTests : NetcodeIntegrationTest
+ public class NetworkTransformTests : IntegrationTestWithApproximation
{
private NetworkObject m_AuthoritativePlayer;
private NetworkObject m_NonAuthoritativePlayer;
@@ -106,11 +121,47 @@ namespace Unity.Netcode.RuntimeTests
EnableInterpolate
}
+ public enum Precision
+ {
+ Half,
+ Full
+ }
+
+ public enum Rotation
+ {
+ Euler,
+ Quaternion
+ }
+
+ public enum TransformSpace
+ {
+ World,
+ Local
+ }
+
+ public enum OverrideState
+ {
+ Update,
+ CommitToTransform,
+ SetState
+ }
+
+ public enum Axis
+ {
+ X,
+ Y,
+ Z,
+ XY,
+ XZ,
+ YZ,
+ XYZ
+ }
+
///
/// Constructor
///
- /// Value is set by TestFixture
- /// Value is set by TestFixture
+ /// Determines if we are running as a server or host
+ /// Determines if we are using server or owner authority
public NetworkTransformTests(HostOrServer testWithHost, Authority authority)
{
m_UseHost = testWithHost == HostOrServer.Host ? true : false;
@@ -118,11 +169,35 @@ namespace Unity.Netcode.RuntimeTests
}
protected override int NumberOfClients => 1;
+ protected override bool m_EnableTimeTravel => true;
+ protected override bool m_SetupIsACoroutine => false;
+ protected override bool m_TearDownIsACoroutine => false;
- protected override IEnumerator OnSetup()
+ private const int k_TickRate = 60;
+ private int m_OriginalTargetFrameRate;
+ protected override void OnOneTimeSetup()
{
+ m_OriginalTargetFrameRate = Application.targetFrameRate;
+ Application.targetFrameRate = 120;
+ base.OnOneTimeSetup();
+ }
+
+ protected override void OnOneTimeTearDown()
+ {
+ Application.targetFrameRate = m_OriginalTargetFrameRate;
+ base.OnOneTimeTearDown();
+ }
+
+ protected override void OnInlineSetup()
+ {
+ m_Precision = Precision.Full;
ChildObjectComponent.Reset();
- return base.OnSetup();
+ }
+
+ protected override void OnInlineTearDown()
+ {
+ m_EnableVerboseDebug = false;
+ Object.DestroyImmediate(m_PlayerPrefab);
}
protected override void OnCreatePlayerPrefab()
@@ -153,9 +228,15 @@ namespace Unity.Netcode.RuntimeTests
clientNetworkManager.LogLevel = LogLevel.Developer;
}
}
+
+ m_ServerNetworkManager.NetworkConfig.TickRate = k_TickRate;
+ foreach (var clientNetworkManager in m_ClientNetworkManagers)
+ {
+ clientNetworkManager.NetworkConfig.TickRate = k_TickRate;
+ }
}
- protected override IEnumerator OnServerAndClientsConnected()
+ protected override void OnTimeTravelServerAndClientsConnected()
{
// Get the client player representation on both the server and the client side
var serverSideClientPlayer = m_ServerNetworkManager.ConnectedClients[m_ClientNetworkManagers[0].LocalClientId].PlayerObject;
@@ -171,26 +252,13 @@ namespace Unity.Netcode.RuntimeTests
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!");
+ var success = WaitForConditionOrTimeOutWithTimeTravel(() => m_NonAuthoritativeTransform.ReadyToReceivePositionUpdate == true);
+ Assert.True(success, "Timed out waiting for client-side to notify it is ready!");
Assert.True(m_AuthoritativeTransform.CanCommitToTransform);
Assert.False(m_NonAuthoritativeTransform.CanCommitToTransform);
-
- yield return base.OnServerAndClientsConnected();
- }
-
- public enum TransformSpace
- {
- World,
- Local
- }
-
- public enum OverrideState
- {
- Update,
- CommitToTransform,
- SetState
+ // Just wait for at least one tick for NetworkTransforms to finish synchronization
+ WaitForNextTick();
}
///
@@ -244,15 +312,15 @@ namespace Unity.Netcode.RuntimeTests
var childLocalRotation = childInstance.transform.localRotation.eulerAngles;
var childLocalScale = childInstance.transform.localScale;
- if (!Aproximately(childLocalPosition, m_ChildObjectLocalPosition))
+ if (!Approximately(childLocalPosition, m_ChildObjectLocalPosition))
{
return false;
}
- if (!AproximatelyEuler(childLocalRotation, m_ChildObjectLocalRotation))
+ if (!ApproximatelyEuler(childLocalRotation, m_ChildObjectLocalRotation))
{
return false;
}
- if (!Aproximately(childLocalScale, m_ChildObjectLocalScale))
+ if (!Approximately(childLocalScale, m_ChildObjectLocalScale))
{
return false;
}
@@ -265,9 +333,9 @@ namespace Unity.Netcode.RuntimeTests
/// If not, it generates a message containing the axial values that did not match
/// the target/start local space values.
///
- private IEnumerator WaitForAllChildrenLocalTransformValuesToMatch()
+ private void WaitForAllChildrenLocalTransformValuesToMatch()
{
- yield return WaitForConditionOrTimeOut(AllInstancesKeptLocalTransformValues);
+ var success = WaitForConditionOrTimeOutWithTimeTravel(AllInstancesKeptLocalTransformValues);
var infoMessage = string.Empty;
if (s_GlobalTimeoutHelper.TimedOut)
{
@@ -277,38 +345,37 @@ namespace Unity.Netcode.RuntimeTests
var childLocalRotation = childInstance.transform.localRotation.eulerAngles;
var childLocalScale = childInstance.transform.localScale;
- if (!Aproximately(childLocalPosition, m_ChildObjectLocalPosition))
+ if (!Approximately(childLocalPosition, m_ChildObjectLocalPosition))
{
infoMessage += $"[{childInstance.name}] Child's Local Position ({childLocalPosition}) | Original Local Position ({m_ChildObjectLocalPosition})\n";
}
- if (!AproximatelyEuler(childLocalRotation, m_ChildObjectLocalRotation))
+ if (!ApproximatelyEuler(childLocalRotation, m_ChildObjectLocalRotation))
{
infoMessage += $"[{childInstance.name}] Child's Local Rotation ({childLocalRotation}) | Original Local Rotation ({m_ChildObjectLocalRotation})\n";
}
- if (!Aproximately(childLocalScale, m_ChildObjectLocalScale))
+ if (!Approximately(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}");
+ Assert.True(success, $"Timed out waiting for all children to have the correct local space values:\n {infoMessage}");
}
- yield return null;
}
///
/// Validates that local space transform values remain the same when a NetworkTransform is
/// parented under another NetworkTransform
///
- [UnityTest]
- public IEnumerator NetworkTransformParentedLocalSpaceTest([Values] Interpolation interpolation)
+ [Test]
+ public void 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!");
+ var success = WaitForConditionOrTimeOutWithTimeTravel(AllChildObjectInstancesAreSpawned);
+ Assert.True(success, "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!");
@@ -323,126 +390,259 @@ namespace Unity.Netcode.RuntimeTests
}
// 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!");
+ success = WaitForConditionOrTimeOutWithTimeTravel(AllChildObjectInstancesHaveChild);
+ Assert.True(success, "Timed out waiting for all instances to have parented a child!");
// This validates each child instance has preserved their local space values
- yield return WaitForAllChildrenLocalTransformValuesToMatch();
+ WaitForAllChildrenLocalTransformValuesToMatch();
}
///
/// Validates that moving, rotating, and scaling the authority side with a single
/// tick will properly synchronize the non-authoritative side with the same values.
///
- private IEnumerator MoveRotateAndScaleAuthority(Vector3 position, Vector3 rotation, Vector3 scale, OverrideState overrideState)
+ private void MoveRotateAndScaleAuthority(Vector3 position, Vector3 rotation, Vector3 scale, OverrideState overrideState)
{
switch (overrideState)
{
case OverrideState.SetState:
{
- m_AuthoritativeTransform.SetState(position, Quaternion.Euler(rotation), scale);
+ var authoritativeRotation = m_AuthoritativeTransform.GetSpaceRelativeRotation();
+ authoritativeRotation.eulerAngles = rotation;
+ if (m_Authority == Authority.OwnerAuthority)
+ {
+ // Under the scenario where the owner is not the server, and non-auth is the server we set the state from the server
+ // to be updated to the owner.
+ if (m_AuthoritativeTransform.IsOwner && !m_AuthoritativeTransform.IsServer && m_NonAuthoritativeTransform.IsServer)
+ {
+ m_NonAuthoritativeTransform.SetState(position, authoritativeRotation, scale);
+ }
+ else
+ {
+ m_AuthoritativeTransform.SetState(position, authoritativeRotation, scale);
+ }
+ }
+ else
+ {
+ m_AuthoritativeTransform.SetState(position, authoritativeRotation, scale);
+ }
+
break;
}
case OverrideState.Update:
default:
{
m_AuthoritativeTransform.transform.position = position;
- yield return null;
- var authoritativeRotation = m_AuthoritativeTransform.transform.rotation;
+
+ var authoritativeRotation = m_AuthoritativeTransform.GetSpaceRelativeRotation();
authoritativeRotation.eulerAngles = rotation;
m_AuthoritativeTransform.transform.rotation = authoritativeRotation;
- yield return null;
m_AuthoritativeTransform.transform.localScale = scale;
break;
}
}
}
- ///
- /// Validates we don't extrapolate beyond the target value
- ///
- ///
- /// 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
- ///
- 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})");
- }
-
///
/// Waits until the next tick
///
- private IEnumerator WaitForNextTick()
+ private void WaitForNextTick()
{
var currentTick = m_AuthoritativeTransform.NetworkManager.LocalTime.Tick;
while (m_AuthoritativeTransform.NetworkManager.LocalTime.Tick == currentTick)
{
- yield return null;
+ var frameRate = Application.targetFrameRate;
+ if (frameRate <= 0)
+ {
+ frameRate = 60;
+ }
+ var frameDuration = 1f / frameRate;
+ TimeTravel(frameDuration, 1);
}
}
// The number of iterations to change position, rotation, and scale for NetworkTransformMultipleChangesOverTime
- private const int k_PositionRotationScaleIterations = 8;
+ // Note: this was reduced from 8 iterations to 3 due to the number of tests based on all of the various parameter combinations
+ private const int k_PositionRotationScaleIterations = 3;
protected override void OnNewClientCreated(NetworkManager networkManager)
{
networkManager.NetworkConfig.Prefabs = m_ServerNetworkManager.NetworkConfig.Prefabs;
+ networkManager.NetworkConfig.TickRate = k_TickRate;
base.OnNewClientCreated(networkManager);
}
+ private Precision m_Precision = Precision.Full;
+ private float m_CurrentHalfPrecision = 0.0f;
+ private const float k_HalfPrecisionPosScale = 0.03f;
+ private const float k_HalfPrecisionRot = 0.725f;
+
+ protected override float GetDeltaVarianceThreshold()
+ {
+ if (m_Precision == Precision.Half)
+ {
+ return m_CurrentHalfPrecision;
+ }
+ return base.GetDeltaVarianceThreshold();
+ }
+
+
+ private Axis m_CurrentAxis;
///
/// This validates that multiple changes can occur within the same tick or over
/// several ticks while still keeping non-authoritative instances synchronized.
///
- [UnityTest]
- public IEnumerator NetworkTransformMultipleChangesOverTime([Values] TransformSpace testLocalTransform, [Values] OverrideState overideState)
+ [Test]
+ public void NetworkTransformMultipleChangesOverTime([Values] TransformSpace testLocalTransform, [Values] OverrideState overideState,
+ [Values] Precision precision, [Values] Rotation rotationSynch, [Values] Axis axis)
{
+ // In the name of reducing the very long time it takes to interpolate and run all of the possible combinations,
+ // we only interpolate when the second client joins
+ m_AuthoritativeTransform.Interpolate = false;
m_AuthoritativeTransform.InLocalSpace = testLocalTransform == TransformSpace.Local;
+ bool axisX = axis == Axis.X || axis == Axis.XY || axis == Axis.XZ || axis == Axis.XYZ;
+ bool axisY = axis == Axis.Y || axis == Axis.XY || axis == Axis.YZ || axis == Axis.XYZ;
+ bool axisZ = axis == Axis.Z || axis == Axis.XZ || axis == Axis.YZ || axis == Axis.XYZ;
+ m_CurrentAxis = axis;
+ // Authority dictates what is synchronized and what the precision is going to be
+ // so we only need to set this on the authoritative side.
+ m_AuthoritativeTransform.UseHalfFloatPrecision = precision == Precision.Half;
+ m_AuthoritativeTransform.UseQuaternionSynchronization = rotationSynch == Rotation.Quaternion;
+ m_Precision = precision;
- 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);
+ m_AuthoritativeTransform.SyncPositionX = axisX;
+ m_AuthoritativeTransform.SyncPositionY = axisY;
+ m_AuthoritativeTransform.SyncPositionZ = axisZ;
+
+ if (!m_AuthoritativeTransform.UseQuaternionSynchronization)
+ {
+ m_AuthoritativeTransform.SyncRotAngleX = axisX;
+ m_AuthoritativeTransform.SyncRotAngleY = axisY;
+ m_AuthoritativeTransform.SyncRotAngleZ = axisZ;
+ }
+ else
+ {
+ // This is not required for usage (setting the value should not matter when quaternion synchronization is enabled)
+ // but is required for this test so we don't get a failure on an axis that is marked to not be synchronized when
+ // validating the authority's values on non-authority instances.
+ m_AuthoritativeTransform.SyncRotAngleX = true;
+ m_AuthoritativeTransform.SyncRotAngleY = true;
+ m_AuthoritativeTransform.SyncRotAngleZ = true;
+ }
+
+ m_AuthoritativeTransform.SyncScaleX = axisX;
+ m_AuthoritativeTransform.SyncScaleY = axisY;
+ m_AuthoritativeTransform.SyncScaleZ = axisZ;
+
+
+ var positionStart = GetRandomVector3(0.25f, 1.75f);
+ var rotationStart = GetRandomVector3(1f, 15f);
+ var scaleStart = GetRandomVector3(0.25f, 2.0f);
var position = positionStart;
var rotation = rotationStart;
var scale = scaleStart;
+ m_AuthoritativeTransform.StatePushed = false;
+ // Wait for the deltas to be pushed
+ WaitForConditionOrTimeOutWithTimeTravel(() => m_AuthoritativeTransform.StatePushed);
+ // Allow the precision settings to propagate first as changing precision
+ // causes a teleport event to occur
+ WaitForNextTick();
// 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++)
+ for (int i = 0; i < k_PositionRotationScaleIterations; i++)
{
+ m_NonAuthoritativeTransform.StateUpdated = false;
+ m_AuthoritativeTransform.StatePushed = false;
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
+ // Apply delta between ticks
MoveRotateAndScaleAuthority(position, rotation, scale, overideState);
+ // Wait for the deltas to be pushed
+ Assert.True(WaitForConditionOrTimeOutWithTimeTravel(() => m_AuthoritativeTransform.StatePushed && m_NonAuthoritativeTransform.StateUpdated), $"[Non-Interpolate {i}] Timed out waiting for state to be pushed ({m_AuthoritativeTransform.StatePushed}) or state to be updated ({m_NonAuthoritativeTransform.StateUpdated})!");
+
// Wait for deltas to synchronize on non-authoritative side
- yield return WaitForPositionRotationAndScaleToMatch(4);
+ var success = WaitForConditionOrTimeOutWithTimeTravel(PositionRotationScaleMatches);
+ // Provide additional debug info about what failed (if it fails)
+ if (!success)
+ {
+ m_EnableVerboseDebug = true;
+ PositionRotationScaleMatches();
+ m_EnableVerboseDebug = false;
+ }
+ Assert.True(success, $"[Non-Interpolate {i}] Timed out waiting for non-authority to match authority's position or rotation");
}
- // 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();
+ // Only enable interpolation when all axis are set (to reduce the test times)
+ if (axis == Axis.XYZ)
+ {
+ // Now, enable interpolation
+ m_AuthoritativeTransform.Interpolate = true;
+ m_NonAuthoritativeTransform.StateUpdated = false;
+ m_AuthoritativeTransform.StatePushed = false;
+ // Wait for the delta (change in interpolation) to be pushed
+ var success = WaitForConditionOrTimeOutWithTimeTravel(() => m_AuthoritativeTransform.StatePushed && m_NonAuthoritativeTransform.StateUpdated);
+ Assert.True(success, $"[Interpolation Enable] Timed out waiting for state to be pushed ({m_AuthoritativeTransform.StatePushed}) or state to be updated ({m_NonAuthoritativeTransform.StateUpdated})!");
+
+ // Continue for one more update with interpolation enabled
+ // Note: We are just verifying one update with interpolation enabled due to the number of tests this integration test has to run
+ // and since the NestedNetworkTransformTests already tests interpolation under the same number of conditions (excluding Axis).
+ // This is just to verify selecting specific axis doesn't cause issues when interpolating as well.
+ m_NonAuthoritativeTransform.StateUpdated = false;
+ m_AuthoritativeTransform.StatePushed = false;
+ position = positionStart * k_PositionRotationScaleIterations;
+ rotation = rotationStart * k_PositionRotationScaleIterations;
+ scale = scaleStart * k_PositionRotationScaleIterations;
+ MoveRotateAndScaleAuthority(position, rotation, scale, overideState);
+
+ // Wait for the deltas to be pushed and updated
+ success = WaitForConditionOrTimeOutWithTimeTravel(() => m_AuthoritativeTransform.StatePushed && m_NonAuthoritativeTransform.StateUpdated);
+ Assert.True(success, $"[Interpolation {k_PositionRotationScaleIterations}] Timed out waiting for state to be pushed ({m_AuthoritativeTransform.StatePushed}) or state to be updated ({m_NonAuthoritativeTransform.StateUpdated})!");
+
+ success = WaitForConditionOrTimeOutWithTimeTravel(PositionRotationScaleMatches, 120);
+
+ // Provide additional debug info about what failed (if it fails)
+ if (!success)
+ {
+ m_EnableVerboseDebug = true;
+ PositionRotationScaleMatches();
+ m_EnableVerboseDebug = false;
+ }
+ Assert.True(success, $"[Interpolation {k_PositionRotationScaleIterations}] Timed out waiting for non-authority to match authority's position or rotation");
+ }
+ }
+
+ ///
+ /// Checks scale of a late joining client for all instances of the late joining client's player
+ ///
+ [Test]
+ public void LateJoiningPlayerInitialScaleValues([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;
+
+ var position = GetRandomVector3(0.25f, 1.75f);
+ var rotation = GetRandomVector3(1f, 45f);
+ var scale = GetRandomVector3(0.25f, 2.0f);
+
+ // Make some changes to the currently connected clients
+ m_NonAuthoritativeTransform.StateUpdated = false;
+ m_AuthoritativeTransform.StatePushed = false;
+ MoveRotateAndScaleAuthority(position, rotation, scale, overideState);
+
+ // Wait for the deltas to be pushed and updated
+ var success = WaitForConditionOrTimeOutWithTimeTravel(() => m_AuthoritativeTransform.StatePushed && m_NonAuthoritativeTransform.StateUpdated);
+ Assert.True(success, $"[Interpolation {k_PositionRotationScaleIterations}] Timed out waiting for state to be pushed ({m_AuthoritativeTransform.StatePushed}) or state to be updated ({m_NonAuthoritativeTransform.StateUpdated})!");
+
+ WaitForConditionOrTimeOutWithTimeTravel(PositionRotationScaleMatches);
+
+ // Validate the use of the prefab's transform values as opposed to the replicated state (which now is only the last deltas)
+ CreateAndStartNewClientWithTimeTravel();
var newClientNetworkManager = m_ClientNetworkManagers[NumberOfClients];
foreach (var playerRelativeEntry in m_PlayerNetworkObjects)
{
@@ -451,56 +651,11 @@ namespace Unity.Netcode.RuntimeTests
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" +
+ Assert.IsTrue(Approximately(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);
}
///
@@ -511,8 +666,8 @@ namespace Unity.Netcode.RuntimeTests
/// - 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)
+ [Test]
+ public void TestAuthoritativeTransformChangeOneAtATime([Values] TransformSpace testLocalTransform, [Values] Interpolation interpolation, [Values] OverrideState overideState)
{
var overrideUpdate = overideState == OverrideState.CommitToTransform;
m_AuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate;
@@ -525,7 +680,9 @@ namespace Unity.Netcode.RuntimeTests
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);
+ m_AuthoritativeTransform.StatePushed = false;
+ var nextPosition = GetRandomVector3(2f, 30f);
+ m_AuthoritativeTransform.transform.position = nextPosition;
if (overideState != OverrideState.SetState)
{
authPlayerTransform.position = nextPosition;
@@ -536,13 +693,22 @@ namespace Unity.Netcode.RuntimeTests
m_OwnerTransform.SetState(nextPosition, null, null, m_AuthoritativeTransform.Interpolate);
}
- yield return WaitForConditionOrTimeOut(PositionsMatch);
- AssertOnTimeout($"Timed out waiting for positions to match");
+ bool success;
+ if (overideState != OverrideState.Update)
+ {
+ // Wait for the deltas to be pushed
+ success = WaitForConditionOrTimeOutWithTimeTravel(() => m_AuthoritativeTransform.StatePushed);
+ Assert.True(success, $"[Position] Timed out waiting for state to be pushed ({m_AuthoritativeTransform.StatePushed})!");
+ }
+
+ success = WaitForConditionOrTimeOutWithTimeTravel(() => PositionsMatch());
+ Assert.True(success, $"Timed out waiting for positions to match {m_AuthoritativeTransform.transform.position} | {m_NonAuthoritativeTransform.transform.position}");
// test rotation
Assert.AreEqual(Quaternion.identity, m_NonAuthoritativeTransform.transform.rotation, "wrong initial value for rotation"); // sanity check
- var nextRotation = Quaternion.Euler(45, 40, 35); // using euler angles instead of quaternions directly to really see issues users might encounter
+ m_AuthoritativeTransform.StatePushed = false;
+ var nextRotation = Quaternion.Euler(GetRandomVector3(5, 60)); // using euler angles instead of quaternions directly to really see issues users might encounter
if (overideState != OverrideState.SetState)
{
authPlayerTransform.rotation = nextRotation;
@@ -552,11 +718,19 @@ namespace Unity.Netcode.RuntimeTests
{
m_OwnerTransform.SetState(null, nextRotation, null, m_AuthoritativeTransform.Interpolate);
}
+ if (overideState != OverrideState.Update)
+ {
+ // Wait for the deltas to be pushed
+ success = WaitForConditionOrTimeOutWithTimeTravel(() => m_AuthoritativeTransform.StatePushed);
+ Assert.True(success, $"[Rotation] Timed out waiting for state to be pushed ({m_AuthoritativeTransform.StatePushed})!");
+ }
- yield return WaitForConditionOrTimeOut(RotationsMatch);
- AssertOnTimeout($"Timed out waiting for rotations to match");
+ // Make sure the values match
+ success = WaitForConditionOrTimeOutWithTimeTravel(() => RotationsMatch());
+ Assert.True(success, $"Timed out waiting for rotations to match");
- var nextScale = new Vector3(2, 3, 4);
+ m_AuthoritativeTransform.StatePushed = false;
+ var nextScale = GetRandomVector3(1, 6);
if (overrideUpdate)
{
authPlayerTransform.localScale = nextScale;
@@ -566,24 +740,38 @@ namespace Unity.Netcode.RuntimeTests
{
m_OwnerTransform.SetState(null, null, nextScale, m_AuthoritativeTransform.Interpolate);
}
+ if (overideState != OverrideState.Update)
+ {
+ // Wait for the deltas to be pushed
+ success = WaitForConditionOrTimeOutWithTimeTravel(() => m_AuthoritativeTransform.StatePushed);
+ Assert.True(success, $"[Rotation] Timed out waiting for state to be pushed ({m_AuthoritativeTransform.StatePushed})!");
+ }
- yield return WaitForConditionOrTimeOut(ScaleValuesMatch);
- AssertOnTimeout($"Timed out waiting for scale values to match");
+ // Make sure the scale values match
+ success = WaitForConditionOrTimeOutWithTimeTravel(() => ScaleValuesMatch());
+ Assert.True(success, $"Timed out waiting for scale values to match");
}
///
/// Test to verify nonAuthority cannot change the transform directly
///
- [UnityTest]
- public IEnumerator VerifyNonAuthorityCantChangeTransform([Values] Interpolation interpolation)
+ [Test]
+ public void VerifyNonAuthorityCantChangeTransform([Values] Interpolation interpolation, [Values] Precision precision)
{
m_AuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate;
+ m_AuthoritativeTransform.UseHalfFloatPrecision = precision == Precision.Half;
+ m_AuthoritativeTransform.UseQuaternionSynchronization = true;
m_NonAuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate;
+ m_NonAuthoritativeTransform.UseHalfFloatPrecision = precision == Precision.Half;
+ m_NonAuthoritativeTransform.UseQuaternionSynchronization = true;
+
+
Assert.AreEqual(Vector3.zero, m_NonAuthoritativeTransform.transform.position, "other side pos should be zero at first"); // sanity check
m_NonAuthoritativeTransform.transform.position = new Vector3(4, 5, 6);
- yield return s_DefaultWaitForTick;
+ WaitForNextTick();
+ WaitForNextTick();
Assert.AreEqual(Vector3.zero, m_NonAuthoritativeTransform.transform.position, "[Position] NonAuthority was able to change the position!");
@@ -594,14 +782,14 @@ namespace Unity.Netcode.RuntimeTests
nonAuthorityEulerRotation.y += 20.0f;
nonAuthorityRotation.eulerAngles = nonAuthorityEulerRotation;
m_NonAuthoritativeTransform.transform.rotation = nonAuthorityRotation;
- yield return s_DefaultWaitForTick;
+ WaitForNextTick();
var nonAuthorityCurrentEuler = m_NonAuthoritativeTransform.transform.rotation.eulerAngles;
Assert.True(originalNonAuthorityEulerRotation.Equals(nonAuthorityCurrentEuler), "[Rotation] NonAuthority was able to change the rotation!");
var nonAuthorityScale = m_NonAuthoritativeTransform.transform.localScale;
m_NonAuthoritativeTransform.transform.localScale = nonAuthorityScale * 100;
- yield return s_DefaultWaitForTick;
+ WaitForNextTick();
Assert.True(nonAuthorityScale.Equals(m_NonAuthoritativeTransform.transform.localScale), "[Scale] NonAuthority was able to change the scale!");
}
@@ -610,25 +798,31 @@ namespace Unity.Netcode.RuntimeTests
/// Validates that rotation checks don't produce false positive
/// results when rolling over between 0 and 360 degrees
///
- [UnityTest]
- public IEnumerator TestRotationThresholdDeltaCheck([Values] Interpolation interpolation)
+ [Test]
+ public void TestRotationThresholdDeltaCheck([Values] Interpolation interpolation, [Values] Precision precision)
{
m_AuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate;
+ m_AuthoritativeTransform.UseHalfFloatPrecision = precision == Precision.Half;
+ m_AuthoritativeTransform.UseQuaternionSynchronization = true;
m_NonAuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate;
-
+ m_NonAuthoritativeTransform.UseHalfFloatPrecision = precision == Precision.Half;
+ m_NonAuthoritativeTransform.UseQuaternionSynchronization = true;
m_NonAuthoritativeTransform.RotAngleThreshold = m_AuthoritativeTransform.RotAngleThreshold = 5.0f;
var halfThreshold = m_AuthoritativeTransform.RotAngleThreshold * 0.5001f;
var authorityRotation = m_AuthoritativeTransform.transform.rotation;
var authorityEulerRotation = authorityRotation.eulerAngles;
+ // Apply the current state which assures all bitset flags are updated
+ var results = m_AuthoritativeTransform.ApplyState();
+
// Verify rotation is not marked dirty when rotated by half of the threshold
authorityEulerRotation.y += halfThreshold;
authorityRotation.eulerAngles = authorityEulerRotation;
m_AuthoritativeTransform.transform.rotation = authorityRotation;
- var results = m_AuthoritativeTransform.ApplyState();
+ 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;
+ WaitForNextTick();
// Verify rotation is marked dirty when rotated by another half threshold value
authorityEulerRotation.y += halfThreshold;
@@ -636,12 +830,12 @@ namespace Unity.Netcode.RuntimeTests
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;
+ WaitForNextTick();
//Reset rotation back to zero on all axis
authorityRotation.eulerAngles = authorityEulerRotation = Vector3.zero;
m_AuthoritativeTransform.transform.rotation = authorityRotation;
- yield return s_DefaultWaitForTick;
+ WaitForNextTick();
// Rotate by 360 minus halfThreshold (which is really just negative halfThreshold) and verify rotation is not marked dirty
authorityEulerRotation.y = 360 - halfThreshold;
@@ -662,7 +856,7 @@ namespace Unity.Netcode.RuntimeTests
//Reset rotation back to zero on all axis
authorityRotation.eulerAngles = authorityEulerRotation = Vector3.zero;
m_AuthoritativeTransform.transform.rotation = authorityRotation;
- yield return s_DefaultWaitForTick;
+ WaitForNextTick();
authorityEulerRotation.y -= halfThreshold;
authorityRotation.eulerAngles = authorityEulerRotation;
@@ -693,22 +887,22 @@ namespace Unity.Netcode.RuntimeTests
///
/// Test to make sure that the bitset value is updated properly
///
- [UnityTest]
- public IEnumerator TestBitsetValue([Values] Interpolation interpolation)
+ [Test]
+ public void TestBitsetValue([Values] Interpolation interpolation)
{
m_AuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate;
m_NonAuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate;
m_NonAuthoritativeTransform.RotAngleThreshold = m_AuthoritativeTransform.RotAngleThreshold = 0.1f;
- yield return s_DefaultWaitForTick;
+ WaitForNextTick();
m_AuthoritativeTransform.transform.rotation = Quaternion.Euler(1, 2, 3);
- var serverLastSentState = m_AuthoritativeTransform.GetLastSentState();
+ var serverLastSentState = m_AuthoritativeTransform.AuthorityLastSentState;
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!");
+ var success = WaitForConditionOrTimeOutWithTimeTravel(() => ValidateBitSetValues(serverLastSentState, clientReplicatedState));
+ Assert.True(success, $"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}");
+ success = WaitForConditionOrTimeOutWithTimeTravel(() => RotationsMatch());
+ Assert.True(success, $"[Timed-Out] Authoritative rotation {m_AuthoritativeTransform.transform.rotation.eulerAngles} != Non-Authoritative rotation {m_NonAuthoritativeTransform.transform.rotation.eulerAngles}");
}
private float m_DetectedPotentialInterpolatedTeleport;
@@ -716,22 +910,35 @@ namespace Unity.Netcode.RuntimeTests
///
/// The tests teleporting with and without interpolation
///
- [UnityTest]
- public IEnumerator TeleportTest([Values] Interpolation interpolation)
+ [Test]
+ public void TeleportTest([Values] Interpolation interpolation, [Values] Precision precision)
{
m_AuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate;
m_NonAuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate;
+ m_AuthoritativeTransform.UseHalfFloatPrecision = precision == Precision.Half;
+ m_Precision = precision;
var authTransform = m_AuthoritativeTransform.transform;
var nonAuthPosition = m_NonAuthoritativeTransform.transform.position;
var currentTick = m_AuthoritativeTransform.NetworkManager.ServerTime.Tick;
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));
+ var teleportDestination = GetRandomVector3(50.0f, 200.0f);
+ m_NonAuthoritativeTransform.StateUpdated = false;
+ m_AuthoritativeTransform.StatePushed = false;
+ m_AuthoritativeTransform.Teleport(teleportDestination, authTransform.rotation, authTransform.localScale);
- 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}");
+ // Wait for the deltas to be pushed and updated
+ var success = WaitForConditionOrTimeOutWithTimeTravel(() => m_AuthoritativeTransform.StatePushed && m_NonAuthoritativeTransform.StateUpdated);
+ Assert.True(success, $"[Teleport] Timed out waiting for state to be pushed ({m_AuthoritativeTransform.StatePushed}) or state to be updated ({m_NonAuthoritativeTransform.StateUpdated})!");
+
+ SimulateOneFrame();
+ Assert.True(TeleportPositionMatches(nonAuthPosition), $"NonAuthoritative position ({m_NonAuthoritativeTransform.GetSpaceRelativePosition()}) is not the same as the destination position {teleportDestination}!");
+
+ var targetDistance = 0.0f;
+ if (!Approximately(m_DetectedPotentialInterpolatedTeleport, 0.0f))
+ {
+ targetDistance = Mathf.Abs(Vector3.Distance(nonAuthPosition, teleportDestination));
+ }
+ Assert.IsTrue(Approximately(m_DetectedPotentialInterpolatedTeleport, 0.0f), $"Detected possible interpolation on non-authority side! NonAuthority distance: {m_DetectedPotentialInterpolatedTeleport} | Target distance: {targetDistance}");
}
///
@@ -744,8 +951,8 @@ namespace Unity.Netcode.RuntimeTests
///
/// This also tests that the original server authoritative model with client-owner driven NetworkTransforms is preserved.
///
- [UnityTest]
- public IEnumerator NonAuthorityOwnerSettingStateTest([Values] Interpolation interpolation)
+ [Test]
+ public void NonAuthorityOwnerSettingStateTest([Values] Interpolation interpolation)
{
var interpolate = interpolation == Interpolation.EnableInterpolate;
m_AuthoritativeTransform.Interpolate = interpolate;
@@ -757,22 +964,22 @@ namespace Unity.Netcode.RuntimeTests
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!");
+ var success = WaitForConditionOrTimeOutWithTimeTravel(() => PositionsMatchesValue(newPosition));
+ Assert.True(success, $"Timed out waiting for non-authoritative position state request to be applied!");
+ Assert.True(Approximately(newPosition, m_AuthoritativeTransform.transform.position), "Authoritative position does not match!");
+ Assert.True(Approximately(newPosition, m_NonAuthoritativeTransform.transform.position), "Non-Authoritative position does not match!");
m_NonAuthoritativeTransform.SetState(null, newRotation, null, interpolate);
- 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!");
+ success = WaitForConditionOrTimeOutWithTimeTravel(() => RotationMatchesValue(newRotation.eulerAngles));
+ Assert.True(success, $"Timed out waiting for non-authoritative rotation state request to be applied!");
+ Assert.True(Approximately(newRotation.eulerAngles, m_AuthoritativeTransform.transform.rotation.eulerAngles), "Authoritative rotation does not match!");
+ Assert.True(Approximately(newRotation.eulerAngles, m_NonAuthoritativeTransform.transform.rotation.eulerAngles), "Non-Authoritative rotation does not match!");
m_NonAuthoritativeTransform.SetState(null, null, newScale, interpolate);
- 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!");
+ success = WaitForConditionOrTimeOutWithTimeTravel(() => ScaleMatchesValue(newScale));
+ Assert.True(success, $"Timed out waiting for non-authoritative scale state request to be applied!");
+ Assert.True(Approximately(newScale, m_AuthoritativeTransform.transform.localScale), "Authoritative scale does not match!");
+ Assert.True(Approximately(newScale, m_NonAuthoritativeTransform.transform.localScale), "Non-Authoritative scale does not match!");
// Test all parameters at once
newPosition = new Vector3(55f, 95f, -25f);
@@ -780,42 +987,23 @@ namespace Unity.Netcode.RuntimeTests
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!");
+ success = WaitForConditionOrTimeOutWithTimeTravel(() => PositionRotationScaleMatches(newPosition, newRotation.eulerAngles, newScale));
+ Assert.True(success, $"Timed out waiting for non-authoritative position, rotation, and scale state request to be applied!");
+ Assert.True(Approximately(newPosition, m_AuthoritativeTransform.transform.position), "Authoritative position does not match!");
+ Assert.True(Approximately(newPosition, m_NonAuthoritativeTransform.transform.position), "Non-Authoritative position does not match!");
+ Assert.True(Approximately(newRotation.eulerAngles, m_AuthoritativeTransform.transform.rotation.eulerAngles), "Authoritative rotation does not match!");
+ Assert.True(Approximately(newRotation.eulerAngles, m_NonAuthoritativeTransform.transform.rotation.eulerAngles), "Non-Authoritative rotation does not match!");
+ Assert.True(Approximately(newScale, m_AuthoritativeTransform.transform.localScale), "Authoritative scale does not match!");
+ Assert.True(Approximately(newScale, m_NonAuthoritativeTransform.transform.localScale), "Non-Authoritative scale does not match!");
}
- private 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 const float k_AproximateDeltaVariance = 0.025f;
private bool PositionsMatchesValue(Vector3 positionToMatch)
{
var authorityPosition = m_AuthoritativeTransform.transform.position;
var nonAuthorityPosition = m_NonAuthoritativeTransform.transform.position;
- var auhtorityIsEqual = Aproximately(authorityPosition, positionToMatch);
- var nonauthorityIsEqual = Aproximately(nonAuthorityPosition, positionToMatch);
+ var auhtorityIsEqual = Approximately(authorityPosition, positionToMatch);
+ var nonauthorityIsEqual = Approximately(nonAuthorityPosition, positionToMatch);
if (!auhtorityIsEqual)
{
@@ -832,8 +1020,8 @@ namespace Unity.Netcode.RuntimeTests
{
var authorityRotationEuler = m_AuthoritativeTransform.transform.rotation.eulerAngles;
var nonAuthorityRotationEuler = m_NonAuthoritativeTransform.transform.rotation.eulerAngles;
- var auhtorityIsEqual = Aproximately(authorityRotationEuler, rotationEulerToMatch);
- var nonauthorityIsEqual = Aproximately(nonAuthorityRotationEuler, rotationEulerToMatch);
+ var auhtorityIsEqual = Approximately(authorityRotationEuler, rotationEulerToMatch);
+ var nonauthorityIsEqual = Approximately(nonAuthorityRotationEuler, rotationEulerToMatch);
if (!auhtorityIsEqual)
{
@@ -850,8 +1038,8 @@ namespace Unity.Netcode.RuntimeTests
{
var authorityScale = m_AuthoritativeTransform.transform.localScale;
var nonAuthorityScale = m_NonAuthoritativeTransform.transform.localScale;
- var auhtorityIsEqual = Aproximately(authorityScale, scaleToMatch);
- var nonauthorityIsEqual = Aproximately(nonAuthorityScale, scaleToMatch);
+ var auhtorityIsEqual = Approximately(authorityScale, scaleToMatch);
+ var nonauthorityIsEqual = Approximately(nonAuthorityScale, scaleToMatch);
if (!auhtorityIsEqual)
{
@@ -864,29 +1052,32 @@ namespace Unity.Netcode.RuntimeTests
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 we are not within our target distance range
+ if (!Approximately(targetDistance, nonAuthorityCurrentDistance))
{
- if (nonAuthorityCurrentDistance >= 0.15f * targetDistance && nonAuthorityCurrentDistance <= 0.75f * targetDistance)
- {
- m_DetectedPotentialInterpolatedTeleport = nonAuthorityCurrentDistance;
- }
+ // Apply the non-authority's distance that is checked at the end of the teleport test
+ 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);
+ else
+ {
+ // Otherwise, if we are within our target distance range then reset any already set value
+ m_DetectedPotentialInterpolatedTeleport = 0.0f;
+ }
+ var xIsEqual = Approximately(authorityPosition.x, nonAuthorityPosition.x);
+ var yIsEqual = Approximately(authorityPosition.y, nonAuthorityPosition.y);
+ var zIsEqual = Approximately(authorityPosition.z, nonAuthorityPosition.z);
if (!xIsEqual || !yIsEqual || !zIsEqual)
{
- VerboseDebug($"Authority position {authorityPosition} != NonAuthority position {nonAuthorityPosition}");
+ VerboseDebug($"[{m_AuthoritativeTransform.gameObject.name}] Authority position {authorityPosition} != [{m_NonAuthoritativeTransform.gameObject.name}] NonAuthority position {nonAuthorityPosition}");
}
- return xIsEqual && yIsEqual && zIsEqual; ;
+ return xIsEqual && yIsEqual && zIsEqual;
}
private bool PositionRotationScaleMatches(Vector3 position, Vector3 eulerRotation, Vector3 scale)
@@ -899,53 +1090,61 @@ namespace Unity.Netcode.RuntimeTests
return RotationsMatch() && PositionsMatch() && ScaleValuesMatch();
}
- private bool RotationsMatch()
+ private void PrintPositionRotationScaleDeltas()
{
- 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);
+ RotationsMatch(true);
+ PositionsMatch(true);
+ ScaleValuesMatch(true);
+ }
+
+ private bool RotationsMatch(bool printDeltas = false)
+ {
+ m_CurrentHalfPrecision = k_HalfPrecisionRot;
+ var authorityEulerRotation = m_AuthoritativeTransform.GetSpaceRelativeRotation().eulerAngles;
+ var nonAuthorityEulerRotation = m_NonAuthoritativeTransform.GetSpaceRelativeRotation().eulerAngles;
+ var xIsEqual = ApproximatelyEuler(authorityEulerRotation.x, nonAuthorityEulerRotation.x) || !m_AuthoritativeTransform.SyncRotAngleX;
+ var yIsEqual = ApproximatelyEuler(authorityEulerRotation.y, nonAuthorityEulerRotation.y) || !m_AuthoritativeTransform.SyncRotAngleY;
+ var zIsEqual = ApproximatelyEuler(authorityEulerRotation.z, nonAuthorityEulerRotation.z) || !m_AuthoritativeTransform.SyncRotAngleZ;
if (!xIsEqual || !yIsEqual || !zIsEqual)
{
- VerboseDebug($"Authority rotation {authorityEulerRotation} != NonAuthority rotation {nonAuthorityEulerRotation}");
+ VerboseDebug($"[{m_AuthoritativeTransform.gameObject.name}][X-{xIsEqual} | Y-{yIsEqual} | Z-{zIsEqual}][{m_CurrentAxis}]" +
+ $"[Sync: X-{m_AuthoritativeTransform.SyncRotAngleX} | X-{m_AuthoritativeTransform.SyncRotAngleY} | X-{m_AuthoritativeTransform.SyncRotAngleZ}] Authority rotation {authorityEulerRotation} != [{m_NonAuthoritativeTransform.gameObject.name}] NonAuthority rotation {nonAuthorityEulerRotation}");
+ }
+ if (printDeltas)
+ {
+ Debug.Log($"[Rotation Match] Euler Delta {EulerDelta(authorityEulerRotation, nonAuthorityEulerRotation)}");
}
return xIsEqual && yIsEqual && zIsEqual;
}
- private bool PositionsMatch()
+ private bool PositionsMatch(bool printDeltas = false)
{
- 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);
+ m_CurrentHalfPrecision = k_HalfPrecisionPosScale;
+ var authorityPosition = m_AuthoritativeTransform.GetSpaceRelativePosition();
+ var nonAuthorityPosition = m_NonAuthoritativeTransform.GetSpaceRelativePosition();
+ var xIsEqual = Approximately(authorityPosition.x, nonAuthorityPosition.x) || !m_AuthoritativeTransform.SyncPositionX;
+ var yIsEqual = Approximately(authorityPosition.y, nonAuthorityPosition.y) || !m_AuthoritativeTransform.SyncPositionY;
+ var zIsEqual = Approximately(authorityPosition.z, nonAuthorityPosition.z) || !m_AuthoritativeTransform.SyncPositionZ;
if (!xIsEqual || !yIsEqual || !zIsEqual)
{
- VerboseDebug($"Authority position {authorityPosition} != NonAuthority position {nonAuthorityPosition}");
+ VerboseDebug($"[{m_AuthoritativeTransform.gameObject.name}] Authority position {authorityPosition} != [{m_NonAuthoritativeTransform.gameObject.name}] NonAuthority position {nonAuthorityPosition}");
}
return xIsEqual && yIsEqual && zIsEqual;
}
- private bool ScaleValuesMatch()
+ private bool ScaleValuesMatch(bool printDeltas = false)
{
+ m_CurrentHalfPrecision = k_HalfPrecisionPosScale;
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);
+ var xIsEqual = Approximately(authorityScale.x, nonAuthorityScale.x) || !m_AuthoritativeTransform.SyncScaleX;
+ var yIsEqual = Approximately(authorityScale.y, nonAuthorityScale.y) || !m_AuthoritativeTransform.SyncScaleY;
+ var zIsEqual = Approximately(authorityScale.z, nonAuthorityScale.z) || !m_AuthoritativeTransform.SyncScaleZ;
if (!xIsEqual || !yIsEqual || !zIsEqual)
{
- VerboseDebug($"Authority scale {authorityScale} != NonAuthority scale {nonAuthorityScale}");
+ VerboseDebug($"[{m_AuthoritativeTransform.gameObject.name}] Authority scale {authorityScale} != [{m_NonAuthoritativeTransform.gameObject.name}] NonAuthority scale {nonAuthorityScale}");
}
return xIsEqual && yIsEqual && zIsEqual;
}
-
- protected override IEnumerator OnTearDown()
- {
- m_EnableVerboseDebug = false;
- Object.DestroyImmediate(m_PlayerPrefab);
- yield return base.OnTearDown();
- }
}
}
diff --git a/Tests/Runtime/NetworkUpdateLoopTests.cs b/Tests/Runtime/NetworkUpdateLoopTests.cs
index bf93176..97f83c1 100644
--- a/Tests/Runtime/NetworkUpdateLoopTests.cs
+++ b/Tests/Runtime/NetworkUpdateLoopTests.cs
@@ -1,11 +1,11 @@
using System;
using System.Collections;
using System.Linq;
-using UnityEngine;
-using UnityEngine.TestTools;
using NUnit.Framework;
+using UnityEngine;
using UnityEngine.LowLevel;
using UnityEngine.PlayerLoop;
+using UnityEngine.TestTools;
namespace Unity.Netcode.RuntimeTests
{
diff --git a/Tests/Runtime/NetworkVarBufferCopyTest.cs b/Tests/Runtime/NetworkVarBufferCopyTest.cs
index 228b230..272e644 100644
--- a/Tests/Runtime/NetworkVarBufferCopyTest.cs
+++ b/Tests/Runtime/NetworkVarBufferCopyTest.cs
@@ -1,8 +1,8 @@
using System.Collections;
using System.Collections.Generic;
using NUnit.Framework;
-using UnityEngine.TestTools;
using Unity.Netcode.TestHelpers.Runtime;
+using UnityEngine.TestTools;
namespace Unity.Netcode.RuntimeTests
{
@@ -21,7 +21,7 @@ namespace Unity.Netcode.RuntimeTests
writer.TryBeginWrite(FastBufferWriter.GetWriteSize(k_DummyValue) + 1);
using (var bitWriter = writer.EnterBitwiseContext())
{
- bitWriter.WriteBits((byte)1, 1);
+ bitWriter.WriteBits(1, 1);
}
writer.WriteValue(k_DummyValue);
@@ -33,7 +33,7 @@ namespace Unity.Netcode.RuntimeTests
writer.TryBeginWrite(FastBufferWriter.GetWriteSize(k_DummyValue) + 1);
using (var bitWriter = writer.EnterBitwiseContext())
{
- bitWriter.WriteBits((byte)1, 1);
+ bitWriter.WriteBits(1, 1);
}
writer.WriteValue(k_DummyValue);
diff --git a/Tests/Runtime/NetworkVariableTests.cs b/Tests/Runtime/NetworkVariableTests.cs
index dfc7a0f..b20d52a 100644
--- a/Tests/Runtime/NetworkVariableTests.cs
+++ b/Tests/Runtime/NetworkVariableTests.cs
@@ -1,12 +1,13 @@
using System;
using System.Collections;
using System.Collections.Generic;
-using UnityEngine.TestTools;
+using System.Linq;
using NUnit.Framework;
using Unity.Collections;
using Unity.Netcode.TestHelpers.Runtime;
-using Random = UnityEngine.Random;
using UnityEngine;
+using UnityEngine.TestTools;
+using Random = UnityEngine.Random;
namespace Unity.Netcode.RuntimeTests
{
@@ -17,6 +18,22 @@ namespace Unity.Netcode.RuntimeTests
public NetworkVariable OwnerReadWrite_Position = new NetworkVariable(Vector3.one, NetworkVariableReadPermission.Owner, NetworkVariableWritePermission.Owner);
}
+ public class NetworkVariableMiddleclass : NetworkVariable
+ {
+
+ }
+
+ public class NetworkVariableSubclass : NetworkVariableMiddleclass
+ {
+
+ }
+
+ public struct TemplatedValueOnlyReferencedByNetworkVariableSubclass : INetworkSerializeByMemcpy
+ where T : unmanaged
+ {
+ public T Value;
+ }
+
// The ILPP code for NetworkVariables to determine how to serialize them relies on them existing as fields of a NetworkBehaviour to find them.
// Some of the tests below create NetworkVariables on the stack, so this class is here just to make sure the relevant types are all accounted for.
public class NetVarILPPClassForTests : NetworkBehaviour
@@ -25,6 +42,7 @@ namespace Unity.Netcode.RuntimeTests
public NetworkVariable ManagedNetworkSerializableTypeVar;
public NetworkVariable StringVar;
public NetworkVariable GuidVar;
+ public NetworkVariableSubclass> SubclassVar;
}
public class TemplateNetworkBehaviourType : NetworkBehaviour
@@ -500,6 +518,10 @@ namespace Unity.Netcode.RuntimeTests
private const int k_TestVal2 = 222;
private const int k_TestVal3 = 333;
+ protected override bool m_EnableTimeTravel => true;
+ protected override bool m_SetupIsACoroutine => false;
+ protected override bool m_TearDownIsACoroutine => false;
+
private static List s_ClientNetworkVariableTestInstances = new List();
public static void ClientNetworkVariableTestSpawned(NetworkVariableTest networkVariableTest)
{
@@ -530,7 +552,7 @@ namespace Unity.Netcode.RuntimeTests
/// This is an adjustment to how the server and clients are started in order
/// to avoid timing issues when running in a stand alone test runner build.
///
- private IEnumerator InitializeServerAndClients(bool useHost)
+ private void InitializeServerAndClients(bool useHost)
{
s_ClientNetworkVariableTestInstances.Clear();
m_PlayerPrefab.AddComponent();
@@ -552,13 +574,13 @@ namespace Unity.Netcode.RuntimeTests
RegisterSceneManagerHandler();
// Wait for connection on client and server side
- yield return WaitForClientsConnectedOrTimeOut();
- AssertOnTimeout($"Timed-out waiting for all clients to connect!");
+ var success = WaitForClientsConnectedOrTimeOutWithTimeTravel();
+ Assert.True(success, $"Timed-out waiting for all clients to connect!");
// These are the *SERVER VERSIONS* of the *CLIENT PLAYER 1 & 2*
var result = new NetcodeIntegrationTestHelpers.ResultWrapper();
- yield return NetcodeIntegrationTestHelpers.GetNetworkObjectByRepresentation(
+ NetcodeIntegrationTestHelpers.GetNetworkObjectByRepresentationWithTimeTravel(
x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId,
m_ServerNetworkManager, result);
@@ -566,7 +588,7 @@ namespace Unity.Netcode.RuntimeTests
m_Player1OnServer = result.Result.GetComponent();
// This is client1's view of itself
- yield return NetcodeIntegrationTestHelpers.GetNetworkObjectByRepresentation(
+ NetcodeIntegrationTestHelpers.GetNetworkObjectByRepresentationWithTimeTravel(
x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId,
m_ClientNetworkManagers[0], result);
@@ -586,18 +608,18 @@ namespace Unity.Netcode.RuntimeTests
var instanceCount = useHost ? NumberOfClients * 3 : NumberOfClients * 2;
// Wait for the client-side to notify it is finished initializing and spawning.
- yield return WaitForConditionOrTimeOut(() => s_ClientNetworkVariableTestInstances.Count == instanceCount);
+ success = WaitForConditionOrTimeOutWithTimeTravel(() => s_ClientNetworkVariableTestInstances.Count == instanceCount);
- Assert.False(s_GlobalTimeoutHelper.TimedOut, "Timed out waiting for all client NetworkVariableTest instances to register they have spawned!");
+ Assert.True(success, "Timed out waiting for all client NetworkVariableTest instances to register they have spawned!");
- yield return s_DefaultWaitForTick;
+ TimeTravelToNextTick();
}
///
/// Runs generalized tests on all predefined NetworkVariable types
///
- [UnityTest]
- public IEnumerator AllNetworkVariableTypes([Values(true, false)] bool useHost)
+ [Test]
+ public void AllNetworkVariableTypes([Values(true, false)] bool useHost)
{
// Create, instantiate, and host
// This would normally go in Setup, but since every other test but this one
@@ -616,8 +638,8 @@ namespace Unity.Netcode.RuntimeTests
// Start Testing
networkVariableTestComponent.EnableTesting = true;
- yield return WaitForConditionOrTimeOut(() => true == networkVariableTestComponent.IsTestComplete());
- Assert.IsFalse(s_GlobalTimeoutHelper.TimedOut, "Timed out waiting for the test to complete!");
+ var success = WaitForConditionOrTimeOutWithTimeTravel(() => true == networkVariableTestComponent.IsTestComplete());
+ Assert.True(success, "Timed out waiting for the test to complete!");
// Stop Testing
networkVariableTestComponent.EnableTesting = false;
@@ -634,10 +656,10 @@ namespace Unity.Netcode.RuntimeTests
NetworkManagerHelper.ShutdownNetworkManager();
}
- [UnityTest]
- public IEnumerator ClientWritePermissionTest([Values(true, false)] bool useHost)
+ [Test]
+ public void ClientWritePermissionTest([Values(true, false)] bool useHost)
{
- yield return InitializeServerAndClients(useHost);
+ InitializeServerAndClients(useHost);
// client must not be allowed to write to a server auth variable
Assert.Throws(() => m_Player1OnClient1.TheScalar.Value = k_TestVal1);
@@ -646,63 +668,63 @@ namespace Unity.Netcode.RuntimeTests
///
/// Runs tests that network variables sync on client whatever the local value of .
///
- [UnityTest]
- public IEnumerator NetworkVariableSync_WithDifferentTimeScale([Values(true, false)] bool useHost, [Values(0.0f, 1.0f, 2.0f)] float timeScale)
+ [Test]
+ public void NetworkVariableSync_WithDifferentTimeScale([Values(true, false)] bool useHost, [Values(0.0f, 1.0f, 2.0f)] float timeScale)
{
Time.timeScale = timeScale;
- yield return InitializeServerAndClients(useHost);
+ InitializeServerAndClients(useHost);
m_Player1OnServer.TheScalar.Value = k_TestVal1;
// Now wait for the client side version to be updated to k_TestVal1
- yield return WaitForConditionOrTimeOut(() => m_Player1OnClient1.TheScalar.Value == k_TestVal1);
- Assert.IsFalse(s_GlobalTimeoutHelper.TimedOut, "Timed out waiting for client-side NetworkVariable to update!");
+ var success = WaitForConditionOrTimeOutWithTimeTravel(() => m_Player1OnClient1.TheScalar.Value == k_TestVal1);
+ Assert.True(success, "Timed out waiting for client-side NetworkVariable to update!");
}
- [UnityTest]
- public IEnumerator FixedString32Test([Values(true, false)] bool useHost)
+ [Test]
+ public void FixedString32Test([Values(true, false)] bool useHost)
{
- yield return InitializeServerAndClients(useHost);
+ InitializeServerAndClients(useHost);
m_Player1OnServer.FixedString32.Value = k_FixedStringTestValue;
// Now wait for the client side version to be updated to k_FixedStringTestValue
- yield return WaitForConditionOrTimeOut(() => m_Player1OnClient1.FixedString32.Value == k_FixedStringTestValue);
- Assert.IsFalse(s_GlobalTimeoutHelper.TimedOut, "Timed out waiting for client-side NetworkVariable to update!");
+ var success = WaitForConditionOrTimeOutWithTimeTravel(() => m_Player1OnClient1.FixedString32.Value == k_FixedStringTestValue);
+ Assert.True(success, "Timed out waiting for client-side NetworkVariable to update!");
}
- [UnityTest]
- public IEnumerator NetworkListAdd([Values(true, false)] bool useHost)
+ [Test]
+ public void NetworkListAdd([Values(true, false)] bool useHost)
{
- yield return InitializeServerAndClients(useHost);
+ InitializeServerAndClients(useHost);
m_NetworkListPredicateHandler = new NetworkListTestPredicate(m_Player1OnServer, m_Player1OnClient1, NetworkListTestPredicate.NetworkListTestStates.Add, 10);
- yield return WaitForConditionOrTimeOut(m_NetworkListPredicateHandler);
+ Assert.True(WaitForConditionOrTimeOutWithTimeTravel(m_NetworkListPredicateHandler));
}
- [UnityTest]
- public IEnumerator WhenListContainsManyLargeValues_OverflowExceptionIsNotThrown([Values(true, false)] bool useHost)
+ [Test]
+ public void WhenListContainsManyLargeValues_OverflowExceptionIsNotThrown([Values(true, false)] bool useHost)
{
- yield return InitializeServerAndClients(useHost);
+ InitializeServerAndClients(useHost);
m_NetworkListPredicateHandler = new NetworkListTestPredicate(m_Player1OnServer, m_Player1OnClient1, NetworkListTestPredicate.NetworkListTestStates.ContainsLarge, 20);
- yield return WaitForConditionOrTimeOut(m_NetworkListPredicateHandler);
+ Assert.True(WaitForConditionOrTimeOutWithTimeTravel(m_NetworkListPredicateHandler));
}
- [UnityTest]
- public IEnumerator NetworkListContains([Values(true, false)] bool useHost)
+ [Test]
+ public void NetworkListContains([Values(true, false)] bool useHost)
{
// Re-use the NetworkListAdd to initialize the server and client as well as make sure the list is populated
- yield return NetworkListAdd(useHost);
+ NetworkListAdd(useHost);
// Now test the NetworkList.Contains method
m_NetworkListPredicateHandler.SetNetworkListTestState(NetworkListTestPredicate.NetworkListTestStates.Contains);
- yield return WaitForConditionOrTimeOut(m_NetworkListPredicateHandler);
+ Assert.True(WaitForConditionOrTimeOutWithTimeTravel(m_NetworkListPredicateHandler));
}
- [UnityTest]
- public IEnumerator NetworkListRemove([Values(true, false)] bool useHost)
+ [Test]
+ public void NetworkListRemove([Values(true, false)] bool useHost)
{
// Re-use the NetworkListAdd to initialize the server and client as well as make sure the list is populated
- yield return NetworkListAdd(useHost);
+ NetworkListAdd(useHost);
// Remove two entries by index
m_Player1OnServer.TheList.Remove(3);
@@ -710,41 +732,41 @@ namespace Unity.Netcode.RuntimeTests
// Really just verifies the data at this point
m_NetworkListPredicateHandler.SetNetworkListTestState(NetworkListTestPredicate.NetworkListTestStates.VerifyData);
- yield return WaitForConditionOrTimeOut(m_NetworkListPredicateHandler);
+ Assert.True(WaitForConditionOrTimeOutWithTimeTravel(m_NetworkListPredicateHandler));
}
- [UnityTest]
- public IEnumerator NetworkListInsert([Values(true, false)] bool useHost)
+ [Test]
+ public void NetworkListInsert([Values(true, false)] bool useHost)
{
// Re-use the NetworkListAdd to initialize the server and client as well as make sure the list is populated
- yield return NetworkListAdd(useHost);
+ NetworkListAdd(useHost);
// Now randomly insert a random value entry
m_Player1OnServer.TheList.Insert(Random.Range(0, 9), Random.Range(1, 99));
// Verify the element count and values on the client matches the server
m_NetworkListPredicateHandler.SetNetworkListTestState(NetworkListTestPredicate.NetworkListTestStates.VerifyData);
- yield return WaitForConditionOrTimeOut(m_NetworkListPredicateHandler);
+ Assert.True(WaitForConditionOrTimeOutWithTimeTravel(m_NetworkListPredicateHandler));
}
- [UnityTest]
- public IEnumerator NetworkListIndexOf([Values(true, false)] bool useHost)
+ [Test]
+ public void NetworkListIndexOf([Values(true, false)] bool useHost)
{
// Re-use the NetworkListAdd to initialize the server and client as well as make sure the list is populated
- yield return NetworkListAdd(useHost);
+ NetworkListAdd(useHost);
m_NetworkListPredicateHandler.SetNetworkListTestState(NetworkListTestPredicate.NetworkListTestStates.IndexOf);
- yield return WaitForConditionOrTimeOut(m_NetworkListPredicateHandler);
+ Assert.True(WaitForConditionOrTimeOutWithTimeTravel(m_NetworkListPredicateHandler));
}
- [UnityTest]
- public IEnumerator NetworkListValueUpdate([Values(true, false)] bool useHost)
+ [Test]
+ public void NetworkListValueUpdate([Values(true, false)] bool useHost)
{
var testSucceeded = false;
- yield return InitializeServerAndClients(useHost);
+ InitializeServerAndClients(useHost);
// Add 1 element value and verify it is the same on the client
m_NetworkListPredicateHandler = new NetworkListTestPredicate(m_Player1OnServer, m_Player1OnClient1, NetworkListTestPredicate.NetworkListTestStates.Add, 1);
- yield return WaitForConditionOrTimeOut(m_NetworkListPredicateHandler);
+ Assert.True(WaitForConditionOrTimeOutWithTimeTravel(m_NetworkListPredicateHandler));
// Setup our original and
var previousValue = m_Player1OnServer.TheList[0];
@@ -763,17 +785,17 @@ namespace Unity.Netcode.RuntimeTests
// Wait until we know the client side matches the server side before checking if the callback was a success
m_NetworkListPredicateHandler.SetNetworkListTestState(NetworkListTestPredicate.NetworkListTestStates.VerifyData);
- yield return WaitForConditionOrTimeOut(m_NetworkListPredicateHandler);
+ WaitForConditionOrTimeOutWithTimeTravel(m_NetworkListPredicateHandler);
Assert.That(testSucceeded);
m_Player1OnClient1.TheList.OnListChanged -= TestValueUpdatedCallback;
}
- [UnityTest]
- public IEnumerator NetworkListRemoveAt([Values(true, false)] bool useHost)
+ [Test]
+ public void NetworkListRemoveAt([Values(true, false)] bool useHost)
{
// Re-use the NetworkListAdd to initialize the server and client as well as make sure the list is populated
- yield return NetworkListAdd(useHost);
+ NetworkListAdd(useHost);
// Randomly remove a few entries
m_Player1OnServer.TheList.RemoveAt(Random.Range(0, m_Player1OnServer.TheList.Count - 1));
@@ -782,24 +804,24 @@ namespace Unity.Netcode.RuntimeTests
// Verify the element count and values on the client matches the server
m_NetworkListPredicateHandler.SetNetworkListTestState(NetworkListTestPredicate.NetworkListTestStates.VerifyData);
- yield return WaitForConditionOrTimeOut(m_NetworkListPredicateHandler);
+ Assert.True(WaitForConditionOrTimeOutWithTimeTravel(m_NetworkListPredicateHandler));
}
- [UnityTest]
- public IEnumerator NetworkListClear([Values(true, false)] bool useHost)
+ [Test]
+ public void NetworkListClear([Values(true, false)] bool useHost)
{
// Re-use the NetworkListAdd to initialize the server and client as well as make sure the list is populated
- yield return NetworkListAdd(useHost);
+ NetworkListAdd(useHost);
m_Player1OnServer.TheList.Clear();
// Verify the element count and values on the client matches the server
m_NetworkListPredicateHandler.SetNetworkListTestState(NetworkListTestPredicate.NetworkListTestStates.VerifyData);
- yield return WaitForConditionOrTimeOut(m_NetworkListPredicateHandler);
+ Assert.True(WaitForConditionOrTimeOutWithTimeTravel(m_NetworkListPredicateHandler));
}
- [UnityTest]
- public IEnumerator TestNetworkVariableClass([Values(true, false)] bool useHost)
+ [Test]
+ public void TestNetworkVariableClass([Values(true, false)] bool useHost)
{
- yield return InitializeServerAndClients(useHost);
+ InitializeServerAndClients(useHost);
bool VerifyClass()
{
@@ -812,13 +834,13 @@ namespace Unity.Netcode.RuntimeTests
m_Player1OnServer.TheClass.SetDirty(true);
// Wait for the client-side to notify it is finished initializing and spawning.
- yield return WaitForConditionOrTimeOut(VerifyClass);
+ Assert.True(WaitForConditionOrTimeOutWithTimeTravel(VerifyClass));
}
- [UnityTest]
- public IEnumerator TestNetworkVariableTemplateClass([Values(true, false)] bool useHost)
+ [Test]
+ public void TestNetworkVariableTemplateClass([Values(true, false)] bool useHost)
{
- yield return InitializeServerAndClients(useHost);
+ InitializeServerAndClients(useHost);
bool VerifyClass()
{
@@ -830,13 +852,13 @@ namespace Unity.Netcode.RuntimeTests
m_Player1OnServer.TheTemplateClass.SetDirty(true);
// Wait for the client-side to notify it is finished initializing and spawning.
- yield return WaitForConditionOrTimeOut(VerifyClass);
+ Assert.True(WaitForConditionOrTimeOutWithTimeTravel(VerifyClass));
}
- [UnityTest]
- public IEnumerator TestNetworkListStruct([Values(true, false)] bool useHost)
+ [Test]
+ public void TestNetworkListStruct([Values(true, false)] bool useHost)
{
- yield return InitializeServerAndClients(useHost);
+ InitializeServerAndClients(useHost);
bool VerifyList()
{
@@ -850,13 +872,13 @@ namespace Unity.Netcode.RuntimeTests
m_Player1OnServer.TheStructList.SetDirty(true);
// Wait for the client-side to notify it is finished initializing and spawning.
- yield return WaitForConditionOrTimeOut(VerifyList);
+ Assert.True(WaitForConditionOrTimeOutWithTimeTravel(VerifyList));
}
- [UnityTest]
- public IEnumerator TestNetworkVariableStruct([Values(true, false)] bool useHost)
+ [Test]
+ public void TestNetworkVariableStruct([Values(true, false)] bool useHost)
{
- yield return InitializeServerAndClients(useHost);
+ InitializeServerAndClients(useHost);
bool VerifyStructure()
{
@@ -868,13 +890,13 @@ namespace Unity.Netcode.RuntimeTests
m_Player1OnServer.TheStruct.SetDirty(true);
// Wait for the client-side to notify it is finished initializing and spawning.
- yield return WaitForConditionOrTimeOut(VerifyStructure);
+ Assert.True(WaitForConditionOrTimeOutWithTimeTravel(VerifyStructure));
}
- [UnityTest]
- public IEnumerator TestNetworkVariableTemplateStruct([Values(true, false)] bool useHost)
+ [Test]
+ public void TestNetworkVariableTemplateStruct([Values(true, false)] bool useHost)
{
- yield return InitializeServerAndClients(useHost);
+ InitializeServerAndClients(useHost);
bool VerifyStructure()
{
@@ -886,13 +908,13 @@ namespace Unity.Netcode.RuntimeTests
m_Player1OnServer.TheTemplateStruct.SetDirty(true);
// Wait for the client-side to notify it is finished initializing and spawning.
- yield return WaitForConditionOrTimeOut(VerifyStructure);
+ Assert.True(WaitForConditionOrTimeOutWithTimeTravel(VerifyStructure));
}
- [UnityTest]
- public IEnumerator TestNetworkVariableTemplateBehaviourClass([Values(true, false)] bool useHost)
+ [Test]
+ public void TestNetworkVariableTemplateBehaviourClass([Values(true, false)] bool useHost)
{
- yield return InitializeServerAndClients(useHost);
+ InitializeServerAndClients(useHost);
bool VerifyClass()
{
@@ -904,13 +926,13 @@ namespace Unity.Netcode.RuntimeTests
m_Player1OnServer.GetComponent().TheVar.SetDirty(true);
// Wait for the client-side to notify it is finished initializing and spawning.
- yield return WaitForConditionOrTimeOut(VerifyClass);
+ Assert.True(WaitForConditionOrTimeOutWithTimeTravel(VerifyClass));
}
- [UnityTest]
- public IEnumerator TestNetworkVariableTemplateBehaviourClassNotReferencedElsewhere([Values(true, false)] bool useHost)
+ [Test]
+ public void TestNetworkVariableTemplateBehaviourClassNotReferencedElsewhere([Values(true, false)] bool useHost)
{
- yield return InitializeServerAndClients(useHost);
+ InitializeServerAndClients(useHost);
bool VerifyClass()
{
@@ -922,13 +944,13 @@ namespace Unity.Netcode.RuntimeTests
m_Player1OnServer.GetComponent().TheVar.SetDirty(true);
// Wait for the client-side to notify it is finished initializing and spawning.
- yield return WaitForConditionOrTimeOut(VerifyClass);
+ Assert.True(WaitForConditionOrTimeOutWithTimeTravel(VerifyClass));
}
- [UnityTest]
- public IEnumerator TestNetworkVariableTemplateBehaviourStruct([Values(true, false)] bool useHost)
+ [Test]
+ public void TestNetworkVariableTemplateBehaviourStruct([Values(true, false)] bool useHost)
{
- yield return InitializeServerAndClients(useHost);
+ InitializeServerAndClients(useHost);
bool VerifyClass()
{
@@ -940,13 +962,13 @@ namespace Unity.Netcode.RuntimeTests
m_Player1OnServer.GetComponent().TheVar.SetDirty(true);
// Wait for the client-side to notify it is finished initializing and spawning.
- yield return WaitForConditionOrTimeOut(VerifyClass);
+ Assert.True(WaitForConditionOrTimeOutWithTimeTravel(VerifyClass));
}
- [UnityTest]
- public IEnumerator TestNetworkVariableEnum([Values(true, false)] bool useHost)
+ [Test]
+ public void TestNetworkVariableEnum([Values(true, false)] bool useHost)
{
- yield return InitializeServerAndClients(useHost);
+ InitializeServerAndClients(useHost);
bool VerifyStructure()
{
@@ -957,13 +979,13 @@ namespace Unity.Netcode.RuntimeTests
m_Player1OnServer.TheEnum.SetDirty(true);
// Wait for the client-side to notify it is finished initializing and spawning.
- yield return WaitForConditionOrTimeOut(VerifyStructure);
+ Assert.True(WaitForConditionOrTimeOutWithTimeTravel(VerifyStructure));
}
- [UnityTest]
- public IEnumerator TestINetworkSerializableClassCallsNetworkSerialize([Values(true, false)] bool useHost)
+ [Test]
+ public void TestINetworkSerializableClassCallsNetworkSerialize([Values(true, false)] bool useHost)
{
- yield return InitializeServerAndClients(useHost);
+ InitializeServerAndClients(useHost);
TestClass.NetworkSerializeCalledOnWrite = false;
TestClass.NetworkSerializeCalledOnRead = false;
m_Player1OnServer.TheClass.Value = new TestClass
@@ -975,13 +997,13 @@ namespace Unity.Netcode.RuntimeTests
static bool VerifyCallback() => TestClass.NetworkSerializeCalledOnWrite && TestClass.NetworkSerializeCalledOnRead;
// Wait for the client-side to notify it is finished initializing and spawning.
- yield return WaitForConditionOrTimeOut(VerifyCallback);
+ Assert.True(WaitForConditionOrTimeOutWithTimeTravel(VerifyCallback));
}
- [UnityTest]
- public IEnumerator TestINetworkSerializableStructCallsNetworkSerialize([Values(true, false)] bool useHost)
+ [Test]
+ public void TestINetworkSerializableStructCallsNetworkSerialize([Values(true, false)] bool useHost)
{
- yield return InitializeServerAndClients(useHost);
+ InitializeServerAndClients(useHost);
TestStruct.NetworkSerializeCalledOnWrite = false;
TestStruct.NetworkSerializeCalledOnRead = false;
m_Player1OnServer.TheStruct.Value = new TestStruct() { SomeInt = k_TestUInt, SomeBool = false };
@@ -989,24 +1011,23 @@ namespace Unity.Netcode.RuntimeTests
static bool VerifyCallback() => TestStruct.NetworkSerializeCalledOnWrite && TestStruct.NetworkSerializeCalledOnRead;
// Wait for the client-side to notify it is finished initializing and spawning.
- yield return WaitForConditionOrTimeOut(VerifyCallback);
+ Assert.True(WaitForConditionOrTimeOutWithTimeTravel(VerifyCallback));
}
- #region COULD_BE_REMOVED
- [UnityTest]
+ [Test]
[Ignore("This is used several times already in the NetworkListPredicate")]
// TODO: If we end up using the new suggested pattern, then delete this
- public IEnumerator NetworkListArrayOperator([Values(true, false)] bool useHost)
+ public void NetworkListArrayOperator([Values(true, false)] bool useHost)
{
- yield return NetworkListAdd(useHost);
+ NetworkListAdd(useHost);
}
- [UnityTest]
+ [Test]
[Ignore("This is used several times already in the NetworkListPredicate")]
// TODO: If we end up using the new suggested pattern, then delete this
- public IEnumerator NetworkListIEnumerator([Values(true, false)] bool useHost)
+ public void NetworkListIEnumerator([Values(true, false)] bool useHost)
{
- yield return InitializeServerAndClients(useHost);
+ InitializeServerAndClients(useHost);
var correctVals = new int[3];
correctVals[0] = k_TestVal1;
correctVals[1] = k_TestVal2;
@@ -1096,6 +1117,21 @@ namespace Unity.Netcode.RuntimeTests
});
}
+ [Test]
+ public void TestTypesReferencedInSubclassSerializeSuccessfully()
+ {
+ var variable = new NetworkVariableSubclass>();
+ using var writer = new FastBufferWriter(1024, Allocator.Temp);
+ var value = new TemplatedValueOnlyReferencedByNetworkVariableSubclass { Value = 12345 };
+ variable.Value = value;
+ variable.WriteField(writer);
+ variable.Value = new TemplatedValueOnlyReferencedByNetworkVariableSubclass { Value = 54321 };
+
+ using var reader = new FastBufferReader(writer, Allocator.None);
+ variable.ReadField(reader);
+ Assert.AreEqual(value.Value, variable.Value.Value);
+ }
+
[Test]
public void TestUnsupportedUnmanagedTypesWithUserSerializationDoNotThrowExceptions()
{
@@ -1133,12 +1169,14 @@ namespace Unity.Netcode.RuntimeTests
[Test]
public void TestManagedINetworkSerializableNetworkVariablesDeserializeInPlace()
{
- var variable = new NetworkVariable();
- variable.Value = new ManagedNetworkSerializableType
+ var variable = new NetworkVariable
{
- InMemoryValue = 1,
- Ints = new[] { 2, 3, 4 },
- Str = "five"
+ Value = new ManagedNetworkSerializableType
+ {
+ InMemoryValue = 1,
+ Ints = new[] { 2, 3, 4 },
+ Str = "five"
+ }
};
using var writer = new FastBufferWriter(1024, Allocator.Temp);
@@ -1163,12 +1201,14 @@ namespace Unity.Netcode.RuntimeTests
[Test]
public void TestUnmnagedINetworkSerializableNetworkVariablesDeserializeInPlace()
{
- var variable = new NetworkVariable();
- variable.Value = new UnmanagedNetworkSerializableType
+ var variable = new NetworkVariable
{
- InMemoryValue = 1,
- Int = 2,
- Str = "three"
+ Value = new UnmanagedNetworkSerializableType
+ {
+ InMemoryValue = 1,
+ Int = 2,
+ Str = "three"
+ }
};
using var writer = new FastBufferWriter(1024, Allocator.Temp);
variable.WriteField(writer);
@@ -1188,7 +1228,6 @@ namespace Unity.Netcode.RuntimeTests
Assert.AreEqual(2, variable.Value.Int, "Int was not correctly deserialized");
Assert.AreEqual("three", variable.Value.Str, "Str was not correctly deserialized");
}
- #endregion
private float m_OriginalTimeScale = 1.0f;
@@ -1377,4 +1416,144 @@ namespace Unity.Netcode.RuntimeTests
}
}
}
+
+ [TestFixtureSource(nameof(TestDataSource))]
+ public class NetworkVariableInheritanceTests : NetcodeIntegrationTest
+ {
+ public NetworkVariableInheritanceTests(HostOrServer hostOrServer)
+ : base(hostOrServer)
+ {
+ }
+
+ protected override int NumberOfClients => 2;
+
+ public static IEnumerable TestDataSource() =>
+ Enum.GetValues(typeof(HostOrServer)).OfType().Select(x => new TestFixtureData(x));
+
+ public class ComponentA : NetworkBehaviour
+ {
+ public NetworkVariable PublicFieldA = new NetworkVariable(1);
+ protected NetworkVariable m_ProtectedFieldA = new NetworkVariable(2);
+ private NetworkVariable m_PrivateFieldA = new NetworkVariable(3);
+
+ public void ChangeValuesA(int pub, int pro, int pri)
+ {
+ PublicFieldA.Value = pub;
+ m_ProtectedFieldA.Value = pro;
+ m_PrivateFieldA.Value = pri;
+ }
+
+ public bool CompareValuesA(ComponentA other)
+ {
+ return PublicFieldA.Value == other.PublicFieldA.Value &&
+ m_ProtectedFieldA.Value == other.m_ProtectedFieldA.Value &&
+ m_PrivateFieldA.Value == other.m_PrivateFieldA.Value;
+ }
+ }
+
+ public class ComponentB : ComponentA
+ {
+ public NetworkVariable PublicFieldB = new NetworkVariable(11);
+ protected NetworkVariable m_ProtectedFieldB = new NetworkVariable(22);
+ private NetworkVariable m_PrivateFieldB = new NetworkVariable(33);
+
+ public void ChangeValuesB(int pub, int pro, int pri)
+ {
+ PublicFieldB.Value = pub;
+ m_ProtectedFieldB.Value = pro;
+ m_PrivateFieldB.Value = pri;
+ }
+
+ public bool CompareValuesB(ComponentB other)
+ {
+ return PublicFieldB.Value == other.PublicFieldB.Value &&
+ m_ProtectedFieldB.Value == other.m_ProtectedFieldB.Value &&
+ m_PrivateFieldB.Value == other.m_PrivateFieldB.Value;
+ }
+ }
+
+ public class ComponentC : ComponentB
+ {
+ public NetworkVariable PublicFieldC = new NetworkVariable(111);
+ protected NetworkVariable m_ProtectedFieldC = new NetworkVariable(222);
+ private NetworkVariable m_PrivateFieldC = new NetworkVariable(333);
+
+ public void ChangeValuesC(int pub, int pro, int pri)
+ {
+ PublicFieldC.Value = pub;
+ m_ProtectedFieldA.Value = pro;
+ m_PrivateFieldC.Value = pri;
+ }
+
+ public bool CompareValuesC(ComponentC other)
+ {
+ return PublicFieldC.Value == other.PublicFieldC.Value &&
+ m_ProtectedFieldC.Value == other.m_ProtectedFieldC.Value &&
+ m_PrivateFieldC.Value == other.m_PrivateFieldC.Value;
+ }
+ }
+
+ private GameObject m_TestObjectPrefab;
+ private ulong m_TestObjectId = 0;
+
+ protected override void OnServerAndClientsCreated()
+ {
+ m_TestObjectPrefab = CreateNetworkObjectPrefab($"[{nameof(NetworkVariableInheritanceTests)}.{nameof(m_TestObjectPrefab)}]");
+ m_TestObjectPrefab.AddComponent();
+ m_TestObjectPrefab.AddComponent();
+ m_TestObjectPrefab.AddComponent();
+ }
+
+ protected override IEnumerator OnServerAndClientsConnected()
+ {
+ var serverTestObject = SpawnObject(m_TestObjectPrefab, m_ServerNetworkManager).GetComponent();
+ m_TestObjectId = serverTestObject.NetworkObjectId;
+
+ var serverTestComponentA = serverTestObject.GetComponent();
+ var serverTestComponentB = serverTestObject.GetComponent();
+ var serverTestComponentC = serverTestObject.GetComponent();
+
+ serverTestComponentA.ChangeValuesA(1000, 2000, 3000);
+ serverTestComponentB.ChangeValuesA(1000, 2000, 3000);
+ serverTestComponentB.ChangeValuesB(1100, 2200, 3300);
+ serverTestComponentC.ChangeValuesA(1000, 2000, 3000);
+ serverTestComponentC.ChangeValuesB(1100, 2200, 3300);
+ serverTestComponentC.ChangeValuesC(1110, 2220, 3330);
+
+ yield return WaitForTicks(m_ServerNetworkManager, 2);
+ }
+
+ private bool CheckTestObjectComponentValuesOnAll()
+ {
+ var serverTestObject = m_ServerNetworkManager.SpawnManager.SpawnedObjects[m_TestObjectId];
+ var serverTestComponentA = serverTestObject.GetComponent();
+ var serverTestComponentB = serverTestObject.GetComponent();
+ var serverTestComponentC = serverTestObject.GetComponent();
+ foreach (var clientNetworkManager in m_ClientNetworkManagers)
+ {
+ var clientTestObject = clientNetworkManager.SpawnManager.SpawnedObjects[m_TestObjectId];
+ var clientTestComponentA = clientTestObject.GetComponent();
+ var clientTestComponentB = clientTestObject.GetComponent();
+ var clientTestComponentC = clientTestObject.GetComponent();
+ if (!serverTestComponentA.CompareValuesA(clientTestComponentA) ||
+ !serverTestComponentB.CompareValuesA(clientTestComponentB) ||
+ !serverTestComponentB.CompareValuesB(clientTestComponentB) ||
+ !serverTestComponentC.CompareValuesA(clientTestComponentC) ||
+ !serverTestComponentC.CompareValuesB(clientTestComponentC) ||
+ !serverTestComponentC.CompareValuesC(clientTestComponentC))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ [UnityTest]
+ public IEnumerator TestInheritedFields()
+ {
+ yield return WaitForConditionOrTimeOut(CheckTestObjectComponentValuesOnAll);
+ Assert.IsFalse(s_GlobalTimeoutHelper.TimedOut, nameof(CheckTestObjectComponentValuesOnAll));
+ }
+ }
}
diff --git a/Tests/Runtime/NetworkVariableUserSerializableTypesTests.cs b/Tests/Runtime/NetworkVariableUserSerializableTypesTests.cs
index 24fae86..bb0c228 100644
--- a/Tests/Runtime/NetworkVariableUserSerializableTypesTests.cs
+++ b/Tests/Runtime/NetworkVariableUserSerializableTypesTests.cs
@@ -1,10 +1,10 @@
using System;
using System.Collections;
using System.Collections.Generic;
-using UnityEngine.TestTools;
using NUnit.Framework;
using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
+using UnityEngine.TestTools;
using Object = UnityEngine.Object;
namespace Unity.Netcode.RuntimeTests
diff --git a/Tests/Runtime/NetworkVisibilityTests.cs b/Tests/Runtime/NetworkVisibilityTests.cs
index 0f95a10..bbd9cb4 100644
--- a/Tests/Runtime/NetworkVisibilityTests.cs
+++ b/Tests/Runtime/NetworkVisibilityTests.cs
@@ -1,9 +1,9 @@
using System.Collections;
using System.Linq;
-using UnityEngine.TestTools;
using NUnit.Framework;
-using UnityEngine;
using Unity.Netcode.TestHelpers.Runtime;
+using UnityEngine;
+using UnityEngine.TestTools;
namespace Unity.Netcode.RuntimeTests
{
diff --git a/Tests/Runtime/OwnerModifiedTests.cs b/Tests/Runtime/OwnerModifiedTests.cs
index 41296c1..cc7b37e 100644
--- a/Tests/Runtime/OwnerModifiedTests.cs
+++ b/Tests/Runtime/OwnerModifiedTests.cs
@@ -1,9 +1,9 @@
using System.Collections;
using System.Collections.Generic;
using NUnit.Framework;
+using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
using UnityEngine.TestTools;
-using Unity.Netcode.TestHelpers.Runtime;
namespace Unity.Netcode.RuntimeTests
{
@@ -16,7 +16,7 @@ namespace Unity.Netcode.RuntimeTests
{
public NetworkList MyNetworkList;
- static internal int Updates = 0;
+ internal static int Updates = 0;
private void Awake()
{
diff --git a/Tests/Runtime/OwnerPermissionTests.cs b/Tests/Runtime/OwnerPermissionTests.cs
index c0d7dd7..7bd2816 100644
--- a/Tests/Runtime/OwnerPermissionTests.cs
+++ b/Tests/Runtime/OwnerPermissionTests.cs
@@ -1,9 +1,9 @@
using System;
using System.Collections;
using System.Collections.Generic;
+using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
using UnityEngine.TestTools;
-using Unity.Netcode.TestHelpers.Runtime;
namespace Unity.Netcode.RuntimeTests
{
diff --git a/Tests/Runtime/Physics/NetworkRigidbodyTest.cs b/Tests/Runtime/Physics/NetworkRigidbodyTest.cs
index 31078d8..cb0b608 100644
--- a/Tests/Runtime/Physics/NetworkRigidbodyTest.cs
+++ b/Tests/Runtime/Physics/NetworkRigidbodyTest.cs
@@ -2,9 +2,9 @@
using System.Collections;
using NUnit.Framework;
using Unity.Netcode.Components;
+using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
using UnityEngine.TestTools;
-using Unity.Netcode.TestHelpers.Runtime;
namespace Unity.Netcode.RuntimeTests
{
diff --git a/Tests/Runtime/PlayerObjectTests.cs b/Tests/Runtime/PlayerObjectTests.cs
index 6b52c4f..bd947f2 100644
--- a/Tests/Runtime/PlayerObjectTests.cs
+++ b/Tests/Runtime/PlayerObjectTests.cs
@@ -1,8 +1,8 @@
using System.Collections;
using NUnit.Framework;
+using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
using UnityEngine.TestTools;
-using Unity.Netcode.TestHelpers.Runtime;
namespace Unity.Netcode.RuntimeTests
{
diff --git a/Tests/Runtime/RpcManyClientsTests.cs b/Tests/Runtime/RpcManyClientsTests.cs
index 2b6e50a..40de8a1 100644
--- a/Tests/Runtime/RpcManyClientsTests.cs
+++ b/Tests/Runtime/RpcManyClientsTests.cs
@@ -1,11 +1,9 @@
using System;
-using System.Collections;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
-using UnityEngine;
-using UnityEngine.TestTools;
using Unity.Netcode.TestHelpers.Runtime;
+using UnityEngine;
namespace Unity.Netcode.RuntimeTests
{
@@ -49,6 +47,10 @@ namespace Unity.Netcode.RuntimeTests
{
protected override int NumberOfClients => 10;
+ protected override bool m_EnableTimeTravel => true;
+ protected override bool m_SetupIsACoroutine => false;
+ protected override bool m_TearDownIsACoroutine => false;
+
private GameObject m_PrefabToSpawn;
protected override void OnServerAndClientsCreated()
@@ -70,8 +72,8 @@ namespace Unity.Netcode.RuntimeTests
return prefabToSpawn;
}
- [UnityTest]
- public IEnumerator RpcManyClientsTest()
+ [Test]
+ public void RpcManyClientsTest()
{
var spawnedObject = UnityEngine.Object.Instantiate(m_PrefabToSpawn);
var netSpawnedObject = spawnedObject.GetComponent();
@@ -97,20 +99,20 @@ namespace Unity.Netcode.RuntimeTests
rpcManyClientsObject.NoParamsClientRpc(); // RPC with no params
// Check that all ServerRpcMessages were sent
- yield return WaitForConditionOrTimeOut(rpcMessageHooks);
+ WaitForConditionOrTimeOutWithTimeTravel(rpcMessageHooks);
// Now provide a small window of time to let the server receive and process all messages
- yield return WaitForConditionOrTimeOut(() => TotalClients == rpcManyClientsObject.Count);
- Assert.False(s_GlobalTimeoutHelper.TimedOut, $"Timed out wait for {nameof(rpcManyClientsObject.NoParamsClientRpc)}! Only {rpcManyClientsObject.Count} of {TotalClients} was received!");
+ var success = WaitForConditionOrTimeOutWithTimeTravel(() => TotalClients == rpcManyClientsObject.Count);
+ Assert.True(success, $"Timed out wait for {nameof(rpcManyClientsObject.NoParamsClientRpc)}! Only {rpcManyClientsObject.Count} of {TotalClients} was received!");
rpcManyClientsObject.Count = 0;
rpcManyClientsObject.OneParamClientRpc(0); // RPC with one param
rpcMessageHooks.Reset();
- yield return WaitForConditionOrTimeOut(rpcMessageHooks);
+ WaitForConditionOrTimeOutWithTimeTravel(rpcMessageHooks);
// Now provide a small window of time to let the server receive and process all messages
- yield return WaitForConditionOrTimeOut(() => TotalClients == rpcManyClientsObject.Count);
- Assert.False(s_GlobalTimeoutHelper.TimedOut, $"Timed out wait for {nameof(rpcManyClientsObject.OneParamClientRpc)}! Only {rpcManyClientsObject.Count} of {TotalClients} was received!");
+ success = WaitForConditionOrTimeOutWithTimeTravel(() => TotalClients == rpcManyClientsObject.Count);
+ Assert.True(success, $"Timed out wait for {nameof(rpcManyClientsObject.OneParamClientRpc)}! Only {rpcManyClientsObject.Count} of {TotalClients} was received!");
var param = new ClientRpcParams();
@@ -118,10 +120,10 @@ namespace Unity.Netcode.RuntimeTests
rpcManyClientsObject.TwoParamsClientRpc(0, 0); // RPC with two params
rpcMessageHooks.Reset();
- yield return WaitForConditionOrTimeOut(rpcMessageHooks);
+ WaitForConditionOrTimeOutWithTimeTravel(rpcMessageHooks);
// Now provide a small window of time to let the server receive and process all messages
- yield return WaitForConditionOrTimeOut(() => TotalClients == rpcManyClientsObject.Count);
- Assert.False(s_GlobalTimeoutHelper.TimedOut, $"Timed out wait for {nameof(rpcManyClientsObject.TwoParamsClientRpc)}! Only {rpcManyClientsObject.Count} of {TotalClients} was received!");
+ success = WaitForConditionOrTimeOutWithTimeTravel(() => TotalClients == rpcManyClientsObject.Count);
+ Assert.True(success, $"Timed out wait for {nameof(rpcManyClientsObject.TwoParamsClientRpc)}! Only {rpcManyClientsObject.Count} of {TotalClients} was received!");
rpcManyClientsObject.ReceivedFrom.Clear();
rpcManyClientsObject.Count = 0;
@@ -138,11 +140,11 @@ namespace Unity.Netcode.RuntimeTests
messageHookList.Add(targetedClientMessageHookEntry);
rpcMessageHooks = new MessageHooksConditional(messageHookList);
- yield return WaitForConditionOrTimeOut(rpcMessageHooks);
+ WaitForConditionOrTimeOutWithTimeTravel(rpcMessageHooks);
// Now provide a small window of time to let the server receive and process all messages
- yield return WaitForConditionOrTimeOut(() => 2 == rpcManyClientsObject.Count);
- Assert.False(s_GlobalTimeoutHelper.TimedOut, $"Timed out wait for {nameof(rpcManyClientsObject.TwoParamsClientRpc)}! Only {rpcManyClientsObject.Count} of 2 was received!");
+ success = WaitForConditionOrTimeOutWithTimeTravel(() => 2 == rpcManyClientsObject.Count);
+ Assert.True(success, $"Timed out wait for {nameof(rpcManyClientsObject.TwoParamsClientRpc)}! Only {rpcManyClientsObject.Count} of 2 was received!");
// either of the 2 selected clients can reply to the server first, due to network timing
var possibility1 = new List { m_ClientNetworkManagers[1].LocalClientId, m_ClientNetworkManagers[2].LocalClientId };
diff --git a/Tests/Runtime/RpcQueueTests.cs b/Tests/Runtime/RpcQueueTests.cs
index 5a6c659..dfa5f5e 100644
--- a/Tests/Runtime/RpcQueueTests.cs
+++ b/Tests/Runtime/RpcQueueTests.cs
@@ -1,9 +1,9 @@
using System;
using System.Collections;
-using UnityEngine;
-using UnityEngine.TestTools;
using NUnit.Framework;
using Unity.Netcode.TestHelpers.Runtime;
+using UnityEngine;
+using UnityEngine.TestTools;
namespace Unity.Netcode.RuntimeTests
{
diff --git a/Tests/Runtime/RpcTests.cs b/Tests/Runtime/RpcTests.cs
index c6c56ef..9c574c2 100644
--- a/Tests/Runtime/RpcTests.cs
+++ b/Tests/Runtime/RpcTests.cs
@@ -3,9 +3,9 @@ using System.Collections;
using System.Collections.Generic;
using NUnit.Framework;
using Unity.Collections;
-using UnityEngine.TestTools;
using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
+using UnityEngine.TestTools;
using Debug = UnityEngine.Debug;
namespace Unity.Netcode.RuntimeTests
diff --git a/Tests/Runtime/Serialization/NetworkBehaviourReferenceTests.cs b/Tests/Runtime/Serialization/NetworkBehaviourReferenceTests.cs
index 8b7f756..472d617 100644
--- a/Tests/Runtime/Serialization/NetworkBehaviourReferenceTests.cs
+++ b/Tests/Runtime/Serialization/NetworkBehaviourReferenceTests.cs
@@ -1,9 +1,9 @@
using System;
using System.Collections;
using NUnit.Framework;
+using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
using UnityEngine.TestTools;
-using Unity.Netcode.TestHelpers.Runtime;
namespace Unity.Netcode.RuntimeTests
{
diff --git a/Tests/Runtime/Serialization/NetworkObjectReferenceTests.cs b/Tests/Runtime/Serialization/NetworkObjectReferenceTests.cs
index 07cd576..8b29d17 100644
--- a/Tests/Runtime/Serialization/NetworkObjectReferenceTests.cs
+++ b/Tests/Runtime/Serialization/NetworkObjectReferenceTests.cs
@@ -2,9 +2,9 @@ using System;
using System.Collections;
using NUnit.Framework;
using Unity.Collections;
+using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
using UnityEngine.TestTools;
-using Unity.Netcode.TestHelpers.Runtime;
using Object = UnityEngine.Object;
namespace Unity.Netcode.RuntimeTests
diff --git a/Tests/Runtime/StopStartRuntimeTests.cs b/Tests/Runtime/StopStartRuntimeTests.cs
index 421746e..16fa90e 100644
--- a/Tests/Runtime/StopStartRuntimeTests.cs
+++ b/Tests/Runtime/StopStartRuntimeTests.cs
@@ -1,8 +1,8 @@
using System.Collections;
using NUnit.Framework;
+using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
using UnityEngine.TestTools;
-using Unity.Netcode.TestHelpers.Runtime;
namespace Unity.Netcode.RuntimeTests
{
diff --git a/Tests/Runtime/Timing/NetworkTimeSystemTests.cs b/Tests/Runtime/Timing/NetworkTimeSystemTests.cs
index a59d035..047435f 100644
--- a/Tests/Runtime/Timing/NetworkTimeSystemTests.cs
+++ b/Tests/Runtime/Timing/NetworkTimeSystemTests.cs
@@ -1,9 +1,9 @@
using System.Collections;
using NUnit.Framework;
+using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
using UnityEngine.Assertions.Comparers;
using UnityEngine.TestTools;
-using Unity.Netcode.TestHelpers.Runtime;
namespace Unity.Netcode.RuntimeTests
{
diff --git a/Tests/Runtime/Timing/TimeInitializationTest.cs b/Tests/Runtime/Timing/TimeInitializationTest.cs
index be5d1e3..cbd7b99 100644
--- a/Tests/Runtime/Timing/TimeInitializationTest.cs
+++ b/Tests/Runtime/Timing/TimeInitializationTest.cs
@@ -1,8 +1,8 @@
using System.Collections;
using NUnit.Framework;
+using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
using UnityEngine.TestTools;
-using Unity.Netcode.TestHelpers.Runtime;
namespace Unity.Netcode.RuntimeTests
{
diff --git a/Tests/Runtime/Timing/TimeMultiInstanceTest.cs b/Tests/Runtime/Timing/TimeIntegrationTest.cs
similarity index 85%
rename from Tests/Runtime/Timing/TimeMultiInstanceTest.cs
rename to Tests/Runtime/Timing/TimeIntegrationTest.cs
index a33dbb9..a04430d 100644
--- a/Tests/Runtime/Timing/TimeMultiInstanceTest.cs
+++ b/Tests/Runtime/Timing/TimeIntegrationTest.cs
@@ -2,9 +2,9 @@ using System;
using System.Collections;
using System.Linq;
using NUnit.Framework;
+using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
using UnityEngine.TestTools;
-using Unity.Netcode.TestHelpers.Runtime;
namespace Unity.Netcode.RuntimeTests
{
@@ -50,6 +50,14 @@ namespace Unity.Netcode.RuntimeTests
{
yield return StartSomeClientsAndServerWithPlayersCustom(true, NumberOfClients, targetFrameRate, tickRate);
+ var additionalTimeTolerance = k_AdditionalTimeTolerance;
+ // Mac can dip down below 10fps when set at a 10fps range (i.e. known to hit as low as 8.85 fps)
+ // With the really low frame rate, add some additional time tolerance
+ if (targetFrameRate == 10)
+ {
+ additionalTimeTolerance += 0.0333333333333f;
+ }
+
double frameInterval = 1d / targetFrameRate;
double tickInterval = 1d / tickRate;
@@ -76,18 +84,33 @@ namespace Unity.Netcode.RuntimeTests
var framesToRun = 3d / frameInterval;
+ var waitForNextFrame = new WaitForFixedUpdate();
+
for (int i = 0; i < framesToRun; i++)
{
- yield return null;
+ // Assure we wait for 1 frame to get the current frame time to check for low frame rates relative to expected frame rates
+ yield return waitForNextFrame;
+
+ // Adjust the time tolerance based on slower than expected FPS
+ var currentFPS = 1.0f / Time.deltaTime;
+ var fpsAdjustment = 1.0f;
+ var currentAdjustment = additionalTimeTolerance;
+ if (currentFPS < targetFrameRate)
+ {
+ // Get the % slower and increase the time tolerance based on that %
+ var fpsDelta = targetFrameRate - currentFPS;
+ fpsAdjustment = 1.0f / fpsDelta;
+ currentAdjustment += additionalTimeTolerance * fpsAdjustment;
+ }
UpdateTimeStates(networkManagers);
// compares whether client times have the correct offset to server
- m_ServerState.AssertCheckDifference(m_Client1State, tickInterval, tickInterval, tickInterval * 2 + frameInterval * 2 + k_AdditionalTimeTolerance);
- m_ServerState.AssertCheckDifference(m_Client2State, 0.2, 0.1, tickInterval * 2 + frameInterval * 2 + k_AdditionalTimeTolerance);
+ m_ServerState.AssertCheckDifference(m_Client1State, tickInterval, tickInterval, tickInterval * 2 + frameInterval * 2 + currentAdjustment);
+ m_ServerState.AssertCheckDifference(m_Client2State, 0.2, 0.1, tickInterval * 2 + frameInterval * 2 + currentAdjustment);
// compares the two client times, only difference should be based on buffering.
- m_Client1State.AssertCheckDifference(m_Client2State, 0.2 - tickInterval, (0.1 - tickInterval), tickInterval * 2 + frameInterval * 2 + k_AdditionalTimeTolerance);
+ m_Client1State.AssertCheckDifference(m_Client2State, 0.2 - tickInterval, (0.1 - tickInterval), tickInterval * 2 + frameInterval * 2 + currentAdjustment);
}
}
diff --git a/Tests/Runtime/Timing/TimeMultiInstanceTest.cs.meta b/Tests/Runtime/Timing/TimeIntegrationTest.cs.meta
similarity index 100%
rename from Tests/Runtime/Timing/TimeMultiInstanceTest.cs.meta
rename to Tests/Runtime/Timing/TimeIntegrationTest.cs.meta
diff --git a/Tests/Runtime/TransformInterpolationTests.cs b/Tests/Runtime/TransformInterpolationTests.cs
index dd0b3ef..67c0d66 100644
--- a/Tests/Runtime/TransformInterpolationTests.cs
+++ b/Tests/Runtime/TransformInterpolationTests.cs
@@ -1,28 +1,77 @@
using System.Collections;
-using Unity.Netcode.Components;
using NUnit.Framework;
+using Unity.Netcode.Components;
+using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
using UnityEngine.TestTools;
-using Unity.Netcode.TestHelpers.Runtime;
namespace Unity.Netcode.RuntimeTests
{
- public class TransformInterpolationObject : NetworkBehaviour
+ public class TransformInterpolationObject : NetworkTransform
{
+ public static bool TestComplete = false;
// Set the minimum threshold which we will use as our margin of error
+#if UNITY_EDITOR
public const float MinThreshold = 0.005f;
+#else
+ // Add additional room for error on console tests
+ public const float MinThreshold = 0.009999f;
+#endif
+
+ private const int k_TargetLocalSpaceToggles = 10;
public bool CheckPosition;
public bool IsMoving;
public bool IsFixed;
- private void Update()
+ private float m_FrameRateFractional;
+ private bool m_CurrentLocalSpace;
+
+ private int m_LocalSpaceToggles;
+ private int m_LastFrameCount;
+
+ public bool ReachedTargetLocalSpaceTransitionCount()
{
+ TestComplete = m_LocalSpaceToggles >= k_TargetLocalSpaceToggles;
+ return TestComplete;
+ }
+
+ protected override void OnInitialize(ref NetworkVariable replicatedState)
+ {
+ m_LocalSpaceToggles = 0;
+ m_FrameRateFractional = 1.0f / Application.targetFrameRate;
+ PositionThreshold = MinThreshold;
+ SetMaxInterpolationBound(1.0f);
+ base.OnInitialize(ref replicatedState);
+ }
+
+ private int m_StartFrameCount;
+
+ public void StartMoving()
+ {
+ m_StartFrameCount = Time.frameCount;
+ IsMoving = true;
+ }
+
+ public void StopMoving()
+ {
+ IsMoving = false;
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (!IsSpawned || TestComplete)
+ {
+ return;
+ }
+
// Check the position of the nested object on the client
if (CheckPosition)
{
- if (transform.position.y < -MinThreshold || transform.position.y > 100.0f + MinThreshold)
+ if (transform.position.y < -MinThreshold || transform.position.y > Application.targetFrameRate + MinThreshold)
{
Debug.LogError($"Interpolation failure. transform.position.y is {transform.position.y}. Should be between 0.0 and 100.0. Current threshold is [+/- {MinThreshold}].");
}
@@ -31,19 +80,47 @@ namespace Unity.Netcode.RuntimeTests
// Move the nested object on the server
if (IsMoving)
{
- var y = Time.realtimeSinceStartup % 10.0f;
+ Assert.True(CanCommitToTransform, $"Using non-authority instance to update transform!");
- // change the space between local and global every second
- GetComponent().InLocalSpace = ((int)y % 2 == 0);
+ if (m_LastFrameCount == Time.frameCount)
+ {
+ Debug.Log($"Detected duplicate frame update count {Time.frameCount}. Ignoring this update.");
+ return;
+ }
- transform.position = new Vector3(0.0f, y * 10, 0.0f);
+ m_LastFrameCount = Time.frameCount;
+
+ // Leaving this here for reference.
+ // If a system is running at a slower frame rate than expected, then the below code could toggle
+ // the local to world space value at a higher frequency which might not provide enough updates to
+ // handle interpolating between the transitions.
+ //var y = Time.realtimeSinceStartup % 10.0f;
+ //// change the space between local and global every second
+ //GetComponent().InLocalSpace = ((int)y % 2 == 0);
+
+ // Reduce the total frame count down to the frame rate
+ var y = (Time.frameCount - m_StartFrameCount) % Application.targetFrameRate;
+
+ // change the space between local and global every time we hit the expected number of frames
+ // (or every second if running at the target frame rate)
+ InLocalSpace = y == 0 ? !InLocalSpace : InLocalSpace;
+
+ if (m_CurrentLocalSpace != InLocalSpace)
+ {
+ m_LocalSpaceToggles++;
+ m_CurrentLocalSpace = InLocalSpace;
+ }
+
+ transform.position = new Vector3(0.0f, (y * m_FrameRateFractional), 0.0f);
}
// On the server, make sure to keep the parent object at a fixed position
if (IsFixed)
{
+ Assert.True(CanCommitToTransform, $"Using non-authority instance to update transform!");
transform.position = new Vector3(1000.0f, 1000.0f, 1000.0f);
}
+
}
}
@@ -63,9 +140,7 @@ namespace Unity.Netcode.RuntimeTests
protected override void OnServerAndClientsCreated()
{
m_PrefabToSpawn = CreateNetworkObjectPrefab("InterpTestObject");
- var networkTransform = m_PrefabToSpawn.AddComponent();
- networkTransform.PositionThreshold = TransformInterpolationObject.MinThreshold;
- m_PrefabToSpawn.AddComponent();
+ var networkTransform = m_PrefabToSpawn.AddComponent();
}
private IEnumerator RefreshNetworkObjects()
@@ -89,6 +164,7 @@ namespace Unity.Netcode.RuntimeTests
[UnityTest]
public IEnumerator TransformInterpolationTest()
{
+ TransformInterpolationObject.TestComplete = false;
// create an object
var spawnedObject = Object.Instantiate(m_PrefabToSpawn);
var baseObject = Object.Instantiate(m_PrefabToSpawn);
@@ -101,7 +177,6 @@ namespace Unity.Netcode.RuntimeTests
m_BaseAsNetworkObject = baseObject.GetComponent();
m_BaseAsNetworkObject.NetworkManagerOwner = m_ServerNetworkManager;
-
m_SpawnedAsNetworkObject.TrySetParent(baseObject);
m_SpawnedAsNetworkObject.Spawn();
@@ -109,10 +184,9 @@ namespace Unity.Netcode.RuntimeTests
yield return RefreshNetworkObjects();
m_SpawnedAsNetworkObject.TrySetParent(baseObject);
-
+ var spawnedObjectNetworkTransform = spawnedObject.GetComponent();
baseObject.GetComponent().IsFixed = true;
- spawnedObject.GetComponent().IsMoving = true;
- spawnedObject.GetComponent().SetMaxInterpolationBound(1.0f);
+ spawnedObject.GetComponent().StartMoving();
const float maxPlacementError = 0.01f;
@@ -125,13 +199,15 @@ namespace Unity.Netcode.RuntimeTests
yield return new WaitForSeconds(0.01f);
}
- m_SpawnedObjectOnClient.GetComponent().SetMaxInterpolationBound(1.0f);
m_SpawnedObjectOnClient.GetComponent().CheckPosition = true;
- // Test that interpolation works correctly for 10 seconds
+ // Test that interpolation works correctly for ~10 seconds or 10 local to world space transitions while moving
// Increasing this duration gives you the opportunity to go check in the Editor how the objects are setup
// and how they move
- yield return new WaitForSeconds(10.0f);
+ var timeOutHelper = new TimeoutFrameCountHelper(10);
+ yield return WaitForConditionOrTimeOut(spawnedObjectNetworkTransform.ReachedTargetLocalSpaceTransitionCount, timeOutHelper);
+ Debug.Log($"[TransformInterpolationTest] Wait condition reached or timed out. Frame Count ({timeOutHelper.GetFrameCount()}) | Time Elapsed ({timeOutHelper.GetTimeElapsed()})");
+ AssertOnTimeout($"Failed to reach desired local to world space transitions in the given time!", timeOutHelper);
}
}
}
diff --git a/Tests/Runtime/Transports/UnityTransportConnectionTests.cs b/Tests/Runtime/Transports/UnityTransportConnectionTests.cs
index df1f094..671515d 100644
--- a/Tests/Runtime/Transports/UnityTransportConnectionTests.cs
+++ b/Tests/Runtime/Transports/UnityTransportConnectionTests.cs
@@ -1,8 +1,8 @@
-using NUnit.Framework;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
+using NUnit.Framework;
using Unity.Netcode.Transports.UTP;
using UnityEngine;
using UnityEngine.TestTools;
diff --git a/Tests/Runtime/Transports/UnityTransportDriverClient.cs b/Tests/Runtime/Transports/UnityTransportDriverClient.cs
index d41d9bf..e2d6ef1 100644
--- a/Tests/Runtime/Transports/UnityTransportDriverClient.cs
+++ b/Tests/Runtime/Transports/UnityTransportDriverClient.cs
@@ -1,12 +1,11 @@
-using NUnit.Framework;
using System.Collections;
+using NUnit.Framework;
+using Unity.Netcode.Transports.UTP;
using Unity.Networking.Transport;
using Unity.Networking.Transport.Utilities;
-using Unity.Netcode.Transports.UTP;
using UnityEngine;
-
-using UTPNetworkEvent = Unity.Networking.Transport.NetworkEvent;
using static Unity.Netcode.RuntimeTests.UnityTransportTestHelpers;
+using UTPNetworkEvent = Unity.Networking.Transport.NetworkEvent;
namespace Unity.Netcode.RuntimeTests
{
diff --git a/Tests/Runtime/Transports/UnityTransportTestHelpers.cs b/Tests/Runtime/Transports/UnityTransportTestHelpers.cs
index f6d73f9..75144f6 100644
--- a/Tests/Runtime/Transports/UnityTransportTestHelpers.cs
+++ b/Tests/Runtime/Transports/UnityTransportTestHelpers.cs
@@ -1,7 +1,7 @@
-using NUnit.Framework;
using System;
using System.Collections;
using System.Collections.Generic;
+using NUnit.Framework;
using Unity.Netcode.Transports.UTP;
using Unity.Networking.Transport;
using UnityEngine;
diff --git a/Tests/Runtime/Transports/UnityTransportTests.cs b/Tests/Runtime/Transports/UnityTransportTests.cs
index 53fccb0..85bdea8 100644
--- a/Tests/Runtime/Transports/UnityTransportTests.cs
+++ b/Tests/Runtime/Transports/UnityTransportTests.cs
@@ -1,9 +1,9 @@
-using NUnit.Framework;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
+using NUnit.Framework;
using Unity.Netcode.Transports.UTP;
using Unity.Networking.Transport;
using UnityEngine;
diff --git a/Tests/Runtime/com.unity.netcode.runtimetests.asmdef b/Tests/Runtime/com.unity.netcode.runtimetests.asmdef
index e245a30..719b7be 100644
--- a/Tests/Runtime/com.unity.netcode.runtimetests.asmdef
+++ b/Tests/Runtime/com.unity.netcode.runtimetests.asmdef
@@ -11,7 +11,8 @@
"Unity.Multiplayer.Tools.NetStats",
"Unity.Networking.Transport",
"ClientNetworkTransform",
- "Unity.Netcode.TestHelpers.Runtime"
+ "Unity.Netcode.TestHelpers.Runtime",
+ "Unity.Mathematics"
],
"optionalUnityReferences": [
"TestAssemblies"
diff --git a/package.json b/package.json
index 8c35058..8dc0693 100644
--- a/package.json
+++ b/package.json
@@ -2,23 +2,23 @@
"name": "com.unity.netcode.gameobjects",
"displayName": "Netcode for GameObjects",
"description": "Netcode for GameObjects is a high-level netcode SDK that provides networking capabilities to GameObject/MonoBehaviour workflows within Unity and sits on top of underlying transport layer.",
- "version": "1.3.1",
+ "version": "1.4.0",
"unity": "2020.3",
"dependencies": {
"com.unity.nuget.mono-cecil": "1.10.1",
"com.unity.transport": "1.3.3"
},
"_upm": {
- "changelog": "### Added\n\n- Added detection and graceful handling of corrupt packets for additional safety. (#2419)\n\n### Changed\n\n- The UTP component UI has been updated to be more user-friendly for new users by adding a simple toggle to switch between local-only (127.0.0.1) and remote (0.0.0.0) binding modes, using the toggle \"Allow Remote Connections\" (#2408)\n- Updated `UnityTransport` dependency on `com.unity.transport` to 1.3.3. (#2450)\n- `NetworkShow()` of `NetworkObject`s are delayed until the end of the frame to ensure consistency of delta-driven variables like `NetworkList`.\n- Dirty `NetworkObject` are reset at end-of-frame and not at serialization time.\n- `NetworkHide()` of an object that was just `NetworkShow()`n produces a warning, as remote clients will _not_ get a spawn/despawn pair.\n- Renamed the NetworkTransform.SetState parameter `shouldGhostsInterpolate` to `teleportDisabled` for better clarity of what that parameter does. (#2228)\n- Network prefabs are now stored in a ScriptableObject that can be shared between NetworkManagers, and have been exposed for public access. By default, a Default Prefabs List is created that contains all NetworkObject prefabs in the project, and new NetworkManagers will default to using that unless that option is turned off in the Netcode for GameObjects settings. Existing NetworkManagers will maintain their existing lists, which can be migrated to the new format via a button in their inspector. (#2322)\n\n### Fixed\n\n- Fixed issue where changes to a layer's weight would not synchronize unless a state transition was occurring.(#2399)\n- Fixed issue where `NetworkManager.LocalClientId` was returning the `NetworkTransport.ServerClientId` as opposed to the `NetworkManager.m_LocalClientId`. (#2398)\n- Fixed issue where a dynamically spawned `NetworkObject` parented under an in-scene placed `NetworkObject` would have its `InScenePlaced` value changed to `true`. This would result in a soft synchronization error for late joining clients. (#2396)\n- Fixed a UTP test that was failing when you install Unity Transport package 2.0.0 or newer. (#2347)\n- Fixed issue where `NetcodeSettingsProvider` would throw an exception in Unity 2020.3.x versions. (#2345)\n- Fixed server side issue where, depending upon component ordering, some NetworkBehaviour components might not have their OnNetworkDespawn method invoked if the client side disconnected. (#2323)\n- Fixed a case where data corruption could occur when using UnityTransport when reaching a certain level of send throughput. (#2332)\n- Fixed an issue in `UnityTransport` where an exception would be thrown if starting a Relay host/server on WebGL. This exception should only be thrown if using direct connections (where WebGL can't act as a host/server). (#2321)\n- Fixed `NetworkAnimator` issue where it was not checking for `AnimatorStateTtansition.destinationStateMachine` and any possible sub-states defined within it. (#2309)\n- Fixed `NetworkAnimator` issue where the host client was receiving the ClientRpc animation updates when the host was the owner.(#2309)\n- Fixed `NetworkAnimator` issue with using pooled objects and when specific properties are cleaned during despawn and destroy.(#2309)\n- Fixed issue where `NetworkAnimator` was checking for animation changes when the associated `NetworkObject` was not spawned.(#2309)\n- Corrected an issue with the documentation for BufferSerializer (#2401)"
+ "changelog": "### Added\n\n- Added a way to access the GlobalObjectIdHash via PrefabIdHash for use in the Connection Approval Callback. (#2437)\n- Added `OnServerStarted` and `OnServerStopped` events that will trigger only on the server (or host player) to notify that the server just started or is no longer active (#2420)\n- Added `OnClientStarted` and `OnClientStopped` events that will trigger only on the client (or host player) to notify that the client just started or is no longer active (#2420)\n- Added `NetworkTransform.UseHalfFloatPrecision` property that, when enabled, will use half float values for position, rotation, and scale. This yields a 50% bandwidth savings a the cost of precision. (#2388)\n- Added `NetworkTransform.UseQuaternionSynchronization` property that, when enabled, will synchronize the entire quaternion. (#2388)\n- Added `NetworkTransform.UseQuaternionCompression` property that, when enabled, will use a smallest three implementation reducing a full quaternion synchronization update to the size of an unsigned integer. (#2388)\n- Added `NetworkTransform.SlerpPosition` property that, when enabled along with interpolation being enabled, will interpolate using `Vector3.Slerp`. (#2388)\n- Added `BufferedLinearInterpolatorVector3` that replaces the float version, is now used by `NetworkTransform`, and provides the ability to enable or disable `Slerp`. (#2388)\n- Added `HalfVector3` used for scale when half float precision is enabled. (#2388)\n- Added `HalfVector4` used for rotation when half float precision and quaternion synchronization is enabled. (#2388)\n- Added `HalfVector3DeltaPosition` used for position when half float precision is enabled. This handles loss in position precision by updating only the delta position as opposed to the full position. (#2388)\n- Added `NetworkTransform.GetSpaceRelativePosition` and `NetworkTransform.GetSpaceRelativeRotation` helper methods to return the proper values depending upon whether local or world space. (#2388)\n- Added `NetworkTransform.OnAuthorityPushTransformState` virtual method that is invoked just prior to sending the `NetworkTransformState` to non-authoritative instances. This provides users with the ability to obtain more precise delta values for prediction related calculations. (#2388)\n- Added `NetworkTransform.OnNetworkTransformStateUpdated` virtual method that is invoked just after the authoritative `NetworkTransformState` is applied. This provides users with the ability to obtain more precise delta values for prediction related calculations. (#2388)\n- Added `NetworkTransform.OnInitialize`virtual method that is invoked after the `NetworkTransform` has been initialized or re-initialized when ownership changes. This provides for a way to make adjustments when `NetworkTransform` is initialized (i.e. resetting client prediction etc) (#2388)\n- Added `NetworkObject.SynchronizeTransform` property (default is true) that provides users with another way to help with bandwidth optimizations where, when set to false, the `NetworkObject`'s associated transform will not be included when spawning and/or synchronizing late joining players. (#2388)\n- Added `NetworkSceneManager.ActiveSceneSynchronizationEnabled` property, disabled by default, that enables client synchronization of server-side active scene changes. (#2383)\n- Added `NetworkObject.ActiveSceneSynchronization`, disabled by default, that will automatically migrate a `NetworkObject` to a newly assigned active scene. (#2383)\n- Added `NetworkObject.SceneMigrationSynchronization`, enabled by default, that will synchronize client(s) when a `NetworkObject` is migrated into a new scene on the server side via `SceneManager.MoveGameObjectToScene`. (#2383)\n\n### Changed\n\n- Made sure the `CheckObjectVisibility` delegate is checked and applied, upon `NetworkShow` attempt. Found while supporting (#2454), although this is not a fix for this (already fixed) issue. (#2463)\n- Changed `NetworkTransform` authority handles delta checks on each new network tick and no longer consumes processing cycles checking for deltas for all frames in-between ticks. (#2388)\n- Changed the `NetworkTransformState` structure is now public and now has public methods that provide access to key properties of the `NetworkTransformState` structure. (#2388)\n- Changed `NetworkTransform` interpolation adjusts its interpolation \"ticks ago\" to be 2 ticks latent if it is owner authoritative and the instance is not the server or 1 tick latent if the instance is the server and/or is server authoritative. (#2388)\n- Updated `NetworkSceneManager` to migrate dynamically spawned `NetworkObject`s with `DestroyWithScene` set to false into the active scene if their current scene is unloaded. (#2383)\n- Updated the server to synchronize its local `NetworkSceneManager.ClientSynchronizationMode` during the initial client synchronization. (#2383)\n\n### Fixed\n\n- Fixed issue where during client synchronization the synchronizing client could receive a ObjectSceneChanged message before the client-side NetworkObject instance had been instantiated and spawned. (#2502)\n- Fixed issue where `NetworkAnimator` was building client RPC parameters to exclude the host from sending itself messages but was not including it in the ClientRpc parameters. (#2492)\n- Fixed issue where `NetworkAnimator` was not properly detecting and synchronizing cross fade initiated transitions. (#2481)\n- Fixed issue where `NetworkAnimator` was not properly synchronizing animation state updates. (#2481)\n- Fixed float NetworkVariables not being rendered properly in the inspector of NetworkObjects. (#2441)\n- Fixed an issue where Named Message Handlers could remove themselves causing an exception when the metrics tried to access the name of the message.(#2426)\n- Fixed registry of public `NetworkVariable`s in derived `NetworkBehaviour`s (#2423)\n- Fixed issue where runtime association of `Animator` properties to `AnimationCurve`s would cause `NetworkAnimator` to attempt to update those changes. (#2416)\n- Fixed issue where `NetworkAnimator` would not check if its associated `Animator` was valid during serialization and would spam exceptions in the editor console. (#2416)\n- Fixed issue with a child's rotation rolling over when interpolation is enabled on a `NetworkTransform`. Now using half precision or full quaternion synchronization will always update all axis. (#2388)\n- Fixed issue where `NetworkTransform` was not setting the teleport flag when the `NetworkTransform.InLocalSpace` value changed. This issue only impacted `NetworkTransform` when interpolation was enabled. (#2388)\n- Fixed issue when the `NetworkSceneManager.ClientSynchronizationMode` is `LoadSceneMode.Additive` and the server changes the currently active scene prior to a client connecting then upon a client connecting and being synchronized the NetworkSceneManager would clear its internal ScenePlacedObjects list that could already be populated. (#2383)\n- Fixed issue where a client would load duplicate scenes of already preloaded scenes during the initial client synchronization and `NetworkSceneManager.ClientSynchronizationMode` was set to `LoadSceneMode.Additive`. (#2383)"
},
"upmCi": {
- "footprint": "edebf4ce36d2ecd3e2a7632dbf88da8cbc89aa50"
+ "footprint": "dad9d9cf0a4704c51c7f587ac264ae49fe2efa1f"
},
- "documentationUrl": "https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@1.3/manual/index.html",
+ "documentationUrl": "https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@1.4/manual/index.html",
"repository": {
"url": "https://github.com/Unity-Technologies/com.unity.netcode.gameobjects.git",
"type": "git",
- "revision": "469b46fabe1a78032a31ab04a15490a6912dadfb"
+ "revision": "a418eabe59ed5f5e918d1756ee051436d2819828"
},
"samples": [
{