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)
This commit is contained in:
Unity Technologies
2023-04-10 00:00:00 +00:00
parent 8060718e04
commit b5abc3ff7c
138 changed files with 7892 additions and 1852 deletions

View File

@@ -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). 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 ## [1.3.1] - 2023-03-27
### Added ### Added

159
Components/HalfVector3.cs Normal file
View File

@@ -0,0 +1,159 @@
using System.Runtime.CompilerServices;
using Unity.Mathematics;
using UnityEngine;
namespace Unity.Netcode.Components
{
/// <summary>
/// Half float precision <see cref="Vector3"/>.
/// </summary>
/// <remarks>
/// The Vector3T<ushort> values are half float values returned by <see cref="Mathf.FloatToHalf(float)"/> for each
/// individual axis and the 16 bits of the half float are stored as <see cref="ushort"/> values since C# does not have
/// a half float type.
/// </remarks>
public struct HalfVector3 : INetworkSerializable
{
internal const int Length = 3;
/// <summary>
/// The half float precision value of the x-axis as a <see cref="half"/>.
/// </summary>
public half X => Axis.x;
/// <summary>
/// The half float precision value of the y-axis as a <see cref="half"/>.
/// </summary>
public half Y => Axis.y;
/// <summary>
/// The half float precision value of the z-axis as a <see cref="half"/>.
/// </summary>
public half Z => Axis.x;
/// <summary>
/// Used to store the half float precision values as a <see cref="half3"/>
/// </summary>
public half3 Axis;
/// <summary>
/// Determine which axis will be synchronized during serialization
/// </summary>
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;
}
}
}
/// <summary>
/// The serialization implementation of <see cref="INetworkSerializable"/>.
/// </summary>
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
if (serializer.IsReader)
{
SerializeRead(serializer.GetFastBufferReader());
}
else
{
SerializeWrite(serializer.GetFastBufferWriter());
}
}
/// <summary>
/// Gets the full precision value as a <see cref="Vector3"/>.
/// </summary>
/// <returns>a <see cref="Vector3"/> as the full precision value.</returns>
[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;
}
/// <summary>
/// Converts a full precision <see cref="Vector3"/> to half precision and updates the current instance.
/// </summary>
/// <param name="vector3">The <see cref="Vector3"/> to convert.</param>
[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];
}
}
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="vector3">The initial axial values (converted to half floats) when instantiated.</param>
/// <param name="vector3AxisToSynchronize">The axis to synchronize.</param>
public HalfVector3(Vector3 vector3, bool3 axisToSynchronize)
{
Axis = half3.zero;
AxisToSynchronize = axisToSynchronize;
UpdateFrom(ref vector3);
}
/// <summary>
/// Constructor that defaults to all axis being synchronized.
/// </summary>
/// <param name="vector3">The initial axial values (converted to half floats) when instantiated.</param>
public HalfVector3(Vector3 vector3) : this(vector3, math.bool3(true))
{
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="x">The initial x axis (converted to half float) value when instantiated.</param>
/// <param name="y">The initial y axis (converted to half float) value when instantiated.</param>
/// <param name="z">The initial z axis (converted to half float) value when instantiated.</param>
/// <param name="axisToSynchronize">The axis to synchronize.</param>
public HalfVector3(float x, float y, float z, bool3 axisToSynchronize) : this(new Vector3(x, y, z), axisToSynchronize)
{
}
/// <summary>
/// Constructor that defaults to all axis being synchronized.
/// </summary>
/// <param name="x">The initial x axis (converted to half float) value when instantiated.</param>
/// <param name="y">The initial y axis (converted to half float) value when instantiated.</param>
/// <param name="z">The initial z axis (converted to half float) value when instantiated.</param>
public HalfVector3(float x, float y, float z) : this(new Vector3(x, y, z), math.bool3(true))
{
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b0e371533eaeac446b16b10886f64f84
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

137
Components/HalfVector4.cs Normal file
View File

@@ -0,0 +1,137 @@
using System.Runtime.CompilerServices;
using Unity.Mathematics;
using UnityEngine;
namespace Unity.Netcode.Components
{
/// <summary>
/// Half Precision <see cref="Vector4"/> that can also be used to convert a <see cref="Quaternion"/> to half precision.
/// </summary>
/// <remarks>
/// The Vector4T<ushort> values are half float values returned by <see cref="Mathf.FloatToHalf(float)"/> for each
/// individual axis and the 16 bits of the half float are stored as <see cref="ushort"/> values since C# does not have
/// a half float type.
/// </remarks>
public struct HalfVector4 : INetworkSerializable
{
internal const int Length = 4;
/// <summary>
/// The half float precision value of the x-axis as a <see cref="half"/>.
/// </summary>
public half X => Axis.x;
/// <summary>
/// The half float precision value of the y-axis as a <see cref="half"/>.
/// </summary>
public half Y => Axis.y;
/// <summary>
/// The half float precision value of the z-axis as a <see cref="half"/>.
/// </summary>
public half Z => Axis.z;
/// <summary>
/// The half float precision value of the w-axis as a <see cref="half"/>.
/// </summary>
public half W => Axis.w;
/// <summary>
/// Used to store the half float precision values as a <see cref="half4"/>
/// </summary>
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;
}
}
/// <summary>
/// The serialization implementation of <see cref="INetworkSerializable"/>.
/// </summary>
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
if (serializer.IsReader)
{
SerializeRead(serializer.GetFastBufferReader());
}
else
{
SerializeWrite(serializer.GetFastBufferWriter());
}
}
/// <summary>
/// Converts this instance to a full precision <see cref="Vector4"/>.
/// </summary>
/// <returns>A <see cref="Vector4"/> as the full precision value.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 ToVector4()
{
return math.float4(Axis);
}
/// <summary>
/// Converts this instance to a full precision <see cref="Quaternion"/>.
/// </summary>
/// <returns>A <see cref="Quaternion"/> as the full precision value.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Quaternion ToQuaternion()
{
return math.quaternion(Axis);
}
/// <summary>
/// Converts a full precision <see cref="Vector4"/> to half precision and updates the current instance.
/// </summary>
/// <param name="vector4">The <see cref="Vector4"/> to convert and update this instance with.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void UpdateFrom(ref Vector4 vector4)
{
Axis = math.half4(vector4);
}
/// <summary>
/// Converts a full precision <see cref="Vector4"/> to half precision and updates the current instance.
/// </summary>
/// <param name="quaternion">The <see cref="Quaternion"/> to convert and update this instance with.</param>
[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));
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="vector4">The initial axial values (converted to half floats) when instantiated.</param>
public HalfVector4(Vector4 vector4)
{
Axis = default;
UpdateFrom(ref vector4);
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="x">The initial x axis (converted to half float) value when instantiated.</param>
/// <param name="y">The initial y axis (converted to half float) value when instantiated.</param>
/// <param name="z">The initial z axis (converted to half float) value when instantiated.</param>
/// <param name="w">The initial w axis (converted to half float) value when instantiated.</param>
public HalfVector4(float x, float y, float z, float w) : this(new Vector4(x, y, z, w))
{
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 03c78136f41ff84499e2a6ac4a7dd7a5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -312,20 +312,79 @@ namespace Unity.Netcode
/// </remarks> /// </remarks>
public class BufferedLinearInterpolatorQuaternion : BufferedLinearInterpolator<Quaternion> public class BufferedLinearInterpolatorQuaternion : BufferedLinearInterpolator<Quaternion>
{ {
/// <summary>
/// Use <see cref="Quaternion.Slerp"/> when <see cref="true"/>.
/// Use <see cref="Quaternion.Lerp"/> when <see cref="false"/>
/// </summary>
/// <remarks>
/// When using half precision (due to the imprecision) using <see cref="Quaternion.Lerp"/> is
/// less processor intensive (i.e. precision is already "imprecise").
/// When using full precision (to maintain precision) using <see cref="Quaternion.Slerp"/> is
/// more processor intensive yet yields more precise results.
/// </remarks>
public bool IsSlerp;
/// <inheritdoc /> /// <inheritdoc />
protected override Quaternion InterpolateUnclamped(Quaternion start, Quaternion end, float time) protected override Quaternion InterpolateUnclamped(Quaternion start, Quaternion end, float time)
{ {
// Disabling Extrapolation: if (IsSlerp)
// TODO: Add Jira Ticket {
return Quaternion.Slerp(start, end, time); return Quaternion.Slerp(start, end, time);
} }
else
{
return Quaternion.Lerp(start, end, time);
}
}
/// <inheritdoc /> /// <inheritdoc />
protected override Quaternion Interpolate(Quaternion start, Quaternion end, float time) protected override Quaternion Interpolate(Quaternion start, Quaternion end, float time)
{ {
// Disabling Extrapolation: if (IsSlerp)
// TODO: Add Jira Ticket {
return Quaternion.Slerp(start, end, time); return Quaternion.Slerp(start, end, time);
} }
else
{
return Quaternion.Lerp(start, end, time);
}
}
}
/// <summary>
/// A <see cref="BufferedLinearInterpolator<T>"/> <see cref="Vector3"/> implementation.
/// </summary>
public class BufferedLinearInterpolatorVector3 : BufferedLinearInterpolator<Vector3>
{
/// <summary>
/// Use <see cref="Vector3.Slerp"/> when <see cref="true"/>.
/// Use <see cref="Vector3.Lerp"/> when <see cref="false"/>
/// </summary>
public bool IsSlerp;
/// <inheritdoc />
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);
}
}
/// <inheritdoc />
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);
}
}
} }
} }

View File

@@ -23,6 +23,13 @@ namespace Unity.Netcode.Components
/// </summary> /// </summary>
private void FlushMessages() 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) foreach (var sendEntry in m_SendParameterUpdates)
{ {
m_NetworkAnimator.SendParametersUpdateClientRpc(sendEntry.ParametersUpdateMessage, sendEntry.ClientRpcParams); m_NetworkAnimator.SendParametersUpdateClientRpc(sendEntry.ParametersUpdateMessage, sendEntry.ClientRpcParams);
@@ -64,9 +71,11 @@ namespace Unity.Netcode.Components
m_NetworkAnimator.UpdateParameters(ref parameterUpdate); m_NetworkAnimator.UpdateParameters(ref parameterUpdate);
} }
m_ProcessParameterUpdates.Clear(); m_ProcessParameterUpdates.Clear();
var isServerAuthority = m_NetworkAnimator.IsServerAuthoritative();
// Only owners check for Animator changes // owners when owner authoritative or the server when server authoritative are the only instances that
if (m_NetworkAnimator.IsOwner && !m_NetworkAnimator.IsServerAuthoritative() || m_NetworkAnimator.IsServerAuthoritative() && m_NetworkAnimator.NetworkManager.IsServer) // checks for Animator changes
if ((!isServerAuthority && m_NetworkAnimator.IsOwner) || (isServerAuthority && m_NetworkAnimator.IsServer))
{ {
m_NetworkAnimator.CheckForAnimatorChanges(); m_NetworkAnimator.CheckForAnimatorChanges();
} }
@@ -157,11 +166,11 @@ namespace Unity.Netcode.Components
[AddComponentMenu("Netcode/Network Animator")] [AddComponentMenu("Netcode/Network Animator")]
[RequireComponent(typeof(Animator))] [RequireComponent(typeof(Animator))]
public class NetworkAnimator : NetworkBehaviour, ISerializationCallbackReceiver public class NetworkAnimator : NetworkBehaviour, ISerializationCallbackReceiver
{ {
[Serializable] [Serializable]
internal class TransitionStateinfo internal class TransitionStateinfo
{ {
public bool IsCrossFadeExit;
public int Layer; public int Layer;
public int OriginatingState; public int OriginatingState;
public int DestinationState; public int DestinationState;
@@ -279,6 +288,11 @@ namespace Unity.Netcode.Components
{ {
return; return;
} }
if (m_Animator == null)
{
return;
}
TransitionStateInfoList = new List<TransitionStateinfo>(); TransitionStateInfoList = new List<TransitionStateinfo>();
var animatorController = m_Animator.runtimeAnimatorController as AnimatorController; var animatorController = m_Animator.runtimeAnimatorController as AnimatorController;
if (animatorController == null) if (animatorController == null)
@@ -312,9 +326,19 @@ namespace Unity.Netcode.Components
internal float NormalizedTime; internal float NormalizedTime;
internal int Layer; internal int Layer;
internal float Weight; internal float Weight;
internal float Duration;
// For synchronizing transitions // For synchronizing transitions
internal bool Transition; 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 // The StateHash is where the transition starts
// and the DestinationStateHash is the destination state // and the DestinationStateHash is the destination state
internal int DestinationStateHash; internal int DestinationStateHash;
@@ -324,65 +348,46 @@ namespace Unity.Netcode.Components
if (serializer.IsWriter) if (serializer.IsWriter)
{ {
var writer = serializer.GetFastBufferWriter(); var writer = serializer.GetFastBufferWriter();
var writeSize = FastBufferWriter.GetWriteSize(Transition); m_StateFlags = 0x00;
writeSize += FastBufferWriter.GetWriteSize(StateHash);
writeSize += FastBufferWriter.GetWriteSize(NormalizedTime);
writeSize += FastBufferWriter.GetWriteSize(Layer);
writeSize += FastBufferWriter.GetWriteSize(Weight);
if (Transition) if (Transition)
{ {
writeSize += FastBufferWriter.GetWriteSize(DestinationStateHash); m_StateFlags |= k_IsTransition;
} }
if (CrossFade)
if (!writer.TryBeginWrite(writeSize))
{ {
throw new OverflowException($"[{GetType().Name}] Could not serialize: Out of buffer space."); m_StateFlags |= k_IsCrossFade;
} }
serializer.SerializeValue(ref m_StateFlags);
writer.WriteValue(Transition); BytePacker.WriteValuePacked(writer, StateHash);
writer.WriteValue(StateHash); BytePacker.WriteValuePacked(writer, Layer);
writer.WriteValue(NormalizedTime);
writer.WriteValue(Layer);
writer.WriteValue(Weight);
if (Transition) if (Transition)
{ {
writer.WriteValue(DestinationStateHash); BytePacker.WriteValuePacked(writer, DestinationStateHash);
} }
} }
else else
{ {
var reader = serializer.GetFastBufferReader(); var reader = serializer.GetFastBufferReader();
// Begin reading the Transition flag serializer.SerializeValue(ref m_StateFlags);
if (!reader.TryBeginRead(FastBufferWriter.GetWriteSize(Transition))) Transition = (m_StateFlags & k_IsTransition) == k_IsTransition;
{ CrossFade = (m_StateFlags & k_IsCrossFade) == k_IsCrossFade;
throw new OverflowException($"[{GetType().Name}] Could not deserialize: Out of buffer space.");
}
reader.ReadValue(out Transition);
// Now determine what remains to be read ByteUnpacker.ReadValuePacked(reader, out StateHash);
var readSize = FastBufferWriter.GetWriteSize(StateHash); ByteUnpacker.ReadValuePacked(reader, out Layer);
readSize += FastBufferWriter.GetWriteSize(NormalizedTime);
readSize += FastBufferWriter.GetWriteSize(Layer);
readSize += FastBufferWriter.GetWriteSize(Weight);
if (Transition) if (Transition)
{ {
readSize += FastBufferWriter.GetWriteSize(DestinationStateHash); ByteUnpacker.ReadValuePacked(reader, out DestinationStateHash);
}
} }
// Now read the remaining information about this AnimationState serializer.SerializeValue(ref NormalizedTime);
if (!reader.TryBeginRead(readSize)) serializer.SerializeValue(ref Weight);
{
throw new OverflowException($"[{GetType().Name}] Could not deserialize: Out of buffer space.");
}
reader.ReadValue(out StateHash); // Cross fading includes the duration of the cross fade.
reader.ReadValue(out NormalizedTime); if (CrossFade)
reader.ReadValue(out Layer);
reader.ReadValue(out Weight);
if (Transition)
{ {
reader.ReadValue(out DestinationStateHash); serializer.SerializeValue(ref Duration);
}
} }
} }
} }
@@ -565,8 +570,10 @@ namespace Unity.Netcode.Components
// We initialize the m_AnimationMessage for all instances in the event that // We initialize the m_AnimationMessage for all instances in the event that
// ownership or authority changes during runtime. // ownership or authority changes during runtime.
m_AnimationMessage = new AnimationMessage(); m_AnimationMessage = new AnimationMessage
m_AnimationMessage.AnimationStates = new List<AnimationState>(); {
AnimationStates = new List<AnimationState>()
};
// Store off our current layer weights and create our animation // Store off our current layer weights and create our animation
// state entries per layer. // state entries per layer.
@@ -588,17 +595,13 @@ namespace Unity.Netcode.Components
m_CachedAnimatorParameters = new NativeArray<AnimatorParamCache>(parameters.Length, Allocator.Persistent); m_CachedAnimatorParameters = new NativeArray<AnimatorParamCache>(parameters.Length, Allocator.Persistent);
m_ParametersToUpdate = new List<int>(parameters.Length); m_ParametersToUpdate = new List<int>(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++) for (var i = 0; i < parameters.Length; i++)
{ {
var parameter = parameters[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 var cacheParam = new AnimatorParamCache
{ {
Type = UnsafeUtility.EnumToInt(parameter.type), Type = UnsafeUtility.EnumToInt(parameter.type),
@@ -643,12 +646,22 @@ namespace Unity.Netcode.Components
/// <inheritdoc/> /// <inheritdoc/>
public override void OnNetworkSpawn() 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) if (IsServer)
{ {
m_ClientSendList = new List<ulong>(128); m_ClientSendList = new List<ulong>(128);
m_ClientRpcParams = new ClientRpcParams(); m_ClientRpcParams = new ClientRpcParams
m_ClientRpcParams.Send = new ClientRpcSendParams(); {
m_ClientRpcParams.Send.TargetClientIds = m_ClientSendList; Send = new ClientRpcSendParams
{
TargetClientIds = m_ClientSendList
}
};
} }
// Create a handler for state changes // Create a handler for state changes
@@ -691,10 +704,7 @@ namespace Unity.Netcode.Components
for (int layer = 0; layer < m_Animator.layerCount; layer++) for (int layer = 0; layer < m_Animator.layerCount; layer++)
{ {
var synchronizationStateInfo = m_Animator.GetCurrentAnimatorStateInfo(layer); var synchronizationStateInfo = m_Animator.GetCurrentAnimatorStateInfo(layer);
if (SynchronizationStateInfo != null) SynchronizationStateInfo?.Add(synchronizationStateInfo);
{
SynchronizationStateInfo.Add(synchronizationStateInfo);
}
var stateHash = synchronizationStateInfo.fullPathHash; var stateHash = synchronizationStateInfo.fullPathHash;
var normalizedTime = synchronizationStateInfo.normalizedTime; var normalizedTime = synchronizationStateInfo.normalizedTime;
var isInTransition = m_Animator.IsInTransition(layer); var isInTransition = m_Animator.IsInTransition(layer);
@@ -767,11 +777,97 @@ namespace Unity.Netcode.Components
else else
{ {
var parameters = new ParametersUpdateMessage(); var parameters = new ParametersUpdateMessage();
var animationStates = new AnimationMessage(); var animationMessage = new AnimationMessage();
serializer.SerializeValue(ref parameters); serializer.SerializeValue(ref parameters);
UpdateParameters(ref parameters); UpdateParameters(ref parameters);
serializer.SerializeValue(ref animationStates); serializer.SerializeValue(ref animationMessage);
HandleAnimStateUpdate(ref animationStates); foreach (var animationState in animationMessage.AnimationStates)
{
UpdateAnimationState(animationState);
}
}
}
/// <summary>
/// Checks for animation state changes in:
/// -Layer weights
/// -Cross fades
/// -Transitions
/// -Layer AnimationStates
/// </summary>
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
/// </remarks> /// </remarks>
internal void CheckForAnimatorChanges() internal void CheckForAnimatorChanges()
{ {
if (!IsSpawned || (!IsOwner && !IsServerAuthoritative()) || (IsServerAuthoritative() && !IsServer))
{
return;
}
if (CheckParametersChanged()) if (CheckParametersChanged())
{ {
SendParametersUpdate(); SendParametersUpdate();
@@ -803,9 +894,6 @@ namespace Unity.Netcode.Components
return; return;
} }
int stateHash;
float normalizedTime;
// Reset the dirty count before checking for AnimationState updates // Reset the dirty count before checking for AnimationState updates
m_AnimationMessage.IsDirtyCount = 0; m_AnimationMessage.IsDirtyCount = 0;
@@ -815,26 +903,7 @@ namespace Unity.Netcode.Components
AnimatorStateInfo st = m_Animator.GetCurrentAnimatorStateInfo(layer); AnimatorStateInfo st = m_Animator.GetCurrentAnimatorStateInfo(layer);
var totalSpeed = st.speed * st.speedMultiplier; var totalSpeed = st.speed * st.speedMultiplier;
var adjustedNormalizedMaxTime = totalSpeed > 0.0f ? 1.0f / totalSpeed : 0.0f; var adjustedNormalizedMaxTime = totalSpeed > 0.0f ? 1.0f / totalSpeed : 0.0f;
CheckForStateChange(layer);
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++;
} }
// Send an AnimationMessage only if there are dirty AnimationStates to send // 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.AddRange(NetworkManager.ConnectedClientsIds);
m_ClientSendList.Remove(NetworkManager.LocalClientId); m_ClientSendList.Remove(NetworkManager.LocalClientId);
m_ClientRpcParams.Send.TargetClientIds = m_ClientSendList; m_ClientRpcParams.Send.TargetClientIds = m_ClientSendList;
SendAnimStateClientRpc(m_AnimationMessage); SendAnimStateClientRpc(m_AnimationMessage, m_ClientRpcParams);
} }
} }
} }
@@ -885,7 +954,7 @@ namespace Unity.Netcode.Components
/// <summary> /// <summary>
/// Helper function to get the cached value /// Helper function to get the cached value
/// </summary> /// </summary>
unsafe private T GetValue<T>(ref AnimatorParamCache animatorParamCache) private unsafe T GetValue<T>(ref AnimatorParamCache animatorParamCache)
{ {
T currentValue; T currentValue;
fixed (void* value = animatorParamCache.Value) 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 /// If so, it fills out m_ParametersToUpdate with the indices of the parameters
/// that have changed. Returns true if any parameters changed. /// that have changed. Returns true if any parameters changed.
/// </summary> /// </summary>
unsafe private bool CheckParametersChanged() private unsafe bool CheckParametersChanged()
{ {
m_ParametersToUpdate.Clear(); m_ParametersToUpdate.Clear();
for (int i = 0; i < m_CachedAnimatorParameters.Length; i++) for (int i = 0; i < m_CachedAnimatorParameters.Length; i++)
{ {
ref var cacheValue = ref UnsafeUtility.ArrayElementAsRef<AnimatorParamCache>(m_CachedAnimatorParameters.GetUnsafePtr(), i); ref var cacheValue = ref UnsafeUtility.ArrayElementAsRef<AnimatorParamCache>(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; var hash = cacheValue.Hash;
if (cacheValue.Type == AnimationParamEnumWrapper.AnimatorControllerParameterInt) if (cacheValue.Type == AnimationParamEnumWrapper.AnimatorControllerParameterInt)
{ {
@@ -941,52 +1018,6 @@ namespace Unity.Netcode.Components
return m_ParametersToUpdate.Count > 0; return m_ParametersToUpdate.Count > 0;
} }
/// <summary>
/// Checks if any of the Animator's states have changed
/// </summary>
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;
}
/// <summary> /// <summary>
/// Writes all of the Animator's parameters /// Writes all of the Animator's parameters
/// This uses the m_ParametersToUpdate list to write out only /// 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 there is no state transition then return
if (animationState.StateHash == 0) if (animationState.StateHash == 0 && !animationState.Transition)
{ {
return; return;
} }
var currentState = m_Animator.GetCurrentAnimatorStateInfo(animationState.Layer); 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 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 // We should have all valid entries for any animation state transition update
// Verify the AnimationState's assigned Layer exists // 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!"); 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 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); 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);
}
}
}
/// <summary> /// <summary>
/// Internally-called RPC client receiving function to update some animation state on a client /// Internally-called RPC client receiving function to update some animation state on a client
/// </summary> /// </summary>
[ClientRpc] [ClientRpc]
private unsafe void SendAnimStateClientRpc(AnimationMessage animationMessage, ClientRpcParams clientRpcParams = default) internal unsafe void SendAnimStateClientRpc(AnimationMessage animationMessage, ClientRpcParams clientRpcParams = default)
{ {
// This should never happen // This should never happen
if (IsHost) if (IsHost)
@@ -1264,7 +1288,10 @@ namespace Unity.Netcode.Components
} }
return; return;
} }
HandleAnimStateUpdate(ref animationMessage); foreach (var animationState in animationMessage.AnimationStates)
{
UpdateAnimationState(animationState);
}
} }
/// <summary> /// <summary>
@@ -1273,21 +1300,6 @@ namespace Unity.Netcode.Components
/// </summary> /// </summary>
[ServerRpc] [ServerRpc]
internal void SendAnimTriggerServerRpc(AnimationTriggerMessage animationTriggerMessage, ServerRpcParams serverRpcParams = default) internal void SendAnimTriggerServerRpc(AnimationTriggerMessage animationTriggerMessage, ServerRpcParams serverRpcParams = default)
{
// If it is server authoritative
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.");
}
}
else
{ {
// Ignore if a non-owner sent this. // Ignore if a non-owner sent this.
if (serverRpcParams.Receive.SenderClientId != OwnerClientId) if (serverRpcParams.Receive.SenderClientId != OwnerClientId)
@@ -1302,16 +1314,18 @@ namespace Unity.Netcode.Components
// set the trigger locally on the server // set the trigger locally on the server
InternalSetTrigger(animationTriggerMessage.Hash, animationTriggerMessage.IsTriggerSet); 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.Clear();
m_ClientSendList.AddRange(NetworkManager.ConnectedClientsIds); m_ClientSendList.AddRange(NetworkManager.ConnectedClientsIds);
m_ClientSendList.Remove(serverRpcParams.Receive.SenderClientId);
m_ClientSendList.Remove(NetworkManager.ServerClientId); m_ClientSendList.Remove(NetworkManager.ServerClientId);
m_ClientRpcParams.Send.TargetClientIds = m_ClientSendList;
if (IsServerAuthoritative())
{
m_NetworkAnimatorStateChangeHandler.QueueTriggerUpdateToClient(animationTriggerMessage, m_ClientRpcParams); m_NetworkAnimatorStateChangeHandler.QueueTriggerUpdateToClient(animationTriggerMessage, m_ClientRpcParams);
} }
else if (NetworkManager.ConnectedClientsIds.Count > (IsHost ? 2 : 1))
{
m_ClientSendList.Remove(serverRpcParams.Receive.SenderClientId);
m_NetworkAnimatorStateChangeHandler.QueueTriggerUpdateToClient(animationTriggerMessage, m_ClientRpcParams);
} }
} }

View File

@@ -0,0 +1,205 @@
using System.Runtime.CompilerServices;
using Unity.Mathematics;
using UnityEngine;
namespace Unity.Netcode.Components
{
/// <summary>
/// Used to synchromnize delta position when half float precision is enabled
/// </summary>
public struct NetworkDeltaPosition : INetworkSerializable
{
internal const float MaxDeltaBeforeAdjustment = 64f;
/// <summary>
/// The HalfVector3 used to synchronize the delta in position
/// </summary>
public HalfVector3 HalfVector3;
internal Vector3 CurrentBasePosition;
internal Vector3 PrecisionLossDelta;
internal Vector3 HalfDeltaConvertedBack;
internal Vector3 PreviousPosition;
internal Vector3 DeltaPosition;
internal int NetworkTick;
/// <summary>
/// The serialization implementation of <see cref="INetworkSerializable"/>
/// </summary>
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
HalfVector3.NetworkSerialize(serializer);
}
/// <summary>
/// Gets the full precision value of Vector3 position while also potentially updating the current base position.
/// </summary>
/// <param name="networkTick">Use the current network tick value.</param>
/// <returns>The full position as a <see cref="Vector3"/>.</returns>
[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;
}
/// <summary>
/// Returns the current base position (excluding the delta position offset).
/// </summary>
/// <returns>The current base position as a <see cref="Vector3"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector3 GetCurrentBasePosition()
{
return CurrentBasePosition;
}
/// <summary>
/// Returns the full position which includes the delta offset position.
/// </summary>
/// <returns>The full position as a <see cref="Vector3"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector3 GetFullPosition()
{
return CurrentBasePosition + DeltaPosition;
}
/// <summary>
/// The half float vector3 version of the current delta position.
/// </summary>
/// <remarks>
/// Only applies to the authoritative side for <see cref="NetworkTransform"/> instances.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector3 GetConvertedDelta()
{
return HalfDeltaConvertedBack;
}
/// <summary>
/// The full precision current delta position.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector3 GetDeltaPosition()
{
return DeltaPosition;
}
/// <summary>
/// Updates the position delta based off of the current base position.
/// </summary>
/// <param name="vector3">The full precision <see cref="Vector3"/> value to (converted to half floats) used to determine the delta offset positon.</param>
/// <param name="networkTick">Set the current network tick value when updating.</param>
[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];
}
}
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="vector3">The initial axial values (converted to half floats) when instantiated.</param>
/// <param name="networkTick">Set the network tick value to the current network tick when instantiating.</param>
/// <param name="axisToSynchronize">The axis to be synchronized.</param>
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);
}
/// <summary>
/// Constructor that defaults to all axis being synchronized.
/// </summary>
/// <param name="vector3">The initial axial values (converted to half floats) when instantiated.</param>
/// <param name="networkTick">Set the network tick value to the current network tick when instantiating.</param>
public NetworkDeltaPosition(Vector3 vector3, int networkTick) : this(vector3, networkTick, math.bool3(true))
{
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="x">The initial x axis (converted to half float) value when instantiated.</param>
/// <param name="y">The initial y axis (converted to half float) value when instantiated.</param>
/// <param name="z">The initial z axis (converted to half float) value when instantiated.</param>
/// <param name="networkTick">Set the network tick value to the current network tick when instantiating.</param>
/// <param name="axisToSynchronize">The axis to be synchronized.</param>
public NetworkDeltaPosition(float x, float y, float z, int networkTick, bool3 axisToSynchronize) :
this(new Vector3(x, y, z), networkTick, axisToSynchronize)
{
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="x">The initial x axis (converted to half float) value when instantiated.</param>
/// <param name="y">The initial y axis (converted to half float) value when instantiated.</param>
/// <param name="z">The initial z axis (converted to half float) value when instantiated.</param>
/// <param name="networkTick">Set the network tick value to the current network tick when instantiating.</param>
public NetworkDeltaPosition(float x, float y, float z, int networkTick) :
this(new Vector3(x, y, z), networkTick, math.bool3(true))
{
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e45e6886578116f4c92fa0fe0d77fb85
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,123 @@
using System.Runtime.CompilerServices;
using UnityEngine;
namespace Unity.Netcode
{
/// <summary>
/// A Smallest Three Quaternion Compressor Implementation
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
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;
/// <summary>
/// Compresses a Quaternion into an unsigned integer
/// </summary>
/// <param name="quaternion">the <see cref="Quaternion"/> to be compressed</param>
/// <returns>the <see cref="Quaternion"/> compressed as an unsigned integer</returns>
[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;
}
/// <summary>
/// Decompress a compressed quaternion
/// </summary>
/// <param name="quaternion">quaternion to store the decompressed values within</param>
/// <param name="compressed">the compressed quaternion</param>
[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);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bb9d8b98d3c8bca469c8ee152353336f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -3,7 +3,8 @@
"rootNamespace": "Unity.Netcode.Components", "rootNamespace": "Unity.Netcode.Components",
"references": [ "references": [
"Unity.Netcode.Runtime", "Unity.Netcode.Runtime",
"Unity.Collections" "Unity.Collections",
"Unity.Mathematics"
], ],
"allowUnsafeCode": true, "allowUnsafeCode": true,
"versionDefines": [ "versionDefines": [

View File

@@ -1,7 +1,7 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Collections.Generic;
using Mono.Cecil; using Mono.Cecil;
using Mono.Cecil.Cil; using Mono.Cecil.Cil;
using Mono.Cecil.Rocks; using Mono.Cecil.Rocks;

View File

@@ -1,7 +1,7 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Collections.Generic;
using Mono.Cecil; using Mono.Cecil;
using Mono.Cecil.Cil; using Mono.Cecil.Cil;
using Unity.CompilationPipeline.Common.Diagnostics; using Unity.CompilationPipeline.Common.Diagnostics;

View File

@@ -1,7 +1,7 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Mono.Cecil; using Mono.Cecil;
using Mono.Cecil.Cil; using Mono.Cecil.Cil;
@@ -9,9 +9,9 @@ using Mono.Cecil.Rocks;
using Unity.CompilationPipeline.Common.Diagnostics; using Unity.CompilationPipeline.Common.Diagnostics;
using Unity.CompilationPipeline.Common.ILPostProcessing; using Unity.CompilationPipeline.Common.ILPostProcessing;
using UnityEngine; using UnityEngine;
using ILPPInterface = Unity.CompilationPipeline.Common.ILPostProcessing.ILPostProcessor;
using MethodAttributes = Mono.Cecil.MethodAttributes; using MethodAttributes = Mono.Cecil.MethodAttributes;
using ParameterAttributes = Mono.Cecil.ParameterAttributes; using ParameterAttributes = Mono.Cecil.ParameterAttributes;
using ILPPInterface = Unity.CompilationPipeline.Common.ILPostProcessing.ILPostProcessor;
namespace Unity.Netcode.Editor.CodeGen namespace Unity.Netcode.Editor.CodeGen
{ {
@@ -837,6 +837,58 @@ namespace Unity.Netcode.Editor.CodeGen
GetAllFieldsAndResolveGenerics(resolved, ref fieldTypes, genericParams); GetAllFieldsAndResolveGenerics(resolved, ref fieldTypes, genericParams);
} }
private void GetAllBaseTypesAndResolveGenerics(TypeDefinition type, ref List<TypeReference> baseTypes, Dictionary<string, TypeReference> genericParameters)
{
if (type == null || type.BaseType == null || type.BaseType.Name == "Object")
{
return;
}
var baseType = type.BaseType;
var genericParams = new Dictionary<string, TypeReference>();
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) private void ProcessNetworkBehaviour(TypeDefinition typeDefinition, string[] assemblyDefines)
{ {
var rpcHandlers = new List<(uint RpcMethodId, MethodDefinition RpcHandler)>(); var rpcHandlers = new List<(uint RpcMethodId, MethodDefinition RpcHandler)>();
@@ -898,6 +950,34 @@ namespace Unity.Netcode.Editor.CodeGen
} }
} }
} }
{
var baseTypes = new List<TypeReference>();
var genericParams = new Dictionary<string, TypeReference>();
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);
}
}
}
}
} }
} }

View File

@@ -1,9 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection; using System.Reflection;
using UnityEngine;
using UnityEditor;
using Unity.Netcode.Editor.Configuration; using Unity.Netcode.Editor.Configuration;
using UnityEditor;
using UnityEngine;
namespace Unity.Netcode.Editor namespace Unity.Netcode.Editor
{ {
@@ -135,23 +135,23 @@ namespace Unity.Netcode.Editor
} }
else if (type == typeof(uint)) 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)) 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)) 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)) 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)) 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)) else if (type == typeof(long))
{ {
@@ -161,6 +161,10 @@ namespace Unity.Netcode.Editor
{ {
val = (ulong)EditorGUILayout.LongField(variableName, (long)((ulong)val)); 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)) else if (type == typeof(bool))
{ {
val = EditorGUILayout.Toggle(variableName, (bool)val); val = EditorGUILayout.Toggle(variableName, (bool)val);

View File

@@ -1,9 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using Unity.Netcode.Editor.Configuration;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
using Unity.Netcode.Editor.Configuration;
namespace Unity.Netcode.Editor namespace Unity.Netcode.Editor
{ {
@@ -231,13 +231,7 @@ namespace Unity.Netcode.Editor
{ {
ReloadTransports(); ReloadTransports();
var transportComponent = m_NetworkManager.gameObject.GetComponent(m_TransportTypes[selection - 1]); var transportComponent = m_NetworkManager.gameObject.GetComponent(m_TransportTypes[selection - 1]) ?? m_NetworkManager.gameObject.AddComponent(m_TransportTypes[selection - 1]);
if (transportComponent == null)
{
transportComponent = m_NetworkManager.gameObject.AddComponent(m_TransportTypes[selection - 1]);
}
m_NetworkTransportProperty.objectReferenceValue = transportComponent; m_NetworkTransportProperty.objectReferenceValue = transportComponent;
Repaint(); Repaint();
@@ -355,15 +349,19 @@ namespace Unity.Netcode.Editor
if (s_CenteredWordWrappedLabelStyle == null) if (s_CenteredWordWrappedLabelStyle == null)
{ {
s_CenteredWordWrappedLabelStyle = new GUIStyle(GUI.skin.label); s_CenteredWordWrappedLabelStyle = new GUIStyle(GUI.skin.label)
s_CenteredWordWrappedLabelStyle.wordWrap = true; {
s_CenteredWordWrappedLabelStyle.alignment = TextAnchor.MiddleLeft; wordWrap = true,
alignment = TextAnchor.MiddleLeft
};
} }
if (s_HelpBoxStyle == null) if (s_HelpBoxStyle == null)
{ {
s_HelpBoxStyle = new GUIStyle(EditorStyles.helpBox); s_HelpBoxStyle = new GUIStyle(EditorStyles.helpBox)
s_HelpBoxStyle.padding = new RectOffset(10, 10, 10, 10); {
padding = new RectOffset(10, 10, 10, 10)
};
} }
var openDocsButtonStyle = GUI.skin.button; var openDocsButtonStyle = GUI.skin.button;

View File

@@ -1,9 +1,9 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Unity.Netcode.Editor.Configuration; using Unity.Netcode.Editor.Configuration;
using UnityEditor;
using UnityEngine; using UnityEngine;
using UnityEngine.SceneManagement; using UnityEngine.SceneManagement;
using UnityEditor;
namespace Unity.Netcode.Editor namespace Unity.Netcode.Editor
{ {

View File

@@ -1,6 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using UnityEngine;
using UnityEditor; using UnityEditor;
using UnityEngine;
namespace Unity.Netcode.Editor namespace Unity.Netcode.Editor
{ {

View File

@@ -1,6 +1,6 @@
using Unity.Netcode.Components;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
using Unity.Netcode.Components;
namespace Unity.Netcode.Editor namespace Unity.Netcode.Editor
{ {
@@ -25,6 +25,11 @@ namespace Unity.Netcode.Editor
private SerializedProperty m_InLocalSpaceProperty; private SerializedProperty m_InLocalSpaceProperty;
private SerializedProperty m_InterpolateProperty; 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 int s_ToggleOffset = 45;
private static float s_MaxRowWidth = EditorGUIUtility.labelWidth + EditorGUIUtility.fieldWidth + 5; private static float s_MaxRowWidth = EditorGUIUtility.labelWidth + EditorGUIUtility.fieldWidth + 5;
private static GUIContent s_PositionLabel = EditorGUIUtility.TrTextContent("Position"); private static GUIContent s_PositionLabel = EditorGUIUtility.TrTextContent("Position");
@@ -48,6 +53,10 @@ namespace Unity.Netcode.Editor
m_ScaleThresholdProperty = serializedObject.FindProperty(nameof(NetworkTransform.ScaleThreshold)); m_ScaleThresholdProperty = serializedObject.FindProperty(nameof(NetworkTransform.ScaleThreshold));
m_InLocalSpaceProperty = serializedObject.FindProperty(nameof(NetworkTransform.InLocalSpace)); m_InLocalSpaceProperty = serializedObject.FindProperty(nameof(NetworkTransform.InLocalSpace));
m_InterpolateProperty = serializedObject.FindProperty(nameof(NetworkTransform.Interpolate)); 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));
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -71,6 +80,8 @@ namespace Unity.Netcode.Editor
GUILayout.EndHorizontal(); GUILayout.EndHorizontal();
} }
if (!m_UseQuaternionSynchronization.boolValue)
{ {
GUILayout.BeginHorizontal(); GUILayout.BeginHorizontal();
@@ -88,6 +99,13 @@ namespace Unity.Netcode.Editor
GUILayout.EndHorizontal(); GUILayout.EndHorizontal();
} }
else
{
m_SyncRotationXProperty.boolValue = true;
m_SyncRotationYProperty.boolValue = true;
m_SyncRotationZProperty.boolValue = true;
}
{ {
GUILayout.BeginHorizontal(); GUILayout.BeginHorizontal();
@@ -116,6 +134,17 @@ namespace Unity.Netcode.Editor
EditorGUILayout.LabelField("Configurations", EditorStyles.boldLabel); EditorGUILayout.LabelField("Configurations", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(m_InLocalSpaceProperty); EditorGUILayout.PropertyField(m_InLocalSpaceProperty);
EditorGUILayout.PropertyField(m_InterpolateProperty); 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 COM_UNITY_MODULES_PHYSICS
// if rigidbody is present but network rigidbody is not present // if rigidbody is present but network rigidbody is not present

View File

@@ -1,5 +1,5 @@
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Unity.Netcode.Components")]
#if UNITY_EDITOR #if UNITY_EDITOR
[assembly: InternalsVisibleTo("Unity.Netcode.Editor")] [assembly: InternalsVisibleTo("Unity.Netcode.Editor")]
[assembly: InternalsVisibleTo("Unity.Netcode.Editor.CodeGen")] [assembly: InternalsVisibleTo("Unity.Netcode.Editor.CodeGen")]

View File

@@ -1,8 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using UnityEngine;
using System.Linq; using System.Linq;
using Unity.Collections; using Unity.Collections;
using UnityEngine;
using UnityEngine.Serialization; using UnityEngine.Serialization;
namespace Unity.Netcode namespace Unity.Netcode
@@ -208,6 +208,14 @@ namespace Unity.Netcode
private ulong? m_ConfigHash = null; private ulong? m_ConfigHash = null;
/// <summary>
/// Clears out the configuration hash value generated for a specific network session
/// </summary>
internal void ClearConfigHash()
{
m_ConfigHash = null;
}
/// <summary> /// <summary>
/// Gets a SHA256 hash of parts of the NetworkConfig instance /// Gets a SHA256 hash of parts of the NetworkConfig instance
/// </summary> /// </summary>
@@ -273,8 +281,6 @@ namespace Unity.Netcode
Prefabs.Initialize(); Prefabs.Initialize();
} }
#region Legacy Network Prefab List
[NonSerialized] [NonSerialized]
private bool m_DidWarnOldPrefabList = false; private bool m_DidWarnOldPrefabList = false;
@@ -334,7 +340,5 @@ namespace Unity.Netcode
[FormerlySerializedAs("NetworkPrefabs")] [FormerlySerializedAs("NetworkPrefabs")]
[SerializeField] [SerializeField]
internal List<NetworkPrefab> OldPrefabList; internal List<NetworkPrefab> OldPrefabList;
#endregion
} }
} }

View File

@@ -52,12 +52,23 @@ namespace Unity.Netcode
} }
~NetworkPrefabs() ~NetworkPrefabs()
{
Shutdown();
}
/// <summary>
/// Deregister from add and remove events
/// Clear the list
/// </summary>
internal void Shutdown()
{ {
foreach (var list in NetworkPrefabsLists) foreach (var list in NetworkPrefabsLists)
{ {
list.OnAdd -= AddTriggeredByNetworkPrefabList; list.OnAdd -= AddTriggeredByNetworkPrefabList;
list.OnRemove -= RemoveTriggeredByNetworkPrefabList; list.OnRemove -= RemoveTriggeredByNetworkPrefabList;
} }
NetworkPrefabsLists.Clear();
} }
/// <summary> /// <summary>

View File

@@ -52,6 +52,8 @@ namespace Unity.Netcode
public static void SetDefaults() public static void SetDefaults()
{ {
SetDefault<IDeferredMessageManager>(networkManager => new DeferredMessageManager(networkManager)); SetDefault<IDeferredMessageManager>(networkManager => new DeferredMessageManager(networkManager));
SetDefault<IRealTimeProvider>(networkManager => new RealTimeProvider());
} }
private static void SetDefault<T>(CreateObjectDelegate creator) private static void SetDefault<T>(CreateObjectDelegate creator)

View File

@@ -1,9 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using UnityEngine;
using System.Reflection; using System.Reflection;
using Unity.Collections; using Unity.Collections;
using UnityEngine;
namespace Unity.Netcode namespace Unity.Netcode
{ {
@@ -82,7 +82,7 @@ namespace Unity.Netcode
var context = new NetworkContext var context = new NetworkContext
{ {
SenderId = NetworkManager.ServerClientId, SenderId = NetworkManager.ServerClientId,
Timestamp = Time.realtimeSinceStartup, Timestamp = NetworkManager.RealTimeProvider.RealTimeSinceStartup,
SystemOwner = NetworkManager, SystemOwner = NetworkManager,
// header information isn't valid since it's not a real message. // header information isn't valid since it's not a real message.
// RpcMessage doesn't access this stuff so it's just left empty. // RpcMessage doesn't access this stuff so it's just left empty.
@@ -219,7 +219,7 @@ namespace Unity.Netcode
var context = new NetworkContext var context = new NetworkContext
{ {
SenderId = NetworkManager.ServerClientId, SenderId = NetworkManager.ServerClientId,
Timestamp = Time.realtimeSinceStartup, Timestamp = NetworkManager.RealTimeProvider.RealTimeSinceStartup,
SystemOwner = NetworkManager, SystemOwner = NetworkManager,
// header information isn't valid since it's not a real message. // header information isn't valid since it's not a real message.
// RpcMessage doesn't access this stuff so it's just left empty. // RpcMessage doesn't access this stuff so it's just left empty.
@@ -570,12 +570,9 @@ namespace Unity.Netcode
if (list == null) if (list == null)
{ {
list = new List<FieldInfo>(); list = new List<FieldInfo>();
}
list.AddRange(type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); list.AddRange(type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
}
else
{
list.AddRange(type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
}
if (type.BaseType != null && type.BaseType != typeof(NetworkBehaviour)) if (type.BaseType != null && type.BaseType != typeof(NetworkBehaviour))
{ {
@@ -600,13 +597,7 @@ namespace Unity.Netcode
var fieldType = sortedFields[i].FieldType; var fieldType = sortedFields[i].FieldType;
if (fieldType.IsSubclassOf(typeof(NetworkVariableBase))) if (fieldType.IsSubclassOf(typeof(NetworkVariableBase)))
{ {
var instance = (NetworkVariableBase)sortedFields[i].GetValue(this); 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.");
if (instance == null)
{
throw new Exception($"{GetType().FullName}.{sortedFields[i].Name} cannot be null. All {nameof(NetworkVariableBase)} instances must be initialized.");
}
instance.Initialize(this); instance.Initialize(this);
var instanceNameProperty = fieldType.GetProperty(nameof(NetworkVariableBase.Name)); var instanceNameProperty = fieldType.GetProperty(nameof(NetworkVariableBase.Name));
@@ -899,11 +890,23 @@ namespace Unity.Netcode
/// Either BufferSerializerReader or BufferSerializerWriter, depending whether the serializer /// Either BufferSerializerReader or BufferSerializerWriter, depending whether the serializer
/// is in read mode or write mode. /// is in read mode or write mode.
/// </typeparam> /// </typeparam>
/// <param name="targetClientId">the relative client identifier being synchronized</param>
protected virtual void OnSynchronize<T>(ref BufferSerializer<T> serializer) where T : IReaderWriter protected virtual void OnSynchronize<T>(ref BufferSerializer<T> serializer) where T : IReaderWriter
{ {
} }
/// <summary>
/// The relative client identifier targeted for the serialization of this <see cref="NetworkBehaviour"/> instance.
/// </summary>
/// <remarks>
/// This value will be set prior to <see cref="OnSynchronize{T}(ref BufferSerializer{T})"/> 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 <see cref="NetworkManager.LocalClientId"/>.
/// When synchronization of this instance is complete, this value will be reset to 0
/// </remarks>
protected ulong m_TargetIdBeingSynchronized { get; private set; }
/// <summary> /// <summary>
/// Internal method that determines if a NetworkBehaviour has additional synchronization data to /// Internal method that determines if a NetworkBehaviour has additional synchronization data to
/// be synchronized when first instantiated prior to its associated NetworkObject being spawned. /// be synchronized when first instantiated prior to its associated NetworkObject being spawned.
@@ -913,8 +916,9 @@ namespace Unity.Netcode
/// synchronize any remaining NetworkBehaviours. /// synchronize any remaining NetworkBehaviours.
/// </remarks> /// </remarks>
/// <returns>true if it wrote synchronization data and false if it did not</returns> /// <returns>true if it wrote synchronization data and false if it did not</returns>
internal bool Synchronize<T>(ref BufferSerializer<T> serializer) where T : IReaderWriter internal bool Synchronize<T>(ref BufferSerializer<T> serializer, ulong targetClientId = 0) where T : IReaderWriter
{ {
m_TargetIdBeingSynchronized = targetClientId;
if (serializer.IsWriter) if (serializer.IsWriter)
{ {
// Get the writer to handle seeking and determining how many bytes were written // Get the writer to handle seeking and determining how many bytes were written
@@ -949,6 +953,8 @@ namespace Unity.Netcode
} }
var finalPosition = writer.Position; var finalPosition = writer.Position;
// Reset before exiting
m_TargetIdBeingSynchronized = default;
// If we wrote nothing then skip writing anything for this NetworkBehaviour // If we wrote nothing then skip writing anything for this NetworkBehaviour
if (finalPosition == positionBeforeSynchronize || threwException) if (finalPosition == positionBeforeSynchronize || threwException)
{ {
@@ -1002,6 +1008,9 @@ namespace Unity.Netcode
synchronizationError = true; synchronizationError = true;
} }
// Reset before exiting
m_TargetIdBeingSynchronized = default;
// Skip over the entry if deserialization fails // Skip over the entry if deserialization fails
if (synchronizationError) if (synchronizationError)
{ {

View File

@@ -298,6 +298,8 @@ namespace Unity.Netcode
internal IDeferredMessageManager DeferredMessageManager { get; private set; } internal IDeferredMessageManager DeferredMessageManager { get; private set; }
internal IRealTimeProvider RealTimeProvider { get; private set; }
/// <summary> /// <summary>
/// Gets the CustomMessagingManager for this NetworkManager /// Gets the CustomMessagingManager for this NetworkManager
/// </summary> /// </summary>
@@ -449,10 +451,28 @@ namespace Unity.Netcode
public event Action<ulong> OnClientDisconnectCallback = null; public event Action<ulong> OnClientDisconnectCallback = null;
/// <summary> /// <summary>
/// The callback to invoke once the server is ready /// This callback is invoked when the local server is started and listening for incoming connections.
/// </summary> /// </summary>
public event Action OnServerStarted = null; public event Action OnServerStarted = null;
/// <summary>
/// The callback to invoke once the local client is ready
/// </summary>
public event Action OnClientStarted = null;
/// <summary>
/// This callback is invoked once the local server is stopped.
/// </summary>
/// <param name="arg1">The first parameter of this event will be set to <see cref="true"/> when stopping a host instance and <see cref="false"/> when stopping a server instance.</param>
public event Action<bool> OnServerStopped = null;
/// <summary>
/// The callback to invoke once the local client stops
/// </summary>
/// <remarks>The parameter states whether the client was running in host mode</remarks>
/// <param name="arg1">The first parameter of this event will be set to <see cref="true"/> when stopping the host client and <see cref="false"/> when stopping a standard client instance.</param>
public event Action<bool> OnClientStopped = null;
/// <summary> /// <summary>
/// The callback to invoke if the <see cref="NetworkTransport"/> fails. /// The callback to invoke if the <see cref="NetworkTransport"/> fails.
/// </summary> /// </summary>
@@ -735,6 +755,8 @@ namespace Unity.Netcode
DeferredMessageManager = ComponentFactory.Create<IDeferredMessageManager>(this); DeferredMessageManager = ComponentFactory.Create<IDeferredMessageManager>(this);
RealTimeProvider = ComponentFactory.Create<IRealTimeProvider>(this);
CustomMessagingManager = new CustomMessagingManager(this); CustomMessagingManager = new CustomMessagingManager(this);
SceneManager = new NetworkSceneManager(this); SceneManager = new NetworkSceneManager(this);
@@ -908,6 +930,7 @@ namespace Unity.Netcode
IsClient = true; IsClient = true;
IsListening = true; IsListening = true;
OnClientStarted?.Invoke();
return true; return true;
} }
@@ -989,13 +1012,14 @@ namespace Unity.Netcode
SpawnManager.ServerSpawnSceneObjectsOnStartSweep(); SpawnManager.ServerSpawnSceneObjectsOnStartSweep();
OnServerStarted?.Invoke();
OnClientStarted?.Invoke();
// This assures that any in-scene placed NetworkObject is spawned and // This assures that any in-scene placed NetworkObject is spawned and
// any associated NetworkBehaviours' netcode related properties are // any associated NetworkBehaviours' netcode related properties are
// set prior to invoking OnClientConnected. // set prior to invoking OnClientConnected.
InvokeOnClientConnectedCallback(LocalClientId); InvokeOnClientConnectedCallback(LocalClientId);
OnServerStarted?.Invoke();
return true; return true;
} }
@@ -1108,13 +1132,13 @@ namespace Unity.Netcode
return isParented; 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"; return $"{transform.name} is nested under {transform.root.name}. NetworkManager cannot be nested.\n";
} }
#if UNITY_EDITOR #if UNITY_EDITOR
static internal INetworkManagerHelper NetworkManagerHelper; internal static INetworkManagerHelper NetworkManagerHelper;
/// <summary> /// <summary>
/// Interface for NetworkManagerHelper /// Interface for NetworkManagerHelper
/// </summary> /// </summary>
@@ -1195,13 +1219,12 @@ namespace Unity.Netcode
NetworkLog.LogInfo(nameof(ShutdownInternal)); NetworkLog.LogInfo(nameof(ShutdownInternal));
} }
if (IsServer) bool wasServer = IsServer;
bool wasClient = IsClient;
if (wasServer)
{ {
// make sure all messages are flushed before transport disconnect clients // make sure all messages are flushed before transport disconnect clients
if (MessagingSystem != null) MessagingSystem?.ProcessSendQueues();
{
MessagingSystem.ProcessSendQueues();
}
var disconnectedIds = new HashSet<ulong>(); var disconnectedIds = new HashSet<ulong>();
@@ -1237,11 +1260,23 @@ namespace Unity.Netcode
} }
} }
// Unregister network updates before trying to disconnect the client
this.UnregisterAllNetworkUpdates();
if (IsClient && IsListening) if (IsClient && IsListening)
{ {
// Client only, send disconnect to server // Client only, send disconnect to server
// If transport throws and exception, log the exception and
// continue the shutdown sequence (or forever be shutting down)
try
{
NetworkConfig.NetworkTransport.DisconnectLocalClient(); NetworkConfig.NetworkTransport.DisconnectLocalClient();
} }
catch (Exception ex)
{
Debug.LogException(ex);
}
}
IsConnectedClient = false; IsConnectedClient = false;
IsApproved = false; IsApproved = false;
@@ -1261,8 +1296,6 @@ namespace Unity.Netcode
IsServer = false; IsServer = false;
IsClient = false; IsClient = false;
this.UnregisterAllNetworkUpdates();
if (NetworkTickSystem != null) if (NetworkTickSystem != null)
{ {
NetworkTickSystem.Tick -= OnNetworkManagerTick; NetworkTickSystem.Tick -= OnNetworkManagerTick;
@@ -1280,10 +1313,7 @@ namespace Unity.Netcode
NetworkConfig.NetworkTransport.OnTransportEvent -= HandleRawTransportPoll; NetworkConfig.NetworkTransport.OnTransportEvent -= HandleRawTransportPoll;
} }
if (DeferredMessageManager != null) DeferredMessageManager?.CleanupAllTriggers();
{
DeferredMessageManager.CleanupAllTriggers();
}
if (SceneManager != null) if (SceneManager != null)
{ {
@@ -1318,6 +1348,22 @@ namespace Unity.Netcode
m_StopProcessingMessages = false; m_StopProcessingMessages = false;
ClearClients(); 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();
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -1417,7 +1463,7 @@ namespace Unity.Netcode
} }
// Only update RTT here, server time is updated by time sync messages // 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) if (reset)
{ {
NetworkTickSystem.Reset(NetworkTimeSystem.LocalTime, NetworkTimeSystem.ServerTime); NetworkTickSystem.Reset(NetworkTimeSystem.LocalTime, NetworkTimeSystem.ServerTime);
@@ -1426,7 +1472,7 @@ namespace Unity.Netcode
if (IsServer == false) 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) if (!m_ShuttingDown || !m_StopProcessingMessages)
{ {
// This should be invoked just prior to the MessagingSystem
// processes its outbound queue.
SceneManager.CheckForAndSendNetworkObjectSceneChanged();
MessagingSystem.ProcessSendQueues(); MessagingSystem.ProcessSendQueues();
NetworkMetrics.UpdateNetworkObjectsCount(SpawnManager.SpawnedObjects.Count); NetworkMetrics.UpdateNetworkObjectsCount(SpawnManager.SpawnedObjects.Count);
NetworkMetrics.UpdateConnectionsCount((IsServer) ? ConnectedClients.Count : 1); NetworkMetrics.UpdateConnectionsCount((IsServer) ? ConnectedClients.Count : 1);
@@ -1486,10 +1536,9 @@ namespace Unity.Netcode
// we should always force the rebuilding of the NetworkConfig hash value // we should always force the rebuilding of the NetworkConfig hash value
ConfigHash = NetworkConfig.GetConfig(false), ConfigHash = NetworkConfig.GetConfig(false),
ShouldSendConnectionData = NetworkConfig.ConnectionApproval, ShouldSendConnectionData = NetworkConfig.ConnectionApproval,
ConnectionData = NetworkConfig.ConnectionData ConnectionData = NetworkConfig.ConnectionData,
MessageVersions = new NativeArray<MessageVersionData>(MessagingSystem.MessageHandlers.Length, Allocator.Temp)
}; };
message.MessageVersions = new NativeArray<MessageVersionData>(MessagingSystem.MessageHandlers.Length, Allocator.Temp);
for (int index = 0; index < MessagingSystem.MessageHandlers.Length; index++) for (int index = 0; index < MessagingSystem.MessageHandlers.Length; index++)
{ {
if (MessagingSystem.MessageTypes[index] != null) if (MessagingSystem.MessageTypes[index] != null)
@@ -1509,7 +1558,7 @@ namespace Unity.Netcode
private IEnumerator ApprovalTimeout(ulong clientId) private IEnumerator ApprovalTimeout(ulong clientId)
{ {
var timeStarted = IsServer ? LocalTime.TimeAsFloat : Time.realtimeSinceStartup; var timeStarted = IsServer ? LocalTime.TimeAsFloat : RealTimeProvider.RealTimeSinceStartup;
var timedOut = false; var timedOut = false;
var connectionApproved = false; var connectionApproved = false;
var connectionNotApproved = false; var connectionNotApproved = false;
@@ -1519,7 +1568,7 @@ namespace Unity.Netcode
{ {
yield return null; yield return null;
// Check if we timed out // Check if we timed out
timedOut = timeoutMarker < (IsServer ? LocalTime.TimeAsFloat : Time.realtimeSinceStartup); timedOut = timeoutMarker < (IsServer ? LocalTime.TimeAsFloat : RealTimeProvider.RealTimeSinceStartup);
if (IsServer) if (IsServer)
{ {
@@ -1861,8 +1910,10 @@ namespace Unity.Netcode
if (!string.IsNullOrEmpty(reason)) if (!string.IsNullOrEmpty(reason))
{ {
var disconnectReason = new DisconnectReasonMessage(); var disconnectReason = new DisconnectReasonMessage
disconnectReason.Reason = reason; {
Reason = reason
};
SendMessage(ref disconnectReason, NetworkDelivery.Reliable, clientId); SendMessage(ref disconnectReason, NetworkDelivery.Reliable, clientId);
} }
MessagingSystem.ProcessSendQueues(); MessagingSystem.ProcessSendQueues();
@@ -2011,15 +2062,19 @@ namespace Unity.Netcode
if (response.CreatePlayerObject) if (response.CreatePlayerObject)
{ {
var playerPrefabHash = response.PlayerPrefabHash ?? NetworkConfig.PlayerPrefab.GetComponent<NetworkObject>().GlobalObjectIdHash; var prefabNetworkObject = NetworkConfig.PlayerPrefab.GetComponent<NetworkObject>();
var playerPrefabHash = response.PlayerPrefabHash ?? prefabNetworkObject.GlobalObjectIdHash;
// Generate a SceneObject for the player object to spawn // 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 var sceneObject = new NetworkObject.SceneObject
{ {
OwnerClientId = ownerClientId, OwnerClientId = ownerClientId,
IsPlayerObject = true, IsPlayerObject = true,
IsSceneObject = false, IsSceneObject = false,
HasTransform = true, HasTransform = prefabNetworkObject.SynchronizeTransform,
Hash = playerPrefabHash, Hash = playerPrefabHash,
TargetClientId = ownerClientId, TargetClientId = ownerClientId,
Transform = new NetworkObject.SceneObject.TransformData Transform = new NetworkObject.SceneObject.TransformData
@@ -2105,8 +2160,10 @@ namespace Unity.Netcode
{ {
if (!string.IsNullOrEmpty(response.Reason)) if (!string.IsNullOrEmpty(response.Reason))
{ {
var disconnectReason = new DisconnectReasonMessage(); var disconnectReason = new DisconnectReasonMessage
disconnectReason.Reason = response.Reason; {
Reason = response.Reason
};
SendMessage(ref disconnectReason, NetworkDelivery.Reliable, ownerClientId); SendMessage(ref disconnectReason, NetworkDelivery.Reliable, ownerClientId);
MessagingSystem.ProcessSendQueues(); MessagingSystem.ProcessSendQueues();

View File

@@ -17,6 +17,28 @@ namespace Unity.Netcode
[SerializeField] [SerializeField]
internal uint GlobalObjectIdHash; internal uint GlobalObjectIdHash;
/// <summary>
/// Gets the Prefab Hash Id of this object if the object is registerd as a prefab otherwise it returns 0
/// </summary>
[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 #if UNITY_EDITOR
private void OnValidate() private void OnValidate()
{ {
@@ -75,6 +97,18 @@ namespace Unity.Netcode
/// </summary> /// </summary>
public bool IsPlayerObject { get; internal set; } public bool IsPlayerObject { get; internal set; }
/// <summary>
/// Determines if the associated NetworkObject's transform will get
/// synchronized when spawned.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public bool SynchronizeTransform = true;
/// <summary> /// <summary>
/// Gets if the object is the personal clients player object /// Gets if the object is the personal clients player object
/// </summary> /// </summary>
@@ -105,6 +139,55 @@ namespace Unity.Netcode
/// </summary> /// </summary>
public bool DestroyWithScene { get; set; } public bool DestroyWithScene { get; set; }
/// <summary>
/// When set to true and the active scene is changed, this will automatically migrate the <see cref="NetworkObject"/>
/// into the new active scene on both the server and client instances.
/// </summary>
/// <remarks>
/// - This only applies to dynamically spawned <see cref="NetworkObject"/>s.
/// - This only works when using integrated scene management (<see cref="NetworkSceneManager"/>).
///
/// If there are more than one scenes loaded and the currently active scene is unloaded, then typically
/// the <see cref="SceneManager"/> will automatically assign a new active scene. Similar to <see cref="DestroyWithScene"/>
/// being set to <see cref="false"/>, this prevents any <see cref="NetworkObject"/> 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 <see cref="ActiveSceneSynchronization"/> set to true will *not* synchronize clients when
/// changing a <see cref="NetworkObject"/>'s scene via <see cref="SceneManager.MoveGameObjectToScene(GameObject, Scene)"/>.
/// To synchronize clients of a <see cref="NetworkObject"/>'s scene being changed via <see cref="SceneManager.MoveGameObjectToScene(GameObject, Scene)"/>,
/// make sure <see cref="SceneMigrationSynchronization"/> is enabled (it is by default).
/// </remarks>
public bool ActiveSceneSynchronization;
/// <summary>
/// When enabled (the default), if a <see cref="NetworkObject"/> is migrated to a different scene (active or not)
/// via <see cref="SceneManager.MoveGameObjectToScene(GameObject, Scene)"/> on the server side all client
/// instances will be synchronized and the <see cref="NetworkObject"/> migrated into the newly assigned scene.
/// The updated scene migration will get synchronized with late joining clients as well.
/// </summary>
/// <remarks>
/// - This only applies to dynamically spawned <see cref="NetworkObject"/>s.
/// - This only works when using integrated scene management (<see cref="NetworkSceneManager"/>).
/// Note:
/// You can have both <see cref="ActiveSceneSynchronization"/> and <see cref="SceneMigrationSynchronization"/> enabled.
/// The primary difference between the two is that <see cref="SceneMigrationSynchronization"/> only synchronizes clients
/// when the server migrates a <see cref="NetworkObject"/> to a new scene. If the scene is unloaded and <see cref="DestroyWithScene"/>
/// is <see cref="true"/> and <see cref="ActiveSceneSynchronization"/> is <see cref="false"/> and the scene is not the currently
/// active scene, then the <see cref="NetworkObject"/> will be destroyed.
/// </remarks>
public bool SceneMigrationSynchronization = true;
/// <summary>
/// Notifies when the NetworkObject is migrated into a new scene
/// </summary>
/// <remarks>
/// - <see cref="ActiveSceneSynchronization"/> or <see cref="SceneMigrationSynchronization"/> (or both) need to be enabled
/// - This only applies to dynamically spawned <see cref="NetworkObject"/>s.
/// - This only works when using integrated scene management (<see cref="NetworkSceneManager"/>).
/// </remarks>
public Action OnMigratedToNewScene;
/// <summary> /// <summary>
/// Delegate type for checking visibility /// Delegate type for checking visibility
/// </summary> /// </summary>
@@ -188,6 +271,11 @@ namespace Unity.Netcode
/// </summary> /// </summary>
internal int SceneOriginHandle = 0; internal int SceneOriginHandle = 0;
/// <summary>
/// The server-side scene origin handle
/// </summary>
internal int NetworkSceneHandle = 0;
private Scene m_SceneOrigin; private Scene m_SceneOrigin;
/// <summary> /// <summary>
/// The scene where the NetworkObject was first instantiated /// The scene where the NetworkObject was first instantiated
@@ -265,6 +353,15 @@ namespace Unity.Netcode
throw new VisibilityChangeException("The object is already visible"); 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); NetworkManager.MarkObjectForShowingTo(this, clientId);
Observers.Add(clientId); Observers.Add(clientId);
} }
@@ -578,6 +675,22 @@ namespace Unity.Netcode
private Transform m_CachedParent; // What is our last set parent Transform reference? 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 private bool m_CachedWorldPositionStays = true; // Used to preserve the world position stays parameter passed in TrySetParent
/// <summary>
/// Returns the last known cached WorldPositionStays value for this NetworkObject
/// </summary>
/// <remarks>
/// When parenting NetworkObjects, the optional WorldPositionStays value is cached and synchronized with clients.
/// This method provides access to the instance relative cached value.
/// <see cref="TrySetParent(GameObject, bool)"/>
/// <see cref="TrySetParent(NetworkObject, bool)"/>
/// <see cref="TrySetParent(Transform, bool)"/>
/// </remarks>
/// <returns><see cref="true"/> or <see cref="false"/></returns>
public bool WorldPositionStays()
{
return m_CachedWorldPositionStays;
}
internal void SetCachedParent(Transform parentTransform) internal void SetCachedParent(Transform parentTransform)
{ {
m_CachedParent = parentTransform; m_CachedParent = parentTransform;
@@ -1118,6 +1231,18 @@ namespace Unity.Netcode
set => ByteUtility.SetBit(ref m_BitField, 5, value); set => ByteUtility.SetBit(ref m_BitField, 5, value);
} }
/// <summary>
/// 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)
/// </summary>
public bool DestroyWithScene
{
get => ByteUtility.GetBit(m_BitField, 6);
set => ByteUtility.SetBit(ref m_BitField, 6, value);
}
//If(Metadata.HasParent) //If(Metadata.HasParent)
public ulong ParentObjectId; public ulong ParentObjectId;
@@ -1160,7 +1285,7 @@ namespace Unity.Netcode
var writeSize = 0; var writeSize = 0;
writeSize += HasTransform ? FastBufferWriter.GetWriteSize<TransformData>() : 0; writeSize += HasTransform ? FastBufferWriter.GetWriteSize<TransformData>() : 0;
writeSize += IsSceneObject ? FastBufferWriter.GetWriteSize<int>() : 0; writeSize += FastBufferWriter.GetWriteSize<int>();
if (!writer.TryBeginWrite(writeSize)) if (!writer.TryBeginWrite(writeSize))
{ {
@@ -1172,14 +1297,9 @@ namespace Unity.Netcode
writer.WriteValue(Transform); writer.WriteValue(Transform);
} }
// In-Scene NetworkObjects are uniquely identified NetworkPrefabs defined by their // The NetworkSceneHandle is the server-side relative
// NetworkSceneHandle and GlobalObjectIdHash. Client-side NetworkSceneManagers use // scene handle that the NetworkObject resides in.
// 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()); writer.WriteValue(OwnerObject.GetSceneOriginHandle());
}
// Synchronize NetworkVariables and NetworkBehaviours // Synchronize NetworkVariables and NetworkBehaviours
var bufferSerializer = new BufferSerializer<BufferSerializerWriter>(new BufferSerializerWriter(writer)); var bufferSerializer = new BufferSerializer<BufferSerializerWriter>(new BufferSerializerWriter(writer));
@@ -1205,7 +1325,7 @@ namespace Unity.Netcode
var readSize = 0; var readSize = 0;
readSize += HasTransform ? FastBufferWriter.GetWriteSize<TransformData>() : 0; readSize += HasTransform ? FastBufferWriter.GetWriteSize<TransformData>() : 0;
readSize += IsSceneObject ? FastBufferWriter.GetWriteSize<int>() : 0; readSize += FastBufferWriter.GetWriteSize<int>();
// Try to begin reading the remaining bytes // Try to begin reading the remaining bytes
if (!reader.TryBeginRead(readSize)) if (!reader.TryBeginRead(readSize))
@@ -1218,16 +1338,11 @@ namespace Unity.Netcode
reader.ReadValue(out Transform); reader.ReadValue(out Transform);
} }
// In-Scene NetworkObjects are uniquely identified NetworkPrefabs defined by their // The NetworkSceneHandle is the server-side relative
// NetworkSceneHandle and GlobalObjectIdHash. Client-side NetworkSceneManagers use // scene handle that the NetworkObject resides in.
// 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); reader.ReadValue(out NetworkSceneHandle);
} }
} }
}
internal void PostNetworkVariableWrite() internal void PostNetworkVariableWrite()
{ {
@@ -1265,7 +1380,7 @@ namespace Unity.Netcode
var synchronizationCount = (byte)0; var synchronizationCount = (byte)0;
foreach (var childBehaviour in ChildNetworkBehaviours) foreach (var childBehaviour in ChildNetworkBehaviours)
{ {
if (childBehaviour.Synchronize(ref serializer)) if (childBehaviour.Synchronize(ref serializer, targetClientId))
{ {
synchronizationCount++; synchronizationCount++;
} }
@@ -1304,7 +1419,7 @@ namespace Unity.Netcode
{ {
serializer.SerializeValue(ref networkBehaviourId); serializer.SerializeValue(ref networkBehaviourId);
var networkBehaviour = GetNetworkBehaviourAtOrderIndex(networkBehaviourId); var networkBehaviour = GetNetworkBehaviourAtOrderIndex(networkBehaviourId);
networkBehaviour.Synchronize(ref serializer); networkBehaviour.Synchronize(ref serializer, targetClientId);
} }
} }
} }
@@ -1317,6 +1432,7 @@ namespace Unity.Netcode
OwnerClientId = OwnerClientId, OwnerClientId = OwnerClientId,
IsPlayerObject = IsPlayerObject, IsPlayerObject = IsPlayerObject,
IsSceneObject = IsSceneObject ?? true, IsSceneObject = IsSceneObject ?? true,
DestroyWithScene = DestroyWithScene,
Hash = HostCheckForGlobalObjectIdHashOverride(), Hash = HostCheckForGlobalObjectIdHashOverride(),
OwnerObject = this, OwnerObject = this,
TargetClientId = targetClientId TargetClientId = targetClientId
@@ -1352,7 +1468,7 @@ namespace Unity.Netcode
if (IncludeTransformWhenSpawning == null || IncludeTransformWhenSpawning(OwnerClientId)) 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 // We start with the default AutoObjectParentSync values to determine which transform space we will
// be synchronizing clients with. // be synchronizing clients with.
@@ -1435,11 +1551,126 @@ namespace Unity.Netcode
networkObject.SynchronizeNetworkBehaviours(ref bufferSerializer, networkManager.LocalClientId); networkObject.SynchronizeNetworkBehaviours(ref bufferSerializer, networkManager.LocalClientId);
// Spawn the NetworkObject // Spawn the NetworkObject
networkManager.SpawnManager.SpawnNetworkObjectLocally(networkObject, sceneObject, false); networkManager.SpawnManager.SpawnNetworkObjectLocally(networkObject, sceneObject, sceneObject.DestroyWithScene);
return networkObject; return networkObject;
} }
/// <summary>
/// Subscribes to changes in the currently active scene
/// </summary>
/// <remarks>
/// Only for dynamically spawned NetworkObjects
/// </remarks>
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;
}
}
}
/// <summary>
/// If AutoSynchActiveScene is enabled, then this is the callback that handles updating
/// a NetworkObject's scene information.
/// </summary>
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);
}
}
}
/// <summary>
/// Handles updating the NetworkObject's tracked scene handles
/// </summary>
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);
}
}
/// <summary>
/// Update
/// Detects if a NetworkObject's scene has changed for both server and client instances
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
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);
}
/// <summary> /// <summary>
/// Only applies to Host mode. /// Only applies to Host mode.
/// Will return the registered source NetworkPrefab's GlobalObjectIdHash if one exists. /// Will return the registered source NetworkPrefab's GlobalObjectIdHash if one exists.

View File

@@ -167,7 +167,7 @@ namespace Unity.Netcode
/// </summary> /// </summary>
public static NetworkUpdateStage UpdateStage; public static NetworkUpdateStage UpdateStage;
private static void RunNetworkUpdateStage(NetworkUpdateStage updateStage) internal static void RunNetworkUpdateStage(NetworkUpdateStage updateStage)
{ {
UpdateStage = updateStage; UpdateStage = updateStage;

View File

@@ -1,6 +1,6 @@
using System; using System;
using System.Text;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text;
namespace Unity.Netcode namespace Unity.Netcode
{ {

View File

@@ -151,14 +151,18 @@ namespace Unity.Netcode
// We dont know what size to use. Try every (more collision prone) // We dont know what size to use. Try every (more collision prone)
if (m_NamedMessageHandlers32.TryGetValue(hash, out HandleNamedMessageDelegate messageHandler32)) 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); 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)) 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); messageHandler64(sender, reader);
m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, m_MessageHandlerNameLookup64[hash], bytesCount); m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, messageName, bytesCount);
} }
} }
else else
@@ -169,15 +173,19 @@ namespace Unity.Netcode
case HashSize.VarIntFourBytes: case HashSize.VarIntFourBytes:
if (m_NamedMessageHandlers32.TryGetValue(hash, out HandleNamedMessageDelegate messageHandler32)) 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); messageHandler32(sender, reader);
m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, m_MessageHandlerNameLookup32[hash], bytesCount); m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, messageName, bytesCount);
} }
break; break;
case HashSize.VarIntEightBytes: case HashSize.VarIntEightBytes:
if (m_NamedMessageHandlers64.TryGetValue(hash, out HandleNamedMessageDelegate messageHandler64)) 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); messageHandler64(sender, reader);
m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, m_MessageHandlerNameLookup64[hash], bytesCount); m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, messageName, bytesCount);
} }
break; break;
} }

View File

@@ -1,6 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using Unity.Collections; using Unity.Collections;
using Time = UnityEngine.Time;
namespace Unity.Netcode namespace Unity.Netcode
{ {
@@ -49,7 +48,7 @@ namespace Unity.Netcode
{ {
triggerInfo = new TriggerInfo triggerInfo = new TriggerInfo
{ {
Expiry = Time.realtimeSinceStartup + m_NetworkManager.NetworkConfig.SpawnTimeout, Expiry = m_NetworkManager.RealTimeProvider.RealTimeSinceStartup + m_NetworkManager.NetworkConfig.SpawnTimeout,
TriggerData = new NativeList<TriggerData>(Allocator.Persistent) TriggerData = new NativeList<TriggerData>(Allocator.Persistent)
}; };
triggers[key] = triggerInfo; triggers[key] = triggerInfo;
@@ -77,7 +76,7 @@ namespace Unity.Netcode
int index = 0; int index = 0;
foreach (var kvp2 in kvp.Value) foreach (var kvp2 in kvp.Value)
{ {
if (kvp2.Value.Expiry < Time.realtimeSinceStartup) if (kvp2.Value.Expiry < m_NetworkManager.RealTimeProvider.RealTimeSinceStartup)
{ {
staleKeys[index++] = kvp2.Key; staleKeys[index++] = kvp2.Key;
PurgeTrigger(kvp.Key, kvp2.Key, kvp2.Value); PurgeTrigger(kvp.Key, kvp2.Key, kvp2.Value);

View File

@@ -8,11 +8,7 @@ namespace Unity.Netcode
public void Serialize(FastBufferWriter writer, int targetVersion) public void Serialize(FastBufferWriter writer, int targetVersion)
{ {
string reasonSent = Reason; string reasonSent = Reason ?? string.Empty;
if (reasonSent == null)
{
reasonSent = string.Empty;
}
// Since we don't send a ConnectionApprovedMessage, the version for this message is encded with the message // 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 // itself. However, note that we HAVE received a ConnectionRequestMessage, so we DO have a valid targetVersion

View File

@@ -1,6 +1,6 @@
using System; using System;
using UnityEngine;
using Unity.Collections; using Unity.Collections;
using UnityEngine;
namespace Unity.Netcode namespace Unity.Netcode
{ {

View File

@@ -364,11 +364,12 @@ namespace Unity.Netcode
} }
public void HandleMessage(in MessageHeader header, FastBufferReader reader, ulong senderId, float timestamp, int serializedHeaderSize) public void HandleMessage(in MessageHeader header, FastBufferReader reader, ulong senderId, float timestamp, int serializedHeaderSize)
{
using (reader)
{ {
if (header.MessageType >= m_HighMessageType) if (header.MessageType >= m_HighMessageType)
{ {
Debug.LogWarning($"Received a message with invalid message type value {header.MessageType}"); Debug.LogWarning($"Received a message with invalid message type value {header.MessageType}");
reader.Dispose();
return; return;
} }
var context = new NetworkContext var context = new NetworkContext
@@ -384,18 +385,15 @@ namespace Unity.Netcode
var type = m_ReverseTypeMap[header.MessageType]; var type = m_ReverseTypeMap[header.MessageType];
if (!CanReceive(senderId, type, reader, ref context)) if (!CanReceive(senderId, type, reader, ref context))
{ {
reader.Dispose();
return; return;
} }
var handler = m_MessageHandlers[header.MessageType];
for (var hookIdx = 0; hookIdx < m_Hooks.Count; ++hookIdx) for (var hookIdx = 0; hookIdx < m_Hooks.Count; ++hookIdx)
{ {
m_Hooks[hookIdx].OnBeforeReceiveMessage(senderId, type, reader.Length + FastBufferWriter.GetWriteSize<MessageHeader>()); m_Hooks[hookIdx].OnBeforeReceiveMessage(senderId, type, reader.Length + FastBufferWriter.GetWriteSize<MessageHeader>());
} }
var handler = m_MessageHandlers[header.MessageType];
using (reader)
{
// This will also log an exception is if the server knows about a message type the client doesn't know // 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 // 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 // two connecting builds know about different messages, the server should not send a message to a client
@@ -420,12 +418,12 @@ namespace Unity.Netcode
Debug.LogException(e); Debug.LogException(e);
} }
} }
}
for (var hookIdx = 0; hookIdx < m_Hooks.Count; ++hookIdx) for (var hookIdx = 0; hookIdx < m_Hooks.Count; ++hookIdx)
{ {
m_Hooks[hookIdx].OnAfterReceiveMessage(senderId, type, reader.Length + FastBufferWriter.GetWriteSize<MessageHeader>()); m_Hooks[hookIdx].OnAfterReceiveMessage(senderId, type, reader.Length + FastBufferWriter.GetWriteSize<MessageHeader>());
} }
} }
}
internal unsafe void ProcessIncomingMessageQueue() internal unsafe void ProcessIncomingMessageQueue()
{ {

View File

@@ -1,5 +1,5 @@
using UnityEngine;
using System; using System;
using UnityEngine;
namespace Unity.Netcode namespace Unity.Netcode
{ {

View File

@@ -0,0 +1,377 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace Unity.Netcode
{
/// <summary>
/// The default SceneManagerHandler that interfaces between the SceneManager and NetworkSceneManager
/// </summary>
internal class DefaultSceneManagerHandler : ISceneManagerHandler
{
private Scene m_InvalidScene = new Scene();
internal struct SceneEntry
{
public bool IsAssigned;
public Scene Scene;
}
internal Dictionary<string, Dictionary<int, SceneEntry>> SceneNameToSceneHandles = new Dictionary<string, Dictionary<int, SceneEntry>>();
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;
}
/// <summary>
/// Resets scene tracking
/// </summary>
public void ClearSceneTracking(NetworkManager networkManager)
{
SceneNameToSceneHandles.Clear();
}
/// <summary>
/// Stops tracking a specific scene
/// </summary>
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);
}
}
}
}
/// <summary>
/// Starts tracking a specific scene
/// </summary>
public void StartTrackingScene(Scene scene, bool assigned, NetworkManager networkManager)
{
if (!SceneNameToSceneHandles.ContainsKey(scene.name))
{
SceneNameToSceneHandles.Add(scene.name, new Dictionary<int, SceneEntry>());
}
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!");
}
}
/// <summary>
/// Determines if there is an existing scene loaded that matches the scene name but has not been assigned
/// </summary>
public bool DoesSceneHaveUnassignedEntry(string sceneName, NetworkManager networkManager)
{
var scenesWithSceneName = new List<Scene>();
// 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;
}
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// 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)
/// </summary>
public void PopulateLoadedScenes(ref Dictionary<int, Scene> 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<int, SceneEntry>());
}
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<Scene> m_ScenesToUnload = new List<Scene>();
/// <summary>
/// Unloads any scenes that have not been assigned.
/// </summary>
/// <param name="networkManager"></param>
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;
}
}
}
/// <summary>
/// Handles determining if a client should attempt to load a scene during synchronization.
/// </summary>
/// <param name="sceneName">name of the scene to be loaded</param>
/// <param name="isPrimaryScene">when in client synchronization mode single, this determines if the scene is the primary active scene</param>
/// <param name="clientSynchronizationMode">the current client synchronization mode</param>
/// <param name="networkManager"><see cref="NetworkManager"/> instance</param>
/// <returns></returns>
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;
}
/// <summary>
/// Handles migrating dynamically spawned NetworkObjects to the DDOL when a scene is unloaded
/// </summary>
/// <param name="networkManager"><see cref="NetworkManager"/>relative instance</param>
/// <param name="scene">scene being unloaded</param>
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<NetworkObject>(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);
}
}
}
/// <summary>
/// Sets the client synchronization mode which impacts whether both the server or client take into consideration scenes loaded before
/// starting the <see cref="NetworkManager"/>.
/// </summary>
/// <remarks>
/// <see cref="LoadSceneMode.Single"/>: Does not take preloaded scenes into consideration
/// <see cref="LoadSceneMode.Single"/>: Does take preloaded scenes into consideration
/// </remarks>
/// <param name="networkManager">relative <see cref="NetworkManager"/> instance</param>
/// <param name="mode"><see cref="LoadSceneMode.Single"/> or <see cref="LoadSceneMode.Additive"/></param>
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;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8c18076bb9734cf4ea7297f85b7729be
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using UnityEngine; using UnityEngine;
using UnityEngine.SceneManagement; using UnityEngine.SceneManagement;
@@ -12,5 +13,24 @@ namespace Unity.Netcode
AsyncOperation LoadSceneAsync(string sceneName, LoadSceneMode loadSceneMode, SceneEventProgress sceneEventProgress); AsyncOperation LoadSceneAsync(string sceneName, LoadSceneMode loadSceneMode, SceneEventProgress sceneEventProgress);
AsyncOperation UnloadSceneAsync(Scene scene, SceneEventProgress sceneEventProgress); AsyncOperation UnloadSceneAsync(Scene scene, SceneEventProgress sceneEventProgress);
void PopulateLoadedScenes(ref Dictionary<int, Scene> 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);
} }
} }

View File

@@ -1,5 +1,5 @@
using System.Collections.Generic;
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using UnityEngine; using UnityEngine;
using UnityEngine.SceneManagement; using UnityEngine.SceneManagement;
@@ -19,7 +19,7 @@ namespace Unity.Netcode
public class SceneEvent public class SceneEvent
{ {
/// <summary> /// <summary>
/// The <see cref="UnityEngine.AsyncOperation"/> returned by <see cref="SceneManager"/><BR/> /// The <see cref="UnityEngine.AsyncOperation"/> returned by <see cref="SceneManager"/><br />
/// This is set for the following <see cref="Netcode.SceneEventType"/>s: /// This is set for the following <see cref="Netcode.SceneEventType"/>s:
/// <list type="bullet"> /// <list type="bullet">
/// <item><term><see cref="SceneEventType.Load"/></term></item> /// <item><term><see cref="SceneEventType.Load"/></term></item>
@@ -34,7 +34,7 @@ namespace Unity.Netcode
public SceneEventType SceneEventType; public SceneEventType SceneEventType;
/// <summary> /// <summary>
/// If applicable, this reflects the type of scene loading or unloading that is occurring.<BR/> /// If applicable, this reflects the type of scene loading or unloading that is occurring.<br />
/// This is set for the following <see cref="Netcode.SceneEventType"/>s: /// This is set for the following <see cref="Netcode.SceneEventType"/>s:
/// <list type="bullet"> /// <list type="bullet">
/// <item><term><see cref="SceneEventType.Load"/></term></item> /// <item><term><see cref="SceneEventType.Load"/></term></item>
@@ -48,7 +48,7 @@ namespace Unity.Netcode
public LoadSceneMode LoadSceneMode; public LoadSceneMode LoadSceneMode;
/// <summary> /// <summary>
/// This will be set to the scene name that the event pertains to.<BR/> /// This will be set to the scene name that the event pertains to.<br />
/// This is set for the following <see cref="Netcode.SceneEventType"/>s: /// This is set for the following <see cref="Netcode.SceneEventType"/>s:
/// <list type="bullet"> /// <list type="bullet">
/// <item><term><see cref="SceneEventType.Load"/></term></item> /// <item><term><see cref="SceneEventType.Load"/></term></item>
@@ -62,7 +62,7 @@ namespace Unity.Netcode
public string SceneName; public string SceneName;
/// <summary> /// <summary>
/// When a scene is loaded, the Scene structure is returned.<BR/> /// When a scene is loaded, the Scene structure is returned.<br />
/// This is set for the following <see cref="Netcode.SceneEventType"/>s: /// This is set for the following <see cref="Netcode.SceneEventType"/>s:
/// <list type="bullet"> /// <list type="bullet">
/// <item><term><see cref="SceneEventType.LoadComplete"/></term></item> /// <item><term><see cref="SceneEventType.LoadComplete"/></term></item>
@@ -322,37 +322,79 @@ namespace Unity.Netcode
/// <summary> /// <summary>
/// Delegate handler defined by <see cref="VerifySceneBeforeLoadingDelegateHandler"/> that is invoked before the /// Delegate handler defined by <see cref="VerifySceneBeforeLoadingDelegateHandler"/> that is invoked before the
/// server or client loads a scene during an active netcode game session.<br/> /// server or client loads a scene during an active netcode game session.
/// </summary>
/// <remarks>
/// <b>Client Side:</b> In order for clients to be notified of this condition you must assign the <see cref="VerifySceneBeforeLoading"/> delegate handler.<br /> /// <b>Client Side:</b> In order for clients to be notified of this condition you must assign the <see cref="VerifySceneBeforeLoading"/> delegate handler.<br />
/// <b>Server Side:</b> <see cref="LoadScene(string, LoadSceneMode)"/> will return <see cref="SceneEventProgressStatus"/>. /// <b>Server Side:</b> <see cref="LoadScene(string, LoadSceneMode)"/> will return <see cref="SceneEventProgressStatus"/>.
/// </summary> /// </remarks>
public VerifySceneBeforeLoadingDelegateHandler VerifySceneBeforeLoading; public VerifySceneBeforeLoadingDelegateHandler VerifySceneBeforeLoading;
/// <summary>
/// Delegate declaration for the <see cref="VerifySceneBeforeUnloading"/> handler that provides
/// an additional level of scene unloading validation to assure the scene being unloaded should
/// be unloaded.
/// </summary>
/// <param name="scene">The scene to be unloaded</param>
/// <returns>true (valid) or false (not valid)</returns>
public delegate bool VerifySceneBeforeUnloadingDelegateHandler(Scene scene);
/// <summary>
/// Client Side Only: <br />
/// Delegate handler defined by <see cref="VerifySceneBeforeUnloadingDelegateHandler"/> that is only invoked when the client
/// is finished synchronizing and when <see cref="ClientSynchronizationMode"/> is set to <see cref="LoadSceneMode.Additive"/>.
/// </summary>
public VerifySceneBeforeUnloadingDelegateHandler VerifySceneBeforeUnloading;
/// <summary>
/// When enabled and <see cref="ClientSynchronizationMode"/> is <see cref="LoadSceneMode.Additive"/>, any scenes not synchronized with
/// the server will be unloaded unless <see cref="VerifySceneBeforeUnloading"/> returns true. This provides more granular control over
/// which already loaded client-side scenes not synchronized with the server should be unloaded.
/// </summary>
/// <remarks>
/// If the <see cref="VerifySceneBeforeUnloading"/> 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 <see cref="VerifySceneBeforeUnloading"/>
/// 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.
/// </remarks>
public bool PostSynchronizationSceneUnloading;
private bool m_ActiveSceneSynchronizationEnabled;
/// <summary>
/// When enabled, the server or host will synchronize clients with changes to the currently active scene
/// </summary>
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;
}
}
}
}
/// <summary> /// <summary>
/// The SceneManagerHandler implementation /// The SceneManagerHandler implementation
/// </summary> /// </summary>
internal ISceneManagerHandler SceneManagerHandler = new DefaultSceneManagerHandler(); internal ISceneManagerHandler SceneManagerHandler = new DefaultSceneManagerHandler();
/// <summary>
/// The default SceneManagerHandler that interfaces between the SceneManager and NetworkSceneManager
/// </summary>
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<Guid, SceneEventProgress> SceneEventProgressTracking = new Dictionary<Guid, SceneEventProgress>(); internal readonly Dictionary<Guid, SceneEventProgress> SceneEventProgressTracking = new Dictionary<Guid, SceneEventProgress>();
/// <summary> /// <summary>
@@ -385,6 +427,77 @@ namespace Unity.Netcode
/// instances with client unique scene instances /// instances with client unique scene instances
/// </summary> /// </summary>
internal Dictionary<int, int> ServerSceneHandleToClientSceneHandle = new Dictionary<int, int>(); internal Dictionary<int, int> ServerSceneHandleToClientSceneHandle = new Dictionary<int, int>();
internal Dictionary<int, int> ClientSceneHandleToServerSceneHandle = new Dictionary<int, int>();
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// Removes the client to server (and vice versa) scene handles.
/// If it fails (i.e. already removed) it returns false.
/// </summary>
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;
}
/// <summary> /// <summary>
/// Hash to build index lookup table /// Hash to build index lookup table
@@ -413,16 +526,22 @@ namespace Unity.Netcode
private NetworkManager m_NetworkManager { get; } private NetworkManager m_NetworkManager { get; }
// Keep track of this scene until the NetworkSceneManager is destroyed.
internal Scene DontDestroyOnLoadScene; internal Scene DontDestroyOnLoadScene;
/// <summary> /// <summary>
/// <b>LoadSceneMode.Single:</b> All currently loaded scenes on the client will be unloaded and /// This setting changes how clients handle scene loading when initially synchronizing with the server.<br />
/// the server's currently active scene will be loaded in single mode on the client /// See: <see cref="SetClientSynchronizationMode(LoadSceneMode)"/>
/// unless it was already loaded.<br/> /// </summary>
/// <remarks>
/// <b>LoadSceneMode.Single:</b> 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.<br />
/// <b>LoadSceneMode.Additive:</b> All currently loaded scenes are left as they are and any newly loaded /// <b>LoadSceneMode.Additive:</b> 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 /// scenes will be loaded additively. Users need to determine which scenes are valid to load via the
/// <see cref="VerifySceneBeforeLoading"/> method. /// <see cref="VerifySceneBeforeLoading"/> and, if <see cref="PostSynchronizationSceneUnloading"/> is
/// </summary> /// set, <see cref="VerifySceneBeforeUnloading"/> callback(s).
/// </remarks>
public LoadSceneMode ClientSynchronizationMode { get; internal set; } public LoadSceneMode ClientSynchronizationMode { get; internal set; }
/// <summary> /// <summary>
@@ -435,11 +554,12 @@ namespace Unity.Netcode
/// </summary> /// </summary>
public void Dispose() public void Dispose()
{ {
// Always assure we no longer listen to scene changes when disposed.
SceneManager.activeSceneChanged -= SceneManager_ActiveSceneChanged;
SceneUnloadEventHandler.Shutdown(); SceneUnloadEventHandler.Shutdown();
foreach (var keypair in SceneEventDataStore) 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}'."); NetworkLog.LogInfo($"{nameof(SceneEventDataStore)} is disposing {nameof(SceneEventData.SceneEventId)} '{keypair.Key}'.");
} }
@@ -493,6 +613,11 @@ namespace Unity.Netcode
/// </summary> /// </summary>
internal void GenerateScenesInBuild() 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(); HashToBuildIndex.Clear();
BuildIndexToHash.Clear(); BuildIndexToHash.Clear();
for (int i = 0; i < SceneManager.sceneCountInBuildSettings; i++) for (int i = 0; i < SceneManager.sceneCountInBuildSettings; i++)
@@ -581,18 +706,22 @@ namespace Unity.Netcode
} }
/// <summary> /// <summary>
/// This will change how clients are initially synchronized.<br/> /// This setting changes how clients handle scene loading when initially synchronizing with the server.<br />
/// <b>LoadSceneMode.Single:</b> All currently loaded scenes on the client will be unloaded and /// The server or host should set this value as clients will automatically be synchronized with the server (or host) side.
/// the server's currently active scene will be loaded in single mode on the client /// <remarks>
/// unless it was already loaded. <br/> /// <b>LoadSceneMode.Single:</b> 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.<br />
/// <b>LoadSceneMode.Additive:</b> All currently loaded scenes are left as they are and any newly loaded /// <b>LoadSceneMode.Additive:</b> 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 /// scenes will be loaded additively. Users need to determine which scenes are valid to load via the
/// <see cref="VerifySceneBeforeLoading"/> method. /// <see cref="VerifySceneBeforeLoading"/> and, if <see cref="PostSynchronizationSceneUnloading"/> is
/// </summary> /// set, <see cref="VerifySceneBeforeUnloading"/> callback(s).
/// </remarks>
/// <param name="mode"><see cref="LoadSceneMode"/> for initial client synchronization</param> /// <param name="mode"><see cref="LoadSceneMode"/> for initial client synchronization</param>
public void SetClientSynchronizationMode(LoadSceneMode mode) public void SetClientSynchronizationMode(LoadSceneMode mode)
{ {
ClientSynchronizationMode = mode; var networkManager = m_NetworkManager;
SceneManagerHandler.SetClientSynchronizationMode(ref networkManager, mode);
} }
/// <summary> /// <summary>
@@ -605,13 +734,46 @@ namespace Unity.Netcode
m_NetworkManager = networkManager; m_NetworkManager = networkManager;
SceneEventDataStore = new Dictionary<uint, SceneEventData>(); SceneEventDataStore = new Dictionary<uint, SceneEventData>();
// Generates the scene name to hash value
GenerateScenesInBuild(); GenerateScenesInBuild();
// Since NetworkManager is now always migrated to the DDOL we will use this to get the DDOL scene // Since NetworkManager is now always migrated to the DDOL we will use this to get the DDOL scene
DontDestroyOnLoadScene = networkManager.gameObject.scene; DontDestroyOnLoadScene = networkManager.gameObject.scene;
ServerSceneHandleToClientSceneHandle.Add(DontDestroyOnLoadScene.handle, DontDestroyOnLoadScene.handle); // Add to the server to client scene handle table
ScenesLoaded.Add(DontDestroyOnLoadScene.handle, DontDestroyOnLoadScene); UpdateServerClientSceneHandle(DontDestroyOnLoadScene.handle, DontDestroyOnLoadScene.handle, DontDestroyOnLoadScene);
}
/// <summary>
/// Synchronizes clients when the currently active scene is changed
/// </summary>
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);
}
} }
/// <summary> /// <summary>
@@ -629,7 +791,7 @@ namespace Unity.Netcode
var sceneIndex = SceneUtility.GetBuildIndexByScenePath(sceneName); var sceneIndex = SceneUtility.GetBuildIndexByScenePath(sceneName);
if (VerifySceneBeforeLoading != null) if (VerifySceneBeforeLoading != null)
{ {
validated = VerifySceneBeforeLoading.Invoke((int)sceneIndex, sceneName, loadSceneMode); validated = VerifySceneBeforeLoading.Invoke(sceneIndex, sceneName, loadSceneMode);
} }
if (!validated && !m_DisableValidationWarningMessages) if (!validated && !m_DisableValidationWarningMessages)
{ {
@@ -675,11 +837,11 @@ namespace Unity.Netcode
if (!ScenesLoaded.ContainsKey(sceneLoaded.handle)) if (!ScenesLoaded.ContainsKey(sceneLoaded.handle))
{ {
ScenesLoaded.Add(sceneLoaded.handle, sceneLoaded); ScenesLoaded.Add(sceneLoaded.handle, sceneLoaded);
SceneManagerHandler.StartTrackingScene(sceneLoaded, true, m_NetworkManager);
return sceneLoaded; return sceneLoaded;
} }
} }
} }
throw new Exception($"Failed to find any loaded scene named {sceneName}!"); throw new Exception($"Failed to find any loaded scene named {sceneName}!");
} }
} }
@@ -788,7 +950,7 @@ namespace Unity.Netcode
/// </summary> /// </summary>
/// <param name="scene">the scene to be unloaded</param> /// <param name="scene">the scene to be unloaded</param>
/// <returns></returns> /// <returns></returns>
private SceneEventProgress ValidateSceneEventUnLoading(Scene scene) private SceneEventProgress ValidateSceneEventUnloading(Scene scene)
{ {
if (!m_NetworkManager.IsServer) if (!m_NetworkManager.IsServer)
{ {
@@ -798,9 +960,9 @@ namespace Unity.Netcode
if (!m_NetworkManager.NetworkConfig.EnableSceneManagement) if (!m_NetworkManager.NetworkConfig.EnableSceneManagement)
{ {
//Log message about enabling SceneManagement //Log message about enabling SceneManagement
throw new Exception($"{nameof(NetworkConfig.EnableSceneManagement)} flag is not enabled in the {nameof(NetworkManager)}'s {nameof(NetworkConfig)}. " + throw new Exception(
$"Please set {nameof(NetworkConfig.EnableSceneManagement)} flag to true before calling " + $"{nameof(NetworkConfig.EnableSceneManagement)} flag is not enabled in the {nameof(NetworkManager)}'s {nameof(NetworkConfig)}. " +
$"{nameof(NetworkSceneManager.LoadScene)} or {nameof(NetworkSceneManager.UnloadScene)}."); $"Please set {nameof(NetworkConfig.EnableSceneManagement)} flag to true before calling {nameof(LoadScene)} or {nameof(UnloadScene)}.");
} }
if (!scene.isLoaded) if (!scene.isLoaded)
@@ -826,9 +988,9 @@ namespace Unity.Netcode
if (!m_NetworkManager.NetworkConfig.EnableSceneManagement) if (!m_NetworkManager.NetworkConfig.EnableSceneManagement)
{ {
//Log message about enabling SceneManagement //Log message about enabling SceneManagement
throw new Exception($"{nameof(NetworkConfig.EnableSceneManagement)} flag is not enabled in the {nameof(NetworkManager)}'s {nameof(NetworkConfig)}. " + throw new Exception(
$"Please set {nameof(NetworkConfig.EnableSceneManagement)} flag to true before calling " + $"{nameof(NetworkConfig.EnableSceneManagement)} flag is not enabled in the {nameof(NetworkManager)}'s {nameof(NetworkConfig)}. " +
$"{nameof(NetworkSceneManager.LoadScene)} or {nameof(NetworkSceneManager.UnloadScene)}."); $"Please set {nameof(NetworkConfig.EnableSceneManagement)} flag to true before calling {nameof(LoadScene)} or {nameof(UnloadScene)}.");
} }
return ValidateSceneEvent(sceneName); return ValidateSceneEvent(sceneName);
@@ -939,7 +1101,7 @@ namespace Unity.Netcode
return SceneEventProgressStatus.SceneNotLoaded; return SceneEventProgressStatus.SceneNotLoaded;
} }
var sceneEventProgress = ValidateSceneEventUnLoading(scene); var sceneEventProgress = ValidateSceneEventUnloading(scene);
if (sceneEventProgress.Status != SceneEventProgressStatus.Started) if (sceneEventProgress.Status != SceneEventProgressStatus.Started)
{ {
return sceneEventProgress.Status; 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!"); Debug.LogError($"{nameof(UnloadScene)} internal error! {sceneName} with handle {scene.handle} is not within the internal scenes loaded dictionary!");
return SceneEventProgressStatus.InternalNetcodeError; 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(); var sceneEventData = BeginSceneEvent();
sceneEventData.SceneEventProgressId = sceneEventProgress.Guid; sceneEventData.SceneEventProgressId = sceneEventProgress.Guid;
sceneEventData.SceneEventType = SceneEventType.Unload; sceneEventData.SceneEventType = SceneEventType.Unload;
@@ -964,7 +1133,6 @@ namespace Unity.Netcode
sceneEventProgress.SceneEventId = sceneEventData.SceneEventId; sceneEventProgress.SceneEventId = sceneEventData.SceneEventId;
sceneEventProgress.OnSceneEventCompleted = OnSceneUnloaded; sceneEventProgress.OnSceneEventCompleted = OnSceneUnloaded;
var sceneUnload = SceneManagerHandler.UnloadSceneAsync(scene, sceneEventProgress); var sceneUnload = SceneManagerHandler.UnloadSceneAsync(scene, sceneEventProgress);
// Notify local server that a scene is going to be unloaded // Notify local server that a scene is going to be unloaded
OnSceneEvent?.Invoke(new SceneEvent() OnSceneEvent?.Invoke(new SceneEvent()
{ {
@@ -1006,16 +1174,30 @@ namespace Unity.Netcode
throw new Exception($"Client failed to unload scene {sceneName} " + throw new Exception($"Client failed to unload scene {sceneName} " +
$"because the client scene handle {sceneHandle} was not found in ScenesLoaded!"); $"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 // 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 // Notify the local client that a scene is going to be unloaded
OnSceneEvent?.Invoke(new SceneEvent() OnSceneEvent?.Invoke(new SceneEvent()
@@ -1042,6 +1224,9 @@ namespace Unity.Netcode
return; return;
} }
// Migrate the NetworkObjects marked to not be destroyed with the scene into the currently active scene
MoveObjectsFromDontDestroyOnLoadToScene(SceneManager.GetActiveScene());
var sceneEventData = SceneEventDataStore[sceneEventId]; var sceneEventData = SceneEventDataStore[sceneEventId];
// First thing we do, if we are a server, is to send the unload scene event. // First thing we do, if we are a server, is to send the unload scene event.
if (m_NetworkManager.IsServer) 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) // 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) if (currentActiveScene.name != keyHandleEntry.Value.name && keyHandleEntry.Value.buildIndex >= 0)
{ {
var sceneEventProgress = new SceneEventProgress(m_NetworkManager); var sceneEventProgress = new SceneEventProgress(m_NetworkManager)
sceneEventProgress.SceneEventId = sceneEventId; {
sceneEventProgress.OnSceneEventCompleted = EmptySceneUnloadedOperation; SceneEventId = sceneEventId,
OnSceneEventCompleted = EmptySceneUnloadedOperation
};
var sceneUnload = SceneManagerHandler.UnloadSceneAsync(keyHandleEntry.Value, sceneEventProgress); var sceneUnload = SceneManagerHandler.UnloadSceneAsync(keyHandleEntry.Value, sceneEventProgress);
SceneUnloadEventHandler.RegisterScene(this, keyHandleEntry.Value, LoadSceneMode.Additive, sceneUnload); SceneUnloadEventHandler.RegisterScene(this, keyHandleEntry.Value, LoadSceneMode.Additive, sceneUnload);
} }
} }
// clear out our scenes loaded list // clear out our scenes loaded list
ScenesLoaded.Clear(); ScenesLoaded.Clear();
SceneManagerHandler.ClearSceneTracking(m_NetworkManager);
} }
/// <summary> /// <summary>
@@ -1339,9 +1527,11 @@ namespace Unity.Netcode
SceneUnloadEventHandler.RegisterScene(this, SceneManager.GetActiveScene(), LoadSceneMode.Single); SceneUnloadEventHandler.RegisterScene(this, SceneManager.GetActiveScene(), LoadSceneMode.Single);
} }
var sceneEventProgress = new SceneEventProgress(m_NetworkManager); var sceneEventProgress = new SceneEventProgress(m_NetworkManager)
sceneEventProgress.SceneEventId = sceneEventId; {
sceneEventProgress.OnSceneEventCompleted = OnSceneLoaded; SceneEventId = sceneEventId,
OnSceneEventCompleted = OnSceneLoaded
};
var sceneLoad = SceneManagerHandler.LoadSceneAsync(sceneName, sceneEventData.LoadSceneMode, sceneEventProgress); var sceneLoad = SceneManagerHandler.LoadSceneAsync(sceneName, sceneEventData.LoadSceneMode, sceneEventProgress);
OnSceneEvent?.Invoke(new SceneEvent() OnSceneEvent?.Invoke(new SceneEvent()
@@ -1403,11 +1593,7 @@ namespace Unity.Netcode
else else
{ {
// For the client, we make a server scene handle to client scene handle look up table // For the client, we make a server scene handle to client scene handle look up table
if (!ServerSceneHandleToClientSceneHandle.ContainsKey(sceneEventData.SceneHandle)) if (!UpdateServerClientSceneHandle(sceneEventData.SceneHandle, nextScene.handle, nextScene))
{
ServerSceneHandleToClientSceneHandle.Add(sceneEventData.SceneHandle, nextScene.handle);
}
else
{ {
// If the exact same handle exists then there are problems with using handles // 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})"); 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); m_NetworkManager.SpawnManager.UpdateObservedNetworkObjects(clientId);
var sceneEventData = BeginSceneEvent(); var sceneEventData = BeginSceneEvent();
sceneEventData.ClientSynchronizationMode = ClientSynchronizationMode;
sceneEventData.InitializeForSynch(); sceneEventData.InitializeForSynch();
sceneEventData.TargetClientId = clientId; sceneEventData.TargetClientId = clientId;
sceneEventData.LoadSceneMode = ClientSynchronizationMode; sceneEventData.LoadSceneMode = ClientSynchronizationMode;
var activeScene = SceneManager.GetActiveScene(); var activeScene = SceneManager.GetActiveScene();
sceneEventData.SceneEventType = SceneEventType.Synchronize; sceneEventData.SceneEventType = SceneEventType.Synchronize;
if (BuildIndexToHash.ContainsKey(activeScene.buildIndex))
{
sceneEventData.ActiveSceneHash = BuildIndexToHash[activeScene.buildIndex];
}
// Organize how (and when) we serialize our NetworkObjects // Organize how (and when) we serialize our NetworkObjects
for (int i = 0; i < SceneManager.sceneCount; i++) for (int i = 0; i < SceneManager.sceneCount; i++)
@@ -1549,6 +1739,11 @@ namespace Unity.Netcode
continue; continue;
} }
if (scene == DontDestroyOnLoadScene)
{
continue;
}
var sceneHash = SceneHashFromNameOrPath(scene.path); var sceneHash = SceneHashFromNameOrPath(scene.path);
// This would depend upon whether we are additive or not // This would depend upon whether we are additive or not
@@ -1620,9 +1815,6 @@ namespace Unity.Netcode
}); });
OnSynchronize?.Invoke(m_NetworkManager.LocalClientId); 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 // Always check to see if the scene needs to be validated
@@ -1636,23 +1828,23 @@ namespace Unity.Netcode
return; return;
} }
var shouldPassThrough = false;
var sceneLoad = (AsyncOperation)null; var sceneLoad = (AsyncOperation)null;
// Check to see if the client already has loaded the scene to be loaded // 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
if (sceneName == activeScene.name) // 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).
// If the client is already in the same scene, then pass through and // 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
// don't try to reload it. // be the initial primary scene) then go ahead and pass through to post load processing (ClientLoadedSynchronization).
shouldPassThrough = true; var shouldPassThrough = SceneManagerHandler.ClientShouldPassThrough(sceneName, sceneHash == sceneEventData.SceneHash, ClientSynchronizationMode, m_NetworkManager);
}
if (!shouldPassThrough) if (!shouldPassThrough)
{ {
// If not, then load the scene // If not, then load the scene
var sceneEventProgress = new SceneEventProgress(m_NetworkManager); var sceneEventProgress = new SceneEventProgress(m_NetworkManager)
sceneEventProgress.SceneEventId = sceneEventId; {
sceneEventProgress.OnSceneEventCompleted = ClientLoadedSynchronization; SceneEventId = sceneEventId,
OnSceneEventCompleted = ClientLoadedSynchronization
};
sceneLoad = SceneManagerHandler.LoadSceneAsync(sceneName, loadSceneMode, sceneEventProgress); sceneLoad = SceneManagerHandler.LoadSceneAsync(sceneName, loadSceneMode, sceneEventProgress);
// Notify local client that a scene load has begun // Notify local client that a scene load has begun
@@ -1683,7 +1875,11 @@ namespace Unity.Netcode
{ {
var sceneEventData = SceneEventDataStore[sceneEventId]; var sceneEventData = SceneEventDataStore[sceneEventId];
var sceneName = SceneNameFromHash(sceneEventData.ClientSceneHash); 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()) if (!nextScene.isLoaded || !nextScene.IsValid())
{ {
@@ -1698,11 +1894,8 @@ namespace Unity.Netcode
SceneManager.SetActiveScene(nextScene); SceneManager.SetActiveScene(nextScene);
} }
if (!ServerSceneHandleToClientSceneHandle.ContainsKey(sceneEventData.NetworkSceneHandle)) // For the client, we make a server scene handle to client scene handle look up table
{ if (!UpdateServerClientSceneHandle(sceneEventData.NetworkSceneHandle, nextScene.handle, nextScene))
ServerSceneHandleToClientSceneHandle.Add(sceneEventData.NetworkSceneHandle, nextScene.handle);
}
else
{ {
// If the exact same handle exists then there are problems with using handles // 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})"); 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); HandleClientSceneEvent(sceneEventId);
} }
/// <summary>
/// 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.
/// </summary>
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})!");
}
}
}
}
}
/// <summary> /// <summary>
/// Client Side: /// Client Side:
/// Handles incoming Scene_Event messages for clients /// Handles incoming Scene_Event messages for clients
@@ -1754,6 +1989,23 @@ namespace Unity.Netcode
var sceneEventData = SceneEventDataStore[sceneEventId]; var sceneEventData = SceneEventDataStore[sceneEventId];
switch (sceneEventData.SceneEventType) 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: case SceneEventType.Load:
{ {
OnClientSceneLoadingEvent(sceneEventId); OnClientSceneLoadingEvent(sceneEventId);
@@ -1777,6 +2029,19 @@ namespace Unity.Netcode
// Synchronize the NetworkObjects for this scene // Synchronize the NetworkObjects for this scene
sceneEventData.SynchronizeSceneNetworkObjects(m_NetworkManager); 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; sceneEventData.SceneEventType = SceneEventType.SynchronizeComplete;
SendSceneEventData(sceneEventId, new ulong[] { NetworkManager.ServerClientId }); SendSceneEventData(sceneEventId, new ulong[] { NetworkManager.ServerClientId });
@@ -1793,6 +2058,19 @@ namespace Unity.Netcode
ClientId = m_NetworkManager.LocalClientId, // Client sent this to the server 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); OnSynchronizeComplete?.Invoke(m_NetworkManager.LocalClientId);
EndSceneEvent(sceneEventId); EndSceneEvent(sceneEventId);
@@ -1907,9 +2185,12 @@ namespace Unity.Netcode
OnSynchronizeComplete?.Invoke(clientId); OnSynchronizeComplete?.Invoke(clientId);
// We now can call the client connected callback on the server at this time // At this time the client is fully synchronized with all loaded scenes and
// This assures the client is fully synchronized with all loaded scenes and // NetworkObjects and should be considered "fully connected". Send the
// NetworkObjects // 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); 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 // 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 (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); HandleClientSceneEvent(sceneEventData.SceneEventId);
} }
else else
@@ -1964,7 +2262,7 @@ namespace Unity.Netcode
} }
else 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
/// </summary> /// </summary>
internal void MoveObjectsToDontDestroyOnLoad() internal void MoveObjectsToDontDestroyOnLoad()
{ {
// Move ALL NetworkObjects marked to persist scene transitions into the DDOL scene // Create a local copy of the spawned objects list since the spawn manager will adjust the list as objects
var objectsToKeep = new HashSet<NetworkObject>(m_NetworkManager.SpawnManager.SpawnedObjectsList); // are despawned.
foreach (var sobj in objectsToKeep) var localSpawnedObjectsHashSet = new HashSet<NetworkObject>(m_NetworkManager.SpawnManager.SpawnedObjectsList);
foreach (var networkObject in localSpawnedObjectsHashSet)
{ {
if (sobj == null) if (networkObject == null || (networkObject != null && networkObject.gameObject.scene == DontDestroyOnLoadScene))
{ {
continue; 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 // Only move dynamically spawned NetworkObjects with no parent as the children will follow
if (sobj.gameObject.transform.parent == null && sobj.IsSceneObject != null && !sobj.IsSceneObject.Value) 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) else if (m_NetworkManager.IsServer)
{ {
sobj.Despawn(); networkObject.Despawn();
} }
} }
} }
@@ -2028,9 +2328,10 @@ namespace Unity.Netcode
foreach (var networkObjectInstance in networkObjects) foreach (var networkObjectInstance in networkObjects)
{ {
var globalObjectIdHash = networkObjectInstance.GlobalObjectIdHash; 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) // 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)) if (!ScenePlacedObjects.ContainsKey(globalObjectIdHash))
{ {
@@ -2057,26 +2358,139 @@ namespace Unity.Netcode
/// <param name="scene">scene to move the NetworkObjects to</param> /// <param name="scene">scene to move the NetworkObjects to</param>
internal void MoveObjectsFromDontDestroyOnLoadToScene(Scene scene) internal void MoveObjectsFromDontDestroyOnLoadToScene(Scene scene)
{ {
// Move ALL NetworkObjects to the temp scene foreach (var networkObject in m_NetworkManager.SpawnManager.SpawnedObjectsList)
var objectsToKeep = m_NetworkManager.SpawnManager.SpawnedObjectsList;
foreach (var sobj in objectsToKeep)
{ {
if (sobj == null) if (networkObject == null)
{ {
continue; continue;
} }
// If it is in the DDOL then // 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, // only move dynamically spawned network objects, with no parent as child objects will follow,
// back into the currently active scene // 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);
}
}
}
}
/// <summary>
/// Holds a list of scene handles (server-side relative) and NetworkObjects migrated into it
/// during the current frame.
/// </summary>
internal Dictionary<int, List<NetworkObject>> ObjectsMigratedIntoNewScene = new Dictionary<int, List<NetworkObject>>();
/// <summary>
/// Handles notifying clients when a NetworkObject has been migrated into a new scene
/// </summary>
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<NetworkObject>());
}
ObjectsMigratedIntoNewScene[networkObject.gameObject.scene.handle].Add(networkObject);
}
/// <summary>
/// Invoked by clients when processing a <see cref="SceneEventType.ObjectSceneChanged"/> event
/// or invoked by <see cref="SceneEventData.ProcessDeferredObjectSceneChangedEvents"/> when a client finishes
/// synchronization.
/// </summary>
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();
}
/// <summary>
/// Should be invoked during PostLateUpdate just prior to the
/// MessagingSystem processes its outbound message queue.
/// </summary>
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<int, List<ulong>> ObjectsMigratedTable;
}
internal List<DeferredObjectsMovedEvent> DeferredObjectsMovedEvents = new List<DeferredObjectsMovedEvent>();
}
} }

View File

@@ -1,5 +1,5 @@
using System.Collections.Generic;
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Unity.Collections; using Unity.Collections;
using UnityEngine.SceneManagement; using UnityEngine.SceneManagement;
@@ -80,6 +80,16 @@ namespace Unity.Netcode
/// <b>Event Notification:</b> Both server and client receive a local notification. /// <b>Event Notification:</b> Both server and client receive a local notification.
/// </summary> /// </summary>
SynchronizeComplete, SynchronizeComplete,
/// <summary>
/// Synchronizes clients when the active scene has changed
/// See: <see cref="NetworkObject.ActiveSceneSynchronization"/>
/// </summary>
ActiveSceneChanged,
/// <summary>
/// Synchronizes clients when one or more NetworkObjects are migrated into a new scene
/// See: <see cref="NetworkObject.SceneMigrationSynchronization"/>
/// </summary>
ObjectSceneChanged,
} }
/// <summary> /// <summary>
@@ -94,7 +104,7 @@ namespace Unity.Netcode
internal ForceNetworkSerializeByMemcpy<Guid> SceneEventProgressId; internal ForceNetworkSerializeByMemcpy<Guid> SceneEventProgressId;
internal uint SceneEventId; internal uint SceneEventId;
internal uint ActiveSceneHash;
internal uint SceneHash; internal uint SceneHash;
internal int SceneHandle; internal int SceneHandle;
@@ -139,6 +149,8 @@ namespace Unity.Netcode
internal Queue<uint> ScenesToSynchronize; internal Queue<uint> ScenesToSynchronize;
internal Queue<uint> SceneHandlesToSynchronize; internal Queue<uint> SceneHandlesToSynchronize;
internal LoadSceneMode ClientSynchronizationMode;
/// <summary> /// <summary>
/// Server Side: /// Server Side:
@@ -315,6 +327,8 @@ namespace Unity.Netcode
case SceneEventType.ReSynchronize: case SceneEventType.ReSynchronize:
case SceneEventType.LoadEventCompleted: case SceneEventType.LoadEventCompleted:
case SceneEventType.UnloadEventCompleted: case SceneEventType.UnloadEventCompleted:
case SceneEventType.ActiveSceneChanged:
case SceneEventType.ObjectSceneChanged:
{ {
return true; return true;
} }
@@ -384,6 +398,18 @@ namespace Unity.Netcode
// Write the scene event type // Write the scene event type
writer.WriteValueSafe(SceneEventType); writer.WriteValueSafe(SceneEventType);
if (SceneEventType == SceneEventType.ActiveSceneChanged)
{
writer.WriteValueSafe(ActiveSceneHash);
return;
}
if (SceneEventType == SceneEventType.ObjectSceneChanged)
{
SerializeObjectsMovedIntoNewScene(writer);
return;
}
// Write the scene loading mode // Write the scene loading mode
writer.WriteValueSafe((byte)LoadSceneMode); writer.WriteValueSafe((byte)LoadSceneMode);
@@ -392,6 +418,10 @@ namespace Unity.Netcode
{ {
writer.WriteValueSafe(SceneEventProgressId); writer.WriteValueSafe(SceneEventProgressId);
} }
else
{
writer.WriteValueSafe(ClientSynchronizationMode);
}
// Write the scene index and handle // Write the scene index and handle
writer.WriteValueSafe(SceneHash); writer.WriteValueSafe(SceneHash);
@@ -401,6 +431,7 @@ namespace Unity.Netcode
{ {
case SceneEventType.Synchronize: case SceneEventType.Synchronize:
{ {
writer.WriteValueSafe(ActiveSceneHash);
WriteSceneSynchronizationData(writer); WriteSceneSynchronizationData(writer);
break; break;
} }
@@ -445,7 +476,7 @@ namespace Unity.Netcode
// Size Place Holder -- Start // Size Place Holder -- Start
// !!NOTE!!: Since this is a placeholder to be set after we know how much we have written, // !!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! // for stream offset purposes this MUST not be a packed value!
writer.WriteValueSafe((int)0); writer.WriteValueSafe(0);
int totalBytes = 0; int totalBytes = 0;
// Write the number of NetworkObjects we are serializing // Write the number of NetworkObjects we are serializing
@@ -458,7 +489,7 @@ namespace Unity.Netcode
var sceneObject = m_NetworkObjectsSync[i].GetMessageSceneObject(TargetClientId); var sceneObject = m_NetworkObjectsSync[i].GetMessageSceneObject(TargetClientId);
sceneObject.Serialize(writer); sceneObject.Serialize(writer);
var noStop = writer.Position; var noStop = writer.Position;
totalBytes += (int)(noStop - noStart); totalBytes += noStop - noStart;
} }
// Write the number of despawned in-scene placed NetworkObjects // 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].GetSceneOriginHandle());
writer.WriteValueSafe(m_DespawnedInSceneObjectsSync[i].GlobalObjectIdHash); writer.WriteValueSafe(m_DespawnedInSceneObjectsSync[i].GlobalObjectIdHash);
var noStop = writer.Position; var noStop = writer.Position;
totalBytes += (int)(noStop - noStart); totalBytes += noStop - noStart;
} }
// Size Place Holder -- End // Size Place Holder -- End
@@ -536,6 +567,26 @@ namespace Unity.Netcode
internal void Deserialize(FastBufferReader reader) internal void Deserialize(FastBufferReader reader)
{ {
reader.ReadValueSafe(out SceneEventType); 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); reader.ReadValueSafe(out byte loadSceneMode);
LoadSceneMode = (LoadSceneMode)loadSceneMode; LoadSceneMode = (LoadSceneMode)loadSceneMode;
@@ -543,6 +594,10 @@ namespace Unity.Netcode
{ {
reader.ReadValueSafe(out SceneEventProgressId); reader.ReadValueSafe(out SceneEventProgressId);
} }
else
{
reader.ReadValueSafe(out ClientSynchronizationMode);
}
reader.ReadValueSafe(out SceneHash); reader.ReadValueSafe(out SceneHash);
reader.ReadValueSafe(out SceneHandle); reader.ReadValueSafe(out SceneHandle);
@@ -551,6 +606,7 @@ namespace Unity.Netcode
{ {
case SceneEventType.Synchronize: case SceneEventType.Synchronize:
{ {
reader.ReadValueSafe(out ActiveSceneHash);
CopySceneSynchronizationData(reader); CopySceneSynchronizationData(reader);
break; break;
} }
@@ -939,6 +995,143 @@ namespace Unity.Netcode
} }
} }
/// <summary>
/// Serialize scene handles and associated NetworkObjects that were migrated
/// into a new scene.
/// </summary>
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();
}
/// <summary>
/// Deserialize scene handles and associated NetworkObjects that need to
/// be migrated into a new scene.
/// </summary>
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<NetworkObject>());
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]);
}
}
}
/// <summary>
/// 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.
/// </summary>
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<int, List<ulong>>()
};
reader.ReadValueSafe(out numberOfScenes);
for (int i = 0; i < numberOfScenes; i++)
{
reader.ReadValueSafe(out sceneHandle);
deferredObjectsMovedEvent.ObjectsMigratedTable.Add(sceneHandle, new List<ulong>());
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<NetworkObject>());
}
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();
}
}
/// <summary> /// <summary>
/// Used to release the pooled network buffer /// Used to release the pooled network buffer
/// </summary> /// </summary>

View File

@@ -83,7 +83,7 @@ namespace Unity.Netcode
/// </summary> /// </summary>
internal bool HasTimedOut() internal bool HasTimedOut()
{ {
return WhenSceneEventHasTimedOut <= Time.realtimeSinceStartup; return WhenSceneEventHasTimedOut <= m_NetworkManager.RealTimeProvider.RealTimeSinceStartup;
} }
/// <summary> /// <summary>
@@ -164,7 +164,7 @@ namespace Unity.Netcode
ClientsProcessingSceneEvent.Add(connectedClientId, false); ClientsProcessingSceneEvent.Add(connectedClientId, false);
} }
WhenSceneEventHasTimedOut = Time.realtimeSinceStartup + networkManager.NetworkConfig.LoadSceneTimeOut; WhenSceneEventHasTimedOut = networkManager.RealTimeProvider.RealTimeSinceStartup + networkManager.NetworkConfig.LoadSceneTimeOut;
m_TimeOutCoroutine = m_NetworkManager.StartCoroutine(TimeOutSceneEventProgress()); m_TimeOutCoroutine = m_NetworkManager.StartCoroutine(TimeOutSceneEventProgress());
} }
} }

View File

@@ -1299,8 +1299,10 @@ namespace Unity.Netcode
where T : unmanaged, INativeList<byte>, IUTF8Bytes where T : unmanaged, INativeList<byte>, IUTF8Bytes
{ {
ReadUnmanaged(out int length); ReadUnmanaged(out int length);
value = new T(); value = new T
value.Length = length; {
Length = length
};
ReadBytes(value.GetUnsafePtr(), length); ReadBytes(value.GetUnsafePtr(), length);
} }
@@ -1319,8 +1321,10 @@ namespace Unity.Netcode
where T : unmanaged, INativeList<byte>, IUTF8Bytes where T : unmanaged, INativeList<byte>, IUTF8Bytes
{ {
ReadUnmanagedSafe(out int length); ReadUnmanagedSafe(out int length);
value = new T(); value = new T
value.Length = length; {
Length = length
};
ReadBytesSafe(value.GetUnsafePtr(), length); ReadBytesSafe(value.GetUnsafePtr(), length);
} }

View File

@@ -54,13 +54,7 @@ namespace Unity.Netcode
throw new ArgumentNullException(nameof(gameObject)); throw new ArgumentNullException(nameof(gameObject));
} }
var networkObject = gameObject.GetComponent<NetworkObject>(); var networkObject = gameObject.GetComponent<NetworkObject>() ?? throw new ArgumentException($"Cannot create {nameof(NetworkObjectReference)} from {nameof(GameObject)} without a {nameof(NetworkObject)} component.");
if (networkObject == null)
{
throw new ArgumentException($"Cannot create {nameof(NetworkObjectReference)} from {nameof(GameObject)} without a {nameof(NetworkObject)} component.");
}
if (networkObject.IsSpawned == false) if (networkObject.IsSpawned == false)
{ {
throw new ArgumentException($"{nameof(NetworkObjectReference)} can only be created from spawned {nameof(NetworkObject)}s."); throw new ArgumentException($"{nameof(NetworkObjectReference)} can only be created from spawned {nameof(NetworkObject)}s.");
@@ -90,7 +84,7 @@ namespace Unity.Netcode
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static NetworkObject Resolve(NetworkObjectReference networkObjectRef, NetworkManager networkManager = null) 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); networkManager.SpawnManager.SpawnedObjects.TryGetValue(networkObjectRef.m_NetworkObjectId, out NetworkObject networkObject);
return networkObject; return networkObject;

View File

@@ -154,7 +154,7 @@ namespace Unity.Netcode
internal ulong GetNetworkObjectId() 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; return ReleasedNetworkObjectIds.Dequeue().NetworkId;
} }
@@ -405,6 +405,9 @@ namespace Unity.Netcode
if (networkObject != null) if (networkObject != null)
{ {
networkObject.DestroyWithScene = sceneObject.DestroyWithScene;
networkObject.NetworkSceneHandle = sceneObject.NetworkSceneHandle;
// SPECIAL CASE FOR IN-SCENE PLACED: (only when the parent has a NetworkObject) // 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 // 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 // more scenes that contain nested in-scene placed NetworkObject children yet the server's
@@ -610,6 +613,12 @@ namespace Unity.Netcode
} }
childObject.IsSceneObject = sceneObject; childObject.IsSceneObject = sceneObject;
} }
// Only dynamically spawned NetworkObjects are allowed
if (!sceneObject)
{
networkObject.SubscribeToActiveSceneForSynch();
}
} }
internal void SendSpawnCallForObject(ulong clientId, NetworkObject networkObject) internal void SendSpawnCallForObject(ulong clientId, NetworkObject networkObject)
@@ -848,7 +857,7 @@ namespace Unity.Netcode
ReleasedNetworkObjectIds.Enqueue(new ReleasedNetworkId() ReleasedNetworkObjectIds.Enqueue(new ReleasedNetworkId()
{ {
NetworkId = networkObject.NetworkObjectId, NetworkId = networkObject.NetworkObjectId,
ReleaseTime = Time.unscaledTime ReleaseTime = NetworkManager.RealTimeProvider.UnscaledTime
}); });
} }

View File

@@ -0,0 +1,10 @@
namespace Unity.Netcode
{
internal interface IRealTimeProvider
{
float RealTimeSinceStartup { get; }
float UnscaledTime { get; }
float UnscaledDeltaTime { get; }
float DeltaTime { get; }
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 73bdda41e36846e893fd14dbd6de9978
timeCreated: 1679413210

View File

@@ -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;
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5aa5470767d64d8e89ac69ff52a8c404
timeCreated: 1679413182

View File

@@ -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); 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); clientId = GetNetcodeClientId((byte)hostId, (ushort)connectionId, false);
receiveTime = Time.realtimeSinceStartup; receiveTime = NetworkManager.RealTimeProvider.RealTimeSinceStartup;
var networkError = (NetworkError)error; var networkError = (NetworkError)error;
if (networkError == NetworkError.MessageToLong) if (networkError == NetworkError.MessageToLong)
@@ -214,7 +214,7 @@ namespace Unity.Netcode.Transports.UNET
{ {
GetUNetConnectionDetails(clientId, out byte hostId, out ushort connectionId); 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() public override void DisconnectLocalClient()
@@ -226,7 +226,7 @@ namespace Unity.Netcode.Transports.UNET
{ {
GetUNetConnectionDetails(clientId, out byte hostId, out ushort connectionId); 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() public override void Shutdown()

View File

@@ -450,6 +450,8 @@ namespace Unity.Netcode.Transports.UTP
internal NetworkManager NetworkManager; internal NetworkManager NetworkManager;
private IRealTimeProvider m_RealTimeProvider;
/// <summary> /// <summary>
/// SendQueue dictionary is used to batch events instead of sending them immediately. /// SendQueue dictionary is used to batch events instead of sending them immediately.
/// </summary> /// </summary>
@@ -763,6 +765,10 @@ namespace Unity.Netcode.Transports.UTP
// Send as many batched messages from the queue as possible. // Send as many batched messages from the queue as possible.
private void SendBatchedMessages(SendTarget sendTarget, BatchedSendQueue queue) private void SendBatchedMessages(SendTarget sendTarget, BatchedSendQueue queue)
{ {
if (!m_Driver.IsCreated)
{
return;
}
new SendBatchedMessagesJob new SendBatchedMessagesJob
{ {
Driver = m_Driver.ToConcurrent(), Driver = m_Driver.ToConcurrent(),
@@ -784,7 +790,7 @@ namespace Unity.Netcode.Transports.UTP
InvokeOnTransportEvent(NetcodeNetworkEvent.Connect, InvokeOnTransportEvent(NetcodeNetworkEvent.Connect,
ParseClientId(connection), ParseClientId(connection),
default, default,
Time.realtimeSinceStartup); m_RealTimeProvider.RealTimeSinceStartup);
return true; return true;
@@ -819,7 +825,7 @@ namespace Unity.Netcode.Transports.UTP
break; 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, InvokeOnTransportEvent(NetcodeNetworkEvent.Connect,
clientId, clientId,
default, default,
Time.realtimeSinceStartup); m_RealTimeProvider.RealTimeSinceStartup);
m_State = State.Connected; m_State = State.Connected;
return true; return true;
@@ -863,7 +869,7 @@ namespace Unity.Netcode.Transports.UTP
InvokeOnTransportEvent(NetcodeNetworkEvent.Disconnect, InvokeOnTransportEvent(NetcodeNetworkEvent.Disconnect,
clientId, clientId,
default, default,
Time.realtimeSinceStartup); m_RealTimeProvider.RealTimeSinceStartup);
return true; return true;
} }
@@ -893,7 +899,7 @@ namespace Unity.Netcode.Transports.UTP
Debug.LogError("Transport failure! Relay allocation needs to be recreated, and NetworkManager restarted. " + Debug.LogError("Transport failure! Relay allocation needs to be recreated, and NetworkManager restarted. " +
"Use NetworkManager.OnTransportFailure to be notified of such events programmatically."); "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; return;
} }
@@ -1116,7 +1122,7 @@ namespace Unity.Netcode.Transports.UTP
InvokeOnTransportEvent(NetcodeNetworkEvent.Disconnect, InvokeOnTransportEvent(NetcodeNetworkEvent.Disconnect,
m_ServerClientId, m_ServerClientId,
default, default,
Time.realtimeSinceStartup); m_RealTimeProvider.RealTimeSinceStartup);
} }
} }
} }
@@ -1179,6 +1185,8 @@ namespace Unity.Netcode.Transports.UTP
NetworkManager = networkManager; NetworkManager = networkManager;
m_RealTimeProvider = NetworkManager ? NetworkManager.RealTimeProvider : new RealTimeProvider();
m_NetworkSettings = new NetworkSettings(Allocator.Persistent); m_NetworkSettings = new NetworkSettings(Allocator.Persistent);
// If the user sends a message of exactly m_MaxPayloadSize in length, we need to // If the user sends a message of exactly m_MaxPayloadSize in length, we need to
@@ -1203,7 +1211,7 @@ namespace Unity.Netcode.Transports.UTP
/// </summary> /// </summary>
/// <param name="clientId">The clientId this event is for</param> /// <param name="clientId">The clientId this event is for</param>
/// <param name="payload">The incoming data payload</param> /// <param name="payload">The incoming data payload</param>
/// <param name="receiveTime">The time the event was received, as reported by Time.realtimeSinceStartup.</param> /// <param name="receiveTime">The time the event was received, as reported by m_RealTimeProvider.RealTimeSinceStartup.</param>
/// <returns>Returns the event type</returns> /// <returns>Returns the event type</returns>
public override NetcodeNetworkEvent PollEvent(out ulong clientId, out ArraySegment<byte> payload, out float receiveTime) public override NetcodeNetworkEvent PollEvent(out ulong clientId, out ArraySegment<byte> payload, out float receiveTime)
{ {
@@ -1276,7 +1284,7 @@ namespace Unity.Netcode.Transports.UTP
InvokeOnTransportEvent(NetcodeNetworkEvent.Disconnect, InvokeOnTransportEvent(NetcodeNetworkEvent.Disconnect,
clientId, clientId,
default(ArraySegment<byte>), default(ArraySegment<byte>),
Time.realtimeSinceStartup); m_RealTimeProvider.RealTimeSinceStartup);
} }
} }
else else
@@ -1424,6 +1432,10 @@ namespace Unity.Netcode.Transports.UTP
private string m_ClientCaCertificate; private string m_ClientCaCertificate;
/// <summary>Set the server parameters for encryption.</summary> /// <summary>Set the server parameters for encryption.</summary>
/// <remarks>
/// The public certificate and private key are expected to be in the PEM format, including
/// the begin/end markers like <c>-----BEGIN CERTIFICATE-----</c>.
/// </remarks>
/// <param name="serverCertificate">Public certificate for the server (PEM format).</param> /// <param name="serverCertificate">Public certificate for the server (PEM format).</param>
/// <param name="serverPrivateKey">Private key for the server (PEM format).</param> /// <param name="serverPrivateKey">Private key for the server (PEM format).</param>
public void SetServerSecrets(string serverCertificate, string serverPrivateKey) public void SetServerSecrets(string serverCertificate, string serverPrivateKey)
@@ -1434,9 +1446,15 @@ namespace Unity.Netcode.Transports.UTP
/// <summary>Set the client parameters for encryption.</summary> /// <summary>Set the client parameters for encryption.</summary>
/// <remarks> /// <remarks>
/// <para>
/// If the CA certificate is not provided, validation will be done against the OS/browser /// 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. /// 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. /// For self-signed certificates, the CA certificate needs to be provided.
/// </para>
/// <para>
/// The CA certificate (if provided) is expected to be in the PEM format, including the
/// begin/end markers like <c>-----BEGIN CERTIFICATE-----</c>.
/// </para>
/// </remarks> /// </remarks>
/// <param name="serverCommonName">Common name of the server (typically hostname).</param> /// <param name="serverCommonName">Common name of the server (typically hostname).</param>
/// <param name="caCertificate">CA certificate used to validate the server's authenticity.</param> /// <param name="caCertificate">CA certificate used to validate the server's authenticity.</param>

View File

@@ -12,7 +12,8 @@
"Unity.Multiplayer.Tools.NetworkSolutionInterface", "Unity.Multiplayer.Tools.NetworkSolutionInterface",
"Unity.Networking.Transport", "Unity.Networking.Transport",
"Unity.Collections", "Unity.Collections",
"Unity.Burst" "Unity.Burst",
"Unity.Mathematics"
], ],
"allowUnsafeCode": true, "allowUnsafeCode": true,
"versionDefines": [ "versionDefines": [

View File

@@ -15,6 +15,16 @@ namespace Unity.Netcode.TestHelpers.Runtime
/// </summary> /// </summary>
internal class IntegrationTestSceneHandler : ISceneManagerHandler, IDisposable internal class IntegrationTestSceneHandler : ISceneManagerHandler, IDisposable
{ {
private Scene m_InvalidScene = new Scene();
internal struct SceneEntry
{
public bool IsAssigned;
public Scene Scene;
}
internal static Dictionary<NetworkManager, Dictionary<string, Dictionary<int, SceneEntry>>> SceneNameToSceneHandles = new Dictionary<NetworkManager, Dictionary<string, Dictionary<int, SceneEntry>>>();
// All IntegrationTestSceneHandler instances register their associated NetworkManager // All IntegrationTestSceneHandler instances register their associated NetworkManager
internal static List<NetworkManager> NetworkManagers = new List<NetworkManager>(); internal static List<NetworkManager> NetworkManagers = new List<NetworkManager>();
@@ -96,7 +106,7 @@ namespace Unity.Netcode.TestHelpers.Runtime
/// Processes scene loading jobs /// Processes scene loading jobs
/// </summary> /// </summary>
/// <param name="queuedSceneJob">job to process</param> /// <param name="queuedSceneJob">job to process</param>
static internal IEnumerator ProcessLoadingSceneJob(QueuedSceneJob queuedSceneJob) internal static IEnumerator ProcessLoadingSceneJob(QueuedSceneJob queuedSceneJob)
{ {
var itegrationTestSceneHandler = queuedSceneJob.IntegrationTestSceneHandler; var itegrationTestSceneHandler = queuedSceneJob.IntegrationTestSceneHandler;
while (!itegrationTestSceneHandler.OnCanClientsLoad()) while (!itegrationTestSceneHandler.OnCanClientsLoad())
@@ -170,7 +180,7 @@ namespace Unity.Netcode.TestHelpers.Runtime
/// Processes scene unloading jobs /// Processes scene unloading jobs
/// </summary> /// </summary>
/// <param name="queuedSceneJob">job to process</param> /// <param name="queuedSceneJob">job to process</param>
static internal IEnumerator ProcessUnloadingSceneJob(QueuedSceneJob queuedSceneJob) internal static IEnumerator ProcessUnloadingSceneJob(QueuedSceneJob queuedSceneJob)
{ {
var itegrationTestSceneHandler = queuedSceneJob.IntegrationTestSceneHandler; var itegrationTestSceneHandler = queuedSceneJob.IntegrationTestSceneHandler;
while (!itegrationTestSceneHandler.OnCanClientsUnload()) while (!itegrationTestSceneHandler.OnCanClientsUnload())
@@ -213,7 +223,7 @@ namespace Unity.Netcode.TestHelpers.Runtime
/// Processes all jobs within the queue. /// Processes all jobs within the queue.
/// When all jobs are finished, the coroutine stops. /// When all jobs are finished, the coroutine stops.
/// </summary> /// </summary>
static internal IEnumerator JobQueueProcessor() internal static IEnumerator JobQueueProcessor()
{ {
while (QueuedSceneJobs.Count != 0) while (QueuedSceneJobs.Count != 0)
{ {
@@ -267,8 +277,8 @@ namespace Unity.Netcode.TestHelpers.Runtime
{ {
if (m_ServerSceneBeingLoaded == scene.name) if (m_ServerSceneBeingLoaded == scene.name)
{ {
ProcessInSceneObjects(scene, NetworkManager);
SceneManager.sceneLoaded -= Sever_SceneLoaded; SceneManager.sceneLoaded -= Sever_SceneLoaded;
ProcessInSceneObjects(scene, NetworkManager);
} }
} }
@@ -330,6 +340,7 @@ namespace Unity.Netcode.TestHelpers.Runtime
{ {
continue; continue;
} }
if (networkManager.SceneManager.ScenesLoaded.ContainsKey(sceneLoaded.handle)) if (networkManager.SceneManager.ScenesLoaded.ContainsKey(sceneLoaded.handle))
{ {
if (NetworkManager.LogLevel == LogLevel.Developer) 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."); 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); NetworkManager.SceneManager.ScenesLoaded.Add(sceneLoaded.handle, sceneLoaded);
StartTrackingScene(sceneLoaded, true, NetworkManager);
return sceneLoaded; return sceneLoaded;
} }
} }
@@ -365,6 +381,521 @@ namespace Unity.Netcode.TestHelpers.Runtime
return true; 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<string, Dictionary<int, SceneEntry>>());
}
if (!SceneNameToSceneHandles[networkManager].ContainsKey(scene.name))
{
SceneNameToSceneHandles[networkManager].Add(scene.name, new Dictionary<int, SceneEntry>());
}
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<Scene>();
var scenesAssigned = new List<Scene>();
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<int, Scene> scenesLoaded, NetworkManager networkManager)
{
if (!SceneNameToSceneHandles.ContainsKey(networkManager))
{
SceneNameToSceneHandles.Add(networkManager, new Dictionary<string, Dictionary<int, SceneEntry>>());
}
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<int, SceneEntry>());
}
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<Scene, NetworkManager> m_ScenesToUnload = new Dictionary<Scene, NetworkManager>();
/// <summary>
/// Handles unloading any scenes that might remain on a client that
/// need to be unloaded.
/// </summary>
/// <param name="networkManager"></param>
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);
}
}
/// <summary>
/// Removes the scene entry from the scene name to scene handle table
/// </summary>
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;
}
}
}
}
}
/// <summary>
/// Integration test version that handles migrating dynamically spawned NetworkObjects to
/// the DDOL when a scene is unloaded
/// </summary>
/// <param name="networkManager"><see cref="NetworkManager"/> relative instance</param>
/// <param name="scene">scene being unloaded</param>
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<NetworkObject>(FindObjectsSortMode.InstanceID).Where((c) => c.IsSpawned);
#else
var networkObjects = Object.FindObjectsOfType<NetworkObject>().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<NetworkObject>())
{
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);
}
}
}
}
/// <summary>
/// Sets the client synchronization mode which impacts whether both the server or client take into consideration scenes loaded before
/// starting the <see cref="NetworkManager"/>.
/// </summary>
/// <remarks>
/// <see cref="LoadSceneMode.Single"/>: Does not take preloaded scenes into consideration
/// <see cref="LoadSceneMode.Single"/>: Does take preloaded scenes into consideration
/// </remarks>
/// <param name="networkManager">relative <see cref="NetworkManager"/> instance</param>
/// <param name="mode"><see cref="LoadSceneMode.Single"/> or <see cref="LoadSceneMode.Additive"/></param>
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<string, Dictionary<int, SceneEntry>>());
}
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;
}
/// <summary>
/// During integration testing, if the server loads a scene then
/// we want to start tracking it.
/// </summary>
/// <param name="sceneEvent"></param>
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;
}
}
}
/// <summary>
/// Handles determining if a client should attempt to load a scene during synchronization.
/// </summary>
/// <param name="sceneName">name of the scene to be loaded</param>
/// <param name="isPrimaryScene">when in client synchronization mode single, this determines if the scene is the primary active scene</param>
/// <param name="clientSynchronizationMode">the current client synchronization mode</param>
/// <param name="networkManager"><see cref="NetworkManager"/>relative instance</param>
/// <returns></returns>
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;
}
/// <summary> /// <summary>
/// Constructor now must take NetworkManager /// Constructor now must take NetworkManager
/// </summary> /// </summary>
@@ -410,7 +941,11 @@ namespace Unity.Netcode.TestHelpers.Runtime
} }
} }
QueuedSceneJobs.Clear(); QueuedSceneJobs.Clear();
if (CoroutineRunner != null && CoroutineRunner.gameObject != null)
{
Object.Destroy(CoroutineRunner.gameObject); Object.Destroy(CoroutineRunner.gameObject);
} }
}
} }
} }

View File

@@ -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));
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 50a3b194bb5b8714d883dafd911db1ba
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: bcc9a7faadea4b8ebeb041ee6e395a92
timeCreated: 1679414015

View File

@@ -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<byte> Payload;
public NetworkEvent Event;
}
private static Dictionary<ulong, Queue<MessageData>> s_MessageQueue = new Dictionary<ulong, Queue<MessageData>>();
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<byte> 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<byte>(copy, payload.Offset, payload.Count), Event = NetworkEvent.Data });
}
public override NetworkEvent PollEvent(out ulong clientId, out ArraySegment<byte> 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<byte>() });
}
return data.Event;
}
clientId = 0;
payload = new ArraySegment<byte>();
receiveTime = 0;
return NetworkEvent.Nothing;
}
public override bool StartClient()
{
TransportId = ++HighTransportId;
s_MessageQueue[TransportId] = new Queue<MessageData>();
s_MessageQueue[ServerClientId].Enqueue(new MessageData { Event = NetworkEvent.Connect, FromClientId = TransportId, Payload = new ArraySegment<byte>() });
return true;
}
public override bool StartServer()
{
s_MessageQueue[ServerClientId] = new Queue<MessageData>();
return true;
}
public override void DisconnectRemoteClient(ulong clientId)
{
s_MessageQueue[clientId].Enqueue(new MessageData { Event = NetworkEvent.Disconnect, FromClientId = TransportId, Payload = new ArraySegment<byte>() });
}
public override void DisconnectLocalClient()
{
s_MessageQueue[ServerClientId].Enqueue(new MessageData { Event = NetworkEvent.Disconnect, FromClientId = TransportId, Payload = new ArraySegment<byte>() });
}
public override ulong GetCurrentRtt(ulong clientId)
{
return 0;
}
public override void Shutdown()
{
}
public override void Initialize(NetworkManager networkManager = null)
{
NetworkManager = networkManager;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 335908e9a37f428ba087acf00563c7be
timeCreated: 1679415868

View File

@@ -2,12 +2,14 @@ using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using NUnit.Framework; using NUnit.Framework;
using Unity.Netcode.RuntimeTests;
using Unity.Netcode.Transports.UTP;
using UnityEngine; using UnityEngine;
using UnityEngine.SceneManagement; using UnityEngine.SceneManagement;
using UnityEngine.TestTools; using UnityEngine.TestTools;
using System.Runtime.CompilerServices;
using Unity.Netcode.RuntimeTests;
using Object = UnityEngine.Object; using Object = UnityEngine.Object;
namespace Unity.Netcode.TestHelpers.Runtime namespace Unity.Netcode.TestHelpers.Runtime
@@ -22,6 +24,7 @@ namespace Unity.Netcode.TestHelpers.Runtime
/// determine how clients will load scenes /// determine how clients will load scenes
/// </summary> /// </summary>
internal static bool IsRunning { get; private set; } internal static bool IsRunning { get; private set; }
protected static TimeoutHelper s_GlobalTimeoutHelper = new TimeoutHelper(8.0f); protected static TimeoutHelper s_GlobalTimeoutHelper = new TimeoutHelper(8.0f);
protected static WaitForSecondsRealtime s_DefaultWaitForTick = new WaitForSecondsRealtime(1.0f / k_DefaultTickRate); 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<ulong, NetworkObject>()); s_GlobalNetworkObjects.Add(networkObject.NetworkManager.LocalClientId, new Dictionary<ulong, NetworkObject>());
} }
if (s_GlobalNetworkObjects[networkObject.NetworkManager.LocalClientId].ContainsKey(networkObject.NetworkObjectId)) if (s_GlobalNetworkObjects[networkObject.NetworkManager.LocalClientId].ContainsKey(networkObject.NetworkObjectId))
{ {
if (s_GlobalNetworkObjects[networkObject.NetworkManager.LocalClientId] == null) if (s_GlobalNetworkObjects[networkObject.NetworkManager.LocalClientId] == null)
@@ -143,6 +147,75 @@ namespace Unity.Netcode.TestHelpers.Runtime
/// </remarks> /// </remarks>
protected bool m_BypassConnectionTimeout { get; set; } protected bool m_BypassConnectionTimeout { get; set; }
/// <summary>
/// Enables "Time Travel" within the test, which swaps the time provider for the SDK from Unity's
/// <see cref="Time"/> class to <see cref="MockTimeProvider"/>, and also swaps the transport implementation
/// from <see cref="UnityTransport"/> to <see cref="MockTransport"/>.
///
/// 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:
///
/// <see cref="TimeTravel"/>: Simulates a specific number of frames passing over a specific time period
/// <see cref="TimeTravelToNextTick"/>: Skips forward to the next tick, siumlating at the current application frame rate
/// <see cref="WaitForConditionOrTimeOutWithTimeTravel(Func{bool},int)"/>: Simulates frames at the application frame rate until the given condition is true
/// <see cref="WaitForMessageReceivedWithTimeTravel{T}"/>: Simulates frames at the application frame rate until the required message is received
/// <see cref="WaitForMessagesReceivedWithTimeTravel"/>: Simulates frames at the application frame rate until the required messages are received
/// <see cref="StartServerAndClientsWithTimeTravel"/>: Starts a server and client and allows them to connect via simulated frames
/// <see cref="CreateAndStartNewClientWithTimeTravel"/>: Creates a client and waits for it to connect via simulated frames
/// <see cref="WaitForClientsConnectedOrTimeOutWithTimeTravel(Unity.Netcode.NetworkManager[])"/> Simulates frames at the application frame rate until the given clients are connected
/// <see cref="StopOneClientWithTimeTravel"/>: Stops a client and simulates frames until it's fully disconnected.
///
/// When time travel is enabled, <see cref="NetcodeIntegrationTest"/> 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:
/// <see cref="OnTimeTravelStartedServerAndClients"/> - called when server and clients are started
/// <see cref="OnTimeTravelServerAndClientsConnected"/> - 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
/// <see cref="MockTimeProvider.TimeTravel"/> to cover the equivalent time span in the mock time provider.
/// (Calling <see cref="MockTimeProvider.TimeTravel"/> instead of <see cref="TimeTravel"/>
/// 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.
/// </summary>
protected virtual bool m_EnableTimeTravel => false;
/// <summary>
/// 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.
/// </summary>
protected virtual bool m_SetupIsACoroutine => true;
/// <summary>
/// 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.
/// </summary>
protected virtual bool m_TearDownIsACoroutine => true;
/// <summary> /// <summary>
/// Used to display the various integration test /// Used to display the various integration test
/// stages and can be used to log verbose information /// stages and can be used to log verbose information
@@ -216,20 +289,54 @@ namespace Unity.Netcode.TestHelpers.Runtime
yield return null; yield return null;
} }
/// <summary>
/// Called before creating and starting the server and clients
/// Note: For <see cref="NetworkManagerInstatiationMode.AllTests"/> and
/// <see cref="NetworkManagerInstatiationMode.PerTest"/> mode integration tests.
/// For those two modes, if you want to have access to the server or client
/// <see cref="NetworkManager"/>s then override <see cref="OnServerAndClientsCreated"/>.
/// <see cref="m_ServerNetworkManager"/> and <see cref="m_ClientNetworkManagers"/>
/// </summary>
protected virtual void OnInlineSetup()
{
}
[UnitySetUp] [UnitySetUp]
public IEnumerator SetUp() public IEnumerator SetUp()
{ {
VerboseDebug($"Entering {nameof(SetUp)}"); VerboseDebug($"Entering {nameof(SetUp)}");
NetcodeLogAssert = new NetcodeLogAssert(); NetcodeLogAssert = new NetcodeLogAssert();
if (m_SetupIsACoroutine)
{
yield return OnSetup(); yield return OnSetup();
}
else
{
OnInlineSetup();
}
if (m_EnableTimeTravel)
{
MockTimeProvider.Reset();
ComponentFactory.Register<IRealTimeProvider>(manager => new MockTimeProvider());
}
if (m_NetworkManagerInstatiationMode == NetworkManagerInstatiationMode.AllTests && m_ServerNetworkManager == null || if (m_NetworkManagerInstatiationMode == NetworkManagerInstatiationMode.AllTests && m_ServerNetworkManager == null ||
m_NetworkManagerInstatiationMode == NetworkManagerInstatiationMode.PerTest) m_NetworkManagerInstatiationMode == NetworkManagerInstatiationMode.PerTest)
{ {
CreateServerAndClients(); CreateServerAndClients();
if (m_EnableTimeTravel)
{
StartServerAndClientsWithTimeTravel();
}
else
{
yield return StartServerAndClients(); yield return StartServerAndClients();
} }
}
VerboseDebug($"Exiting {nameof(SetUp)}"); VerboseDebug($"Exiting {nameof(SetUp)}");
} }
@@ -294,6 +401,7 @@ namespace Unity.Netcode.TestHelpers.Runtime
{ {
clientNetworkManagersList.Remove(networkManager); clientNetworkManagersList.Remove(networkManager);
} }
m_ClientNetworkManagers = clientNetworkManagersList.ToArray(); m_ClientNetworkManagers = clientNetworkManagersList.ToArray();
m_NumberOfClients = clientNetworkManagersList.Count; m_NumberOfClients = clientNetworkManagersList.Count;
} }
@@ -304,7 +412,6 @@ namespace Unity.Netcode.TestHelpers.Runtime
/// </summary> /// </summary>
protected virtual void OnNewClientCreated(NetworkManager networkManager) protected virtual void OnNewClientCreated(NetworkManager networkManager)
{ {
} }
/// <summary> /// <summary>
@@ -322,7 +429,6 @@ namespace Unity.Netcode.TestHelpers.Runtime
/// </summary> /// </summary>
protected virtual void OnNewClientStartedAndConnected(NetworkManager networkManager) protected virtual void OnNewClientStartedAndConnected(NetworkManager networkManager)
{ {
} }
/// <summary> /// <summary>
@@ -331,7 +437,7 @@ namespace Unity.Netcode.TestHelpers.Runtime
/// </summary> /// </summary>
protected IEnumerator CreateAndStartNewClient() protected IEnumerator CreateAndStartNewClient()
{ {
var networkManager = NetcodeIntegrationTestHelpers.CreateNewClient(m_ClientNetworkManagers.Length); var networkManager = NetcodeIntegrationTestHelpers.CreateNewClient(m_ClientNetworkManagers.Length, m_EnableTimeTravel);
networkManager.NetworkConfig.PlayerPrefab = m_PlayerPrefab; networkManager.NetworkConfig.PlayerPrefab = m_PlayerPrefab;
// Notification that the new client (NetworkManager) has been created // Notification that the new client (NetworkManager) has been created
@@ -356,13 +462,53 @@ namespace Unity.Netcode.TestHelpers.Runtime
if (s_GlobalTimeoutHelper.TimedOut) if (s_GlobalTimeoutHelper.TimedOut)
{ {
AddRemoveNetworkManager(networkManager, false); AddRemoveNetworkManager(networkManager, false);
Object.Destroy(networkManager.gameObject); Object.DestroyImmediate(networkManager.gameObject);
} }
AssertOnTimeout($"{nameof(CreateAndStartNewClient)} timed out waiting for the new client to be connected!"); AssertOnTimeout($"{nameof(CreateAndStartNewClient)} timed out waiting for the new client to be connected!");
ClientNetworkManagerPostStart(networkManager); ClientNetworkManagerPostStart(networkManager);
VerboseDebug($"[{networkManager.name}] Created and connected!"); VerboseDebug($"[{networkManager.name}] Created and connected!");
} }
/// <summary>
/// This will create, start, and connect a new client while in the middle of an
/// integration test.
/// </summary>
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!");
}
/// <summary> /// <summary>
/// This will stop a client while in the middle of an integration test /// This will stop a client while in the middle of an integration test
/// </summary> /// </summary>
@@ -373,6 +519,16 @@ namespace Unity.Netcode.TestHelpers.Runtime
yield return WaitForConditionOrTimeOut(() => !networkManager.IsConnectedClient); yield return WaitForConditionOrTimeOut(() => !networkManager.IsConnectedClient);
} }
/// <summary>
/// This will stop a client while in the middle of an integration test
/// </summary>
protected void StopOneClientWithTimeTravel(NetworkManager networkManager, bool destroy = false)
{
NetcodeIntegrationTestHelpers.StopOneClient(networkManager, destroy);
AddRemoveNetworkManager(networkManager, false);
Assert.True(WaitForConditionOrTimeOutWithTimeTravel(() => !networkManager.IsConnectedClient));
}
/// <summary> /// <summary>
/// Creates the server and clients /// Creates the server and clients
/// </summary> /// </summary>
@@ -383,8 +539,13 @@ namespace Unity.Netcode.TestHelpers.Runtime
CreatePlayerPrefab(); CreatePlayerPrefab();
if (m_EnableTimeTravel)
{
m_TargetFrameRate = -1;
}
// Create multiple NetworkManager instances // 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"); Debug.LogError("Failed to create instances");
Assert.Fail("Failed to create instances"); Assert.Fail("Failed to create instances");
@@ -431,6 +592,14 @@ namespace Unity.Netcode.TestHelpers.Runtime
yield return null; yield return null;
} }
/// <summary>
/// Invoked after the server and clients have started.
/// Note: No connection verification has been done at this point
/// </summary>
protected virtual void OnTimeTravelStartedServerAndClients()
{
}
/// <summary> /// <summary>
/// Invoked after the server and clients have started and verified /// Invoked after the server and clients have started and verified
/// their connections with each other. /// their connections with each other.
@@ -440,6 +609,14 @@ namespace Unity.Netcode.TestHelpers.Runtime
yield return null; yield return null;
} }
/// <summary>
/// Invoked after the server and clients have started and verified
/// their connections with each other.
/// </summary>
protected virtual void OnTimeTravelServerAndClientsConnected()
{
}
private void ClientNetworkManagerPostStart(NetworkManager networkManager) private void ClientNetworkManagerPostStart(NetworkManager networkManager)
{ {
networkManager.name = $"NetworkManager - Client - {networkManager.LocalClientId}"; networkManager.name = $"NetworkManager - Client - {networkManager.LocalClientId}";
@@ -466,6 +643,7 @@ namespace Unity.Netcode.TestHelpers.Runtime
{ {
m_PlayerNetworkObjects.Add(playerNetworkObject.NetworkManager.LocalClientId, new Dictionary<ulong, NetworkObject>()); m_PlayerNetworkObjects.Add(playerNetworkObject.NetworkManager.LocalClientId, new Dictionary<ulong, NetworkObject>());
} }
if (!m_PlayerNetworkObjects[playerNetworkObject.NetworkManager.LocalClientId].ContainsKey(networkManager.LocalClientId)) if (!m_PlayerNetworkObjects[playerNetworkObject.NetworkManager.LocalClientId].ContainsKey(networkManager.LocalClientId))
{ {
m_PlayerNetworkObjects[playerNetworkObject.NetworkManager.LocalClientId].Add(networkManager.LocalClientId, playerNetworkObject); m_PlayerNetworkObjects[playerNetworkObject.NetworkManager.LocalClientId].Add(networkManager.LocalClientId, playerNetworkObject);
@@ -495,6 +673,7 @@ namespace Unity.Netcode.TestHelpers.Runtime
{ {
ClientNetworkManagerPostStart(networkManager); ClientNetworkManagerPostStart(networkManager);
} }
if (m_UseHost) if (m_UseHost)
{ {
#if UNITY_2023_1_OR_NEWER #if UNITY_2023_1_OR_NEWER
@@ -509,6 +688,7 @@ namespace Unity.Netcode.TestHelpers.Runtime
{ {
m_PlayerNetworkObjects.Add(playerNetworkObject.NetworkManager.LocalClientId, new Dictionary<ulong, NetworkObject>()); m_PlayerNetworkObjects.Add(playerNetworkObject.NetworkManager.LocalClientId, new Dictionary<ulong, NetworkObject>());
} }
if (!m_PlayerNetworkObjects[playerNetworkObject.NetworkManager.LocalClientId].ContainsKey(m_ServerNetworkManager.LocalClientId)) if (!m_PlayerNetworkObjects[playerNetworkObject.NetworkManager.LocalClientId].ContainsKey(m_ServerNetworkManager.LocalClientId))
{ {
m_PlayerNetworkObjects[playerNetworkObject.NetworkManager.LocalClientId].Add(m_ServerNetworkManager.LocalClientId, playerNetworkObject); 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<ulong, NetworkObject>()); m_PlayerNetworkObjects.Add(playerNetworkObject.NetworkManager.LocalClientId, new Dictionary<ulong, NetworkObject>());
} }
m_PlayerNetworkObjects[playerNetworkObject.NetworkManager.LocalClientId].Add(m_ServerNetworkManager.LocalClientId, playerNetworkObject); m_PlayerNetworkObjects[playerNetworkObject.NetworkManager.LocalClientId].Add(m_ServerNetworkManager.LocalClientId, playerNetworkObject);
} }
} }
@@ -585,6 +766,73 @@ namespace Unity.Netcode.TestHelpers.Runtime
} }
} }
/// <summary>
/// This starts the server and clients as long as <see cref="CanStartServerAndClients"/>
/// returns true.
/// </summary>
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<NetworkObject>(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<NetworkObject>().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<ulong, NetworkObject>());
}
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)}");
}
}
}
/// <summary> /// <summary>
/// Override this method to control when clients /// Override this method to control when clients
/// can fake-load a scene. /// can fake-load a scene.
@@ -660,12 +908,15 @@ namespace Unity.Netcode.TestHelpers.Runtime
m_PlayerNetworkObjects.Clear(); m_PlayerNetworkObjects.Clear();
s_GlobalNetworkObjects.Clear(); s_GlobalNetworkObjects.Clear();
} }
catch (Exception e) { throw e; } catch (Exception e)
{
throw e;
}
finally finally
{ {
if (m_PlayerPrefab != null) if (m_PlayerPrefab != null)
{ {
Object.Destroy(m_PlayerPrefab); Object.DestroyImmediate(m_PlayerPrefab);
m_PlayerPrefab = null; m_PlayerPrefab = null;
} }
} }
@@ -689,17 +940,34 @@ namespace Unity.Netcode.TestHelpers.Runtime
yield return null; yield return null;
} }
protected virtual void OnInlineTearDown()
{
}
[UnityTearDown] [UnityTearDown]
public IEnumerator TearDown() public IEnumerator TearDown()
{ {
IntegrationTestSceneHandler.SceneNameToSceneHandles.Clear();
VerboseDebug($"Entering {nameof(TearDown)}"); VerboseDebug($"Entering {nameof(TearDown)}");
if (m_TearDownIsACoroutine)
{
yield return OnTearDown(); yield return OnTearDown();
}
else
{
OnInlineTearDown();
}
if (m_NetworkManagerInstatiationMode == NetworkManagerInstatiationMode.PerTest) if (m_NetworkManagerInstatiationMode == NetworkManagerInstatiationMode.PerTest)
{ {
ShutdownAndCleanUp(); ShutdownAndCleanUp();
} }
if (m_EnableTimeTravel)
{
ComponentFactory.Deregister<IRealTimeProvider>();
}
VerboseDebug($"Exiting {nameof(TearDown)}"); VerboseDebug($"Exiting {nameof(TearDown)}");
LogWaitForMessages(); LogWaitForMessages();
NetcodeLogAssert.Dispose(); NetcodeLogAssert.Dispose();
@@ -773,6 +1041,7 @@ namespace Unity.Netcode.TestHelpers.Runtime
{ {
continue; continue;
} }
if (CanDestroyNetworkObject(networkObject)) if (CanDestroyNetworkObject(networkObject))
{ {
networkObject.NetworkManagerOwner = m_ServerNetworkManager; networkObject.NetworkManagerOwner = m_ServerNetworkManager;
@@ -831,10 +1100,49 @@ namespace Unity.Netcode.TestHelpers.Runtime
// Otherwise wait for 1 tick interval // Otherwise wait for 1 tick interval
yield return s_DefaultWaitForTick; yield return s_DefaultWaitForTick;
} }
// Stop checking for a timeout // Stop checking for a timeout
timeOutHelper.Stop(); timeOutHelper.Stop();
} }
/// <summary>
/// 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.
/// </summary>
public bool WaitForConditionOrTimeOutWithTimeTravel(Func<bool> 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;
}
/// <summary> /// <summary>
/// This version accepts an IConditionalPredicate implementation to provide /// This version accepts an IConditionalPredicate implementation to provide
/// more flexibility for checking complex conditional cases. /// more flexibility for checking complex conditional cases.
@@ -857,6 +1165,29 @@ namespace Unity.Netcode.TestHelpers.Runtime
conditionalPredicate.Finished(timeOutHelper.TimedOut); conditionalPredicate.Finished(timeOutHelper.TimedOut);
} }
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary> /// <summary>
/// Validates that all remote clients (i.e. non-server) detect they are connected /// 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 /// to the server and that the server reflects the appropriate number of clients
@@ -872,6 +1203,22 @@ namespace Unity.Netcode.TestHelpers.Runtime
m_ServerNetworkManager.ConnectedClients.Count == serverClientCount); m_ServerNetworkManager.ConnectedClients.Count == serverClientCount);
} }
/// <summary>
/// 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.
/// </summary>
/// <param name="clientsToCheck">An array of clients to be checked</param>
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);
}
/// <summary> /// <summary>
/// Overloaded method that just passes in all clients to /// Overloaded method that just passes in all clients to
/// <see cref="WaitForClientsConnectedOrTimeOut(NetworkManager[])"/> /// <see cref="WaitForClientsConnectedOrTimeOut(NetworkManager[])"/>
@@ -881,6 +1228,16 @@ namespace Unity.Netcode.TestHelpers.Runtime
yield return WaitForClientsConnectedOrTimeOut(m_ClientNetworkManagers); yield return WaitForClientsConnectedOrTimeOut(m_ClientNetworkManagers);
} }
/// <summary>
/// Overloaded method that just passes in all clients to
/// <see cref="WaitForClientsConnectedOrTimeOut(NetworkManager[])"/> Uses time travel to simulate this
/// for the given number of frames, simulating delta times at the application frame rate.
/// </summary>
protected bool WaitForClientsConnectedOrTimeOutWithTimeTravel()
{
return WaitForClientsConnectedOrTimeOutWithTimeTravel(m_ClientNetworkManagers);
}
internal IEnumerator WaitForMessageReceived<T>(List<NetworkManager> wiatForReceivedBy, ReceiptType type = ReceiptType.Handled) where T : INetworkMessage internal IEnumerator WaitForMessageReceived<T>(List<NetworkManager> 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 // 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<T>(); messageHook.AssignMessageType<T>();
messageHookEntriesForSpawn.Add(messageHook); messageHookEntriesForSpawn.Add(messageHook);
} }
// Used to determine if all clients received the CreateObjectMessage // Used to determine if all clients received the CreateObjectMessage
var hooks = new MessageHooksConditional(messageHookEntriesForSpawn); var hooks = new MessageHooksConditional(messageHookEntriesForSpawn);
yield return WaitForConditionOrTimeOut(hooks); yield return WaitForConditionOrTimeOut(hooks);
Assert.False(s_GlobalTimeoutHelper.TimedOut); Assert.False(s_GlobalTimeoutHelper.TimedOut);
} }
internal IEnumerator WaitForMessagesReceived(List<Type> messagesInOrder, List<NetworkManager> wiatForReceivedBy, ReceiptType type = ReceiptType.Handled) internal IEnumerator WaitForMessagesReceived(List<Type> messagesInOrder, List<NetworkManager> waitForReceivedBy, ReceiptType type = ReceiptType.Handled)
{ {
// Build our message hook entries tables so we can determine if all clients received spawn or ownership messages // Build our message hook entries tables so we can determine if all clients received spawn or ownership messages
var messageHookEntriesForSpawn = new List<MessageHookEntry>(); var messageHookEntriesForSpawn = new List<MessageHookEntry>();
foreach (var clientNetworkManager in wiatForReceivedBy) foreach (var clientNetworkManager in waitForReceivedBy)
{ {
foreach (var message in messagesInOrder) foreach (var message in messagesInOrder)
{ {
@@ -910,12 +1268,49 @@ namespace Unity.Netcode.TestHelpers.Runtime
messageHookEntriesForSpawn.Add(messageHook); messageHookEntriesForSpawn.Add(messageHook);
} }
} }
// Used to determine if all clients received the CreateObjectMessage // Used to determine if all clients received the CreateObjectMessage
var hooks = new MessageHooksConditional(messageHookEntriesForSpawn); var hooks = new MessageHooksConditional(messageHookEntriesForSpawn);
yield return WaitForConditionOrTimeOut(hooks); yield return WaitForConditionOrTimeOut(hooks);
Assert.False(s_GlobalTimeoutHelper.TimedOut); Assert.False(s_GlobalTimeoutHelper.TimedOut);
} }
internal void WaitForMessageReceivedWithTimeTravel<T>(List<NetworkManager> 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<MessageHookEntry>();
foreach (var clientNetworkManager in waitForReceivedBy)
{
var messageHook = new MessageHookEntry(clientNetworkManager, type);
messageHook.AssignMessageType<T>();
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<Type> messagesInOrder, List<NetworkManager> 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<MessageHookEntry>();
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));
}
/// <summary> /// <summary>
/// Creates a basic NetworkObject test prefab, assigns it to a new /// Creates a basic NetworkObject test prefab, assigns it to a new
/// NetworkPrefab entry, and then adds it to the server and client(s) /// NetworkPrefab entry, and then adds it to the server and client(s)
@@ -1000,6 +1395,7 @@ namespace Unity.Netcode.TestHelpers.Runtime
{ {
gameObjectsSpawned.Add(SpawnObject(prefabNetworkObject, owner, destroyWithScene)); gameObjectsSpawned.Add(SpawnObject(prefabNetworkObject, owner, destroyWithScene));
} }
return gameObjectsSpawned; return gameObjectsSpawned;
} }
@@ -1008,7 +1404,6 @@ namespace Unity.Netcode.TestHelpers.Runtime
/// </summary> /// </summary>
public NetcodeIntegrationTest() public NetcodeIntegrationTest()
{ {
} }
/// <summary> /// <summary>
@@ -1038,7 +1433,7 @@ namespace Unity.Netcode.TestHelpers.Runtime
/// </summary> /// </summary>
protected void AssertOnTimeout(string timeOutErrorMessage, TimeoutHelper assignedTimeoutHelper = null) 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); Assert.False(timeoutHelper.TimedOut, timeOutErrorMessage);
} }
@@ -1054,6 +1449,7 @@ namespace Unity.Netcode.TestHelpers.Runtime
{ {
continue; continue;
} }
VerboseDebug($"Unloading scene {scene.name}-{scene.handle}"); VerboseDebug($"Unloading scene {scene.name}-{scene.handle}");
var asyncOperation = SceneManager.UnloadSceneAsync(scene); 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"); 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; 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"); 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); yield return WaitForTickAndFrames(networkManager, count, totalFrameCount);
} }
/// <summary>
/// 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)
/// </summary>
/// <param name="amountOfTimeInSeconds"></param>
/// <param name="numFramesToSimulate"></param>
protected static void TimeTravel(double amountOfTimeInSeconds, int numFramesToSimulate)
{
var interval = amountOfTimeInSeconds / numFramesToSimulate;
for (var i = 0; i < numFramesToSimulate; ++i)
{
MockTimeProvider.TimeTravel(interval);
SimulateOneFrame();
}
}
/// <summary>
/// Helper function to time travel exactly one tick's worth of time at the current frame and tick rates.
/// </summary>
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);
}
/// <summary>
/// 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.
/// </summary>
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<NetworkBehaviour>(FindObjectsSortMode.InstanceID))
#else
foreach (var behaviour in Object.FindObjectsOfType<NetworkBehaviour>())
#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[] { });
}
}
}
}
} }
} }

View File

@@ -186,7 +186,16 @@ namespace Unity.Netcode.TestHelpers.Runtime
networkManager.NetworkConfig.NetworkTransport = unityTransport; networkManager.NetworkConfig.NetworkTransport = unityTransport;
} }
public static NetworkManager CreateServer() private static void AddMockTransport(NetworkManager networkManager)
{
// Create transport
var mockTransport = networkManager.gameObject.AddComponent<MockTransport>();
// Set the NetworkConfig
networkManager.NetworkConfig ??= new NetworkConfig();
networkManager.NetworkConfig.NetworkTransport = mockTransport;
}
public static NetworkManager CreateServer(bool mockTransport = false)
{ {
// Create gameObject // Create gameObject
var go = new GameObject("NetworkManager - Server"); var go = new GameObject("NetworkManager - Server");
@@ -194,7 +203,14 @@ namespace Unity.Netcode.TestHelpers.Runtime
// Create networkManager component // Create networkManager component
var server = go.AddComponent<NetworkManager>(); var server = go.AddComponent<NetworkManager>();
NetworkManagerInstances.Insert(0, server); NetworkManagerInstances.Insert(0, server);
if (mockTransport)
{
AddMockTransport(server);
}
else
{
AddUnityTransport(server); AddUnityTransport(server);
}
return server; return server;
} }
@@ -206,20 +222,20 @@ namespace Unity.Netcode.TestHelpers.Runtime
/// <param name="clients">The clients NetworkManagers</param> /// <param name="clients">The clients NetworkManagers</param>
/// <param name="targetFrameRate">The targetFrameRate of the Unity engine to use while the multi instance helper is running. Will be reset on shutdown.</param> /// <param name="targetFrameRate">The targetFrameRate of the Unity engine to use while the multi instance helper is running. Will be reset on shutdown.</param>
/// <param name="serverFirst">This determines if the server or clients will be instantiated first (defaults to server first)</param> /// <param name="serverFirst">This determines if the server or clients will be instantiated first (defaults to server first)</param>
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<NetworkManager>(); s_NetworkManagerInstances = new List<NetworkManager>();
server = null; server = null;
if (serverFirst) if (serverFirst)
{ {
server = CreateServer(); server = CreateServer(useMockTransport);
} }
CreateNewClients(clientCount, out clients); CreateNewClients(clientCount, out clients, useMockTransport);
if (!serverFirst) if (!serverFirst)
{ {
server = CreateServer(); server = CreateServer(useMockTransport);
} }
s_OriginalTargetFrameRate = Application.targetFrameRate; s_OriginalTargetFrameRate = Application.targetFrameRate;
@@ -228,13 +244,20 @@ namespace Unity.Netcode.TestHelpers.Runtime
return true; return true;
} }
internal static NetworkManager CreateNewClient(int identifier) internal static NetworkManager CreateNewClient(int identifier, bool mockTransport = false)
{ {
// Create gameObject // Create gameObject
var go = new GameObject("NetworkManager - Client - " + identifier); var go = new GameObject("NetworkManager - Client - " + identifier);
// Create networkManager component // Create networkManager component
var networkManager = go.AddComponent<NetworkManager>(); var networkManager = go.AddComponent<NetworkManager>();
if (mockTransport)
{
AddMockTransport(networkManager);
}
else
{
AddUnityTransport(networkManager); AddUnityTransport(networkManager);
}
return networkManager; return networkManager;
} }
@@ -244,13 +267,13 @@ namespace Unity.Netcode.TestHelpers.Runtime
/// </summary> /// </summary>
/// <param name="clientCount">The amount of clients</param> /// <param name="clientCount">The amount of clients</param>
/// <param name="clients"></param> /// <param name="clients"></param>
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]; clients = new NetworkManager[clientCount];
for (int i = 0; i < clientCount; i++) for (int i = 0; i < clientCount; i++)
{ {
// Create networkManager component // Create networkManager component
clients[i] = CreateNewClient(i); clients[i] = CreateNewClient(i, useMockTransport);
} }
NetworkManagerInstances.AddRange(clients); NetworkManagerInstances.AddRange(clients);
@@ -314,7 +337,7 @@ namespace Unity.Netcode.TestHelpers.Runtime
{ {
if (networkManager.gameObject != null) if (networkManager.gameObject != null)
{ {
Object.Destroy(networkManager.gameObject); Object.DestroyImmediate(networkManager.gameObject);
} }
} }
@@ -339,6 +362,12 @@ namespace Unity.Netcode.TestHelpers.Runtime
return true; return true;
} }
private static bool VerifySceneIsValidForClientsToUnload(Scene scene)
{
// Unless specifically set, we always return false
return false;
}
/// <summary> /// <summary>
/// This registers scene validation callback for the server to prevent it from telling connecting /// 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 /// 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) if (networkManager.IsServer && networkManager.SceneManager.VerifySceneBeforeLoading == null)
{ {
networkManager.SceneManager.VerifySceneBeforeLoading = VerifySceneIsValidForClientsToLoad; networkManager.SceneManager.VerifySceneBeforeLoading = VerifySceneIsValidForClientsToLoad;
// If a unit/integration test does not handle this on their own, then Ignore the validation warning // If a unit/integration test does not handle this on their own, then Ignore the validation warning
networkManager.SceneManager.DisableValidationWarnings(true); 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 // Register the test runner scene so it will be able to synchronize NetworkObjects without logging a
// warning about using the currently active scene // warning about using the currently active scene
var scene = SceneManager.GetActiveScene(); var scene = SceneManager.GetActiveScene();
@@ -494,8 +534,10 @@ namespace Unity.Netcode.TestHelpers.Runtime
Assert.IsNotNull(server, prefabCreateAssertError); Assert.IsNotNull(server, prefabCreateAssertError);
Assert.IsFalse(server.IsListening, prefabCreateAssertError); Assert.IsFalse(server.IsListening, prefabCreateAssertError);
var gameObject = new GameObject(); var gameObject = new GameObject
gameObject.name = baseName; {
name = baseName
};
var networkObject = gameObject.AddComponent<NetworkObject>(); var networkObject = gameObject.AddComponent<NetworkObject>();
networkObject.NetworkManagerOwner = server; networkObject.NetworkManagerOwner = server;
MakeNetworkObjectTestPrefab(networkObject); MakeNetworkObjectTestPrefab(networkObject);
@@ -715,6 +757,40 @@ namespace Unity.Netcode.TestHelpers.Runtime
} }
} }
/// <summary>
/// Gets a NetworkObject instance as it's represented by a certain peer.
/// </summary>
/// <param name="predicate">The predicate used to filter for your target NetworkObject</param>
/// <param name="representation">The representation to get the object from</param>
/// <param name="result">The result</param>
/// <param name="failIfNull">Whether or not to fail if no object is found and result is null</param>
/// <param name="maxFrames">The max frames to wait for</param>
public static void GetNetworkObjectByRepresentationWithTimeTravel(Func<NetworkObject, bool> predicate, NetworkManager representation, ResultWrapper<NetworkObject> 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");
}
}
/// <summary> /// <summary>
/// Waits for a predicate condition to be met /// Waits for a predicate condition to be met
/// </summary> /// </summary>

View File

@@ -1,8 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using UnityEngine;
using NUnit.Framework; using NUnit.Framework;
using Unity.Netcode.Transports.UTP; using Unity.Netcode.Transports.UTP;
using UnityEngine;
namespace Unity.Netcode.TestHelpers.Runtime namespace Unity.Netcode.TestHelpers.Runtime
{ {

View File

@@ -8,30 +8,67 @@ namespace Unity.Netcode.TestHelpers.Runtime
/// </summary> /// </summary>
public class TimeoutHelper public class TimeoutHelper
{ {
private const float k_DefaultTimeOutWaitPeriod = 2.0f; protected const float k_DefaultTimeOutWaitPeriod = 2.0f;
private float m_MaximumTimeBeforeTimeOut; private float m_MaximumTimeBeforeTimeOut;
private float m_TimeOutPeriod; private float m_TimeOutPeriod;
private bool m_IsStarted; protected bool m_IsStarted { get; private set; }
public bool TimedOut { get; internal 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() public void Start()
{ {
m_TimeStopped = 0.0f;
m_TimeStarted = Time.realtimeSinceStartup;
m_MaximumTimeBeforeTimeOut = Time.realtimeSinceStartup + m_TimeOutPeriod; m_MaximumTimeBeforeTimeOut = Time.realtimeSinceStartup + m_TimeOutPeriod;
m_IsStarted = true; m_IsStarted = true;
TimedOut = false; TimedOut = false;
OnStart();
}
protected virtual void OnStop()
{
} }
public void Stop() public void Stop()
{ {
if (m_TimeStopped == 0.0f)
{
m_TimeStopped = Time.realtimeSinceStartup;
}
TimedOut = HasTimedOut(); TimedOut = HasTimedOut();
m_IsStarted = false; m_IsStarted = false;
OnStop();
}
protected virtual bool OnHasTimedOut()
{
return m_IsStarted ? m_MaximumTimeBeforeTimeOut < Time.realtimeSinceStartup : TimedOut;
} }
public bool HasTimedOut() public bool HasTimedOut()
{ {
return m_IsStarted ? m_MaximumTimeBeforeTimeOut < Time.realtimeSinceStartup : TimedOut; return OnHasTimedOut();
} }
public TimeoutHelper(float timeOutPeriod = k_DefaultTimeOutWaitPeriod) public TimeoutHelper(float timeOutPeriod = k_DefaultTimeOutWaitPeriod)
@@ -39,4 +76,70 @@ namespace Unity.Netcode.TestHelpers.Runtime
m_TimeOutPeriod = timeOutPeriod; m_TimeOutPeriod = timeOutPeriod;
} }
} }
/// <summary>
/// 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.
/// </summary>
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;
}
}
} }

View File

@@ -10,8 +10,10 @@ namespace Unity.Netcode.EditorTests
{ {
var networkContext = new NetworkContext(); var networkContext = new NetworkContext();
var writer = new FastBufferWriter(20, Allocator.Temp, 20); var writer = new FastBufferWriter(20, Allocator.Temp, 20);
var msg = new DisconnectReasonMessage(); var msg = new DisconnectReasonMessage
msg.Reason = string.Empty; {
Reason = string.Empty
};
msg.Serialize(writer, msg.Version); msg.Serialize(writer, msg.Version);
var fbr = new FastBufferReader(writer, Allocator.Temp); var fbr = new FastBufferReader(writer, Allocator.Temp);
@@ -26,8 +28,10 @@ namespace Unity.Netcode.EditorTests
{ {
var networkContext = new NetworkContext(); var networkContext = new NetworkContext();
var writer = new FastBufferWriter(20, Allocator.Temp, 20); var writer = new FastBufferWriter(20, Allocator.Temp, 20);
var msg = new DisconnectReasonMessage(); var msg = new DisconnectReasonMessage
msg.Reason = "Foo"; {
Reason = "Foo"
};
msg.Serialize(writer, msg.Version); msg.Serialize(writer, msg.Version);
var fbr = new FastBufferReader(writer, Allocator.Temp); var fbr = new FastBufferReader(writer, Allocator.Temp);
@@ -42,8 +46,10 @@ namespace Unity.Netcode.EditorTests
{ {
var networkContext = new NetworkContext(); var networkContext = new NetworkContext();
var writer = new FastBufferWriter(20, Allocator.Temp, 20); var writer = new FastBufferWriter(20, Allocator.Temp, 20);
var msg = new DisconnectReasonMessage(); var msg = new DisconnectReasonMessage
msg.Reason = "ThisStringIsWayLongerThanTwentyBytes"; {
Reason = "ThisStringIsWayLongerThanTwentyBytes"
};
msg.Serialize(writer, msg.Version); msg.Serialize(writer, msg.Version);
var fbr = new FastBufferReader(writer, Allocator.Temp); var fbr = new FastBufferReader(writer, Allocator.Temp);

View File

@@ -222,10 +222,11 @@ namespace Unity.Netcode.EditorTests
{ {
var listMessages = new List<MessagingSystem.MessageWithHandler>(); var listMessages = new List<MessagingSystem.MessageWithHandler>();
var messageWithHandler = new MessagingSystem.MessageWithHandler(); var messageWithHandler = new MessagingSystem.MessageWithHandler
{
messageWithHandler.MessageType = typeof(zzzLateLexicographicNetworkMessage); MessageType = typeof(zzzLateLexicographicNetworkMessage),
messageWithHandler.GetVersion = MessagingSystem.CreateMessageAndGetVersion<zzzLateLexicographicNetworkMessage>; GetVersion = MessagingSystem.CreateMessageAndGetVersion<zzzLateLexicographicNetworkMessage>
};
listMessages.Add(messageWithHandler); listMessages.Add(messageWithHandler);
messageWithHandler.MessageType = typeof(ConnectionRequestMessage); messageWithHandler.MessageType = typeof(ConnectionRequestMessage);

View File

@@ -149,7 +149,7 @@ namespace Unity.Netcode.EditorTests
var v1 = new VersionedTestMessage_v1(); var v1 = new VersionedTestMessage_v1();
v1.Deserialize(reader, ref context, receivedMessageVersion); v1.Deserialize(reader, ref context, receivedMessageVersion);
A = v1.A; A = v1.A;
D = (float)v1.D; D = v1.D;
E = k_DefaultE; E = k_DefaultE;
Upgraded = true; Upgraded = true;
return true; return true;

View File

@@ -1,9 +1,9 @@
using System.Collections.Generic; using System.Collections.Generic;
using NUnit.Framework; using NUnit.Framework;
using UnityEngine;
using Unity.Netcode.Editor; using Unity.Netcode.Editor;
using Unity.Netcode.Transports.UTP; using Unity.Netcode.Transports.UTP;
using UnityEditor.SceneManagement; using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement; using UnityEngine.SceneManagement;
using UnityEngine.TestTools; using UnityEngine.TestTools;
@@ -130,9 +130,11 @@ namespace Unity.Netcode.EditorTests
overridingTargetPrefab.GlobalObjectIdHash = 3; overridingTargetPrefab.GlobalObjectIdHash = 3;
sourcePrefabToOverride.GlobalObjectIdHash = 4; sourcePrefabToOverride.GlobalObjectIdHash = 4;
networkConfig.OldPrefabList = new List<NetworkPrefab>(); networkConfig.OldPrefabList = new List<NetworkPrefab>
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 }); new NetworkPrefab { Prefab = regularPrefab.gameObject },
new NetworkPrefab { Prefab = overriddenPrefab.gameObject, Override = NetworkPrefabOverride.Prefab, OverridingTargetPrefab = overridingTargetPrefab.gameObject, SourcePrefabToOverride = sourcePrefabToOverride.gameObject, SourceHashToOverride = 123456 }
};
networkConfig.InitializePrefabs(); networkConfig.InitializePrefabs();
@@ -159,15 +161,20 @@ namespace Unity.Netcode.EditorTests
// Setup // Setup
var networkManagerObject = new GameObject(nameof(NestedNetworkObjectPrefabCheck)); var networkManagerObject = new GameObject(nameof(NestedNetworkObjectPrefabCheck));
var networkManager = networkManagerObject.AddComponent<NetworkManager>(); var networkManager = networkManagerObject.AddComponent<NetworkManager>();
networkManager.NetworkConfig = new NetworkConfig(); networkManager.NetworkConfig = new NetworkConfig
networkManager.NetworkConfig.NetworkTransport = networkManager.gameObject.AddComponent<UnityTransport>(); {
NetworkTransport = networkManager.gameObject.AddComponent<UnityTransport>()
};
var networkManagerObject2 = new GameObject(nameof(NestedNetworkObjectPrefabCheck)); var networkManagerObject2 = new GameObject(nameof(NestedNetworkObjectPrefabCheck));
var networkManager2 = networkManagerObject2.AddComponent<NetworkManager>(); var networkManager2 = networkManagerObject2.AddComponent<NetworkManager>();
networkManager2.NetworkConfig = new NetworkConfig(); networkManager2.NetworkConfig = new NetworkConfig
networkManager2.NetworkConfig.NetworkTransport = networkManager.gameObject.AddComponent<UnityTransport>(); {
NetworkTransport = networkManager.gameObject.AddComponent<UnityTransport>()
};
var object1 = new GameObject("Object 1").AddComponent<NetworkObject>(); var object1 = new GameObject("Object 1").AddComponent<NetworkObject>();
var object2 = new GameObject("Object 2").AddComponent<NetworkObject>(); var object2 = new GameObject("Object 2").AddComponent<NetworkObject>();
var object3 = new GameObject("Object 3").AddComponent<NetworkObject>(); var object3 = new GameObject("Object 3").AddComponent<NetworkObject>();
@@ -205,13 +212,17 @@ namespace Unity.Netcode.EditorTests
// Setup // Setup
var networkManagerObject = new GameObject(nameof(NestedNetworkObjectPrefabCheck)); var networkManagerObject = new GameObject(nameof(NestedNetworkObjectPrefabCheck));
var networkManager = networkManagerObject.AddComponent<NetworkManager>(); var networkManager = networkManagerObject.AddComponent<NetworkManager>();
networkManager.NetworkConfig = new NetworkConfig(); networkManager.NetworkConfig = new NetworkConfig
networkManager.NetworkConfig.NetworkTransport = networkManager.gameObject.AddComponent<UnityTransport>(); {
NetworkTransport = networkManager.gameObject.AddComponent<UnityTransport>()
};
var networkManagerObject2 = new GameObject(nameof(NestedNetworkObjectPrefabCheck)); var networkManagerObject2 = new GameObject(nameof(NestedNetworkObjectPrefabCheck));
var networkManager2 = networkManagerObject2.AddComponent<NetworkManager>(); var networkManager2 = networkManagerObject2.AddComponent<NetworkManager>();
networkManager2.NetworkConfig = new NetworkConfig(); networkManager2.NetworkConfig = new NetworkConfig
networkManager2.NetworkConfig.NetworkTransport = networkManager.gameObject.AddComponent<UnityTransport>(); {
NetworkTransport = networkManager.gameObject.AddComponent<UnityTransport>()
};
var object1 = new GameObject("Object 1").AddComponent<NetworkObject>(); var object1 = new GameObject("Object 1").AddComponent<NetworkObject>();
var object2 = new GameObject("Object 2").AddComponent<NetworkObject>(); var object2 = new GameObject("Object 2").AddComponent<NetworkObject>();
@@ -251,13 +262,17 @@ namespace Unity.Netcode.EditorTests
// Setup // Setup
var networkManagerObject = new GameObject(nameof(NestedNetworkObjectPrefabCheck)); var networkManagerObject = new GameObject(nameof(NestedNetworkObjectPrefabCheck));
var networkManager = networkManagerObject.AddComponent<NetworkManager>(); var networkManager = networkManagerObject.AddComponent<NetworkManager>();
networkManager.NetworkConfig = new NetworkConfig(); networkManager.NetworkConfig = new NetworkConfig
networkManager.NetworkConfig.NetworkTransport = networkManager.gameObject.AddComponent<UnityTransport>(); {
NetworkTransport = networkManager.gameObject.AddComponent<UnityTransport>()
};
var networkManagerObject2 = new GameObject(nameof(NestedNetworkObjectPrefabCheck)); var networkManagerObject2 = new GameObject(nameof(NestedNetworkObjectPrefabCheck));
var networkManager2 = networkManagerObject2.AddComponent<NetworkManager>(); var networkManager2 = networkManagerObject2.AddComponent<NetworkManager>();
networkManager2.NetworkConfig = new NetworkConfig(); networkManager2.NetworkConfig = new NetworkConfig
networkManager2.NetworkConfig.NetworkTransport = networkManager.gameObject.AddComponent<UnityTransport>(); {
NetworkTransport = networkManager.gameObject.AddComponent<UnityTransport>()
};
var object1 = new GameObject("Object 1").AddComponent<NetworkObject>(); var object1 = new GameObject("Object 1").AddComponent<NetworkObject>();
var object2 = new GameObject("Object 2").AddComponent<NetworkObject>(); var object2 = new GameObject("Object 2").AddComponent<NetworkObject>();

View File

@@ -1,7 +1,7 @@
using NUnit.Framework; using NUnit.Framework;
using UnityEngine;
using Unity.Netcode.Editor.Configuration; using Unity.Netcode.Editor.Configuration;
using UnityEditor; using UnityEditor;
using UnityEngine;
namespace Unity.Netcode.EditorTests namespace Unity.Netcode.EditorTests
{ {

View File

@@ -7,8 +7,6 @@ namespace Unity.Netcode.EditorTests
{ {
public abstract class BaseFastBufferReaderWriterTest public abstract class BaseFastBufferReaderWriterTest
{ {
#region Test Types
protected enum ByteEnum : byte protected enum ByteEnum : byte
{ {
A, A,
@@ -78,7 +76,6 @@ namespace Unity.Netcode.EditorTests
WriteDirect, WriteDirect,
WriteSafe WriteSafe
} }
#endregion
protected abstract void RunTypeTest<T>(T valueToTest) where T : unmanaged; protected abstract void RunTypeTest<T>(T valueToTest) where T : unmanaged;
@@ -88,7 +85,6 @@ namespace Unity.Netcode.EditorTests
protected abstract void RunTypeArrayTestSafe<T>(T[] valueToTest) where T : unmanaged; protected abstract void RunTypeArrayTestSafe<T>(T[] valueToTest) where T : unmanaged;
#region Helpers
protected TestStruct GetTestStruct() protected TestStruct GetTestStruct()
{ {
var random = new Random(); var random = new Random();
@@ -98,7 +94,7 @@ namespace Unity.Netcode.EditorTests
A = (byte)random.Next(), A = (byte)random.Next(),
B = (short)random.Next(), B = (short)random.Next(),
C = (ushort)random.Next(), C = (ushort)random.Next(),
D = (int)random.Next(), D = random.Next(),
E = (uint)random.Next(), E = (uint)random.Next(),
F = ((long)random.Next() << 32) + random.Next(), F = ((long)random.Next() << 32) + random.Next(),
G = ((ulong)random.Next() << 32) + (ulong)random.Next(), G = ((ulong)random.Next() << 32) + (ulong)random.Next(),
@@ -111,9 +107,6 @@ namespace Unity.Netcode.EditorTests
return testStruct; return testStruct;
} }
#endregion
private void RunTestWithWriteType<T>(T val, WriteType wt, FastBufferWriter.ForPrimitives _ = default) where T : unmanaged private void RunTestWithWriteType<T>(T val, WriteType wt, FastBufferWriter.ForPrimitives _ = default) where T : unmanaged
{ {
switch (wt) switch (wt)
@@ -149,7 +142,7 @@ namespace Unity.Netcode.EditorTests
} }
else if (testType == typeof(int)) else if (testType == typeof(int))
{ {
RunTestWithWriteType((int)random.Next(), writeType); RunTestWithWriteType(random.Next(), writeType);
} }
else if (testType == typeof(uint)) else if (testType == typeof(uint))
{ {
@@ -354,10 +347,10 @@ namespace Unity.Netcode.EditorTests
else if (testType == typeof(long)) else if (testType == typeof(long))
{ {
RunTypeTestLocal(new[]{ RunTypeTestLocal(new[]{
((long)random.Next() << 32) + (long)random.Next(), ((long)random.Next() << 32) + random.Next(),
((long)random.Next() << 32) + (long)random.Next(), ((long)random.Next() << 32) + random.Next(),
((long)random.Next() << 32) + (long)random.Next(), ((long)random.Next() << 32) + random.Next(),
((long)random.Next() << 32) + (long)random.Next() ((long)random.Next() << 32) + random.Next()
}, writeType); }, writeType);
} }
else if (testType == typeof(ulong)) else if (testType == typeof(ulong))

View File

@@ -10,8 +10,6 @@ namespace Unity.Netcode.EditorTests
{ {
public class BytePackerTests public class BytePackerTests
{ {
#region Test Types
private enum ByteEnum : byte private enum ByteEnum : byte
{ {
A, A,
@@ -74,8 +72,6 @@ namespace Unity.Netcode.EditorTests
WriteAsObject WriteAsObject
} }
#endregion
private unsafe void VerifyBytewiseEquality<T>(T value, T otherValue) where T : unmanaged private unsafe void VerifyBytewiseEquality<T>(T value, T otherValue) where T : unmanaged
{ {
byte* asBytePointer = (byte*)&value; byte* asBytePointer = (byte*)&value;

View File

@@ -9,7 +9,6 @@ namespace Unity.Netcode.EditorTests
{ {
public class FastBufferReaderTests : BaseFastBufferReaderWriterTest public class FastBufferReaderTests : BaseFastBufferReaderWriterTest
{ {
#region Common Checks
private void WriteCheckBytes(FastBufferWriter writer, int writeSize, string failMessage = "") private void WriteCheckBytes(FastBufferWriter writer, int writeSize, string failMessage = "")
{ {
Assert.IsTrue(writer.TryBeginWrite(2), "Writer denied write permission"); Assert.IsTrue(writer.TryBeginWrite(2), "Writer denied write permission");
@@ -230,9 +229,7 @@ namespace Unity.Netcode.EditorTests
method.Invoke(reader, args); method.Invoke(reader, args);
value = (T[])args[0]; value = (T[])args[0];
} }
#endregion
#region Generic Checks
protected override unsafe void RunTypeTest<T>(T valueToTest) protected override unsafe void RunTypeTest<T>(T valueToTest)
{ {
var writeSize = FastBufferWriter.GetWriteSize(valueToTest); var writeSize = FastBufferWriter.GetWriteSize(valueToTest);
@@ -343,9 +340,6 @@ namespace Unity.Netcode.EditorTests
} }
} }
#endregion
#region Tests
[Test] [Test]
public void GivenFastBufferWriterContainingValue_WhenReadingUnmanagedType_ValueMatchesWhatWasWritten( public void GivenFastBufferWriterContainingValue_WhenReadingUnmanagedType_ValueMatchesWhatWasWritten(
[Values(typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), typeof(int), typeof(uint), [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); Assert.AreEqual(reader.Handle->AllowedReadMark, 25);
} }
} }
#endregion
} }
} }

View File

@@ -9,9 +9,6 @@ namespace Unity.Netcode.EditorTests
{ {
public class FastBufferWriterTests : BaseFastBufferReaderWriterTest public class FastBufferWriterTests : BaseFastBufferReaderWriterTest
{ {
#region Common Checks
private void WriteCheckBytes(FastBufferWriter writer, int writeSize, string failMessage = "") private void WriteCheckBytes(FastBufferWriter writer, int writeSize, string failMessage = "")
{ {
Assert.IsTrue(writer.TryBeginWrite(2), "Writer denied write permission"); Assert.IsTrue(writer.TryBeginWrite(2), "Writer denied write permission");
@@ -66,10 +63,6 @@ namespace Unity.Netcode.EditorTests
VerifyTypedEquality(valueToTest, writer.GetUnsafePtr()); VerifyTypedEquality(valueToTest, writer.GetUnsafePtr());
} }
#endregion
#region Generic Checks
private void RunMethod<T>(string methodName, FastBufferWriter writer, in T value) where T : unmanaged private void RunMethod<T>(string methodName, FastBufferWriter writer, in T value) where T : unmanaged
{ {
MethodInfo method = typeof(FastBufferWriter).GetMethod(methodName, new[] { typeof(T).MakeByRefType() }); MethodInfo method = typeof(FastBufferWriter).GetMethod(methodName, new[] { typeof(T).MakeByRefType() });
@@ -248,10 +241,7 @@ namespace Unity.Netcode.EditorTests
VerifyCheckBytes(underlyingArray, writeSize); VerifyCheckBytes(underlyingArray, writeSize);
} }
} }
#endregion
#region Tests
[Test, Description("Tests")] [Test, Description("Tests")]
public void WhenWritingUnmanagedType_ValueIsWrittenCorrectly( public void WhenWritingUnmanagedType_ValueIsWrittenCorrectly(
[Values(typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), typeof(int), typeof(uint), [Values(typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), typeof(int), typeof(uint),
@@ -1317,6 +1307,5 @@ namespace Unity.Netcode.EditorTests
Assert.AreEqual(writer.Handle->AllowedWriteMark, 25); Assert.AreEqual(writer.Handle->AllowedWriteMark, 25);
} }
} }
#endregion
} }
} }

View File

@@ -28,7 +28,7 @@ namespace Unity.Netcode.EditorTests
var writer = new DataStreamWriter(data); var writer = new DataStreamWriter(data);
writer.WriteInt(1); writer.WriteInt(1);
writer.WriteByte((byte)42); writer.WriteByte(42);
var reader = new DataStreamReader(data); var reader = new DataStreamReader(data);
var q = new BatchedReceiveQueue(reader); var q = new BatchedReceiveQueue(reader);
@@ -52,9 +52,9 @@ namespace Unity.Netcode.EditorTests
var writer = new DataStreamWriter(data); var writer = new DataStreamWriter(data);
writer.WriteInt(1); writer.WriteInt(1);
writer.WriteByte((byte)42); writer.WriteByte(42);
writer.WriteInt(1); writer.WriteInt(1);
writer.WriteByte((byte)142); writer.WriteByte(142);
var reader = new DataStreamReader(data); var reader = new DataStreamReader(data);
var q = new BatchedReceiveQueue(reader); var q = new BatchedReceiveQueue(reader);
@@ -132,7 +132,7 @@ namespace Unity.Netcode.EditorTests
var writer = new DataStreamWriter(data); var writer = new DataStreamWriter(data);
writer.WriteInt(1); writer.WriteInt(1);
writer.WriteByte((byte)42); writer.WriteByte(42);
var reader = new DataStreamReader(data); var reader = new DataStreamReader(data);
var q = new BatchedReceiveQueue(reader); var q = new BatchedReceiveQueue(reader);
@@ -168,7 +168,7 @@ namespace Unity.Netcode.EditorTests
var writer = new DataStreamWriter(data); var writer = new DataStreamWriter(data);
writer.WriteInt(1); writer.WriteInt(1);
writer.WriteByte((byte)42); writer.WriteByte(42);
var reader = new DataStreamReader(data); var reader = new DataStreamReader(data);
var q = new BatchedReceiveQueue(reader); var q = new BatchedReceiveQueue(reader);

View File

@@ -10,7 +10,8 @@
"Unity.Multiplayer.NetStats", "Unity.Multiplayer.NetStats",
"Unity.Multiplayer.Tools.MetricTypes", "Unity.Multiplayer.Tools.MetricTypes",
"Unity.Multiplayer.Tools.NetStats", "Unity.Multiplayer.Tools.NetStats",
"Unity.Networking.Transport" "Unity.Networking.Transport",
"Unity.Mathematics"
], ],
"optionalUnityReferences": [ "optionalUnityReferences": [
"TestAssemblies" "TestAssemblies"

View File

@@ -1,8 +1,8 @@
using System.Collections; using System.Collections;
using NUnit.Framework; using NUnit.Framework;
using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine; using UnityEngine;
using UnityEngine.TestTools; using UnityEngine.TestTools;
using Unity.Netcode.TestHelpers.Runtime;
using Object = UnityEngine.Object; using Object = UnityEngine.Object;
namespace Unity.Netcode.RuntimeTests namespace Unity.Netcode.RuntimeTests

View File

@@ -1,9 +1,9 @@
using System.Collections; using System.Collections;
using NUnit.Framework; using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
using Unity.Netcode.TestHelpers.Runtime; using Unity.Netcode.TestHelpers.Runtime;
using Unity.Netcode.Transports.UTP; using Unity.Netcode.Transports.UTP;
using UnityEngine;
using UnityEngine.TestTools;
namespace Unity.Netcode.RuntimeTests namespace Unity.Netcode.RuntimeTests
{ {

View File

@@ -1,6 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using UnityEngine;
using Unity.Netcode.TestHelpers.Runtime; using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
namespace Unity.Netcode.RuntimeTests namespace Unity.Netcode.RuntimeTests
{ {

View File

@@ -1,8 +1,8 @@
using System; using System;
using NUnit.Framework; using NUnit.Framework;
using Unity.Collections; using Unity.Collections;
using UnityEngine;
using Unity.Netcode.TestHelpers.Runtime; using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
namespace Unity.Netcode.RuntimeTests namespace Unity.Netcode.RuntimeTests
{ {
@@ -255,14 +255,14 @@ namespace Unity.Netcode.RuntimeTests
// NetworkVariable Value Type Constructor Test Coverage // NetworkVariable Value Type Constructor Test Coverage
m_NetworkVariableBool = new NetworkVariable<bool>(true); m_NetworkVariableBool = new NetworkVariable<bool>(true);
m_NetworkVariableByte = new NetworkVariable<byte>((byte)0); m_NetworkVariableByte = new NetworkVariable<byte>(0);
m_NetworkVariableColor = new NetworkVariable<Color>(new Color(1, 1, 1, 1)); m_NetworkVariableColor = new NetworkVariable<Color>(new Color(1, 1, 1, 1));
m_NetworkVariableColor32 = new NetworkVariable<Color32>(new Color32(1, 1, 1, 1)); m_NetworkVariableColor32 = new NetworkVariable<Color32>(new Color32(1, 1, 1, 1));
m_NetworkVariableDouble = new NetworkVariable<double>(1.0); m_NetworkVariableDouble = new NetworkVariable<double>(1.0);
m_NetworkVariableFloat = new NetworkVariable<float>(1.0f); m_NetworkVariableFloat = new NetworkVariable<float>(1.0f);
m_NetworkVariableInt = new NetworkVariable<int>(1); m_NetworkVariableInt = new NetworkVariable<int>(1);
m_NetworkVariableLong = new NetworkVariable<long>(1); m_NetworkVariableLong = new NetworkVariable<long>(1);
m_NetworkVariableSByte = new NetworkVariable<sbyte>((sbyte)0); m_NetworkVariableSByte = new NetworkVariable<sbyte>(0);
m_NetworkVariableQuaternion = new NetworkVariable<Quaternion>(Quaternion.identity); m_NetworkVariableQuaternion = new NetworkVariable<Quaternion>(Quaternion.identity);
m_NetworkVariableShort = new NetworkVariable<short>(256); m_NetworkVariableShort = new NetworkVariable<short>(256);
m_NetworkVariableVector4 = new NetworkVariable<Vector4>(new Vector4(1, 1, 1, 1)); m_NetworkVariableVector4 = new NetworkVariable<Vector4>(new Vector4(1, 1, 1, 1));

View File

@@ -1,10 +1,10 @@
using System; using System;
using System.Text;
using System.Collections; using System.Collections;
using UnityEngine; using System.Text;
using UnityEngine.TestTools;
using NUnit.Framework; using NUnit.Framework;
using Unity.Netcode.TestHelpers.Runtime; using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
using UnityEngine.TestTools;
namespace Unity.Netcode.RuntimeTests namespace Unity.Netcode.RuntimeTests
{ {
@@ -66,9 +66,11 @@ namespace Unity.Netcode.RuntimeTests
[Test] [Test]
public void VerifyUniqueNetworkConfigPerRequest() public void VerifyUniqueNetworkConfigPerRequest()
{ {
var networkConfig = new NetworkConfig(); var networkConfig = new NetworkConfig
networkConfig.EnableSceneManagement = true; {
networkConfig.TickRate = 30; EnableSceneManagement = true,
TickRate = 30
};
var currentHash = networkConfig.GetConfig(); var currentHash = networkConfig.GetConfig();
networkConfig.EnableSceneManagement = false; networkConfig.EnableSceneManagement = false;
networkConfig.TickRate = 60; networkConfig.TickRate = 60;

View File

@@ -1,11 +1,10 @@
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine; using UnityEngine;
using UnityEngine.TestTools; using UnityEngine.TestTools;
using Unity.Netcode.TestHelpers.Runtime;
using Object = UnityEngine.Object; using Object = UnityEngine.Object;
namespace Unity.Netcode.RuntimeTests namespace Unity.Netcode.RuntimeTests
@@ -194,7 +193,11 @@ namespace Unity.Netcode.RuntimeTests
private int m_NumberOfClientsToLateJoin = 2; 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(); DeferredMessageTestRpcAndNetworkVariableComponent.ClientInstances.Clear();
DeferredMessageTestRpcComponent.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 // Replace the IDeferredMessageManager component with our test one in the component factory
ComponentFactory.Register<IDeferredMessageManager>(networkManager => new TestDeferredMessageManager(networkManager)); ComponentFactory.Register<IDeferredMessageManager>(networkManager => new TestDeferredMessageManager(networkManager));
yield return null;
} }
protected override IEnumerator OnTearDown() protected override void OnInlineTearDown()
{ {
// Revert the IDeferredMessageManager component to its default (DeferredMessageManager) // Revert the IDeferredMessageManager component to its default (DeferredMessageManager)
ComponentFactory.Deregister<IDeferredMessageManager>(); ComponentFactory.Deregister<IDeferredMessageManager>();
m_ClientSpawnCatchers.Clear(); m_ClientSpawnCatchers.Clear();
yield return null;
} }
protected override void OnServerAndClientsCreated() protected override void OnServerAndClientsCreated()
@@ -255,12 +256,12 @@ namespace Unity.Netcode.RuntimeTests
base.OnNewClientCreated(networkManager); base.OnNewClientCreated(networkManager);
} }
private IEnumerator SpawnClients(bool clearTestDeferredMessageManagerCallFlags = true) private void SpawnClients(bool clearTestDeferredMessageManagerCallFlags = true)
{ {
for (int i = 0; i < m_NumberOfClientsToLateJoin; i++) for (int i = 0; i < m_NumberOfClientsToLateJoin; i++)
{ {
// Create and join client // Create and join client
yield return CreateAndStartNewClient(); CreateAndStartNewClientWithTimeTravel();
} }
if (clearTestDeferredMessageManagerCallFlags) if (clearTestDeferredMessageManagerCallFlags)
@@ -308,16 +309,15 @@ namespace Unity.Netcode.RuntimeTests
m_ClientSpawnCatchers.Clear(); 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. // Clear out these values from whatever might have set them during the initial startup.
ClearTestDeferredMessageManagerCallFlags(); 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) foreach (var catcher in m_ClientSpawnCatchers)
{ {
@@ -328,7 +328,7 @@ namespace Unity.Netcode.RuntimeTests
} }
return true; return true;
}); }));
} }
private void ClearTestDeferredMessageManagerCallFlags() private void ClearTestDeferredMessageManagerCallFlags()
@@ -348,28 +348,28 @@ namespace Unity.Netcode.RuntimeTests
Assert.AreEqual(0, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnAddPrefab)); Assert.AreEqual(0, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnAddPrefab));
} }
private IEnumerator WaitForAllClientsToReceive<T>() where T : INetworkMessage private void WaitForAllClientsToReceive<T>() where T : INetworkMessage
{ {
yield return WaitForMessageReceived<T>(m_ClientNetworkManagers.ToList(), ReceiptType.Received); WaitForMessageReceivedWithTimeTravel<T>(m_ClientNetworkManagers.ToList(), ReceiptType.Received);
} }
private IEnumerator WaitForAllClientsToReceive<TFirstMessage, TSecondMessage>() private void WaitForAllClientsToReceive<TFirstMessage, TSecondMessage>()
where TFirstMessage : INetworkMessage where TFirstMessage : INetworkMessage
where TSecondMessage : INetworkMessage where TSecondMessage : INetworkMessage
{ {
yield return WaitForMessagesReceived(new List<Type> WaitForMessagesReceivedWithTimeTravel(new List<Type>
{ {
typeof(TFirstMessage), typeof(TFirstMessage),
typeof(TSecondMessage) typeof(TSecondMessage)
}, m_ClientNetworkManagers.ToList(), ReceiptType.Received); }, m_ClientNetworkManagers.ToList(), ReceiptType.Received);
} }
private IEnumerator WaitForAllClientsToReceive<TFirstMessage, TSecondMessage, TThirdMessage>() private void WaitForAllClientsToReceive<TFirstMessage, TSecondMessage, TThirdMessage>()
where TFirstMessage : INetworkMessage where TFirstMessage : INetworkMessage
where TSecondMessage : INetworkMessage where TSecondMessage : INetworkMessage
where TThirdMessage : INetworkMessage where TThirdMessage : INetworkMessage
{ {
yield return WaitForMessagesReceived(new List<Type> WaitForMessagesReceivedWithTimeTravel(new List<Type>
{ {
typeof(TFirstMessage), typeof(TFirstMessage),
typeof(TSecondMessage), typeof(TSecondMessage),
@@ -377,13 +377,13 @@ namespace Unity.Netcode.RuntimeTests
}, m_ClientNetworkManagers.ToList(), ReceiptType.Received); }, m_ClientNetworkManagers.ToList(), ReceiptType.Received);
} }
private IEnumerator WaitForAllClientsToReceive<TFirstMessage, TSecondMessage, TThirdMessage, TFourthMessage>() private void WaitForAllClientsToReceive<TFirstMessage, TSecondMessage, TThirdMessage, TFourthMessage>()
where TFirstMessage : INetworkMessage where TFirstMessage : INetworkMessage
where TSecondMessage : INetworkMessage where TSecondMessage : INetworkMessage
where TThirdMessage : INetworkMessage where TThirdMessage : INetworkMessage
where TFourthMessage : INetworkMessage where TFourthMessage : INetworkMessage
{ {
yield return WaitForMessagesReceived(new List<Type> WaitForMessagesReceivedWithTimeTravel(new List<Type>
{ {
typeof(TFirstMessage), typeof(TFirstMessage),
typeof(TSecondMessage), typeof(TSecondMessage),
@@ -392,19 +392,19 @@ namespace Unity.Netcode.RuntimeTests
}, m_ClientNetworkManagers.ToList(), ReceiptType.Received); }, m_ClientNetworkManagers.ToList(), ReceiptType.Received);
} }
[UnityTest] [Test]
public IEnumerator WhenAnRpcArrivesBeforeASpawnArrives_ItIsDeferred() public void WhenAnRpcArrivesBeforeASpawnArrives_ItIsDeferred()
{ {
yield return SpawnClients(); SpawnClients();
CatchSpawns(); CatchSpawns();
var serverObject = Object.Instantiate(m_RpcPrefab); var serverObject = Object.Instantiate(m_RpcPrefab);
serverObject.GetComponent<NetworkObject>().NetworkManagerOwner = m_ServerNetworkManager; serverObject.GetComponent<NetworkObject>().NetworkManagerOwner = m_ServerNetworkManager;
serverObject.GetComponent<NetworkObject>().Spawn(); serverObject.GetComponent<NetworkObject>().Spawn();
yield return WaitForClientsToCatchSpawns(); WaitForClientsToCatchSpawns();
serverObject.GetComponent<DeferredMessageTestRpcComponent>().SendTestClientRpc(); serverObject.GetComponent<DeferredMessageTestRpcComponent>().SendTestClientRpc();
yield return WaitForAllClientsToReceive<ClientRpcMessage>(); WaitForAllClientsToReceive<ClientRpcMessage>();
foreach (var client in m_ClientNetworkManagers) foreach (var client in m_ClientNetworkManagers)
{ {
@@ -415,19 +415,19 @@ namespace Unity.Netcode.RuntimeTests
} }
} }
[UnityTest] [Test]
public IEnumerator WhenADespawnArrivesBeforeASpawnArrives_ItIsDeferred() public void WhenADespawnArrivesBeforeASpawnArrives_ItIsDeferred()
{ {
yield return SpawnClients(); SpawnClients();
CatchSpawns(); CatchSpawns();
var serverObject = Object.Instantiate(m_RpcPrefab); var serverObject = Object.Instantiate(m_RpcPrefab);
serverObject.GetComponent<NetworkObject>().NetworkManagerOwner = m_ServerNetworkManager; serverObject.GetComponent<NetworkObject>().NetworkManagerOwner = m_ServerNetworkManager;
serverObject.GetComponent<NetworkObject>().Spawn(); serverObject.GetComponent<NetworkObject>().Spawn();
yield return WaitForClientsToCatchSpawns(); WaitForClientsToCatchSpawns();
serverObject.GetComponent<NetworkObject>().Despawn(false); serverObject.GetComponent<NetworkObject>().Despawn(false);
yield return WaitForAllClientsToReceive<DestroyObjectMessage>(); WaitForAllClientsToReceive<DestroyObjectMessage>();
foreach (var client in m_ClientNetworkManagers) foreach (var client in m_ClientNetworkManagers)
{ {
@@ -438,18 +438,18 @@ namespace Unity.Netcode.RuntimeTests
} }
} }
[UnityTest] [Test]
public IEnumerator WhenAChangeOwnershipMessageArrivesBeforeASpawnArrives_ItIsDeferred() public void WhenAChangeOwnershipMessageArrivesBeforeASpawnArrives_ItIsDeferred()
{ {
yield return SpawnClients(); SpawnClients();
CatchSpawns(); CatchSpawns();
var serverObject = Object.Instantiate(m_RpcPrefab); var serverObject = Object.Instantiate(m_RpcPrefab);
serverObject.GetComponent<NetworkObject>().NetworkManagerOwner = m_ServerNetworkManager; serverObject.GetComponent<NetworkObject>().NetworkManagerOwner = m_ServerNetworkManager;
serverObject.GetComponent<NetworkObject>().Spawn(); serverObject.GetComponent<NetworkObject>().Spawn();
yield return WaitForClientsToCatchSpawns(); WaitForClientsToCatchSpawns();
serverObject.GetComponent<NetworkObject>().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); serverObject.GetComponent<NetworkObject>().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId);
yield return WaitForAllClientsToReceive<ChangeOwnershipMessage>(); WaitForAllClientsToReceive<ChangeOwnershipMessage>();
foreach (var client in m_ClientNetworkManagers) foreach (var client in m_ClientNetworkManagers)
{ {
var manager = (TestDeferredMessageManager)client.DeferredMessageManager; var manager = (TestDeferredMessageManager)client.DeferredMessageManager;
@@ -459,22 +459,22 @@ namespace Unity.Netcode.RuntimeTests
} }
} }
[UnityTest] [Test]
public IEnumerator WhenANetworkVariableDeltaMessageArrivesBeforeASpawnArrives_ItIsDeferred() public void WhenANetworkVariableDeltaMessageArrivesBeforeASpawnArrives_ItIsDeferred()
{ {
m_SkipAddingPrefabsToClient = true; m_SkipAddingPrefabsToClient = true;
yield return SpawnClients(); SpawnClients();
CatchSpawns(); CatchSpawns();
var serverObject = Object.Instantiate(m_NetworkVariablePrefab); var serverObject = Object.Instantiate(m_NetworkVariablePrefab);
serverObject.GetComponent<NetworkObject>().NetworkManagerOwner = m_ServerNetworkManager; serverObject.GetComponent<NetworkObject>().NetworkManagerOwner = m_ServerNetworkManager;
serverObject.GetComponent<NetworkObject>().Spawn(); serverObject.GetComponent<NetworkObject>().Spawn();
yield return WaitForClientsToCatchSpawns(); WaitForClientsToCatchSpawns();
serverObject.GetComponent<DeferredMessageTestNetworkVariableComponent>().TestNetworkVariable.Value = 1; serverObject.GetComponent<DeferredMessageTestNetworkVariableComponent>().TestNetworkVariable.Value = 1;
yield return WaitForAllClientsToReceive<NetworkVariableDeltaMessage>(); WaitForAllClientsToReceive<NetworkVariableDeltaMessage>();
foreach (var client in m_ClientNetworkManagers) 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.")] //[Ignore("Disabling this temporarily until it is migrated into new integration test.")]
public IEnumerator WhenASpawnMessageArrivesBeforeThePrefabIsAvailable_ItIsDeferred() public void WhenASpawnMessageArrivesBeforeThePrefabIsAvailable_ItIsDeferred()
{ {
m_SkipAddingPrefabsToClient = true; m_SkipAddingPrefabsToClient = true;
yield return SpawnClients(); SpawnClients();
var serverObject = Object.Instantiate(m_RpcPrefab); var serverObject = Object.Instantiate(m_RpcPrefab);
serverObject.GetComponent<NetworkObject>().NetworkManagerOwner = m_ServerNetworkManager; serverObject.GetComponent<NetworkObject>().NetworkManagerOwner = m_ServerNetworkManager;
serverObject.GetComponent<NetworkObject>().Spawn(); serverObject.GetComponent<NetworkObject>().Spawn();
yield return WaitForAllClientsToReceive<CreateObjectMessage>(); WaitForAllClientsToReceive<CreateObjectMessage>();
foreach (var client in m_ClientNetworkManagers) foreach (var client in m_ClientNetworkManagers)
{ {
@@ -514,10 +514,10 @@ namespace Unity.Netcode.RuntimeTests
} }
} }
[UnityTest] [Test]
public IEnumerator WhenAnRpcIsDeferred_ItIsProcessedOnSpawn() public void WhenAnRpcIsDeferred_ItIsProcessedOnSpawn()
{ {
yield return WhenAnRpcArrivesBeforeASpawnArrives_ItIsDeferred(); WhenAnRpcArrivesBeforeASpawnArrives_ItIsDeferred();
ReleaseSpawns(); ReleaseSpawns();
foreach (var client in m_ClientNetworkManagers) foreach (var client in m_ClientNetworkManagers)
@@ -532,10 +532,10 @@ namespace Unity.Netcode.RuntimeTests
} }
} }
[UnityTest] [Test]
public IEnumerator WhenADespawnIsDeferred_ItIsProcessedOnSpawn() public void WhenADespawnIsDeferred_ItIsProcessedOnSpawn()
{ {
yield return WhenADespawnArrivesBeforeASpawnArrives_ItIsDeferred(); WhenADespawnArrivesBeforeASpawnArrives_ItIsDeferred();
ReleaseSpawns(); ReleaseSpawns();
foreach (var client in m_ClientNetworkManagers) foreach (var client in m_ClientNetworkManagers)
@@ -551,10 +551,10 @@ namespace Unity.Netcode.RuntimeTests
} }
} }
[UnityTest] [Test]
public IEnumerator WhenAChangeOwnershipMessageIsDeferred_ItIsProcessedOnSpawn() public void WhenAChangeOwnershipMessageIsDeferred_ItIsProcessedOnSpawn()
{ {
yield return WhenAChangeOwnershipMessageArrivesBeforeASpawnArrives_ItIsDeferred(); WhenAChangeOwnershipMessageArrivesBeforeASpawnArrives_ItIsDeferred();
ReleaseSpawns(); ReleaseSpawns();
foreach (var client in m_ClientNetworkManagers) foreach (var client in m_ClientNetworkManagers)
@@ -568,10 +568,10 @@ namespace Unity.Netcode.RuntimeTests
} }
} }
[UnityTest] [Test]
public IEnumerator WhenANetworkVariableDeltaMessageIsDeferred_ItIsProcessedOnSpawn() public void WhenANetworkVariableDeltaMessageIsDeferred_ItIsProcessedOnSpawn()
{ {
yield return WhenANetworkVariableDeltaMessageArrivesBeforeASpawnArrives_ItIsDeferred(); WhenANetworkVariableDeltaMessageArrivesBeforeASpawnArrives_ItIsDeferred();
foreach (var client in m_ClientNetworkManagers) foreach (var client in m_ClientNetworkManagers)
{ {
@@ -592,7 +592,7 @@ namespace Unity.Netcode.RuntimeTests
} }
return true; return true;
} }
yield return WaitForConditionOrTimeOut(HaveAllClientsSpawned); WaitForConditionOrTimeOutWithTimeTravel(HaveAllClientsSpawned);
foreach (var client in m_ClientNetworkManagers) foreach (var client in m_ClientNetworkManagers)
{ {
@@ -605,12 +605,12 @@ namespace Unity.Netcode.RuntimeTests
} }
} }
[UnityTest] [Test]
public IEnumerator WhenASpawnMessageIsDeferred_ItIsProcessedOnAddPrefab() public void WhenASpawnMessageIsDeferred_ItIsProcessedOnAddPrefab()
{ {
// This will prevent spawned clients from adding prefabs // This will prevent spawned clients from adding prefabs
m_SkipAddingPrefabsToClient = true; m_SkipAddingPrefabsToClient = true;
yield return WhenASpawnMessageArrivesBeforeThePrefabIsAvailable_ItIsDeferred(); WhenASpawnMessageArrivesBeforeThePrefabIsAvailable_ItIsDeferred();
// Now add the prefabs // Now add the prefabs
foreach (var client in m_ClientNetworkManagers) foreach (var client in m_ClientNetworkManagers)
@@ -630,7 +630,7 @@ namespace Unity.Netcode.RuntimeTests
} }
return true; return true;
} }
yield return WaitForConditionOrTimeOut(HaveAllClientsSpawned); WaitForConditionOrTimeOutWithTimeTravel(HaveAllClientsSpawned);
// Validate this test // Validate this test
foreach (var client in m_ClientNetworkManagers) foreach (var client in m_ClientNetworkManagers)
@@ -644,28 +644,26 @@ namespace Unity.Netcode.RuntimeTests
} }
} }
protected override bool LogAllMessages => true; [Test]
public void WhenMultipleSpawnTriggeredMessagesAreDeferred_TheyAreAllProcessedOnSpawn()
[UnityTest]
public IEnumerator WhenMultipleSpawnTriggeredMessagesAreDeferred_TheyAreAllProcessedOnSpawn()
{ {
m_SkipAddingPrefabsToClient = true; m_SkipAddingPrefabsToClient = true;
yield return SpawnClients(); SpawnClients();
CatchSpawns(); CatchSpawns();
var serverObject = Object.Instantiate(m_RpcAndNetworkVariablePrefab); var serverObject = Object.Instantiate(m_RpcAndNetworkVariablePrefab);
serverObject.GetComponent<NetworkObject>().NetworkManagerOwner = m_ServerNetworkManager; serverObject.GetComponent<NetworkObject>().NetworkManagerOwner = m_ServerNetworkManager;
serverObject.GetComponent<NetworkObject>().Spawn(); serverObject.GetComponent<NetworkObject>().Spawn();
yield return WaitForClientsToCatchSpawns(); WaitForClientsToCatchSpawns();
serverObject.GetComponent<DeferredMessageTestRpcAndNetworkVariableComponent>().SendTestClientRpc(); serverObject.GetComponent<DeferredMessageTestRpcAndNetworkVariableComponent>().SendTestClientRpc();
serverObject.GetComponent<DeferredMessageTestRpcAndNetworkVariableComponent>().TestNetworkVariable.Value = 1; serverObject.GetComponent<DeferredMessageTestRpcAndNetworkVariableComponent>().TestNetworkVariable.Value = 1;
yield return WaitForAllClientsToReceive<ClientRpcMessage, NetworkVariableDeltaMessage>(); WaitForAllClientsToReceive<ClientRpcMessage, NetworkVariableDeltaMessage>();
serverObject.GetComponent<NetworkObject>().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); serverObject.GetComponent<NetworkObject>().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId);
yield return WaitForAllClientsToReceive<ChangeOwnershipMessage, NetworkVariableDeltaMessage>(); WaitForAllClientsToReceive<ChangeOwnershipMessage, NetworkVariableDeltaMessage>();
foreach (var client in m_ClientNetworkManagers) foreach (var client in m_ClientNetworkManagers)
{ {
@@ -694,8 +692,8 @@ namespace Unity.Netcode.RuntimeTests
} }
return true; return true;
} }
yield return WaitForConditionOrTimeOut(HaveAllClientsSpawned); WaitForConditionOrTimeOutWithTimeTravel(HaveAllClientsSpawned);
yield return new WaitForSeconds(0.1f); TimeTravel(0.1, 1);
// Validate the spawned objects // Validate the spawned objects
foreach (var client in m_ClientNetworkManagers) foreach (var client in m_ClientNetworkManagers)
@@ -711,11 +709,11 @@ namespace Unity.Netcode.RuntimeTests
} }
} }
[UnityTest] [Test]
public IEnumerator WhenMultipleAddPrefabTriggeredMessagesAreDeferred_TheyAreAllProcessedOnAddNetworkPrefab() public void WhenMultipleAddPrefabTriggeredMessagesAreDeferred_TheyAreAllProcessedOnAddNetworkPrefab()
{ {
m_SkipAddingPrefabsToClient = true; m_SkipAddingPrefabsToClient = true;
yield return SpawnClients(); SpawnClients();
var serverObject = Object.Instantiate(m_RpcPrefab); var serverObject = Object.Instantiate(m_RpcPrefab);
serverObject.GetComponent<NetworkObject>().NetworkManagerOwner = m_ServerNetworkManager; serverObject.GetComponent<NetworkObject>().NetworkManagerOwner = m_ServerNetworkManager;
serverObject.GetComponent<NetworkObject>().Spawn(); serverObject.GetComponent<NetworkObject>().Spawn();
@@ -724,7 +722,7 @@ namespace Unity.Netcode.RuntimeTests
serverObject2.GetComponent<NetworkObject>().NetworkManagerOwner = m_ServerNetworkManager; serverObject2.GetComponent<NetworkObject>().NetworkManagerOwner = m_ServerNetworkManager;
serverObject2.GetComponent<NetworkObject>().Spawn(); serverObject2.GetComponent<NetworkObject>().Spawn();
yield return WaitForAllClientsToReceive<CreateObjectMessage, CreateObjectMessage>(); WaitForAllClientsToReceive<CreateObjectMessage, CreateObjectMessage>();
foreach (var client in m_ClientNetworkManagers) foreach (var client in m_ClientNetworkManagers)
{ {
@@ -751,7 +749,7 @@ namespace Unity.Netcode.RuntimeTests
} }
return true; return true;
} }
yield return WaitForConditionOrTimeOut(HaveAllClientsSpawned); WaitForConditionOrTimeOutWithTimeTravel(HaveAllClientsSpawned);
foreach (var client in m_ClientNetworkManagers) foreach (var client in m_ClientNetworkManagers)
@@ -789,11 +787,11 @@ namespace Unity.Netcode.RuntimeTests
} }
} }
[UnityTest] [Test]
public IEnumerator WhenSpawnTriggeredMessagesAreDeferredBeforeThePrefabIsAdded_AddingThePrefabCausesThemToBeProcessed() public void WhenSpawnTriggeredMessagesAreDeferredBeforeThePrefabIsAdded_AddingThePrefabCausesThemToBeProcessed()
{ {
m_SkipAddingPrefabsToClient = true; m_SkipAddingPrefabsToClient = true;
yield return SpawnClients(); SpawnClients();
var serverObject = Object.Instantiate(m_RpcAndNetworkVariablePrefab); var serverObject = Object.Instantiate(m_RpcAndNetworkVariablePrefab);
serverObject.GetComponent<NetworkObject>().NetworkManagerOwner = m_ServerNetworkManager; serverObject.GetComponent<NetworkObject>().NetworkManagerOwner = m_ServerNetworkManager;
@@ -803,11 +801,11 @@ namespace Unity.Netcode.RuntimeTests
serverObject.GetComponent<DeferredMessageTestRpcAndNetworkVariableComponent>().TestNetworkVariable.Value = 1; serverObject.GetComponent<DeferredMessageTestRpcAndNetworkVariableComponent>().TestNetworkVariable.Value = 1;
// TODO: Remove this if we figure out how to work around the NetworkVariableDeltaMessage.Serialized issue at line 59 // 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 // Otherwise, we have to wait for at least 1 tick for the NetworkVariableDeltaMessage to be generated before changing ownership
yield return WaitForAllClientsToReceive<CreateObjectMessage, ClientRpcMessage, NetworkVariableDeltaMessage>(); WaitForAllClientsToReceive<CreateObjectMessage, ClientRpcMessage, NetworkVariableDeltaMessage>();
serverObject.GetComponent<NetworkObject>().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); serverObject.GetComponent<NetworkObject>().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId);
yield return WaitForAllClientsToReceive<ChangeOwnershipMessage, NetworkVariableDeltaMessage>(); WaitForAllClientsToReceive<ChangeOwnershipMessage, NetworkVariableDeltaMessage>();
// Validate messages are deferred and pending // Validate messages are deferred and pending
foreach (var client in m_ClientNetworkManagers) foreach (var client in m_ClientNetworkManagers)
@@ -837,9 +835,9 @@ namespace Unity.Netcode.RuntimeTests
} }
return true; return true;
} }
yield return WaitForConditionOrTimeOut(HaveAllClientsSpawned); WaitForConditionOrTimeOutWithTimeTravel(HaveAllClientsSpawned);
yield return new WaitForSeconds(0.1f); TimeTravel(0.1, 1);
// Validate the test // Validate the test
foreach (var client in m_ClientNetworkManagers) foreach (var client in m_ClientNetworkManagers)
@@ -856,11 +854,11 @@ namespace Unity.Netcode.RuntimeTests
} }
} }
[UnityTest] [Test]
public IEnumerator WhenAMessageIsDeferredForMoreThanTheConfiguredTime_ItIsRemoved([Values(1, 2, 3)] int timeout) public void WhenAMessageIsDeferredForMoreThanTheConfiguredTime_ItIsRemoved([Values(1, 2, 3)] int timeout)
{ {
m_SkipAddingPrefabsToClient = true; m_SkipAddingPrefabsToClient = true;
yield return SpawnClients(); SpawnClients();
CatchSpawns(); CatchSpawns();
foreach (var client in m_ClientNetworkManagers) foreach (var client in m_ClientNetworkManagers)
{ {
@@ -869,7 +867,7 @@ namespace Unity.Netcode.RuntimeTests
var serverObject = Object.Instantiate(m_RpcPrefab); var serverObject = Object.Instantiate(m_RpcPrefab);
serverObject.GetComponent<NetworkObject>().NetworkManagerOwner = m_ServerNetworkManager; serverObject.GetComponent<NetworkObject>().NetworkManagerOwner = m_ServerNetworkManager;
serverObject.GetComponent<NetworkObject>().Spawn(); serverObject.GetComponent<NetworkObject>().Spawn();
yield return WaitForClientsToCatchSpawns(); WaitForClientsToCatchSpawns();
var start = 0f; var start = 0f;
@@ -879,7 +877,7 @@ namespace Unity.Netcode.RuntimeTests
{ {
if (start == 0) if (start == 0)
{ {
start = Time.realtimeSinceStartup; start = client.RealTimeProvider.RealTimeSinceStartup;
} }
}; };
var manager = (TestDeferredMessageManager)client.DeferredMessageManager; var manager = (TestDeferredMessageManager)client.DeferredMessageManager;
@@ -888,7 +886,7 @@ namespace Unity.Netcode.RuntimeTests
serverObject.GetComponent<NetworkObject>().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); serverObject.GetComponent<NetworkObject>().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId);
yield return WaitForAllClientsToReceive<ChangeOwnershipMessage>(); WaitForAllClientsToReceive<ChangeOwnershipMessage>();
foreach (var unused in m_ClientNetworkManagers) foreach (var unused in m_ClientNetworkManagers)
{ {
@@ -901,8 +899,9 @@ namespace Unity.Netcode.RuntimeTests
TestDeferredMessageManager.BeforePurgeDelegate beforePurge = (manager, key) => TestDeferredMessageManager.BeforePurgeDelegate beforePurge = (manager, key) =>
{ {
++purgeCount; ++purgeCount;
var elapsed = Time.realtimeSinceStartup - start; var elapsed = client.RealTimeProvider.RealTimeSinceStartup - start;
Assert.GreaterOrEqual(elapsed, timeout - 0.05f); Debug.Log(client.RealTimeProvider.GetType().FullName);
Assert.GreaterOrEqual(elapsed, timeout);
Assert.AreEqual(1, manager.DeferredMessageCountTotal()); Assert.AreEqual(1, manager.DeferredMessageCountTotal());
Assert.AreEqual(1, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn)); Assert.AreEqual(1, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn));
Assert.AreEqual(1, manager.DeferredMessageCountForKey(IDeferredMessageManager.TriggerType.OnSpawn, key)); Assert.AreEqual(1, manager.DeferredMessageCountForKey(IDeferredMessageManager.TriggerType.OnSpawn, key));
@@ -912,8 +911,20 @@ namespace Unity.Netcode.RuntimeTests
manager.OnBeforePurge = beforePurge; 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() bool HaveAllClientsPurged()
{ {
foreach (var client in m_ClientNetworkManagers) foreach (var client in m_ClientNetworkManagers)
@@ -927,15 +938,18 @@ namespace Unity.Netcode.RuntimeTests
return true; return true;
} }
yield return WaitForConditionOrTimeOut(HaveAllClientsPurged); Assert.IsFalse(HaveAnyClientsPurged());
AssertOnTimeout("Timed out waiting for all clients to purge their deferred messages!");
TimeTravel(0.02, 1);
Assert.IsTrue(HaveAllClientsPurged());
} }
[UnityTest] [Test]
public IEnumerator WhenMultipleMessagesForTheSameObjectAreDeferredForMoreThanTheConfiguredTime_TheyAreAllRemoved([Values(1, 2, 3)] int timeout) public void WhenMultipleMessagesForTheSameObjectAreDeferredForMoreThanTheConfiguredTime_TheyAreAllRemoved([Values(1, 2, 3)] int timeout)
{ {
m_SkipAddingPrefabsToClient = true; m_SkipAddingPrefabsToClient = true;
yield return SpawnClients(); SpawnClients();
CatchSpawns(); CatchSpawns();
foreach (var client in m_ClientNetworkManagers) foreach (var client in m_ClientNetworkManagers)
@@ -945,7 +959,7 @@ namespace Unity.Netcode.RuntimeTests
var serverObject = Object.Instantiate(m_RpcAndNetworkVariablePrefab); var serverObject = Object.Instantiate(m_RpcAndNetworkVariablePrefab);
serverObject.GetComponent<NetworkObject>().NetworkManagerOwner = m_ServerNetworkManager; serverObject.GetComponent<NetworkObject>().NetworkManagerOwner = m_ServerNetworkManager;
serverObject.GetComponent<NetworkObject>().Spawn(); serverObject.GetComponent<NetworkObject>().Spawn();
yield return WaitForClientsToCatchSpawns(); WaitForClientsToCatchSpawns();
var start = 0f; var start = 0f;
@@ -955,7 +969,7 @@ namespace Unity.Netcode.RuntimeTests
{ {
if (start == 0) if (start == 0)
{ {
start = Time.realtimeSinceStartup; start = client.RealTimeProvider.RealTimeSinceStartup;
} }
}; };
var manager = (TestDeferredMessageManager)client.DeferredMessageManager; var manager = (TestDeferredMessageManager)client.DeferredMessageManager;
@@ -966,7 +980,7 @@ namespace Unity.Netcode.RuntimeTests
serverObject.GetComponent<DeferredMessageTestRpcAndNetworkVariableComponent>().TestNetworkVariable.Value = 1; serverObject.GetComponent<DeferredMessageTestRpcAndNetworkVariableComponent>().TestNetworkVariable.Value = 1;
serverObject.GetComponent<NetworkObject>().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); serverObject.GetComponent<NetworkObject>().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId);
yield return WaitForMessagesReceived( WaitForMessagesReceivedWithTimeTravel(
new List<Type> {typeof(ClientRpcMessage), typeof(NetworkVariableDeltaMessage), typeof(ChangeOwnershipMessage), new List<Type> {typeof(ClientRpcMessage), typeof(NetworkVariableDeltaMessage), typeof(ChangeOwnershipMessage),
}, m_ClientNetworkManagers.ToList(), ReceiptType.Received); }, m_ClientNetworkManagers.ToList(), ReceiptType.Received);
@@ -981,8 +995,8 @@ namespace Unity.Netcode.RuntimeTests
TestDeferredMessageManager.BeforePurgeDelegate beforePurge = (manager, key) => TestDeferredMessageManager.BeforePurgeDelegate beforePurge = (manager, key) =>
{ {
++purgeCount; ++purgeCount;
var elapsed = Time.realtimeSinceStartup - start; var elapsed = client.RealTimeProvider.RealTimeSinceStartup - start;
Assert.GreaterOrEqual(elapsed, timeout - 0.25f); Assert.GreaterOrEqual(elapsed, timeout);
Assert.AreEqual(3, manager.DeferredMessageCountTotal()); Assert.AreEqual(3, manager.DeferredMessageCountTotal());
Assert.AreEqual(3, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn)); Assert.AreEqual(3, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn));
Assert.AreEqual(3, manager.DeferredMessageCountForKey(IDeferredMessageManager.TriggerType.OnSpawn, key)); Assert.AreEqual(3, manager.DeferredMessageCountForKey(IDeferredMessageManager.TriggerType.OnSpawn, key));
@@ -992,6 +1006,21 @@ namespace Unity.Netcode.RuntimeTests
manager.OnBeforePurge = beforePurge; 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() bool HaveAllClientsPurged()
{ {
foreach (var client in m_ClientNetworkManagers) foreach (var client in m_ClientNetworkManagers)
@@ -1005,15 +1034,18 @@ namespace Unity.Netcode.RuntimeTests
return true; return true;
} }
yield return WaitForConditionOrTimeOut(HaveAllClientsPurged); Assert.IsFalse(HaveAnyClientsPurged());
AssertOnTimeout("Timed out waiting for all clients to purge their deferred messages!");
TimeTravel(0.02 + timePassedSinceFirstStart, 1);
Assert.IsTrue(HaveAllClientsPurged());
} }
[UnityTest] [Test]
public IEnumerator WhenMultipleMessagesForDifferentObjectsAreDeferredForMoreThanTheConfiguredTime_TheyAreAllRemoved([Values(1, 2, 3)] int timeout) public void WhenMultipleMessagesForDifferentObjectsAreDeferredForMoreThanTheConfiguredTime_TheyAreAllRemoved([Values(1, 2, 3)] int timeout)
{ {
m_SkipAddingPrefabsToClient = true; m_SkipAddingPrefabsToClient = true;
yield return SpawnClients(); SpawnClients();
CatchSpawns(); CatchSpawns();
foreach (var client in m_ClientNetworkManagers) foreach (var client in m_ClientNetworkManagers)
@@ -1028,7 +1060,7 @@ namespace Unity.Netcode.RuntimeTests
serverObject2.GetComponent<NetworkObject>().NetworkManagerOwner = m_ServerNetworkManager; serverObject2.GetComponent<NetworkObject>().NetworkManagerOwner = m_ServerNetworkManager;
serverObject2.GetComponent<NetworkObject>().Spawn(); serverObject2.GetComponent<NetworkObject>().Spawn();
yield return WaitForClientsToCatchSpawns(2); WaitForClientsToCatchSpawns(2);
var start = 0f; var start = 0f;
@@ -1038,7 +1070,7 @@ namespace Unity.Netcode.RuntimeTests
{ {
if (start == 0) if (start == 0)
{ {
start = Time.realtimeSinceStartup; start = client.RealTimeProvider.RealTimeSinceStartup;
} }
}; };
var manager = (TestDeferredMessageManager)client.DeferredMessageManager; var manager = (TestDeferredMessageManager)client.DeferredMessageManager;
@@ -1053,7 +1085,7 @@ namespace Unity.Netcode.RuntimeTests
serverObject2.GetComponent<DeferredMessageTestRpcAndNetworkVariableComponent>().TestNetworkVariable.Value = 1; serverObject2.GetComponent<DeferredMessageTestRpcAndNetworkVariableComponent>().TestNetworkVariable.Value = 1;
serverObject2.GetComponent<NetworkObject>().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); serverObject2.GetComponent<NetworkObject>().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId);
yield return WaitForMessagesReceived( WaitForMessagesReceivedWithTimeTravel(
new List<Type> {typeof(ClientRpcMessage), typeof(NetworkVariableDeltaMessage), typeof(ChangeOwnershipMessage),typeof(ClientRpcMessage), typeof(NetworkVariableDeltaMessage), typeof(ChangeOwnershipMessage), new List<Type> {typeof(ClientRpcMessage), typeof(NetworkVariableDeltaMessage), typeof(ChangeOwnershipMessage),typeof(ClientRpcMessage), typeof(NetworkVariableDeltaMessage), typeof(ChangeOwnershipMessage),
}, m_ClientNetworkManagers.ToList(), ReceiptType.Received); }, m_ClientNetworkManagers.ToList(), ReceiptType.Received);
@@ -1071,7 +1103,7 @@ namespace Unity.Netcode.RuntimeTests
TestDeferredMessageManager.BeforePurgeDelegate beforePurge = (manager, key) => TestDeferredMessageManager.BeforePurgeDelegate beforePurge = (manager, key) =>
{ {
++purgeCount; ++purgeCount;
var elapsed = Time.realtimeSinceStartup - start; var elapsed = client.RealTimeProvider.RealTimeSinceStartup - start;
Assert.GreaterOrEqual(elapsed, timeout - 0.25f); Assert.GreaterOrEqual(elapsed, timeout - 0.25f);
Assert.AreEqual(remainingMessagesTotalThisClient, manager.DeferredMessageCountTotal()); Assert.AreEqual(remainingMessagesTotalThisClient, manager.DeferredMessageCountTotal());
Assert.AreEqual(remainingMessagesTotalThisClient, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn)); Assert.AreEqual(remainingMessagesTotalThisClient, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn));
@@ -1082,7 +1114,7 @@ namespace Unity.Netcode.RuntimeTests
manager.OnBeforePurge = beforePurge; manager.OnBeforePurge = beforePurge;
} }
yield return new WaitForSeconds(timeout + 0.1f); TimeTravel(timeout + 0.1f, 1);
foreach (var client in m_ClientNetworkManagers) foreach (var client in m_ClientNetworkManagers)
{ {
AddPrefabsToClient(client); AddPrefabsToClient(client);
@@ -1095,11 +1127,11 @@ namespace Unity.Netcode.RuntimeTests
} }
} }
[UnityTest] [Test]
public IEnumerator WhenADeferredMessageIsRemoved_OtherMessagesForSameObjectAreRemoved([Values(1, 2, 3)] int timeout) public void WhenADeferredMessageIsRemoved_OtherMessagesForSameObjectAreRemoved([Values(1, 2, 3)] int timeout)
{ {
m_SkipAddingPrefabsToClient = true; m_SkipAddingPrefabsToClient = true;
yield return SpawnClients(); SpawnClients();
CatchSpawns(); CatchSpawns();
foreach (var client in m_ClientNetworkManagers) foreach (var client in m_ClientNetworkManagers)
{ {
@@ -1108,7 +1140,7 @@ namespace Unity.Netcode.RuntimeTests
var serverObject = Object.Instantiate(m_RpcPrefab); var serverObject = Object.Instantiate(m_RpcPrefab);
serverObject.GetComponent<NetworkObject>().NetworkManagerOwner = m_ServerNetworkManager; serverObject.GetComponent<NetworkObject>().NetworkManagerOwner = m_ServerNetworkManager;
serverObject.GetComponent<NetworkObject>().Spawn(); serverObject.GetComponent<NetworkObject>().Spawn();
yield return WaitForClientsToCatchSpawns(); WaitForClientsToCatchSpawns();
var start = 0f; var start = 0f;
@@ -1118,7 +1150,7 @@ namespace Unity.Netcode.RuntimeTests
{ {
if (start == 0) if (start == 0)
{ {
start = Time.realtimeSinceStartup; start = client.RealTimeProvider.RealTimeSinceStartup;
} }
}; };
var manager = (TestDeferredMessageManager)client.DeferredMessageManager; var manager = (TestDeferredMessageManager)client.DeferredMessageManager;
@@ -1127,9 +1159,9 @@ namespace Unity.Netcode.RuntimeTests
serverObject.GetComponent<NetworkObject>().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); serverObject.GetComponent<NetworkObject>().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId);
yield return WaitForAllClientsToReceive<ChangeOwnershipMessage>(); WaitForAllClientsToReceive<ChangeOwnershipMessage>();
yield return new WaitForSeconds(timeout - 0.5f); TimeTravel(timeout - 0.5f, 1);
foreach (var client in m_ClientNetworkManagers) foreach (var client in m_ClientNetworkManagers)
{ {
@@ -1140,7 +1172,7 @@ namespace Unity.Netcode.RuntimeTests
} }
serverObject.GetComponent<NetworkObject>().ChangeOwnership(m_ServerNetworkManager.LocalClientId); serverObject.GetComponent<NetworkObject>().ChangeOwnership(m_ServerNetworkManager.LocalClientId);
yield return WaitForAllClientsToReceive<ChangeOwnershipMessage>(); WaitForAllClientsToReceive<ChangeOwnershipMessage>();
foreach (var client in m_ClientNetworkManagers) foreach (var client in m_ClientNetworkManagers)
{ {
@@ -1161,7 +1193,7 @@ namespace Unity.Netcode.RuntimeTests
TestDeferredMessageManager.BeforePurgeDelegate beforePurge = (manager, key) => TestDeferredMessageManager.BeforePurgeDelegate beforePurge = (manager, key) =>
{ {
++purgeCount; ++purgeCount;
var elapsed = Time.realtimeSinceStartup - start; var elapsed = client.RealTimeProvider.RealTimeSinceStartup - start;
Assert.GreaterOrEqual(elapsed, timeout - 0.05f); Assert.GreaterOrEqual(elapsed, timeout - 0.05f);
Assert.AreEqual(2, manager.DeferredMessageCountTotal()); Assert.AreEqual(2, manager.DeferredMessageCountTotal());
Assert.AreEqual(2, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn)); Assert.AreEqual(2, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn));
@@ -1177,7 +1209,7 @@ namespace Unity.Netcode.RuntimeTests
AddPrefabsToClient(client); AddPrefabsToClient(client);
} }
yield return new WaitForSeconds(0.6f); TimeTravel(0.6f, 1);
Assert.AreEqual(m_NumberOfClientsToLateJoin, purgeCount); Assert.AreEqual(m_NumberOfClientsToLateJoin, purgeCount);
foreach (var client in m_ClientNetworkManagers) foreach (var client in m_ClientNetworkManagers)
@@ -1187,11 +1219,11 @@ namespace Unity.Netcode.RuntimeTests
} }
} }
[UnityTest] [Test]
public IEnumerator WhenADeferredMessageIsRemoved_OtherMessagesForDifferentObjectsAreNotRemoved([Values(1, 2, 3)] int timeout) public void WhenADeferredMessageIsRemoved_OtherMessagesForDifferentObjectsAreNotRemoved([Values(1, 2, 3)] int timeout)
{ {
m_SkipAddingPrefabsToClient = true; m_SkipAddingPrefabsToClient = true;
yield return SpawnClients(); SpawnClients();
CatchSpawns(); CatchSpawns();
foreach (var client in m_ClientNetworkManagers) foreach (var client in m_ClientNetworkManagers)
{ {
@@ -1203,7 +1235,7 @@ namespace Unity.Netcode.RuntimeTests
var serverObject2 = Object.Instantiate(m_RpcPrefab); var serverObject2 = Object.Instantiate(m_RpcPrefab);
serverObject2.GetComponent<NetworkObject>().NetworkManagerOwner = m_ServerNetworkManager; serverObject2.GetComponent<NetworkObject>().NetworkManagerOwner = m_ServerNetworkManager;
serverObject2.GetComponent<NetworkObject>().Spawn(); serverObject2.GetComponent<NetworkObject>().Spawn();
yield return WaitForClientsToCatchSpawns(2); WaitForClientsToCatchSpawns(2);
var start = 0f; var start = 0f;
@@ -1213,7 +1245,7 @@ namespace Unity.Netcode.RuntimeTests
{ {
if (start == 0) if (start == 0)
{ {
start = Time.realtimeSinceStartup; start = client.RealTimeProvider.RealTimeSinceStartup;
} }
}; };
var manager = (TestDeferredMessageManager)client.DeferredMessageManager; var manager = (TestDeferredMessageManager)client.DeferredMessageManager;
@@ -1222,9 +1254,9 @@ namespace Unity.Netcode.RuntimeTests
serverObject.GetComponent<NetworkObject>().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); serverObject.GetComponent<NetworkObject>().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId);
yield return WaitForAllClientsToReceive<ChangeOwnershipMessage>(); WaitForAllClientsToReceive<ChangeOwnershipMessage>();
yield return new WaitForSeconds(timeout - 0.5f); TimeTravel(timeout - 0.5f, 1);
foreach (var client in m_ClientNetworkManagers) foreach (var client in m_ClientNetworkManagers)
{ {
@@ -1236,7 +1268,7 @@ namespace Unity.Netcode.RuntimeTests
} }
serverObject2.GetComponent<NetworkObject>().ChangeOwnership(m_ServerNetworkManager.LocalClientId); serverObject2.GetComponent<NetworkObject>().ChangeOwnership(m_ServerNetworkManager.LocalClientId);
yield return WaitForAllClientsToReceive<ChangeOwnershipMessage>(); WaitForAllClientsToReceive<ChangeOwnershipMessage>();
foreach (var client in m_ClientNetworkManagers) foreach (var client in m_ClientNetworkManagers)
{ {
@@ -1258,7 +1290,7 @@ namespace Unity.Netcode.RuntimeTests
TestDeferredMessageManager.BeforePurgeDelegate beforePurge = (manager, key) => TestDeferredMessageManager.BeforePurgeDelegate beforePurge = (manager, key) =>
{ {
++purgeCount; ++purgeCount;
var elapsed = Time.realtimeSinceStartup - start; var elapsed = client.RealTimeProvider.RealTimeSinceStartup - start;
Assert.GreaterOrEqual(elapsed, timeout - 0.05f); Assert.GreaterOrEqual(elapsed, timeout - 0.05f);
Assert.AreEqual(2, manager.DeferredMessageCountTotal()); Assert.AreEqual(2, manager.DeferredMessageCountTotal());
Assert.AreEqual(2, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn)); Assert.AreEqual(2, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn));
@@ -1277,7 +1309,7 @@ namespace Unity.Netcode.RuntimeTests
AddPrefabsToClient(client); AddPrefabsToClient(client);
} }
yield return new WaitForSeconds(0.6f); TimeTravel(0.6f, 1);
Assert.AreEqual(m_NumberOfClientsToLateJoin, purgeCount); Assert.AreEqual(m_NumberOfClientsToLateJoin, purgeCount);
foreach (var client in m_ClientNetworkManagers) foreach (var client in m_ClientNetworkManagers)

View File

@@ -1,9 +1,9 @@
using System.Collections; using System.Collections;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine; using UnityEngine;
using UnityEngine.TestTools; using UnityEngine.TestTools;
using Unity.Netcode.TestHelpers.Runtime;
namespace Unity.Netcode.RuntimeTests namespace Unity.Netcode.RuntimeTests
{ {

View File

@@ -1,9 +1,9 @@
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using NUnit.Framework; using NUnit.Framework;
using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine; using UnityEngine;
using UnityEngine.TestTools; using UnityEngine.TestTools;
using Unity.Netcode.TestHelpers.Runtime;
namespace Unity.Netcode.RuntimeTests namespace Unity.Netcode.RuntimeTests
{ {

View File

@@ -1,9 +1,9 @@
using System.Collections; using System.Collections;
using System.Linq; using System.Linq;
using UnityEngine;
using UnityEngine.TestTools;
using NUnit.Framework; using NUnit.Framework;
using Unity.Netcode.TestHelpers.Runtime; using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
using UnityEngine.TestTools;
namespace Unity.Netcode.RuntimeTests namespace Unity.Netcode.RuntimeTests
{ {

View File

@@ -5,9 +5,9 @@ using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using NUnit.Framework; using NUnit.Framework;
using Unity.Collections; using Unity.Collections;
using UnityEngine.TestTools;
using Unity.Netcode.TestHelpers.Runtime; using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine; using UnityEngine;
using UnityEngine.TestTools;
using Debug = UnityEngine.Debug; using Debug = UnityEngine.Debug;
namespace Unity.Netcode.RuntimeTests namespace Unity.Netcode.RuntimeTests

View File

@@ -1,7 +1,7 @@
using System.Collections; using System.Collections;
using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine; using UnityEngine;
using UnityEngine.TestTools; using UnityEngine.TestTools;
using Unity.Netcode.TestHelpers.Runtime;
namespace Unity.Netcode.RuntimeTests namespace Unity.Netcode.RuntimeTests
{ {

View File

@@ -2,9 +2,9 @@ using System;
using System.Collections; using System.Collections;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using NUnit.Framework; using NUnit.Framework;
using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine; using UnityEngine;
using UnityEngine.TestTools; using UnityEngine.TestTools;
using Unity.Netcode.TestHelpers.Runtime;
namespace Unity.Netcode.RuntimeTests namespace Unity.Netcode.RuntimeTests
{ {

View File

@@ -3,8 +3,8 @@ using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using NUnit.Framework; using NUnit.Framework;
using Unity.Collections; using Unity.Collections;
using UnityEngine.TestTools;
using Unity.Netcode.TestHelpers.Runtime; using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine.TestTools;
namespace Unity.Netcode.RuntimeTests namespace Unity.Netcode.RuntimeTests
{ {

View File

@@ -3,8 +3,8 @@ using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using NUnit.Framework; using NUnit.Framework;
using Unity.Collections; using Unity.Collections;
using UnityEngine.TestTools;
using Unity.Netcode.TestHelpers.Runtime; using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine.TestTools;
namespace Unity.Netcode.RuntimeTests namespace Unity.Netcode.RuntimeTests
{ {

View File

@@ -1,7 +1,7 @@
using UnityEngine;
using NUnit.Framework; using NUnit.Framework;
using UnityEngine.TestTools;
using Unity.Netcode.Transports.UTP; using Unity.Netcode.Transports.UTP;
using UnityEngine;
using UnityEngine.TestTools;
using Object = UnityEngine.Object; using Object = UnityEngine.Object;
namespace Unity.Netcode.RuntimeTests namespace Unity.Netcode.RuntimeTests

View File

@@ -1,9 +1,9 @@
using System.Collections; using System.Collections;
using UnityEngine;
using NUnit.Framework; using NUnit.Framework;
using UnityEngine.TestTools;
using Unity.Netcode.TestHelpers.Runtime;
using Unity.Netcode.Components; using Unity.Netcode.Components;
using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
using UnityEngine.TestTools;
namespace Unity.Netcode.RuntimeTests namespace Unity.Netcode.RuntimeTests
{ {
@@ -56,8 +56,10 @@ namespace Unity.Netcode.RuntimeTests
yield return StartServerAndClients(); yield return StartServerAndClients();
var parentObject = new GameObject(); var parentObject = new GameObject();
var childObject = new GameObject(); var childObject = new GameObject
childObject.name = "ChildObject"; {
name = "ChildObject"
};
childObject.transform.parent = parentObject.transform; childObject.transform.parent = parentObject.transform;
var parentNetworkObject = parentObject.AddComponent<NetworkObject>(); var parentNetworkObject = parentObject.AddComponent<NetworkObject>();
var childBehaviour = childObject.AddComponent<NetworkTransform>(); var childBehaviour = childObject.AddComponent<NetworkTransform>();

View File

@@ -2,9 +2,9 @@ using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine; using UnityEngine;
using UnityEngine.TestTools; using UnityEngine.TestTools;
using Unity.Netcode.TestHelpers.Runtime;
using Object = UnityEngine.Object; using Object = UnityEngine.Object;
namespace Unity.Netcode.RuntimeTests namespace Unity.Netcode.RuntimeTests

View File

@@ -12,12 +12,11 @@ namespace Unity.Netcode.RuntimeTests
var networkManager = gameObject.AddComponent<NetworkManager>(); var networkManager = gameObject.AddComponent<NetworkManager>();
var transport = gameObject.AddComponent<DummyTransport>(); var transport = gameObject.AddComponent<DummyTransport>();
networkManager.NetworkConfig = new NetworkConfig(); networkManager.NetworkConfig = new NetworkConfig
{
// Set dummy transport that does nothing // Set dummy transport that does nothing
networkManager.NetworkConfig.NetworkTransport = transport; NetworkTransport = transport
};
CustomMessagingManager preManager = networkManager.CustomMessagingManager; CustomMessagingManager preManager = networkManager.CustomMessagingManager;

View File

@@ -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<NetworkManager>();
// Set dummy transport that does nothing
var transport = gameObject.AddComponent<DummyTransport>();
m_ServerManager.NetworkConfig = new NetworkConfig() { NetworkTransport = transport };
Action<bool> 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<bool> 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<NetworkManager>();
// Set dummy transport that does nothing
var transport = gameObject.AddComponent<DummyTransport>();
m_ServerManager.NetworkConfig = new NetworkConfig() { NetworkTransport = transport };
int callbacksInvoked = 0;
Action<bool> onClientStopped = (bool wasAlsoServer) =>
{
callbacksInvoked++;
Assert.IsTrue(wasAlsoServer);
if (m_ServerManager.IsClient)
{
Assert.Fail("onClientStopped called when the client is still active");
}
};
Action<bool> 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<NetworkManager>();
// Set dummy transport that does nothing
var transport = gameObject.AddComponent<DummyTransport>();
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<NetworkManager>();
// Set dummy transport that does nothing
var transport = gameObject.AddComponent<DummyTransport>();
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;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 238d8724ba5ce3947bc20f5d6c056b6e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -11,9 +11,11 @@ namespace Unity.Netcode.RuntimeTests
var gameObject = new GameObject(nameof(SceneManagerAssigned)); var gameObject = new GameObject(nameof(SceneManagerAssigned));
var networkManager = gameObject.AddComponent<NetworkManager>(); var networkManager = gameObject.AddComponent<NetworkManager>();
var transport = gameObject.AddComponent<DummyTransport>(); var transport = gameObject.AddComponent<DummyTransport>();
networkManager.NetworkConfig = new NetworkConfig(); networkManager.NetworkConfig = new NetworkConfig
{
// Set dummy transport that does nothing // Set dummy transport that does nothing
networkManager.NetworkConfig.NetworkTransport = transport; NetworkTransport = transport
};
NetworkSceneManager preManager = networkManager.SceneManager; NetworkSceneManager preManager = networkManager.SceneManager;

View File

@@ -1,8 +1,8 @@
using System; using System;
using System.Collections; using System.Collections;
using NUnit.Framework;
using UnityEngine; using UnityEngine;
using UnityEngine.TestTools; using UnityEngine.TestTools;
using NUnit.Framework;
namespace Unity.Netcode.RuntimeTests namespace Unity.Netcode.RuntimeTests
{ {

View File

@@ -1,8 +1,8 @@
using System.Collections; using System.Collections;
using NUnit.Framework; using NUnit.Framework;
using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine; using UnityEngine;
using UnityEngine.TestTools; using UnityEngine.TestTools;
using Unity.Netcode.TestHelpers.Runtime;
using Object = UnityEngine.Object; using Object = UnityEngine.Object;
namespace Unity.Netcode.RuntimeTests namespace Unity.Netcode.RuntimeTests

View File

@@ -1,9 +1,9 @@
using System.Collections; using System.Collections;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine; using UnityEngine;
using UnityEngine.TestTools; using UnityEngine.TestTools;
using Unity.Netcode.TestHelpers.Runtime;
namespace Unity.Netcode.RuntimeTests namespace Unity.Netcode.RuntimeTests

View File

@@ -1,9 +1,9 @@
using System.Collections; using System.Collections;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine; using UnityEngine;
using UnityEngine.TestTools; using UnityEngine.TestTools;
using Unity.Netcode.TestHelpers.Runtime;
namespace Unity.Netcode.RuntimeTests namespace Unity.Netcode.RuntimeTests
{ {

View File

@@ -1,8 +1,8 @@
using System.Collections; using System.Collections;
using NUnit.Framework; using NUnit.Framework;
using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine; using UnityEngine;
using UnityEngine.TestTools; using UnityEngine.TestTools;
using Unity.Netcode.TestHelpers.Runtime;
namespace Unity.Netcode.RuntimeTests namespace Unity.Netcode.RuntimeTests

Some files were not shown because too many files have changed in this diff Show More