4 Commits
1.5.1 ... 1.7.1

Author SHA1 Message Date
Unity Technologies
514166e159 com.unity.netcode.gameobjects@1.7.1
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.7.1] - 2023-11-15

### Added

### Fixed

- Fixed a bug where having a class with Rpcs that inherits from a class without Rpcs that inherits from NetworkVariable would cause a compile error. (#2751)
- Fixed issue where `NetworkBehaviour.Synchronize` was not truncating the write buffer if nothing was serialized during `NetworkBehaviour.OnSynchronize` causing an additional 6 bytes to be written per `NetworkBehaviour` component instance. (#2749)

### Changed
2023-11-15 00:00:00 +00:00
Unity Technologies
ffef45b50f com.unity.netcode.gameobjects@1.7.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.7.0] - 2023-10-11

### Added

- exposed NetworkObject.GetNetworkBehaviourAtOrderIndex as a public API (#2724)
- Added context menu tool that provides users with the ability to quickly update the GlobalObjectIdHash value for all in-scene placed prefab instances that were created prior to adding a NetworkObject component to it. (#2707)
- Added methods NetworkManager.SetPeerMTU and NetworkManager.GetPeerMTU to be able to set MTU sizes per-peer (#2676)
- Added `GenerateSerializationForGenericParameterAttribute`, which can be applied to user-created Network Variable types to ensure the codegen generates serialization for the generic types they wrap. (#2694)
- Added `GenerateSerializationForTypeAttribute`, which can be applied to any class or method to ensure the codegen generates serialization for the specific provided type. (#2694)
- Exposed `NetworkVariableSerialization<T>.Read`, `NetworkVariableSerialization<T>.Write`, `NetworkVariableSerialization<T>.AreEqual`, and `NetworkVariableSerialization<T>.Duplicate` to further support the creation of user-created network variables by allowing users to access the generated serialization methods and serialize generic types efficiently without boxing. (#2694)
- Added `NetworkVariableBase.MarkNetworkBehaviourDirty` so that user-created network variable types can mark their containing `NetworkBehaviour` to be processed by the update loop. (#2694)

### Fixed

- Fixed issue where the server side `NetworkSceneManager` instance was not adding the currently active scene to its list of scenes loaded. (#2723)
- Generic NetworkBehaviour types no longer result in compile errors or runtime errors (#2720)
- Rpcs within Generic NetworkBehaviour types can now serialize parameters of the class's generic types (but may not have generic types of their own) (#2720)
- Errors are no longer thrown when entering play mode with domain reload disabled (#2720)
- NetworkSpawn is now correctly called each time when entering play mode with scene reload disabled (#2720)
- NetworkVariables of non-integer types will no longer break the inspector (#2714)
- NetworkVariables with NonSerializedAttribute will not appear in the inspector (#2714)
- Fixed issue where `UnityTransport` would attempt to establish WebSocket connections even if using UDP/DTLS Relay allocations when the build target was WebGL. This only applied to working in the editor since UDP/DTLS can't work in the browser. (#2695)
- Fixed issue where a `NetworkBehaviour` component's `OnNetworkDespawn` was not being invoked on the host-server side for an in-scene placed `NetworkObject` when a scene was unloaded (during a scene transition) and the `NetworkBehaviour` component was positioned/ordered before the `NetworkObject` component. (#2685)
- Fixed issue where `SpawnWithObservers` was not being honored when `NetworkConfig.EnableSceneManagement` was disabled. (#2682)
- Fixed issue where `NetworkAnimator` was not internally tracking changes to layer weights which prevented proper layer weight synchronization back to the original layer weight value. (#2674)
- Fixed "writing past the end of the buffer" error when calling ResetDirty() on managed network variables that are larger than 256 bytes when serialized. (#2670)
- Fixed issue where generation of the `DefaultNetworkPrefabs` asset was not enabled by default. (#2662)
- Fixed issue where the `GlobalObjectIdHash` value could be updated but the asset not marked as dirty. (#2662)
- Fixed issue where the `GlobalObjectIdHash` value of a (network) prefab asset could be assigned an incorrect value when editing the prefab in a temporary scene. (#2662)
- Fixed issue where the `GlobalObjectIdHash` value generated after creating a (network) prefab from an object constructed within the scene would not be the correct final value in a stand alone build. (#2662)

### Changed

- Updated dependency on `com.unity.transport` to version 1.4.0. (#2716)
2023-10-11 00:00:00 +00:00
Unity Technologies
b3bd4727ab com.unity.netcode.gameobjects@1.6.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.6.0] - 2023-08-09

### Added

- Added a protected virtual method `NetworkTransform.OnInitialize(ref NetworkTransformState replicatedState)` that just returns the replicated state reference.

### Fixed

- Fixed  issue where invoking `NetworkManager.Shutdown` within `NetworkManager.OnClientStopped` or `NetworkManager.OnServerStopped` would force `NetworkManager.ShutdownInProgress` to remain true after completing the shutdown process. (#2661)
- Fixed issue with client synchronization of position when using half precision and the delta position reaches the maximum value and is collapsed on the host prior to being forwarded to the non-owner clients. (#2636)
- Fixed issue with scale not synchronizing properly depending upon the spawn order of NetworkObjects. (#2636)
- Fixed issue position was not properly transitioning between ownership changes with an owner authoritative NetworkTransform. (#2636)
- Fixed issue where a late joining non-owner client could update an owner authoritative NetworkTransform if ownership changed without any updates to position prior to the non-owner client joining. (#2636)

### Changed
2023-08-09 00:00:00 +00:00
Unity Technologies
0581a42b70 com.unity.netcode.gameobjects@1.5.2
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.5.2] - 2023-07-24

### Added

### Fixed

- Fixed issue where `NetworkClient.OwnedObjects` was not returning any owned objects due to the `NetworkClient.IsConnected` not being properly set. (#2631)
- Fixed a crash when calling TrySetParent with a null Transform (#2625)
- Fixed issue where a `NetworkTransform` using full precision state updates was losing transform state updates when interpolation was enabled. (#2624)
- Fixed issue where `NetworkObject.SpawnWithObservers` was not being honored for late joining clients. (#2623)
- Fixed issue where invoking `NetworkManager.Shutdown` multiple times, depending upon the timing, could cause an exception. (#2622)
- Fixed issue where removing ownership would not notify the server that it gained ownership. This also resolves the issue where an owner authoritative NetworkTransform would not properly initialize upon removing ownership from a remote client. (#2618)
- Fixed ILPP issues when using CoreCLR and for certain dedicated server builds. (#2614)
- Fixed an ILPP compile error when creating a generic NetworkBehaviour singleton with a static T instance. (#2603)

### Changed
2023-07-24 00:00:00 +00:00
63 changed files with 3171 additions and 851 deletions

View File

@@ -6,6 +6,86 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
Additional documentation and release notes are available at [Multiplayer Documentation](https://docs-multiplayer.unity3d.com).
## [1.7.1] - 2023-11-15
### Added
### Fixed
- Fixed a bug where having a class with Rpcs that inherits from a class without Rpcs that inherits from NetworkVariable would cause a compile error. (#2751)
- Fixed issue where `NetworkBehaviour.Synchronize` was not truncating the write buffer if nothing was serialized during `NetworkBehaviour.OnSynchronize` causing an additional 6 bytes to be written per `NetworkBehaviour` component instance. (#2749)
### Changed
## [1.7.0] - 2023-10-11
### Added
- exposed NetworkObject.GetNetworkBehaviourAtOrderIndex as a public API (#2724)
- Added context menu tool that provides users with the ability to quickly update the GlobalObjectIdHash value for all in-scene placed prefab instances that were created prior to adding a NetworkObject component to it. (#2707)
- Added methods NetworkManager.SetPeerMTU and NetworkManager.GetPeerMTU to be able to set MTU sizes per-peer (#2676)
- Added `GenerateSerializationForGenericParameterAttribute`, which can be applied to user-created Network Variable types to ensure the codegen generates serialization for the generic types they wrap. (#2694)
- Added `GenerateSerializationForTypeAttribute`, which can be applied to any class or method to ensure the codegen generates serialization for the specific provided type. (#2694)
- Exposed `NetworkVariableSerialization<T>.Read`, `NetworkVariableSerialization<T>.Write`, `NetworkVariableSerialization<T>.AreEqual`, and `NetworkVariableSerialization<T>.Duplicate` to further support the creation of user-created network variables by allowing users to access the generated serialization methods and serialize generic types efficiently without boxing. (#2694)
- Added `NetworkVariableBase.MarkNetworkBehaviourDirty` so that user-created network variable types can mark their containing `NetworkBehaviour` to be processed by the update loop. (#2694)
### Fixed
- Fixed issue where the server side `NetworkSceneManager` instance was not adding the currently active scene to its list of scenes loaded. (#2723)
- Generic NetworkBehaviour types no longer result in compile errors or runtime errors (#2720)
- Rpcs within Generic NetworkBehaviour types can now serialize parameters of the class's generic types (but may not have generic types of their own) (#2720)
- Errors are no longer thrown when entering play mode with domain reload disabled (#2720)
- NetworkSpawn is now correctly called each time when entering play mode with scene reload disabled (#2720)
- NetworkVariables of non-integer types will no longer break the inspector (#2714)
- NetworkVariables with NonSerializedAttribute will not appear in the inspector (#2714)
- Fixed issue where `UnityTransport` would attempt to establish WebSocket connections even if using UDP/DTLS Relay allocations when the build target was WebGL. This only applied to working in the editor since UDP/DTLS can't work in the browser. (#2695)
- Fixed issue where a `NetworkBehaviour` component's `OnNetworkDespawn` was not being invoked on the host-server side for an in-scene placed `NetworkObject` when a scene was unloaded (during a scene transition) and the `NetworkBehaviour` component was positioned/ordered before the `NetworkObject` component. (#2685)
- Fixed issue where `SpawnWithObservers` was not being honored when `NetworkConfig.EnableSceneManagement` was disabled. (#2682)
- Fixed issue where `NetworkAnimator` was not internally tracking changes to layer weights which prevented proper layer weight synchronization back to the original layer weight value. (#2674)
- Fixed "writing past the end of the buffer" error when calling ResetDirty() on managed network variables that are larger than 256 bytes when serialized. (#2670)
- Fixed issue where generation of the `DefaultNetworkPrefabs` asset was not enabled by default. (#2662)
- Fixed issue where the `GlobalObjectIdHash` value could be updated but the asset not marked as dirty. (#2662)
- Fixed issue where the `GlobalObjectIdHash` value of a (network) prefab asset could be assigned an incorrect value when editing the prefab in a temporary scene. (#2662)
- Fixed issue where the `GlobalObjectIdHash` value generated after creating a (network) prefab from an object constructed within the scene would not be the correct final value in a stand alone build. (#2662)
### Changed
- Updated dependency on `com.unity.transport` to version 1.4.0. (#2716)
## [1.6.0] - 2023-08-09
### Added
- Added a protected virtual method `NetworkTransform.OnInitialize(ref NetworkTransformState replicatedState)` that just returns the replicated state reference.
### Fixed
- Fixed issue where invoking `NetworkManager.Shutdown` within `NetworkManager.OnClientStopped` or `NetworkManager.OnServerStopped` would force `NetworkManager.ShutdownInProgress` to remain true after completing the shutdown process. (#2661)
- Fixed issue where ARMv7 Android builds would crash when trying to validate the batch header. (#2654)
- Fixed issue with client synchronization of position when using half precision and the delta position reaches the maximum value and is collapsed on the host prior to being forwarded to the non-owner clients. (#2636)
- Fixed issue with scale not synchronizing properly depending upon the spawn order of NetworkObjects. (#2636)
- Fixed issue position was not properly transitioning between ownership changes with an owner authoritative NetworkTransform. (#2636)
- Fixed issue where a late joining non-owner client could update an owner authoritative NetworkTransform if ownership changed without any updates to position prior to the non-owner client joining. (#2636)
### Changed
## [1.5.2] - 2023-07-24
### Added
### Fixed
- Fixed issue where `NetworkClient.OwnedObjects` was not returning any owned objects due to the `NetworkClient.IsConnected` not being properly set. (#2631)
- Fixed a crash when calling TrySetParent with a null Transform (#2625)
- Fixed issue where a `NetworkTransform` using full precision state updates was losing transform state updates when interpolation was enabled. (#2624)
- Fixed issue where `NetworkObject.SpawnWithObservers` was not being honored for late joining clients. (#2623)
- Fixed issue where invoking `NetworkManager.Shutdown` multiple times, depending upon the timing, could cause an exception. (#2622)
- Fixed issue where removing ownership would not notify the server that it gained ownership. This also resolves the issue where an owner authoritative NetworkTransform would not properly initialize upon removing ownership from a remote client. (#2618)
- Fixed ILPP issues when using CoreCLR and for certain dedicated server builds. (#2614)
- Fixed an ILPP compile error when creating a generic NetworkBehaviour singleton with a static T instance. (#2603)
### Changed
## [1.5.1] - 2023-06-07
### Added

View File

@@ -27,7 +27,7 @@ namespace Unity.Netcode.Components
/// <summary>
/// The half float precision value of the z-axis as a <see cref="half"/>.
/// </summary>
public half Z => Axis.x;
public half Z => Axis.z;
/// <summary>
/// Used to store the half float precision values as a <see cref="half3"/>
@@ -39,6 +39,17 @@ namespace Unity.Netcode.Components
/// </summary>
public bool3 AxisToSynchronize;
/// <summary>
/// Directly sets each axial value to the passed in full precision values
/// that are converted to half precision
/// </summary>
internal void Set(float x, float y, float z)
{
Axis.x = math.half(x);
Axis.y = math.half(y);
Axis.z = math.half(z);
}
private void SerializeWrite(FastBufferWriter writer)
{
for (int i = 0; i < Length; i++)

View File

@@ -1137,6 +1137,7 @@ namespace Unity.Netcode.Components
if (m_LayerWeights[animationState.Layer] != animationState.Weight)
{
m_Animator.SetLayerWeight(animationState.Layer, animationState.Weight);
m_LayerWeights[animationState.Layer] = animationState.Weight;
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Unity.Collections;
using Unity.Mathematics;
using UnityEngine;
@@ -49,6 +50,11 @@ namespace Unity.Netcode.Components
/// </summary>
public OnClientRequestChangeDelegate OnClientRequestChange;
/// <summary>
/// When set each state update will contain a state identifier
/// </summary>
internal static bool TrackByStateId;
/// <summary>
/// Data structure used to synchronize the <see cref="NetworkTransform"/>
/// </summary>
@@ -71,9 +77,16 @@ namespace Unity.Netcode.Components
private const int k_UseHalfFloats = 0x00004000; // Persists between state updates (authority dictates if this is set)
private const int k_Synchronization = 0x00008000;
private const int k_PositionSlerp = 0x00010000; // Persists between state updates (authority dictates if this is set)
private const int k_IsParented = 0x00020000; // When parented and synchronizing, we need to have both lossy and local scale due to varying spawn order
private const int k_TrackStateId = 0x10000000; // (Internal Debugging) When set each state update will contain a state identifier
// Stores persistent and state relative flags
private uint m_Bitset;
internal uint BitSet
{
get { return m_Bitset; }
set { m_Bitset = value; }
}
// Used to store the tick calculated sent time
internal double SentTime;
@@ -98,6 +111,7 @@ namespace Unity.Netcode.Components
// Used for half precision scale
internal HalfVector3 HalfVectorScale;
internal Vector3 Scale;
internal Vector3 LossyScale;
// Used for half precision quaternion
internal HalfVector4 HalfVectorRotation;
@@ -118,7 +132,6 @@ namespace Unity.Netcode.Components
internal int NetworkTick;
// Used when tracking by state ID is enabled
internal bool TrackByStateId;
internal int StateId;
// Used during serialization
@@ -416,6 +429,24 @@ namespace Unity.Netcode.Components
}
}
internal bool IsParented
{
get => GetFlag(k_IsParented);
set
{
SetFlag(value, k_IsParented);
}
}
internal bool TrackByStateId
{
get => GetFlag(k_TrackStateId);
set
{
SetFlag(value, k_TrackStateId);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool GetFlag(int flag)
{
@@ -468,6 +499,10 @@ namespace Unity.Netcode.Components
/// <remarks>
/// When there is no change in an updated state's position then there are no values to return.
/// Checking for <see cref="HasPositionChange"/> is one way to detect this.
/// When used with half precision it returns the half precision delta position state update
/// which will not be the full position.
/// To get a NettworkTransform's full position, use <see cref="GetSpaceRelativePosition(bool)"/> and
/// pass true as the parameter.
/// </remarks>
/// <returns><see cref="Vector3"/></returns>
public Vector3 GetPosition()
@@ -534,6 +569,8 @@ namespace Unity.Netcode.Components
return NetworkTick;
}
internal HalfVector3 HalfEulerRotation;
/// <summary>
/// Serializes this <see cref="NetworkTransformState"/>
/// </summary>
@@ -553,23 +590,6 @@ namespace Unity.Netcode.Components
positionStart = m_Reader.Position;
}
if (TrackByStateId)
{
var stateId = StateId;
if (IsSynchronizing)
{
StateId = -1;
}
else
{
if (serializer.IsWriter)
{
StateId++;
}
serializer.SerializeValue(ref StateId);
}
}
// Synchronize State Flags and Network Tick
{
if (isWriting)
@@ -589,11 +609,22 @@ namespace Unity.Netcode.Components
}
}
// If debugging states and track by state identifier is enabled, serialize the current state identifier
if (TrackByStateId)
{
serializer.SerializeValue(ref StateId);
}
// Synchronize Position
if (HasPositionChange)
{
if (UseHalfFloatPrecision)
{
// Apply which axis should be updated for both write/read (teleporting, synchronizing, or just updating)
NetworkDeltaPosition.HalfVector3.AxisToSynchronize[0] = HasPositionX;
NetworkDeltaPosition.HalfVector3.AxisToSynchronize[1] = HasPositionY;
NetworkDeltaPosition.HalfVector3.AxisToSynchronize[2] = HasPositionZ;
if (IsTeleportingNextFrame)
{
// **Always use full precision when teleporting and UseHalfFloatPrecision is enabled**
@@ -604,7 +635,7 @@ namespace Unity.Netcode.Components
serializer.SerializeValue(ref DeltaPosition);
if (!isWriting)
{
NetworkDeltaPosition = new NetworkDeltaPosition(Vector3.zero, 0, math.bool3(HasPositionX, HasPositionY, HasPositionZ));
NetworkDeltaPosition.NetworkTick = NetworkTick;
NetworkDeltaPosition.NetworkSerialize(serializer);
}
else
@@ -617,7 +648,7 @@ namespace Unity.Netcode.Components
{
if (!isWriting)
{
NetworkDeltaPosition = new NetworkDeltaPosition(Vector3.zero, 0, math.bool3(HasPositionX, HasPositionY, HasPositionZ));
NetworkDeltaPosition.NetworkTick = NetworkTick;
NetworkDeltaPosition.NetworkSerialize(serializer);
}
else
@@ -626,9 +657,8 @@ namespace Unity.Netcode.Components
}
}
}
else // Legacy Position Synchronization
else // Full precision axis specific position synchronization
{
// Position Values
if (HasPositionX)
{
serializer.SerializeValue(ref PositionX);
@@ -703,11 +733,21 @@ namespace Unity.Netcode.Components
{
if (HasRotAngleChange)
{
var halfPrecisionRotation = new HalfVector3(RotAngleX, RotAngleY, RotAngleZ, math.bool3(HasRotAngleX, HasRotAngleY, HasRotAngleZ));
serializer.SerializeValue(ref halfPrecisionRotation);
// Apply which axis should be updated for both write/read
HalfEulerRotation.AxisToSynchronize[0] = HasRotAngleX;
HalfEulerRotation.AxisToSynchronize[1] = HasRotAngleY;
HalfEulerRotation.AxisToSynchronize[2] = HasRotAngleZ;
if (isWriting)
{
HalfEulerRotation.Set(RotAngleX, RotAngleY, RotAngleZ);
}
serializer.SerializeValue(ref HalfEulerRotation);
if (!isWriting)
{
var eulerRotation = halfPrecisionRotation.ToVector3();
var eulerRotation = HalfEulerRotation.ToVector3();
if (HasRotAngleX)
{
RotAngleX = eulerRotation.x;
@@ -749,6 +789,12 @@ namespace Unity.Netcode.Components
// Synchronize Scale
if (HasScaleChange)
{
// If we are teleporting (which includes synchronizing) and the associated NetworkObject has a parent
// then we want to serialize the LossyScale since NetworkObject spawn order is not guaranteed
if (IsTeleportingNextFrame && IsParented)
{
serializer.SerializeValue(ref LossyScale);
}
// Half precision scale synchronization
if (UseHalfFloatPrecision)
{
@@ -758,9 +804,19 @@ namespace Unity.Netcode.Components
}
else
{
// Apply which axis should be updated for both write/read
HalfVectorScale.AxisToSynchronize[0] = HasScaleX;
HalfVectorScale.AxisToSynchronize[1] = HasScaleY;
HalfVectorScale.AxisToSynchronize[2] = HasScaleZ;
// For scale, when half precision is enabled we can still only send the axis with deltas
HalfVectorScale = new HalfVector3(Scale, math.bool3(HasScaleX, HasScaleY, HasScaleZ));
if (isWriting)
{
HalfVectorScale.Set(Scale[0], Scale[1], Scale[2]);
}
serializer.SerializeValue(ref HalfVectorScale);
if (!isWriting)
{
Scale = HalfVectorScale.ToVector3();
@@ -1029,26 +1085,6 @@ namespace Unity.Netcode.Components
/// </summary>
protected NetworkManager m_CachedNetworkManager; // Note: we no longer use this and are only keeping it until we decide to deprecate it
/// <summary>
/// We have two internal NetworkVariables.
/// One for server authoritative and one for "client/owner" authoritative.
/// </summary>
private readonly NetworkVariable<NetworkTransformState> m_ReplicatedNetworkStateServer = new NetworkVariable<NetworkTransformState>(new NetworkTransformState(), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
private readonly NetworkVariable<NetworkTransformState> m_ReplicatedNetworkStateOwner = new NetworkVariable<NetworkTransformState>(new NetworkTransformState(), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
internal NetworkVariable<NetworkTransformState> ReplicatedNetworkState
{
get
{
if (!IsServerAuthoritative())
{
return m_ReplicatedNetworkStateOwner;
}
return m_ReplicatedNetworkStateServer;
}
}
/// <summary>
/// Helper method that returns the space relative position of the transform.
/// </summary>
@@ -1078,9 +1114,18 @@ namespace Unity.Netcode.Components
}
else
{
// When half float precision is enabled, get the NetworkDeltaPosition's full position
if (UseHalfFloatPrecision)
{
return m_HalfPositionState.GetFullPosition();
}
else
{
// Otherwise, just get the current position
return m_CurrentPosition;
}
}
}
/// <summary>
/// Helper method that returns the space relative rotation of the transform.
@@ -1149,6 +1194,8 @@ namespace Unity.Netcode.Components
// This represents the most recent local authoritative state.
private NetworkTransformState m_LocalAuthoritativeNetworkState;
internal NetworkTransformState LocalAuthoritativeNetworkState => m_LocalAuthoritativeNetworkState;
private ClientRpcParams m_ClientRpcParams = new ClientRpcParams() { Send = new ClientRpcSendParams() };
private List<ulong> m_ClientIds = new List<ulong>() { 0 };
@@ -1159,8 +1206,14 @@ namespace Unity.Netcode.Components
// Non-Authoritative's current position, scale, and rotation that is used to assure the non-authoritative side cannot make adjustments to
// the portions of the transform being synchronized.
private Vector3 m_CurrentPosition;
private Vector3 m_TargetPosition;
private Vector3 m_CurrentScale;
private Vector3 m_TargetScale;
private Quaternion m_CurrentRotation;
private Vector3 m_TargetRotation;
// Used to for each instance to uniquely identify the named message
private string m_MessageName;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -1255,7 +1308,13 @@ namespace Unity.Netcode.Components
protected override void OnSynchronize<T>(ref BufferSerializer<T> serializer)
{
var targetClientId = m_TargetIdBeingSynchronized;
var synchronizationState = new NetworkTransformState();
var synchronizationState = new NetworkTransformState()
{
HalfEulerRotation = new HalfVector3(),
HalfVectorRotation = new HalfVector4(),
HalfVectorScale = new HalfVector3(),
NetworkDeltaPosition = new NetworkDeltaPosition(),
};
if (serializer.IsWriter)
{
@@ -1283,6 +1342,7 @@ namespace Unity.Netcode.Components
m_LocalAuthoritativeNetworkState = synchronizationState;
m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = false;
m_LocalAuthoritativeNetworkState.IsSynchronizing = false;
}
}
@@ -1346,6 +1406,8 @@ namespace Unity.Netcode.Components
{
}
// Tracks the last tick a state update was sent (see further below)
private int m_LastTick;
/// <summary>
/// Authoritative side only
/// If there are any transform delta states, this method will synchronize the
@@ -1363,12 +1425,28 @@ namespace Unity.Netcode.Components
// If the transform has deltas (returns dirty) then...
if (ApplyTransformToNetworkStateWithInfo(ref m_LocalAuthoritativeNetworkState, ref transformToCommit, synchronize))
{
m_LocalAuthoritativeNetworkState.LastSerializedSize = ReplicatedNetworkState.Value.LastSerializedSize;
m_LocalAuthoritativeNetworkState.LastSerializedSize = m_OldState.LastSerializedSize;
// Make sure our network tick is incremented
if (m_LastTick == m_LocalAuthoritativeNetworkState.NetworkTick && !m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame)
{
// When running in authority and a remote client is the owner, the client can hit a perfect window of time where
// it is still on the previous network tick (as a count) but still have had the tick event triggered.
// (This is cheaper than calculating the exact tick each time and only can occur on clients)
if (!IsServer)
{
m_LocalAuthoritativeNetworkState.NetworkTick = m_LocalAuthoritativeNetworkState.NetworkTick + 1;
}
else
{
NetworkLog.LogError($"[NT TICK DUPLICATE] Server already sent an update on tick {m_LastTick} and is attempting to send again on the same network tick!");
}
}
m_LastTick = m_LocalAuthoritativeNetworkState.NetworkTick;
// Update the state
UpdateTransformState();
OnAuthorityPushTransformState(ref m_LocalAuthoritativeNetworkState);
// "push"/commit the state
ReplicatedNetworkState.Value = m_LocalAuthoritativeNetworkState;
m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = false;
}
}
@@ -1648,7 +1726,43 @@ namespace Unity.Netcode.Components
}
}
// Only if we are not synchronizing...
// For scale, we need to check for parenting when synchronizing and/or teleporting
if (isSynchronization || networkState.IsTeleportingNextFrame)
{
// This all has to do with complex nested hierarchies and how it impacts scale
// when set for the first time and depending upon whether the NetworkObject is parented
// (or not parented) at the time the scale values are applied.
var hasParentNetworkObject = false;
// If the NetworkObject belonging to this NetworkTransform instance has a parent
// (i.e. this handles nested NetworkTransforms under a parent at some layer above)
if (NetworkObject.transform.parent != null)
{
var parentNetworkObject = NetworkObject.transform.parent.GetComponent<NetworkObject>();
// In-scene placed NetworkObjects parented under a GameObject with no
// NetworkObject preserve their lossyScale when synchronizing.
if (parentNetworkObject == null && NetworkObject.IsSceneObject != false)
{
hasParentNetworkObject = true;
}
else
{
// Or if the relative NetworkObject has a parent NetworkObject
hasParentNetworkObject = parentNetworkObject != null;
}
}
networkState.IsParented = hasParentNetworkObject;
// If we are synchronizing and the associated NetworkObject has a parent then we want to send the
// LossyScale if the NetworkObject has a parent since NetworkObject spawn order is not guaranteed
if (hasParentNetworkObject)
{
networkState.LossyScale = transform.lossyScale;
}
}
// Checking scale deltas when not synchronizing
if (!isSynchronization)
{
if (!UseHalfFloatPrecision)
@@ -1679,7 +1793,7 @@ namespace Unity.Netcode.Components
var previousScale = networkState.Scale;
for (int i = 0; i < 3; i++)
{
if (Mathf.Abs(Mathf.DeltaAngle(previousScale[i], scale[i])) >= ScaleThreshold || networkState.IsTeleportingNextFrame)
if (Mathf.Abs(scale[i] - previousScale[i]) >= ScaleThreshold || networkState.IsTeleportingNextFrame)
{
isScaleDirty = true;
networkState.Scale[i] = scale[i];
@@ -1688,46 +1802,18 @@ namespace Unity.Netcode.Components
}
}
}
else // If we are synchronizing then we need to determine which scale to use
else // Just apply the full local scale when synchronizing
if (SynchronizeScale)
{
// This all has to do with complex nested hierarchies and how it impacts scale
// when set for the first time.
var hasParentNetworkObject = false;
// If the NetworkObject belonging to this NetworkTransform instance has a parent
// (i.e. this handles nested NetworkTransforms under a parent at some layer above)
if (NetworkObject.transform.parent != null)
{
var parentNetworkObject = NetworkObject.transform.parent.GetComponent<NetworkObject>();
// In-scene placed NetworkObjects parented under a GameObject with no
// NetworkObject preserve their lossyScale when synchronizing.
if (parentNetworkObject == null && NetworkObject.IsSceneObject != false)
{
hasParentNetworkObject = true;
}
else
{
// Or if the relative NetworkObject has a parent NetworkObject
hasParentNetworkObject = parentNetworkObject != null;
}
}
// If world position stays is set and the relative NetworkObject is parented under a NetworkObject
// then we want to use the lossy scale for the initial synchronization.
var useLossy = NetworkObject.WorldPositionStays() && hasParentNetworkObject;
var scaleToUse = useLossy ? transform.lossyScale : transform.localScale;
if (!UseHalfFloatPrecision)
{
networkState.ScaleX = scaleToUse.x;
networkState.ScaleY = scaleToUse.y;
networkState.ScaleZ = scaleToUse.z;
networkState.ScaleX = transform.localScale.x;
networkState.ScaleY = transform.localScale.y;
networkState.ScaleZ = transform.localScale.z;
}
else
{
networkState.Scale = scaleToUse;
networkState.Scale = transform.localScale;
}
networkState.HasScaleX = true;
networkState.HasScaleY = true;
@@ -2009,6 +2095,7 @@ namespace Unity.Netcode.Components
}
m_CurrentPosition = currentPosition;
m_TargetPosition = currentPosition;
// Apply the position
if (newState.InLocalSpace)
@@ -2023,32 +2110,45 @@ namespace Unity.Netcode.Components
if (newState.HasScaleChange)
{
bool shouldUseLossy = false;
if (newState.IsParented)
{
if (transform.parent == null)
{
shouldUseLossy = NetworkObject.WorldPositionStays();
}
else
{
shouldUseLossy = !NetworkObject.WorldPositionStays();
}
}
if (UseHalfFloatPrecision)
{
currentScale = newState.Scale;
m_CurrentScale = currentScale;
currentScale = shouldUseLossy ? newState.LossyScale : newState.Scale;
}
else
{
// Adjust based on which axis changed
if (newState.HasScaleX)
{
currentScale.x = newState.ScaleX;
currentScale.x = shouldUseLossy ? newState.LossyScale.x : newState.ScaleX;
}
if (newState.HasScaleY)
{
currentScale.y = newState.ScaleY;
currentScale.y = shouldUseLossy ? newState.LossyScale.y : newState.ScaleY;
}
if (newState.HasScaleZ)
{
currentScale.z = newState.ScaleZ;
currentScale.z = shouldUseLossy ? newState.LossyScale.z : newState.ScaleZ;
}
}
m_CurrentScale = currentScale;
m_TargetScale = currentScale;
m_ScaleInterpolator.ResetTo(currentScale, sentTime);
// Apply the adjusted scale
@@ -2082,6 +2182,7 @@ namespace Unity.Netcode.Components
}
m_CurrentRotation = currentRotation;
m_TargetRotation = currentRotation.eulerAngles;
m_RotationInterpolator.ResetTo(currentRotation, sentTime);
if (InLocalSpace)
@@ -2107,7 +2208,7 @@ namespace Unity.Netcode.Components
/// <remarks>
/// Only non-authoritative instances should invoke this
/// </remarks>
private void UpdateState(NetworkTransformState oldState, NetworkTransformState newState)
private void ApplyUpdatedState(NetworkTransformState newState)
{
// Set the transforms's synchronization modes
InLocalSpace = newState.InLocalSpace;
@@ -2137,8 +2238,10 @@ namespace Unity.Netcode.Components
{
// assure our local NetworkDeltaPosition state is updated
m_HalfPositionState.HalfVector3.Axis = m_LocalAuthoritativeNetworkState.NetworkDeltaPosition.HalfVector3.Axis;
// and update our current position
m_LocalAuthoritativeNetworkState.CurrentPosition = m_HalfPositionState.ToVector3(newState.NetworkTick);
// and update our target position
m_TargetPosition = m_HalfPositionState.ToVector3(newState.NetworkTick);
m_LocalAuthoritativeNetworkState.NetworkDeltaPosition.CurrentBasePosition = m_HalfPositionState.CurrentBasePosition;
m_LocalAuthoritativeNetworkState.CurrentPosition = m_TargetPosition;
}
if (!Interpolate)
@@ -2149,37 +2252,33 @@ namespace Unity.Netcode.Components
// Apply axial changes from the new state
// Either apply the delta position target position or the current state's delta position
// depending upon whether UsePositionDeltaCompression is enabled
if (m_LocalAuthoritativeNetworkState.HasPositionChange)
{
if (m_LocalAuthoritativeNetworkState.UseHalfFloatPrecision)
if (!m_LocalAuthoritativeNetworkState.UseHalfFloatPrecision)
{
UpdatePositionInterpolator(m_LocalAuthoritativeNetworkState.CurrentPosition, sentTime);
}
else
{
var currentPosition = GetSpaceRelativePosition();
var newTargetPosition = m_TargetPosition;
if (m_LocalAuthoritativeNetworkState.HasPositionX)
{
currentPosition.x = m_LocalAuthoritativeNetworkState.PositionX;
newTargetPosition.x = m_LocalAuthoritativeNetworkState.PositionX;
}
if (m_LocalAuthoritativeNetworkState.HasPositionY)
{
currentPosition.y = m_LocalAuthoritativeNetworkState.PositionY;
newTargetPosition.y = m_LocalAuthoritativeNetworkState.PositionY;
}
if (m_LocalAuthoritativeNetworkState.HasPositionZ)
{
currentPosition.z = m_LocalAuthoritativeNetworkState.PositionZ;
newTargetPosition.z = m_LocalAuthoritativeNetworkState.PositionZ;
}
UpdatePositionInterpolator(currentPosition, sentTime);
m_TargetPosition = newTargetPosition;
}
UpdatePositionInterpolator(m_TargetPosition, sentTime);
}
if (m_LocalAuthoritativeNetworkState.HasScaleChange)
{
var currentScale = transform.localScale;
var currentScale = m_TargetScale;
if (UseHalfFloatPrecision)
{
for (int i = 0; i < 3; i++)
@@ -2207,6 +2306,7 @@ namespace Unity.Netcode.Components
currentScale.z = m_LocalAuthoritativeNetworkState.ScaleZ;
}
}
m_TargetScale = currentScale;
m_ScaleInterpolator.AddMeasurement(currentScale, sentTime);
}
@@ -2221,7 +2321,9 @@ namespace Unity.Netcode.Components
}
else
{
currentEulerAngles = m_TargetRotation;
// Adjust based on which axis changed
// (both half precision and full precision apply Eulers to the RotAngle properties when reading the update)
if (m_LocalAuthoritativeNetworkState.HasRotAngleX)
{
currentEulerAngles.x = m_LocalAuthoritativeNetworkState.RotAngleX;
@@ -2236,6 +2338,7 @@ namespace Unity.Netcode.Components
{
currentEulerAngles.z = m_LocalAuthoritativeNetworkState.RotAngleZ;
}
m_TargetRotation = currentEulerAngles;
currentRotation.eulerAngles = currentEulerAngles;
}
@@ -2253,6 +2356,8 @@ namespace Unity.Netcode.Components
}
private NetworkTransformState m_OldState = new NetworkTransformState();
/// <summary>
/// Only non-authoritative instances should invoke this method
/// </summary>
@@ -2266,8 +2371,8 @@ namespace Unity.Netcode.Components
// Get the time when this new state was sent
newState.SentTime = new NetworkTime(NetworkManager.NetworkConfig.TickRate, newState.NetworkTick).Time;
// Update the state
UpdateState(oldState, newState);
// Apply the new state
ApplyUpdatedState(newState);
// Provide notifications when the state has been updated
OnNetworkTransformStateUpdated(ref oldState, ref newState);
@@ -2350,10 +2455,19 @@ namespace Unity.Netcode.Components
internal void OnUpdateAuthoritativeState(ref Transform transformSource)
{
// If our replicated state is not dirty and our local authority state is dirty, clear it.
if (!ReplicatedNetworkState.IsDirty() && m_LocalAuthoritativeNetworkState.IsDirty && !m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame)
if (m_LocalAuthoritativeNetworkState.IsDirty && !m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame)
{
// Now clear our bitset and prepare for next network tick state update
m_LocalAuthoritativeNetworkState.ClearBitSetForNextTick();
if (TrackByStateId)
{
m_LocalAuthoritativeNetworkState.TrackByStateId = true;
m_LocalAuthoritativeNetworkState.StateId++;
}
else
{
m_LocalAuthoritativeNetworkState.TrackByStateId = false;
}
}
AxisChangedDeltaPositionCheck();
@@ -2373,6 +2487,9 @@ namespace Unity.Netcode.Components
// Update any changes to the transform
var transformSource = transform;
OnUpdateAuthoritativeState(ref transformSource);
m_CurrentPosition = GetSpaceRelativePosition();
m_TargetPosition = GetSpaceRelativePosition();
}
else
{
@@ -2387,17 +2504,25 @@ namespace Unity.Netcode.Components
/// <inheritdoc/>
public override void OnNetworkSpawn()
{
///////////////////////////////////////////////////////////////
// NOTE: Legacy and no longer used (candidates for deprecation)
m_CachedIsServer = IsServer;
m_CachedNetworkManager = NetworkManager;
///////////////////////////////////////////////////////////////
// Register a custom named message specifically for this instance
m_MessageName = $"NTU_{NetworkObjectId}_{NetworkBehaviourId}";
NetworkManager.CustomMessagingManager.RegisterNamedMessageHandler(m_MessageName, TransformStateUpdate);
Initialize();
}
/// <inheritdoc/>
public override void OnNetworkDespawn()
{
ReplicatedNetworkState.OnValueChanged -= OnNetworkStateChanged;
if (!NetworkManager.ShutdownInProgress && NetworkManager.CustomMessagingManager != null)
{
NetworkManager.CustomMessagingManager.UnregisterNamedMessageHandler(m_MessageName);
}
CanCommitToTransform = false;
if (NetworkManager != null && NetworkManager.NetworkTickSystem != null)
{
@@ -2414,41 +2539,53 @@ namespace Unity.Netcode.Components
}
CanCommitToTransform = false;
base.OnDestroy();
m_ReplicatedNetworkStateServer.Dispose();
m_ReplicatedNetworkStateOwner.Dispose();
}
/// <inheritdoc/>
public override void OnGainedOwnership()
{
// Only initialize if we gained ownership
if (OwnerClientId == NetworkManager.LocalClientId)
{
Initialize();
}
}
/// <inheritdoc/>
public override void OnLostOwnership()
{
// Only initialize if we are not authority and lost
// ownership
if (OwnerClientId != NetworkManager.LocalClientId)
base.OnLostOwnership();
}
/// <inheritdoc/>
public override void OnGainedOwnership()
{
base.OnGainedOwnership();
}
protected override void OnOwnershipChanged(ulong previous, ulong current)
{
// If we were the previous owner or the newly assigned owner then reinitialize
if (current == NetworkManager.LocalClientId || previous == NetworkManager.LocalClientId)
{
Initialize();
}
base.OnOwnershipChanged(previous, current);
}
/// <summary>
/// Invoked when first spawned and when ownership changes.
/// </summary>
/// <param name="replicatedState">the <see cref="NetworkVariable{T}"/> replicated <see cref="NetworkTransformState"/></param>
/// <param name="replicatedState">the current <see cref="NetworkTransformState"/> after initializing</param>
protected virtual void OnInitialize(ref NetworkTransformState replicatedState)
{
}
/// <summary>
/// An owner read and owner write NetworkVariable so it doesn't generate any messages
/// </summary>
private NetworkVariable<NetworkTransformState> m_InternalStatNetVar = new NetworkVariable<NetworkTransformState>(default, NetworkVariableReadPermission.Owner, NetworkVariableWritePermission.Owner);
/// <summary>
/// This method is only invoked by the owner
/// Use: OnInitialize(ref NetworkTransformState replicatedState) to be notified on all instances
/// </summary>
/// <param name="replicatedState"></param>
protected virtual void OnInitialize(ref NetworkVariable<NetworkTransformState> replicatedState)
{
}
/// <summary>
/// Initializes NetworkTransform when spawned and ownership changes.
/// </summary>
@@ -2460,7 +2597,6 @@ namespace Unity.Netcode.Components
}
CanCommitToTransform = IsServerAuthoritative() ? IsServer : IsOwner;
var replicatedState = ReplicatedNetworkState;
var currentPosition = GetSpaceRelativePosition();
var currentRotation = GetSpaceRelativeRotation();
@@ -2470,7 +2606,8 @@ namespace Unity.Netcode.Components
{
m_HalfPositionState = new NetworkDeltaPosition(currentPosition, NetworkManager.NetworkTickSystem.ServerTime.Tick, math.bool3(SyncPositionX, SyncPositionY, SyncPositionZ));
}
m_CurrentPosition = currentPosition;
m_TargetPosition = currentPosition;
// Authority only updates once per network tick
NetworkManager.NetworkTickSystem.Tick -= NetworkTickSystem_Tick;
NetworkManager.NetworkTickSystem.Tick += NetworkTickSystem_Tick;
@@ -2480,21 +2617,27 @@ namespace Unity.Netcode.Components
}
else
{
// Sanity check to assure we only subscribe to OnValueChanged once
replicatedState.OnValueChanged -= OnNetworkStateChanged;
replicatedState.OnValueChanged += OnNetworkStateChanged;
// Assure we no longer subscribe to the tick event
NetworkManager.NetworkTickSystem.Tick -= NetworkTickSystem_Tick;
ResetInterpolatedStateToCurrentAuthoritativeState();
m_CurrentPosition = currentPosition;
m_TargetPosition = currentPosition;
m_CurrentScale = transform.localScale;
m_TargetScale = transform.localScale;
m_CurrentRotation = currentRotation;
m_TargetRotation = currentRotation.eulerAngles;
}
OnInitialize(ref m_LocalAuthoritativeNetworkState);
OnInitialize(ref replicatedState);
if (IsOwner)
{
m_InternalStatNetVar.Value = m_LocalAuthoritativeNetworkState;
OnInitialize(ref m_InternalStatNetVar);
}
}
/// <inheritdoc/>
@@ -2649,7 +2792,7 @@ namespace Unity.Netcode.Components
var serverTime = NetworkManager.ServerTime;
var cachedDeltaTime = NetworkManager.RealTimeProvider.DeltaTime;
var cachedServerTime = serverTime.Time;
// TODO: Investigate Further
// With owner authoritative mode, non-authority clients can lag behind
// by more than 1 tick period of time. The current "solution" for now
// is to make their cachedRenderTime run 2 ticks behind.
@@ -2677,12 +2820,6 @@ namespace Unity.Netcode.Components
}
}
// If we have not received any additional state updates since the very
// initial synchronization, then exit early.
if (m_LocalAuthoritativeNetworkState.IsSynchronizing)
{
return;
}
// Apply the current authoritative state
ApplyAuthoritativeState();
}
@@ -2725,6 +2862,111 @@ namespace Unity.Netcode.Components
{
return OnIsServerAuthoritative();
}
/// <summary>
/// Receives the <see cref="NetworkTransformState"/> named message updates
/// </summary>
/// <param name="senderId">authority of the transform</param>
/// <param name="messagePayload">serialzied <see cref="NetworkTransformState"/></param>
private void TransformStateUpdate(ulong senderId, FastBufferReader messagePayload)
{
if (!OnIsServerAuthoritative() && IsServer && OwnerClientId == NetworkManager.ServerClientId)
{
// Ownership must have changed, ignore any additional pending messages that might have
// come from a previous owner client.
return;
}
// Forward owner authoritative messages before doing anything else
if (IsServer && !OnIsServerAuthoritative())
{
ForwardStateUpdateMessage(messagePayload);
}
// Store the previous/old state
m_OldState = m_LocalAuthoritativeNetworkState;
// Deserialize the message
messagePayload.ReadNetworkSerializableInPlace(ref m_LocalAuthoritativeNetworkState);
// Apply the message
OnNetworkStateChanged(m_OldState, m_LocalAuthoritativeNetworkState);
}
/// <summary>
/// Forwards owner authoritative state updates when received by the server
/// </summary>
/// <param name="messagePayload">the owner state message payload</param>
private unsafe void ForwardStateUpdateMessage(FastBufferReader messagePayload)
{
var serverAuthoritative = OnIsServerAuthoritative();
var currentPosition = messagePayload.Position;
var messageSize = messagePayload.Length - currentPosition;
var writer = new FastBufferWriter(messageSize, Allocator.Temp);
using (writer)
{
writer.WriteBytesSafe(messagePayload.GetUnsafePtr(), messageSize, currentPosition);
var clientCount = NetworkManager.ConnectionManager.ConnectedClientsList.Count;
for (int i = 0; i < clientCount; i++)
{
var clientId = NetworkManager.ConnectionManager.ConnectedClientsList[i].ClientId;
if (NetworkManager.ServerClientId == clientId || (!serverAuthoritative && clientId == OwnerClientId))
{
continue;
}
NetworkManager.CustomMessagingManager.SendNamedMessage(m_MessageName, clientId, writer);
}
}
messagePayload.Seek(currentPosition);
}
/// <summary>
/// Sends <see cref="NetworkTransformState"/> named message updates by the authority of the transform
/// </summary>
private void UpdateTransformState()
{
if (NetworkManager.ShutdownInProgress)
{
return;
}
bool isServerAuthoritative = OnIsServerAuthoritative();
if (isServerAuthoritative && !IsServer)
{
Debug.LogError($"Server authoritative {nameof(NetworkTransform)} can only be updated by the server!");
}
else if (!isServerAuthoritative && !IsServer && !IsOwner)
{
Debug.LogError($"Owner authoritative {nameof(NetworkTransform)} can only be updated by the owner!");
}
var customMessageManager = NetworkManager.CustomMessagingManager;
var writer = new FastBufferWriter(128, Allocator.Temp);
using (writer)
{
writer.WriteNetworkSerializable(m_LocalAuthoritativeNetworkState);
// Server-host always sends updates to all clients (but itself)
if (IsServer)
{
var clientCount = NetworkManager.ConnectionManager.ConnectedClientsList.Count;
for (int i = 0; i < clientCount; i++)
{
var clientId = NetworkManager.ConnectionManager.ConnectedClientsList[i].ClientId;
if (NetworkManager.ServerClientId == clientId)
{
continue;
}
customMessageManager.SendNamedMessage(m_MessageName, clientId, writer);
}
}
else
{
// Clients (owner authoritative) send messages to the server-host
customMessageManager.SendNamedMessage(m_MessageName, NetworkManager.ServerClientId, writer);
}
}
}
}
internal interface INetworkTransformLogStateEntry

View File

@@ -9,7 +9,7 @@ See guides below to install Unity Netcode for GameObjects, set up your project,
- [Documentation](https://docs-multiplayer.unity3d.com/netcode/current/about)
- [Installation](https://docs-multiplayer.unity3d.com/netcode/current/installation)
- [First Steps](https://docs-multiplayer.unity3d.com/netcode/current/tutorials/get-started-ngo)
- [API Reference](https://docs-multiplayer.unity3d.com/netcode/current/api/introduction)
- [API Reference](https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@1.6/api/index.html)
# Technical details

View File

@@ -59,7 +59,7 @@ namespace Unity.Netcode.Editor.CodeGen
public static bool IsSubclassOf(this TypeDefinition typeDefinition, string classTypeFullName)
{
if (!typeDefinition.IsClass)
if (typeDefinition == null || !typeDefinition.IsClass)
{
return false;
}
@@ -154,6 +154,10 @@ namespace Unity.Netcode.Editor.CodeGen
public static bool IsSubclassOf(this TypeReference typeReference, TypeReference baseClass)
{
if (typeReference == null)
{
return false;
}
var type = typeReference.Resolve();
if (type?.BaseType == null || type.BaseType.Name == nameof(Object))
{

View File

@@ -7,6 +7,7 @@ using Mono.Cecil.Cil;
using Mono.Cecil.Rocks;
using Unity.CompilationPipeline.Common.Diagnostics;
using Unity.CompilationPipeline.Common.ILPostProcessing;
using UnityEngine;
using ILPPInterface = Unity.CompilationPipeline.Common.ILPostProcessing.ILPostProcessor;
using MethodAttributes = Mono.Cecil.MethodAttributes;
@@ -101,6 +102,8 @@ namespace Unity.Netcode.Editor.CodeGen
private ModuleDefinition m_NetcodeModule;
private PostProcessorAssemblyResolver m_AssemblyResolver;
private MethodReference m_RuntimeInitializeOnLoadAttribute_Ctor;
private MethodReference m_MessageManager_ReceiveMessage_MethodRef;
private MethodReference m_MessageManager_CreateMessageAndGetVersion_MethodRef;
private TypeReference m_MessageManager_MessageWithHandler_TypeRef;
@@ -125,6 +128,7 @@ namespace Unity.Netcode.Editor.CodeGen
// (i.e., there's no #if UNITY_EDITOR in them that could create invalid IL code)
TypeDefinition typeTypeDef = moduleDefinition.ImportReference(typeof(Type)).Resolve();
TypeDefinition listTypeDef = moduleDefinition.ImportReference(typeof(List<>)).Resolve();
m_RuntimeInitializeOnLoadAttribute_Ctor = moduleDefinition.ImportReference(typeof(RuntimeInitializeOnLoadMethodAttribute).GetConstructor(new Type[] { }));
TypeDefinition messageHandlerTypeDef = null;
TypeDefinition versionGetterTypeDef = null;
@@ -232,25 +236,6 @@ namespace Unity.Netcode.Editor.CodeGen
return true;
}
private MethodDefinition GetOrCreateStaticConstructor(TypeDefinition typeDefinition)
{
var staticCtorMethodDef = typeDefinition.GetStaticConstructor();
if (staticCtorMethodDef == null)
{
staticCtorMethodDef = new MethodDefinition(
".cctor", // Static Constructor (constant-constructor)
MethodAttributes.HideBySig |
MethodAttributes.SpecialName |
MethodAttributes.RTSpecialName |
MethodAttributes.Static,
typeDefinition.Module.TypeSystem.Void);
staticCtorMethodDef.Body.Instructions.Add(Instruction.Create(OpCodes.Ret));
typeDefinition.Methods.Add(staticCtorMethodDef);
}
return staticCtorMethodDef;
}
private void CreateInstructionsToRegisterType(ILProcessor processor, List<Instruction> instructions, TypeReference type, MethodReference receiveMethod, MethodReference versionMethod)
{
// NetworkMessageManager.__network_message_types.Add(new NetworkMessageManager.MessageWithHandler{MessageType=typeof(type), Handler=type.Receive});
@@ -295,15 +280,19 @@ namespace Unity.Netcode.Editor.CodeGen
// https://web.archive.org/web/20100212140402/http://blogs.msdn.com/junfeng/archive/2005/11/19/494914.aspx
private void CreateModuleInitializer(AssemblyDefinition assembly, List<TypeDefinition> networkMessageTypes)
{
foreach (var typeDefinition in assembly.MainModule.Types)
{
if (typeDefinition.FullName == "<Module>")
{
var staticCtorMethodDef = GetOrCreateStaticConstructor(typeDefinition);
var typeDefinition = new TypeDefinition("__GEN", "INetworkMessageHelper", TypeAttributes.NotPublic | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit, assembly.MainModule.TypeSystem.Object);
var processor = staticCtorMethodDef.Body.GetILProcessor();
var staticCtorMethodDef = new MethodDefinition(
$"InitializeMessages",
MethodAttributes.Assembly |
MethodAttributes.Static,
assembly.MainModule.TypeSystem.Void);
staticCtorMethodDef.Body.Instructions.Add(Instruction.Create(OpCodes.Ret));
staticCtorMethodDef.CustomAttributes.Add(new CustomAttribute(m_RuntimeInitializeOnLoadAttribute_Ctor));
typeDefinition.Methods.Add(staticCtorMethodDef);
var instructions = new List<Instruction>();
var processor = staticCtorMethodDef.Body.GetILProcessor();
foreach (var type in networkMessageTypes)
{
@@ -315,9 +304,8 @@ namespace Unity.Netcode.Editor.CodeGen
}
instructions.ForEach(instruction => processor.Body.Instructions.Insert(processor.Body.Instructions.Count - 1, instruction));
break;
}
}
assembly.MainModule.Types.Add(typeDefinition);
}
}
}

View File

@@ -8,6 +8,9 @@ using Mono.Cecil.Cil;
using Mono.Cecil.Rocks;
using Unity.CompilationPipeline.Common.Diagnostics;
using Unity.CompilationPipeline.Common.ILPostProcessing;
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;
using ILPPInterface = Unity.CompilationPipeline.Common.ILPostProcessing.ILPostProcessor;
using MethodAttributes = Mono.Cecil.MethodAttributes;
@@ -66,7 +69,7 @@ namespace Unity.Netcode.Editor.CodeGen
{
m_MainModule = mainModule;
if (ImportReferences(mainModule))
if (ImportReferences(mainModule, compiledAssembly.Defines))
{
// process `NetworkBehaviour` types
try
@@ -76,7 +79,38 @@ namespace Unity.Netcode.Editor.CodeGen
.ToList()
.ForEach(b => ProcessNetworkBehaviour(b, compiledAssembly.Defines));
CreateNetworkVariableTypeInitializers(assemblyDefinition);
foreach (var type in mainModule.GetTypes())
{
var resolved = type.Resolve();
foreach (var attribute in resolved.CustomAttributes)
{
if (attribute.AttributeType.Name == nameof(GenerateSerializationForTypeAttribute))
{
var wrappedType = mainModule.ImportReference((TypeReference)attribute.ConstructorArguments[0].Value);
if (!m_WrappedNetworkVariableTypes.Contains(wrappedType))
{
m_WrappedNetworkVariableTypes.Add(wrappedType);
}
}
}
foreach (var method in resolved.Methods)
{
foreach (var attribute in method.CustomAttributes)
{
if (attribute.AttributeType.Name == nameof(GenerateSerializationForTypeAttribute))
{
var wrappedType = mainModule.ImportReference((TypeReference)attribute.ConstructorArguments[0].Value);
if (!m_WrappedNetworkVariableTypes.Contains(wrappedType))
{
m_WrappedNetworkVariableTypes.Add(wrappedType);
}
}
}
}
}
CreateNetworkVariableTypeInitializers(assemblyDefinition, compiledAssembly.Defines);
}
catch (Exception e)
{
@@ -109,25 +143,6 @@ namespace Unity.Netcode.Editor.CodeGen
return new ILPostProcessResult(new InMemoryAssembly(pe.ToArray(), pdb.ToArray()), m_Diagnostics);
}
private MethodDefinition GetOrCreateStaticConstructor(TypeDefinition typeDefinition)
{
var staticCtorMethodDef = typeDefinition.GetStaticConstructor();
if (staticCtorMethodDef == null)
{
staticCtorMethodDef = new MethodDefinition(
".cctor", // Static Constructor (constant-constructor)
MethodAttributes.HideBySig |
MethodAttributes.SpecialName |
MethodAttributes.RTSpecialName |
MethodAttributes.Static,
typeDefinition.Module.TypeSystem.Void);
staticCtorMethodDef.Body.Instructions.Add(Instruction.Create(OpCodes.Ret));
typeDefinition.Methods.Add(staticCtorMethodDef);
}
return staticCtorMethodDef;
}
private bool IsMemcpyableType(TypeReference type)
{
foreach (var supportedType in BaseSupportedTypes)
@@ -154,17 +169,30 @@ namespace Unity.Netcode.Editor.CodeGen
return false;
}
private void CreateNetworkVariableTypeInitializers(AssemblyDefinition assembly)
private void CreateNetworkVariableTypeInitializers(AssemblyDefinition assembly, string[] assemblyDefines)
{
foreach (var typeDefinition in assembly.MainModule.Types)
{
if (typeDefinition.FullName == "<Module>")
{
var staticCtorMethodDef = GetOrCreateStaticConstructor(typeDefinition);
var typeDefinition = new TypeDefinition("__GEN", "NetworkVariableSerializationHelper", TypeAttributes.NotPublic | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit, assembly.MainModule.TypeSystem.Object);
var staticCtorMethodDef = new MethodDefinition(
$"InitializeSerialization",
MethodAttributes.Assembly |
MethodAttributes.Static,
assembly.MainModule.TypeSystem.Void);
staticCtorMethodDef.Body.Instructions.Add(Instruction.Create(OpCodes.Ret));
bool isEditor = assemblyDefines.Contains("UNITY_EDITOR");
if (isEditor)
{
staticCtorMethodDef.CustomAttributes.Add(new CustomAttribute(m_InitializeOnLoadAttribute_Ctor));
}
else
{
staticCtorMethodDef.CustomAttributes.Add(new CustomAttribute(m_RuntimeInitializeOnLoadAttribute_Ctor));
}
typeDefinition.Methods.Add(staticCtorMethodDef);
var processor = staticCtorMethodDef.Body.GetILProcessor();
var instructions = new List<Instruction>();
var processor = staticCtorMethodDef.Body.GetILProcessor();
foreach (var type in m_WrappedNetworkVariableTypes)
{
@@ -210,10 +238,7 @@ namespace Unity.Netcode.Editor.CodeGen
equalityMethod = new GenericInstanceMethod(m_NetworkVariableSerializationTypes_InitializeEqualityChecker_UnmanagedValueEqualsArray_MethodRef);
}
if (serializeMethod != null)
{
serializeMethod.GenericArguments.Add(wrappedType);
}
serializeMethod?.GenericArguments.Add(wrappedType);
equalityMethod.GenericArguments.Add(wrappedType);
}
#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT
@@ -273,10 +298,7 @@ namespace Unity.Netcode.Editor.CodeGen
equalityMethod = new GenericInstanceMethod(m_NetworkVariableSerializationTypes_InitializeEqualityChecker_UnmanagedValueEquals_MethodRef);
}
if (serializeMethod != null)
{
serializeMethod.GenericArguments.Add(type);
}
serializeMethod?.GenericArguments.Add(type);
equalityMethod.GenericArguments.Add(type);
}
else
@@ -310,10 +332,7 @@ namespace Unity.Netcode.Editor.CodeGen
equalityMethod = new GenericInstanceMethod(m_NetworkVariableSerializationTypes_InitializeEqualityChecker_ManagedClassEquals_MethodRef);
}
if (serializeMethod != null)
{
serializeMethod.GenericArguments.Add(type);
}
serializeMethod?.GenericArguments.Add(type);
equalityMethod.GenericArguments.Add(type);
}
@@ -325,9 +344,8 @@ namespace Unity.Netcode.Editor.CodeGen
}
instructions.ForEach(instruction => processor.Body.Instructions.Insert(processor.Body.Instructions.Count - 1, instruction));
break;
}
}
assembly.MainModule.Types.Add(typeDefinition);
}
private ModuleDefinition m_MainModule;
@@ -343,10 +361,7 @@ namespace Unity.Netcode.Editor.CodeGen
private MethodReference m_NetworkManager_getIsServer_MethodRef;
private MethodReference m_NetworkManager_getIsClient_MethodRef;
private FieldReference m_NetworkManager_LogLevel_FieldRef;
private FieldReference m_NetworkManager_rpc_func_table_FieldRef;
private MethodReference m_NetworkManager_rpc_func_table_Add_MethodRef;
private FieldReference m_NetworkManager_rpc_name_table_FieldRef;
private MethodReference m_NetworkManager_rpc_name_table_Add_MethodRef;
private MethodReference m_NetworkBehaviour___registerRpc_MethodRef;
private TypeReference m_NetworkBehaviour_TypeRef;
private TypeReference m_NetworkVariableBase_TypeRef;
private MethodReference m_NetworkVariableBase_Initialize_MethodRef;
@@ -396,6 +411,9 @@ namespace Unity.Netcode.Editor.CodeGen
#endif
private MethodReference m_NetworkVariableSerializationTypes_InitializeEqualityChecker_ManagedClassEquals_MethodRef;
private MethodReference m_RuntimeInitializeOnLoadAttribute_Ctor;
private MethodReference m_InitializeOnLoadAttribute_Ctor;
private MethodReference m_ExceptionCtorMethodReference;
private MethodReference m_List_NetworkVariableBase_Add;
@@ -470,9 +488,9 @@ namespace Unity.Netcode.Editor.CodeGen
private const string k_NetworkManager_IsServer = nameof(NetworkManager.IsServer);
private const string k_NetworkManager_IsClient = nameof(NetworkManager.IsClient);
private const string k_NetworkManager_LogLevel = nameof(NetworkManager.LogLevel);
private const string k_NetworkManager_rpc_func_table = nameof(NetworkManager.__rpc_func_table);
private const string k_NetworkManager_rpc_name_table = nameof(NetworkManager.__rpc_name_table);
private const string k_NetworkBehaviour_rpc_func_table = nameof(NetworkBehaviour.__rpc_func_table);
private const string k_NetworkBehaviour_rpc_name_table = nameof(NetworkBehaviour.__rpc_name_table);
private const string k_NetworkBehaviour_rpc_exec_stage = nameof(NetworkBehaviour.__rpc_exec_stage);
private const string k_NetworkBehaviour_NetworkVariableFields = nameof(NetworkBehaviour.NetworkVariableFields);
private const string k_NetworkBehaviour_beginSendServerRpc = nameof(NetworkBehaviour.__beginSendServerRpc);
@@ -480,10 +498,12 @@ namespace Unity.Netcode.Editor.CodeGen
private const string k_NetworkBehaviour_beginSendClientRpc = nameof(NetworkBehaviour.__beginSendClientRpc);
private const string k_NetworkBehaviour_endSendClientRpc = nameof(NetworkBehaviour.__endSendClientRpc);
private const string k_NetworkBehaviour___initializeVariables = nameof(NetworkBehaviour.__initializeVariables);
private const string k_NetworkBehaviour___initializeRpcs = nameof(NetworkBehaviour.__initializeRpcs);
private const string k_NetworkBehaviour_createNativeList = nameof(NetworkBehaviour.__createNativeList);
private const string k_NetworkBehaviour_NetworkManager = nameof(NetworkBehaviour.NetworkManager);
private const string k_NetworkBehaviour_OwnerClientId = nameof(NetworkBehaviour.OwnerClientId);
private const string k_NetworkBehaviour___nameNetworkVariable = nameof(NetworkBehaviour.__nameNetworkVariable);
private const string k_NetworkBehaviour___registerRpc = nameof(NetworkBehaviour.__registerRpc);
private const string k_NetworkVariableBase_Initialize = nameof(NetworkVariableBase.Initialize);
@@ -497,7 +517,7 @@ namespace Unity.Netcode.Editor.CodeGen
// CodeGen cannot reference the collections assembly to do a typeof() on it due to a bug that causes that to crash.
private const string k_INativeListBool_FullName = "Unity.Collections.INativeList`1<System.Byte>";
private bool ImportReferences(ModuleDefinition moduleDefinition)
private bool ImportReferences(ModuleDefinition moduleDefinition, string[] assemblyDefines)
{
TypeDefinition debugTypeDef = null;
foreach (var unityTypeDef in m_UnityModule.GetAllTypes())
@@ -509,6 +529,15 @@ namespace Unity.Netcode.Editor.CodeGen
}
}
bool isEditor = assemblyDefines.Contains("UNITY_EDITOR");
if (isEditor)
{
m_InitializeOnLoadAttribute_Ctor = moduleDefinition.ImportReference(typeof(InitializeOnLoadMethodAttribute).GetConstructor(new Type[] { }));
}
m_RuntimeInitializeOnLoadAttribute_Ctor = moduleDefinition.ImportReference(typeof(RuntimeInitializeOnLoadMethodAttribute).GetConstructor(new Type[] { }));
TypeDefinition networkManagerTypeDef = null;
TypeDefinition networkBehaviourTypeDef = null;
TypeDefinition networkVariableBaseTypeDef = null;
@@ -541,7 +570,7 @@ namespace Unity.Netcode.Editor.CodeGen
continue;
}
if (networkHandlerDelegateTypeDef == null && netcodeTypeDef.Name == nameof(NetworkManager.RpcReceiveHandler))
if (networkHandlerDelegateTypeDef == null && netcodeTypeDef.Name == nameof(NetworkBehaviour.RpcReceiveHandler))
{
networkHandlerDelegateTypeDef = netcodeTypeDef;
continue;
@@ -640,20 +669,6 @@ namespace Unity.Netcode.Editor.CodeGen
case k_NetworkManager_LogLevel:
m_NetworkManager_LogLevel_FieldRef = moduleDefinition.ImportReference(fieldDef);
break;
case k_NetworkManager_rpc_func_table:
m_NetworkManager_rpc_func_table_FieldRef = moduleDefinition.ImportReference(fieldDef);
m_NetworkManager_rpc_func_table_Add_MethodRef = fieldDef.FieldType.Resolve().Methods.First(m => m.Name == "Add");
m_NetworkManager_rpc_func_table_Add_MethodRef.DeclaringType = fieldDef.FieldType;
m_NetworkManager_rpc_func_table_Add_MethodRef = moduleDefinition.ImportReference(m_NetworkManager_rpc_func_table_Add_MethodRef);
break;
case k_NetworkManager_rpc_name_table:
m_NetworkManager_rpc_name_table_FieldRef = moduleDefinition.ImportReference(fieldDef);
m_NetworkManager_rpc_name_table_Add_MethodRef = fieldDef.FieldType.Resolve().Methods.First(m => m.Name == "Add");
m_NetworkManager_rpc_name_table_Add_MethodRef.DeclaringType = fieldDef.FieldType;
m_NetworkManager_rpc_name_table_Add_MethodRef = moduleDefinition.ImportReference(m_NetworkManager_rpc_name_table_Add_MethodRef);
break;
}
}
@@ -693,6 +708,10 @@ namespace Unity.Netcode.Editor.CodeGen
case k_NetworkBehaviour___nameNetworkVariable:
m_NetworkBehaviour___nameNetworkVariable_MethodRef = moduleDefinition.ImportReference(methodDef);
break;
case k_NetworkBehaviour___registerRpc:
m_NetworkBehaviour___registerRpc_MethodRef = moduleDefinition.ImportReference(methodDef);
break;
}
}
@@ -1106,8 +1125,15 @@ namespace Unity.Netcode.Editor.CodeGen
private void ProcessNetworkBehaviour(TypeDefinition typeDefinition, string[] assemblyDefines)
{
var rpcHandlers = new List<(uint RpcMethodId, MethodDefinition RpcHandler)>();
var rpcNames = new List<(uint RpcMethodId, string RpcMethodName)>();
foreach (var methodDefinition in typeDefinition.Methods)
{
if (methodDefinition.Name == k_NetworkBehaviour___initializeRpcs)
{
// If this hits, we've already generated the method for this class because a child class got processed first.
return;
}
}
var rpcHandlers = new List<(uint RpcMethodId, MethodDefinition RpcHandler, string RpcMethodName)>();
bool isEditorOrDevelopment = assemblyDefines.Contains("UNITY_EDITOR") || assemblyDefines.Contains("DEVELOPMENT_BUILD");
@@ -1138,12 +1164,7 @@ namespace Unity.Netcode.Editor.CodeGen
InjectWriteAndCallBlocks(methodDefinition, rpcAttribute, rpcMethodId);
rpcHandlers.Add((rpcMethodId, GenerateStaticHandler(methodDefinition, rpcAttribute, rpcMethodId)));
if (isEditorOrDevelopment)
{
rpcNames.Add((rpcMethodId, methodDefinition.Name));
}
rpcHandlers.Add((rpcMethodId, GenerateStaticHandler(methodDefinition, rpcAttribute, rpcMethodId), methodDefinition.Name));
}
GenerateVariableInitialization(typeDefinition);
@@ -1157,16 +1178,25 @@ namespace Unity.Netcode.Editor.CodeGen
//var type = field.FieldType;
if (type.IsGenericInstance)
{
if (type.Resolve().Name == typeof(NetworkVariable<>).Name || type.Resolve().Name == typeof(NetworkList<>).Name)
foreach (var attribute in type.Resolve().CustomAttributes)
{
if (attribute.AttributeType.Name == nameof(GenerateSerializationForGenericParameterAttribute))
{
var idx = (int)attribute.ConstructorArguments[0].Value;
var genericInstanceType = (GenericInstanceType)type;
var wrappedType = genericInstanceType.GenericArguments[0];
if (idx < 0 || idx >= genericInstanceType.GenericArguments.Count)
{
m_Diagnostics.AddError($"{type} has a {nameof(GenerateSerializationForGenericParameterAttribute)} referencing a parameter index outside the valid range (0-{genericInstanceType.GenericArguments.Count - 1}");
continue;
}
var wrappedType = genericInstanceType.GenericArguments[idx];
if (!m_WrappedNetworkVariableTypes.Contains(wrappedType))
{
m_WrappedNetworkVariableTypes.Add(wrappedType);
}
}
}
}
{
var baseTypes = new List<TypeReference>();
@@ -1184,10 +1214,18 @@ namespace Unity.Netcode.Editor.CodeGen
GetAllBaseTypesAndResolveGenerics(type.Resolve(), ref baseTypes, genericParams);
foreach (var baseType in baseTypes)
{
if (baseType.Resolve().Name == typeof(NetworkVariable<>).Name || baseType.Resolve().Name == typeof(NetworkList<>).Name)
foreach (var attribute in baseType.Resolve().CustomAttributes)
{
if (attribute.AttributeType.Name == nameof(GenerateSerializationForGenericParameterAttribute))
{
var idx = (int)attribute.ConstructorArguments[0].Value;
var genericInstanceType = (GenericInstanceType)baseType;
var wrappedType = genericInstanceType.GenericArguments[0];
if (idx < 0 || idx >= genericInstanceType.GenericArguments.Count)
{
m_Diagnostics.AddError($"{baseType} has a {nameof(GenerateSerializationForGenericParameterAttribute)} referencing a parameter index outside the valid range (0-{genericInstanceType.GenericArguments.Count - 1}");
continue;
}
var wrappedType = genericInstanceType.GenericArguments[idx];
if (!m_WrappedNetworkVariableTypes.Contains(wrappedType))
{
m_WrappedNetworkVariableTypes.Add(wrappedType);
@@ -1197,48 +1235,88 @@ namespace Unity.Netcode.Editor.CodeGen
}
}
}
if (rpcHandlers.Count > 0 || rpcNames.Count > 0)
{
var staticCtorMethodDef = typeDefinition.GetStaticConstructor();
if (staticCtorMethodDef == null)
{
staticCtorMethodDef = new MethodDefinition(
".cctor", // Static Constructor (constant-constructor)
MethodAttributes.HideBySig |
MethodAttributes.SpecialName |
MethodAttributes.RTSpecialName |
MethodAttributes.Static,
typeDefinition.Module.TypeSystem.Void);
staticCtorMethodDef.Body.Instructions.Add(Instruction.Create(OpCodes.Ret));
typeDefinition.Methods.Add(staticCtorMethodDef);
}
var instructions = new List<Instruction>();
var processor = staticCtorMethodDef.Body.GetILProcessor();
//if (rpcHandlers.Count > 0)
{
foreach (var (rpcMethodId, rpcHandler) in rpcHandlers)
// This always needs to generate even if it's empty.
var initializeRpcsMethodDef = new MethodDefinition(
k_NetworkBehaviour___initializeRpcs,
MethodAttributes.Family | MethodAttributes.Virtual | MethodAttributes.HideBySig,
typeDefinition.Module.TypeSystem.Void);
initializeRpcsMethodDef.Body.Instructions.Add(Instruction.Create(OpCodes.Ret));
typeDefinition.Methods.Add(initializeRpcsMethodDef);
var instructions = new List<Instruction>();
var processor = initializeRpcsMethodDef.Body.GetILProcessor();
foreach (var (rpcMethodId, rpcHandler, rpcMethodName) in rpcHandlers)
{
typeDefinition.Methods.Add(rpcHandler);
// NetworkManager.__rpc_func_table.Add(RpcMethodId, HandleFunc);
instructions.Add(processor.Create(OpCodes.Ldsfld, m_NetworkManager_rpc_func_table_FieldRef));
instructions.Add(processor.Create(OpCodes.Ldc_I4, unchecked((int)rpcMethodId)));
instructions.Add(processor.Create(OpCodes.Ldnull));
instructions.Add(processor.Create(OpCodes.Ldftn, rpcHandler));
instructions.Add(processor.Create(OpCodes.Newobj, m_NetworkHandlerDelegateCtor_MethodRef));
instructions.Add(processor.Create(OpCodes.Call, m_NetworkManager_rpc_func_table_Add_MethodRef));
MethodReference callMethod = rpcHandler;
if (typeDefinition.HasGenericParameters)
{
var genericTypes = new List<TypeReference>();
foreach (var parameter in typeDefinition.GenericParameters)
{
genericTypes.Add(parameter);
}
callMethod = callMethod.MakeGeneric(genericTypes.ToArray());
}
foreach (var (rpcMethodId, rpcMethodName) in rpcNames)
{
// NetworkManager.__rpc_name_table.Add(RpcMethodId, RpcMethodName);
instructions.Add(processor.Create(OpCodes.Ldsfld, m_NetworkManager_rpc_name_table_FieldRef));
// __registerRpc(RpcMethodId, HandleFunc, methodName);
instructions.Add(processor.Create(OpCodes.Ldarg_0));
instructions.Add(processor.Create(OpCodes.Ldc_I4, unchecked((int)rpcMethodId)));
instructions.Add(processor.Create(OpCodes.Ldnull));
instructions.Add(processor.Create(OpCodes.Ldftn, callMethod));
instructions.Add(processor.Create(OpCodes.Newobj, m_NetworkHandlerDelegateCtor_MethodRef));
instructions.Add(processor.Create(OpCodes.Ldstr, rpcMethodName));
instructions.Add(processor.Create(OpCodes.Call, m_NetworkManager_rpc_name_table_Add_MethodRef));
instructions.Add(processor.Create(OpCodes.Call, m_NetworkBehaviour___registerRpc_MethodRef));
}
// Find the base method...
MethodReference initializeRpcsBaseReference = null;
foreach (var methodDefinition in typeDefinition.BaseType.Resolve().Methods)
{
if (methodDefinition.Name == k_NetworkBehaviour___initializeRpcs)
{
initializeRpcsBaseReference = m_MainModule.ImportReference(methodDefinition);
break;
}
}
if (initializeRpcsBaseReference == null)
{
// If we couldn't find it, we have to go ahead and add it.
// The base class could be in another assembly... that's ok, this won't
// actually save but it'll generate the same method the same way later,
// so this at least allows us to reference it.
ProcessNetworkBehaviour(typeDefinition.BaseType.Resolve(), assemblyDefines);
foreach (var methodDefinition in typeDefinition.BaseType.Resolve().Methods)
{
if (methodDefinition.Name == k_NetworkBehaviour___initializeRpcs)
{
initializeRpcsBaseReference = m_MainModule.ImportReference(methodDefinition);
break;
}
}
}
if (typeDefinition.BaseType.Resolve().HasGenericParameters)
{
var baseTypeInstance = (GenericInstanceType)typeDefinition.BaseType;
initializeRpcsBaseReference = initializeRpcsBaseReference.MakeGeneric(baseTypeInstance.GenericArguments.ToArray());
}
// base.__initializeRpcs();
instructions.Add(processor.Create(OpCodes.Nop));
instructions.Add(processor.Create(OpCodes.Ldarg_0));
instructions.Add(processor.Create(OpCodes.Call, initializeRpcsBaseReference));
instructions.Add(processor.Create(OpCodes.Nop));
instructions.Reverse();
instructions.ForEach(instruction => processor.Body.Instructions.Insert(0, instruction));
}
@@ -1254,7 +1332,8 @@ namespace Unity.Netcode.Editor.CodeGen
baseGetTypeNameMethod.ReturnType)
{
ImplAttributes = baseGetTypeNameMethod.ImplAttributes,
SemanticsAttributes = baseGetTypeNameMethod.SemanticsAttributes
SemanticsAttributes = baseGetTypeNameMethod.SemanticsAttributes,
IsFamilyOrAssembly = true
};
var processor = newGetTypeNameMethod.Body.GetILProcessor();
@@ -1468,6 +1547,27 @@ namespace Unity.Netcode.Editor.CodeGen
private bool GetWriteMethodForParameter(TypeReference paramType, out MethodReference methodRef)
{
if (paramType.Resolve() == null)
{
// Handle generic types by passing them to RpcFallbackSerialization
// This just passes directly to NetworkVariableSerialization, but I could not figure out how to
// get ILPP to generate valid code for calling a method of the format
// `GenericClass<T>.StaticMethod(ref T value)` - it would either complain about T being
// defined in another module, or it would end up generating a completely invalid call to a
// random method on another random class.
var serializationHelperType = m_MainModule.ImportReference(typeof(RpcFallbackSerialization));
foreach (var method in serializationHelperType.Resolve().Methods)
{
if (method.Name == nameof(NetworkVariableSerialization<bool>.Write))
{
var reference = new GenericInstanceMethod(m_MainModule.ImportReference(method));
reference.GenericArguments.Add(paramType);
methodRef = reference;
return true;
}
}
}
if (paramType.FullName == typeof(short).FullName)
{
methodRef = m_BytePacker_WriteValueBitPacked_Short_MethodRef;
@@ -1684,6 +1784,27 @@ namespace Unity.Netcode.Editor.CodeGen
private bool GetReadMethodForParameter(TypeReference paramType, out MethodReference methodRef)
{
if (paramType.Resolve() == null)
{
// Handle generic types by passing them to RpcFallbackSerialization
// This just passes directly to NetworkVariableSerialization, but I could not figure out how to
// get ILPP to generate valid code for calling a method of the format
// `GenericClass<T>.StaticMethod(ref T value)` - it would either complain about T being
// defined in another module, or it would end up generating a completely invalid call to a
// random method on another random class.
var serializationHelperType = m_MainModule.ImportReference(typeof(RpcFallbackSerialization));
foreach (var method in serializationHelperType.Resolve().Methods)
{
if (method.Name == nameof(NetworkVariableSerialization<bool>.Read))
{
var reference = new GenericInstanceMethod(m_MainModule.ImportReference(method));
reference.GenericArguments.Add(paramType);
methodRef = reference;
return true;
}
}
}
if (paramType.FullName == typeof(short).FullName)
{
methodRef = m_ByteUnpacker_ReadValueBitPacked_Short_MethodRef;
@@ -1974,7 +2095,7 @@ namespace Unity.Netcode.Editor.CodeGen
Instruction jumpInstruction = null;
if (!paramType.IsValueType)
if (!paramType.IsValueType && paramType.Resolve() != null)
{
if (!GetWriteMethodForParameter(typeSystem.Boolean, out var boolMethodRef))
{
@@ -2225,6 +2346,12 @@ namespace Unity.Netcode.Editor.CodeGen
}
field = new FieldReference(fieldDefinition.Name, fieldDefinition.FieldType, genericType);
}
if (field.FieldType.Resolve() == null)
{
continue;
}
if (!field.FieldType.IsArray && !field.FieldType.Resolve().IsArray && field.FieldType.IsSubclassOf(m_NetworkVariableBase_TypeRef))
{
// if({variable} == null) {
@@ -2441,7 +2568,7 @@ namespace Unity.Netcode.Editor.CodeGen
Instruction jumpInstruction = null;
if (!paramType.IsValueType)
if (!paramType.IsValueType && paramType.Resolve() != null)
{
if (!GetReadMethodForParameter(typeSystem.Boolean, out var boolMethodRef))
{
@@ -2568,9 +2695,21 @@ namespace Unity.Netcode.Editor.CodeGen
// NetworkBehaviour.XXXRpc(...);
processor.Emit(OpCodes.Ldarg_0);
processor.Emit(OpCodes.Castclass, methodDefinition.DeclaringType);
var castType = (TypeReference)methodDefinition.DeclaringType;
var callMethod = (MethodReference)methodDefinition;
if (castType.HasGenericParameters)
{
var genericTypes = new List<TypeReference>();
foreach (var parameter in castType.GenericParameters)
{
genericTypes.Add(parameter);
}
castType = castType.MakeGenericInstanceType(genericTypes.ToArray());
callMethod = callMethod.MakeGeneric(genericTypes.ToArray());
}
processor.Emit(OpCodes.Castclass, castType);
Enumerable.Range(0, paramCount).ToList().ForEach(paramIndex => processor.Emit(OpCodes.Ldloc, paramLocalMap[paramIndex]));
processor.Emit(OpCodes.Callvirt, methodDefinition);
processor.Emit(OpCodes.Callvirt, callMethod);
// NetworkBehaviour.__rpc_exec_stage = __RpcExecStage.None;
processor.Emit(OpCodes.Ldarg_0);

View File

@@ -53,6 +53,7 @@ namespace Unity.Netcode.Editor.CodeGen
ProcessNetworkBehaviour(typeDefinition);
break;
case nameof(__RpcParams):
case nameof(RpcFallbackSerialization):
typeDefinition.IsPublic = true;
break;
}
@@ -79,6 +80,9 @@ namespace Unity.Netcode.Editor.CodeGen
return new ILPostProcessResult(new InMemoryAssembly(pe.ToArray(), pdb.ToArray()), m_Diagnostics);
}
// TODO: Deprecate...
// This is changing accessibility for values that are no longer used, but since our validator runs
// after ILPP and sees those values as public, they cannot be removed until a major version change.
private void ProcessNetworkManager(TypeDefinition typeDefinition, string[] assemblyDefines)
{
foreach (var fieldDefinition in typeDefinition.Fields)
@@ -98,6 +102,14 @@ namespace Unity.Netcode.Editor.CodeGen
fieldDefinition.IsPublic = true;
}
}
foreach (var nestedTypeDefinition in typeDefinition.NestedTypes)
{
if (nestedTypeDefinition.Name == nameof(NetworkManager.RpcReceiveHandler))
{
nestedTypeDefinition.IsNestedPublic = true;
}
}
}
private void ProcessNetworkBehaviour(TypeDefinition typeDefinition)
@@ -108,13 +120,31 @@ namespace Unity.Netcode.Editor.CodeGen
{
nestedType.IsNestedFamily = true;
}
if (nestedType.Name == nameof(NetworkBehaviour.RpcReceiveHandler))
{
nestedType.IsNestedPublic = true;
}
}
foreach (var fieldDefinition in typeDefinition.Fields)
{
if (fieldDefinition.Name == nameof(NetworkBehaviour.__rpc_exec_stage) || fieldDefinition.Name == nameof(NetworkBehaviour.NetworkVariableFields))
{
fieldDefinition.IsFamily = true;
fieldDefinition.IsFamilyOrAssembly = true;
}
if (fieldDefinition.Name == nameof(NetworkBehaviour.__rpc_func_table))
{
fieldDefinition.IsFamilyOrAssembly = true;
}
if (fieldDefinition.Name == nameof(NetworkBehaviour.RpcReceiveHandler))
{
fieldDefinition.IsFamilyOrAssembly = true;
}
if (fieldDefinition.Name == nameof(NetworkBehaviour.__rpc_name_table))
{
fieldDefinition.IsFamilyOrAssembly = true;
}
}
@@ -125,11 +155,18 @@ namespace Unity.Netcode.Editor.CodeGen
methodDefinition.Name == nameof(NetworkBehaviour.__beginSendClientRpc) ||
methodDefinition.Name == nameof(NetworkBehaviour.__endSendClientRpc) ||
methodDefinition.Name == nameof(NetworkBehaviour.__initializeVariables) ||
methodDefinition.Name == nameof(NetworkBehaviour.__initializeRpcs) ||
methodDefinition.Name == nameof(NetworkBehaviour.__registerRpc) ||
methodDefinition.Name == nameof(NetworkBehaviour.__nameNetworkVariable) ||
methodDefinition.Name == nameof(NetworkBehaviour.__createNativeList))
{
methodDefinition.IsFamily = true;
}
if (methodDefinition.Name == nameof(NetworkBehaviour.__getTypeName))
{
methodDefinition.IsFamilyOrAssembly = true;
}
}
}
}

View File

@@ -20,7 +20,7 @@ namespace Unity.Netcode.Editor.Configuration
}
[SerializeField]
public bool GenerateDefaultNetworkPrefabs;
public bool GenerateDefaultNetworkPrefabs = true;
internal void SaveSettings()
{

View File

@@ -37,12 +37,12 @@ namespace Unity.Netcode.Editor
for (int i = 0; i < fields.Length; i++)
{
var ft = fields[i].FieldType;
if (ft.IsGenericType && ft.GetGenericTypeDefinition() == typeof(NetworkVariable<>) && !fields[i].IsDefined(typeof(HideInInspector), true))
if (ft.IsGenericType && ft.GetGenericTypeDefinition() == typeof(NetworkVariable<>) && !fields[i].IsDefined(typeof(HideInInspector), true) && !fields[i].IsDefined(typeof(NonSerializedAttribute), true))
{
m_NetworkVariableNames.Add(ObjectNames.NicifyVariableName(fields[i].Name));
m_NetworkVariableFields.Add(ObjectNames.NicifyVariableName(fields[i].Name), fields[i]);
}
if (ft.IsGenericType && ft.GetGenericTypeDefinition() == typeof(NetworkList<>) && !fields[i].IsDefined(typeof(HideInInspector), true))
if (ft.IsGenericType && ft.GetGenericTypeDefinition() == typeof(NetworkList<>) && !fields[i].IsDefined(typeof(HideInInspector), true) && !fields[i].IsDefined(typeof(NonSerializedAttribute), true))
{
m_NetworkVariableNames.Add(ObjectNames.NicifyVariableName(fields[i].Name));
m_NetworkVariableFields.Add(ObjectNames.NicifyVariableName(fields[i].Name), fields[i]);
@@ -345,8 +345,9 @@ namespace Unity.Netcode.Editor
/// <param name="networkObjectRemoved">used internally</param>
public static void CheckForNetworkObject(GameObject gameObject, bool networkObjectRemoved = false)
{
// If there are no NetworkBehaviours or no gameObject, then exit early
if (gameObject == null || (gameObject.GetComponent<NetworkBehaviour>() == null && gameObject.GetComponentInChildren<NetworkBehaviour>() == null))
// If there are no NetworkBehaviours or gameObjects then exit early
// If we are in play mode and a user is inspecting something then exit early (we don't add NetworkObjects to something when in play mode)
if (EditorApplication.isPlaying || gameObject == null || (gameObject.GetComponent<NetworkBehaviour>() == null && gameObject.GetComponentInChildren<NetworkBehaviour>() == null))
{
return;
}
@@ -416,6 +417,48 @@ namespace Unity.Netcode.Editor
}
}
}
if (networkObject != null)
{
OrderNetworkObject(networkObject);
}
}
// Assures the NetworkObject precedes any NetworkBehaviour on the same GameObject as the NetworkObject
private static void OrderNetworkObject(NetworkObject networkObject)
{
var monoBehaviours = networkObject.gameObject.GetComponents<MonoBehaviour>();
var networkObjectIndex = 0;
var firstNetworkBehaviourIndex = -1;
for (int i = 0; i < monoBehaviours.Length; i++)
{
if (monoBehaviours[i] == networkObject)
{
networkObjectIndex = i;
break;
}
var networkBehaviour = monoBehaviours[i] as NetworkBehaviour;
if (networkBehaviour != null)
{
// Get the index of the first NetworkBehaviour Component
if (firstNetworkBehaviourIndex == -1)
{
firstNetworkBehaviourIndex = i;
}
}
}
if (firstNetworkBehaviourIndex != -1 && networkObjectIndex > firstNetworkBehaviourIndex)
{
var positionsToMove = networkObjectIndex - firstNetworkBehaviourIndex;
for (int i = 0; i < positionsToMove; i++)
{
UnityEditorInternal.ComponentUtility.MoveComponentUp(networkObject);
}
EditorUtility.SetDirty(networkObject.gameObject);
}
}
}
}

View File

@@ -36,15 +36,11 @@ namespace Unity.Netcode
/// <summary>
/// The ClientId of the NetworkClient
/// </summary>
// TODO-2023-Q2: Determine if we want to make this property a public get and internal/private set
// There is no reason for a user to want to set this, but this will fail the package-validation-suite
public ulong ClientId;
/// <summary>
/// The PlayerObject of the Client
/// </summary>
// TODO-2023-Q2: Determine if we want to make this property a public get and internal/private set
// There is no reason for a user to want to set this, but this will fail the package-validation-suite
public NetworkObject PlayerObject;
/// <summary>

View File

@@ -17,7 +17,6 @@ namespace Unity.Netcode
/// - Processing <see cref="NetworkEvent"/>s.
/// - Client Disconnection
/// </summary>
// TODO 2023-Q2: Discuss what kind of public API exposure we want for this
public sealed class NetworkConnectionManager
{
#if DEVELOPMENT_BUILD || UNITY_EDITOR
@@ -628,6 +627,8 @@ namespace Unity.Netcode
};
if (!NetworkManager.NetworkConfig.EnableSceneManagement)
{
// Update the observed spawned NetworkObjects for the newly connected player when scene management is disabled
NetworkManager.SpawnManager.UpdateObservedNetworkObjects(ownerClientId);
if (NetworkManager.SpawnManager.SpawnedObjectsList.Count != 0)
{
message.SpawnedObjectsList = NetworkManager.SpawnManager.SpawnedObjectsList;
@@ -651,12 +652,13 @@ namespace Unity.Netcode
SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, ownerClientId);
message.MessageVersions.Dispose();
// If scene management is enabled, then let NetworkSceneManager handle the initial scene and NetworkObject synchronization
// If scene management is disabled, then we are done and notify the local host-server the client is connected
if (!NetworkManager.NetworkConfig.EnableSceneManagement)
{
NetworkManager.ConnectedClients[ownerClientId].IsConnected = true;
InvokeOnClientConnectedCallback(ownerClientId);
}
else
else // Otherwise, let NetworkSceneManager handle the initial scene and NetworkObject synchronization
{
NetworkManager.SceneManager.SynchronizeNetworkObjects(ownerClientId);
}
@@ -665,6 +667,7 @@ namespace Unity.Netcode
{
LocalClient = client;
NetworkManager.SpawnManager.UpdateObservedNetworkObjects(ownerClientId);
LocalClient.IsConnected = true;
}
if (!response.CreatePlayerObject || (response.PlayerPrefabHash == null && NetworkManager.NetworkConfig.PlayerPrefab == null))
@@ -730,12 +733,10 @@ namespace Unity.Netcode
internal NetworkClient AddClient(ulong clientId)
{
var networkClient = LocalClient;
if (clientId != NetworkManager.ServerClientId)
{
networkClient = new NetworkClient();
networkClient.SetRole(isServer: false, isClient: true, NetworkManager);
networkClient.SetRole(clientId == NetworkManager.ServerClientId, isClient: true, NetworkManager);
networkClient.ClientId = clientId;
}
ConnectedClients.Add(clientId, networkClient);
ConnectedClientsList.Add(networkClient);
@@ -799,7 +800,6 @@ namespace Unity.Netcode
else
{
// Handle changing ownership and prefab handlers
// TODO-2023: Look into whether in-scene placed NetworkObjects could be destroyed if ownership changes to a client
for (int i = clientOwnedObjects.Count - 1; i >= 0; i--)
{
var ownedObject = clientOwnedObjects[i];

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using Unity.Collections;
using UnityEngine;
namespace Unity.Netcode
{
/// <summary>
@@ -11,6 +12,18 @@ namespace Unity.Netcode
public abstract class NetworkBehaviour : MonoBehaviour
{
#pragma warning disable IDE1006 // disable naming rule violation check
// RuntimeAccessModifiersILPP will make this `public`
internal delegate void RpcReceiveHandler(NetworkBehaviour behaviour, FastBufferReader reader, __RpcParams parameters);
// RuntimeAccessModifiersILPP will make this `public`
internal static readonly Dictionary<Type, Dictionary<uint, RpcReceiveHandler>> __rpc_func_table = new Dictionary<Type, Dictionary<uint, RpcReceiveHandler>>();
#if DEVELOPMENT_BUILD || UNITY_EDITOR
// RuntimeAccessModifiersILPP will make this `public`
internal static readonly Dictionary<Type, Dictionary<uint, string>> __rpc_name_table = new Dictionary<Type, Dictionary<uint, string>>();
#endif
// RuntimeAccessModifiersILPP will make this `protected`
internal enum __RpcExecStage
{
@@ -18,8 +31,6 @@ namespace Unity.Netcode
Server = 1,
Client = 2
}
// NetworkBehaviourILPP will override this in derived classes to return the name of the concrete type
internal virtual string __getTypeName() => nameof(NetworkBehaviour);
@@ -98,9 +109,8 @@ namespace Unity.Netcode
}
bufferWriter.Dispose();
#if DEVELOPMENT_BUILD || UNITY_EDITOR
if (NetworkManager.__rpc_name_table.TryGetValue(rpcMethodId, out var rpcMethodName))
if (__rpc_name_table[GetType()].TryGetValue(rpcMethodId, out var rpcMethodName))
{
NetworkManager.NetworkMetrics.TrackRpcSent(
NetworkManager.ServerClientId,
@@ -230,9 +240,8 @@ namespace Unity.Netcode
}
bufferWriter.Dispose();
#if DEVELOPMENT_BUILD || UNITY_EDITOR
if (NetworkManager.__rpc_name_table.TryGetValue(rpcMethodId, out var rpcMethodName))
if (__rpc_name_table[GetType()].TryGetValue(rpcMethodId, out var rpcMethodName))
{
if (clientRpcParams.Send.TargetClientIds != null)
{
@@ -536,6 +545,23 @@ namespace Unity.Netcode
OnGainedOwnership();
}
/// <summary>
/// Invoked on all clients, override this method to be notified of any
/// ownership changes (even if the instance was niether the previous or
/// newly assigned current owner).
/// </summary>
/// <param name="previous">the previous owner</param>
/// <param name="current">the current owner</param>
protected virtual void OnOwnershipChanged(ulong previous, ulong current)
{
}
internal void InternalOnOwnershipChanged(ulong previous, ulong current)
{
OnOwnershipChanged(previous, current);
}
/// <summary>
/// Gets called when we loose ownership of this object
/// </summary>
@@ -569,6 +595,25 @@ namespace Unity.Netcode
// ILPP generates code for all NetworkBehaviour subtypes to initialize each type's network variables.
}
#pragma warning disable IDE1006 // disable naming rule violation check
// RuntimeAccessModifiersILPP will make this `protected`
internal virtual void __initializeRpcs()
#pragma warning restore IDE1006 // restore naming rule violation check
{
// ILPP generates code for all NetworkBehaviour subtypes to initialize each type's RPCs.
}
#pragma warning disable IDE1006 // disable naming rule violation check
// RuntimeAccessModifiersILPP will make this `protected`
internal void __registerRpc(uint hash, RpcReceiveHandler handler, string rpcMethodName)
#pragma warning restore IDE1006 // restore naming rule violation check
{
__rpc_func_table[GetType()][hash] = handler;
#if DEVELOPMENT_BUILD || UNITY_EDITOR
__rpc_name_table[GetType()][hash] = rpcMethodName;
#endif
}
#pragma warning disable IDE1006 // disable naming rule violation check
// RuntimeAccessModifiersILPP will make this `protected`
// Using this method here because ILPP doesn't seem to let us do visibility modification on properties.
@@ -587,6 +632,14 @@ namespace Unity.Netcode
m_VarInit = true;
if (!__rpc_func_table.ContainsKey(GetType()))
{
__rpc_func_table[GetType()] = new Dictionary<uint, RpcReceiveHandler>();
#if UNITY_EDITOR || DEVELOPMENT_BUILD
__rpc_name_table[GetType()] = new Dictionary<uint, string>();
#endif
__initializeRpcs();
}
__initializeVariables();
{
@@ -940,6 +993,8 @@ namespace Unity.Netcode
if (finalPosition == positionBeforeSynchronize || threwException)
{
writer.Seek(positionBeforeWrite);
// Truncate back to the size before
writer.Truncate();
return false;
}
else

View File

@@ -118,7 +118,7 @@ namespace Unity.Netcode
m_NetworkManager.NetworkTickSystem.Tick -= NetworkBehaviourUpdater_Tick;
}
// TODO 2023-Q2: Order of operations requires NetworkVariable updates first then showing NetworkObjects
// Order of operations requires NetworkVariable updates first then showing NetworkObjects
private void NetworkBehaviourUpdater_Tick()
{
// First update NetworkVariables

View File

@@ -15,6 +15,9 @@ namespace Unity.Netcode
[AddComponentMenu("Netcode/Network Manager", -100)]
public class NetworkManager : MonoBehaviour, INetworkUpdateSystem
{
// TODO: Deprecate...
// The following internal values are not used, but because ILPP makes them public in the assembly, they cannot
// be removed thanks to our semver validation.
#pragma warning disable IDE1006 // disable naming rule violation check
// RuntimeAccessModifiersILPP will make this `public`
@@ -59,13 +62,12 @@ namespace Unity.Netcode
// Metrics update needs to be driven by NetworkConnectionManager's update to assure metrics are dispatched after the send queue is processed.
MetricsManager.UpdateMetrics();
// TODO 2023-Q2: Determine a better way to handle this
// TODO: Determine a better way to handle this
NetworkObject.VerifyParentingStatus();
// This is "ok" to invoke when not processing messages since it is just cleaning up messages that never got handled within their timeout period.
DeferredMessageManager.CleanupStaleTriggers();
// TODO 2023-Q2: Determine a better way to handle this
if (m_ShuttingDown)
{
ShutdownInternal();
@@ -410,6 +412,19 @@ namespace Unity.Netcode
internal NetworkConnectionManager ConnectionManager = new NetworkConnectionManager();
internal NetworkMessageManager MessageManager = null;
internal struct Override<T>
{
private T m_Value;
public bool Overidden { get; private set; }
internal T Value
{
get { return Overidden ? m_Value : default(T); }
set { Overidden = true; m_Value = value; }
}
};
internal Override<ushort> PortOverride;
#if UNITY_EDITOR
internal static INetworkManagerHelper NetworkManagerHelper;
@@ -492,6 +507,15 @@ namespace Unity.Netcode
}
}
}
private void ModeChanged(PlayModeStateChange change)
{
if (IsListening && change == PlayModeStateChange.ExitingPlayMode)
{
// Make sure we are not holding onto anything in case domain reload is disabled
ShutdownInternal();
}
}
#endif
/// <summary>
@@ -540,6 +564,9 @@ namespace Unity.Netcode
NetworkConfig?.InitializePrefabs();
UnityEngine.SceneManagement.SceneManager.sceneUnloaded += OnSceneUnloaded;
#if UNITY_EDITOR
EditorApplication.playModeStateChanged += ModeChanged;
#endif
}
private void OnEnable()
@@ -582,15 +609,48 @@ namespace Unity.Netcode
/// <summary>
/// Sets the maximum size of a single non-fragmented message (or message batch) passed through the transport.
/// This should represent the transport's MTU size, minus any transport-level overhead.
/// This should represent the transport's default MTU size, minus any transport-level overhead.
/// This value will be used for any remote endpoints that haven't had per-endpoint MTUs set.
/// This value is also used as the size of the temporary buffer used when serializing
/// a single message (to avoid serializing multiple times when sending to multiple endpoints),
/// and thus should be large enough to ensure it can hold each message type.
/// This value defaults to 1296.
/// </summary>
/// <param name="size"></param>
public int MaximumTransmissionUnitSize
{
set => MessageManager.NonFragmentedMessageMaxSize = value;
set => MessageManager.NonFragmentedMessageMaxSize = value & ~7; // Round down to nearest word aligned size
get => MessageManager.NonFragmentedMessageMaxSize;
}
/// <summary>
/// Set the maximum transmission unit for a specific peer.
/// This determines the maximum size of a message batch that can be sent to that client.
/// If not set for any given client, <see cref="MaximumTransmissionUnitSize"/> will be used instead.
/// </summary>
/// <param name="clientId"></param>
/// <param name="size"></param>
public void SetPeerMTU(ulong clientId, int size)
{
MessageManager.PeerMTUSizes[clientId] = size;
}
/// <summary>
/// Queries the current MTU size for a client.
/// If no MTU has been set for that client, will return <see cref="MaximumTransmissionUnitSize"/>
/// </summary>
/// <param name="clientId"></param>
/// <returns></returns>
public int GetPeerMTU(ulong clientId)
{
if (MessageManager.PeerMTUSizes.TryGetValue(clientId, out var ret))
{
return ret;
}
return MessageManager.NonFragmentedMessageMaxSize;
}
/// <summary>
/// Sets the maximum size of a message (or message batch) passed through the transport with the ReliableFragmented delivery.
/// Warning: setting this value too low may result in the SDK becoming non-functional with projects that have a large number of NetworkBehaviours or NetworkVariables, as the SDK relies on the transport's ability to fragment some messages when they grow beyond the MTU size.
@@ -611,6 +671,8 @@ namespace Unity.Netcode
return;
}
ParseCommandLineOptions();
if (NetworkConfig.NetworkTransport == null)
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Error)
@@ -834,9 +896,7 @@ namespace Unity.Netcode
}
ConnectionManager.LocalClient.SetRole(true, true, this);
Initialize(true);
try
{
IsListening = NetworkConfig.NetworkTransport.StartServer();
@@ -942,11 +1002,17 @@ namespace Unity.Netcode
if (IsServer || IsClient)
{
m_ShuttingDown = true;
if (MessageManager != null)
{
MessageManager.StopProcessing = discardMessageQueue;
}
}
if (NetworkConfig != null && NetworkConfig.NetworkTransport != null)
{
NetworkConfig.NetworkTransport.OnTransportEvent -= ConnectionManager.HandleNetworkEvent;
}
}
// Ensures that the NetworkManager is cleaned up before OnDestroy is run on NetworkObjects and NetworkBehaviours when unloading a scene with a NetworkManager
private void OnSceneUnloaded(Scene scene)
@@ -1010,6 +1076,9 @@ namespace Unity.Netcode
OnServerStopped?.Invoke(ConnectionManager.LocalClient.IsClient);
}
// In the event shutdown is invoked within OnClientStopped or OnServerStopped, set it to false again
m_ShuttingDown = false;
// Reset the client's roles
ConnectionManager.LocalClient.SetRole(false, false);
@@ -1029,6 +1098,8 @@ namespace Unity.Netcode
// Ensures that the NetworkManager is cleaned up before OnDestroy is run on NetworkObjects and NetworkBehaviours when quitting the application.
private void OnApplicationQuit()
{
// Make sure ShutdownInProgress returns true during this time
m_ShuttingDown = true;
OnDestroy();
}
@@ -1044,5 +1115,39 @@ namespace Unity.Netcode
Singleton = null;
}
}
// Command line options
private const string k_OverridePortArg = "-port";
private string GetArg(string[] commandLineArgs, string arg)
{
var argIndex = Array.IndexOf(commandLineArgs, arg);
if (argIndex >= 0 && argIndex < commandLineArgs.Length - 1)
{
return commandLineArgs[argIndex + 1];
}
return null;
}
private void ParseArg<T>(string arg, ref Override<T> value)
{
if (GetArg(Environment.GetCommandLineArgs(), arg) is string argValue)
{
value.Value = (T)Convert.ChangeType(argValue, typeof(T));
}
}
private void ParseCommandLineOptions()
{
#if UNITY_SERVER && UNITY_DEDICATED_SERVER_ARGUMENTS_PRESENT
if ( UnityEngine.DedicatedServer.Arguments.Port != null)
{
PortOverride.Value = (ushort)UnityEngine.DedicatedServer.Arguments.Port;
}
#else
ParseArg(k_OverridePortArg, ref PortOverride);
#endif
}
}
}

View File

@@ -1,9 +1,18 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
#if UNITY_EDITOR
using UnityEditor;
#if UNITY_2021_2_OR_NEWER
using UnityEditor.SceneManagement;
#else
using UnityEditor.Experimental.SceneManagement;
#endif
#endif
using UnityEngine;
using UnityEngine.SceneManagement;
namespace Unity.Netcode
{
/// <summary>
@@ -37,30 +46,171 @@ namespace Unity.Netcode
}
}
private bool m_IsPrefab;
#if UNITY_EDITOR
private void OnValidate()
private const string k_GlobalIdTemplate = "GlobalObjectId_V1-{0}-{1}-{2}-{3}";
/// <summary>
/// Object Types <see href="https://docs.unity3d.com/ScriptReference/GlobalObjectId.html"/>
/// Parameter 0 of <see cref="k_GlobalIdTemplate"/>
/// </summary>
// 0 = Null (when considered a null object type we can ignore)
// 1 = Imported Asset
// 2 = Scene Object
// 3 = Source Asset.
private const int k_NullObjectType = 0;
private const int k_ImportedAssetObjectType = 1;
private const int k_SceneObjectType = 2;
private const int k_SourceAssetObjectType = 3;
[ContextMenu("Refresh In-Scene Prefab Instances")]
internal void RefreshAllPrefabInstances()
{
GenerateGlobalObjectIdHash();
var instanceGlobalId = GlobalObjectId.GetGlobalObjectIdSlow(this);
if (!PrefabUtility.IsPartOfAnyPrefab(this) || instanceGlobalId.identifierType != k_ImportedAssetObjectType)
{
EditorUtility.DisplayDialog("Network Prefab Assets Only", "This action can only be performed on a network prefab asset.", "Ok");
return;
}
internal void GenerateGlobalObjectIdHash()
// Handle updating the currently active scene
var networkObjects = FindObjectsByType<NetworkObject>(FindObjectsInactive.Include, FindObjectsSortMode.None);
foreach (var networkObject in networkObjects)
{
networkObject.OnValidate();
}
NetworkObjectRefreshTool.ProcessActiveScene();
// Refresh all build settings scenes
var activeScene = SceneManager.GetActiveScene();
foreach (var editorScene in EditorBuildSettings.scenes)
{
// skip disabled scenes and the currently active scene
if (!editorScene.enabled || activeScene.path == editorScene.path)
{
continue;
}
// Add the scene to be processed
NetworkObjectRefreshTool.ProcessScene(editorScene.path, false);
}
// Process all added scenes
NetworkObjectRefreshTool.ProcessScenes();
}
private void OnValidate()
{
// do NOT regenerate GlobalObjectIdHash for NetworkPrefabs while Editor is in PlayMode
if (UnityEditor.EditorApplication.isPlaying && !string.IsNullOrEmpty(gameObject.scene.name))
if (EditorApplication.isPlaying && !string.IsNullOrEmpty(gameObject.scene.name))
{
return;
}
// do NOT regenerate GlobalObjectIdHash if Editor is transitioning into or out of PlayMode
if (!UnityEditor.EditorApplication.isPlaying && UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode)
if (!EditorApplication.isPlaying && EditorApplication.isPlayingOrWillChangePlaymode)
{
return;
}
var globalObjectIdString = UnityEditor.GlobalObjectId.GetGlobalObjectIdSlow(this).ToString();
GlobalObjectIdHash = XXHash.Hash32(globalObjectIdString);
// Get a global object identifier for this network prefab
var globalId = GetGlobalId();
// if the identifier type is 0, then don't update the GlobalObjectIdHash
if (globalId.identifierType == k_NullObjectType)
{
return;
}
var oldValue = GlobalObjectIdHash;
GlobalObjectIdHash = globalId.ToString().Hash32();
// If the GlobalObjectIdHash value changed, then mark the asset dirty
if (GlobalObjectIdHash != oldValue)
{
// Check if this is an in-scnee placed NetworkObject (Special Case for In-Scene Placed)
if (!IsEditingPrefab() && gameObject.scene.name != null && gameObject.scene.name != gameObject.name)
{
// Sanity check to make sure this is a scene placed object
if (globalId.identifierType != k_SceneObjectType)
{
// This should never happen, but in the event it does throw and error
Debug.LogError($"[{gameObject.name}] is detected as an in-scene placed object but its identifier is of type {globalId.identifierType}! **Report this error**");
}
// If this is a prefab instance
if (PrefabUtility.IsPartOfAnyPrefab(this))
{
// We must invoke this in order for the modifications to get saved with the scene (does not mark scene as dirty)
PrefabUtility.RecordPrefabInstancePropertyModifications(this);
}
}
else // Otherwise, this is a standard network prefab asset so we just mark it dirty for the AssetDatabase to update it
{
EditorUtility.SetDirty(this);
}
}
}
private bool IsEditingPrefab()
{
// Check if we are directly editing the prefab
var stage = PrefabStageUtility.GetPrefabStage(gameObject);
// if we are not editing the prefab directly (or a sub-prefab), then return the object identifier
if (stage == null || stage.assetPath == null)
{
return false;
}
return true;
}
private GlobalObjectId GetGlobalId()
{
var instanceGlobalId = GlobalObjectId.GetGlobalObjectIdSlow(this);
// If not editing a prefab, then just use the generated id
if (!IsEditingPrefab())
{
return instanceGlobalId;
}
// If the asset doesn't exist at the given path, then return the object identifier
var prefabStageAssetPath = PrefabStageUtility.GetPrefabStage(gameObject).assetPath;
// If (for some reason) the asset path is null return the generated id
if (prefabStageAssetPath == null)
{
return instanceGlobalId;
}
var theAsset = AssetDatabase.LoadAssetAtPath<NetworkObject>(prefabStageAssetPath);
// If there is no asset at that path (for some odd/edge case reason), return the generated id
if (theAsset == null)
{
return instanceGlobalId;
}
// If we can't get the asset GUID and/or the file identifier, then return the object identifier
if (!AssetDatabase.TryGetGUIDAndLocalFileIdentifier(theAsset, out var guid, out long localFileId))
{
return instanceGlobalId;
}
// Note: If we reached this point, then we are most likely opening a prefab to edit.
// The instanceGlobalId will be constructed as if it is a scene object, however when it
// is serialized its value will be treated as a file asset (the "why" to the below code).
// Construct an imported asset identifier with the type being a source asset object type
var prefabGlobalIdText = string.Format(k_GlobalIdTemplate, k_SourceAssetObjectType, guid, (ulong)localFileId, 0);
// If we can't parse the result log an error and return the instanceGlobalId
if (!GlobalObjectId.TryParse(prefabGlobalIdText, out var prefabGlobalId))
{
Debug.LogError($"[GlobalObjectId Gen] Failed to parse ({prefabGlobalIdText}) returning default ({instanceGlobalId})! ** Please Report This Error **");
return instanceGlobalId;
}
// Otherwise, return the constructed identifier for the source prefab asset
return prefabGlobalId;
}
#endif // UNITY_EDITOR
@@ -684,6 +834,21 @@ namespace Unity.Netcode
}
}
internal void InvokeOwnershipChanged(ulong previous, ulong next)
{
for (int i = 0; i < ChildNetworkBehaviours.Count; i++)
{
if (ChildNetworkBehaviours[i].gameObject.activeInHierarchy)
{
ChildNetworkBehaviours[i].InternalOnOwnershipChanged(previous, next);
}
else
{
Debug.LogWarning($"{ChildNetworkBehaviours[i].gameObject.name} is disabled! Netcode for GameObjects does not support disabled NetworkBehaviours! The {ChildNetworkBehaviours[i].GetType().Name} component was skipped during ownership assignment!");
}
}
}
internal void InvokeBehaviourOnNetworkObjectParentChanged(NetworkObject parentNetworkObject)
{
for (int i = 0; i < ChildNetworkBehaviours.Count; i++)
@@ -733,6 +898,12 @@ namespace Unity.Netcode
/// <returns>Whether or not reparenting was successful.</returns>
public bool TrySetParent(Transform parent, bool worldPositionStays = true)
{
// If we are removing ourself from a parent
if (parent == null)
{
return TrySetParent((NetworkObject)null, worldPositionStays);
}
var networkObject = parent.GetComponent<NetworkObject>();
// If the parent doesn't have a NetworkObjet then return false, otherwise continue trying to parent
@@ -1184,7 +1355,7 @@ namespace Unity.Netcode
return 0;
}
internal NetworkBehaviour GetNetworkBehaviourAtOrderIndex(ushort index)
public NetworkBehaviour GetNetworkBehaviourAtOrderIndex(ushort index)
{
if (index >= ChildNetworkBehaviours.Count)
{
@@ -1192,7 +1363,6 @@ namespace Unity.Netcode
{
NetworkLog.LogError($"{nameof(NetworkBehaviour)} index {index} was out of bounds for {name}. NetworkBehaviours must be the same, and in the same order, between server and client.");
}
if (NetworkLog.CurrentLogLevel <= LogLevel.Developer)
{
var currentKnownChildren = new System.Text.StringBuilder();
@@ -1205,7 +1375,6 @@ namespace Unity.Netcode
}
NetworkLog.LogInfo(currentKnownChildren.ToString());
}
return null;
}

View File

@@ -0,0 +1,118 @@
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace Unity.Netcode
{
/// <summary>
/// This is a helper tool to update all in-scene placed instances of a prefab that
/// originally did not have a NetworkObject component but one was added to the prefab
/// later.
/// </summary>
internal class NetworkObjectRefreshTool
{
private static List<string> s_ScenesToUpdate = new List<string>();
private static bool s_ProcessScenes;
private static bool s_CloseScenes;
internal static Action AllScenesProcessed;
internal static void ProcessScene(string scenePath, bool processScenes = true)
{
if (!s_ScenesToUpdate.Contains(scenePath))
{
if (s_ScenesToUpdate.Count == 0)
{
EditorSceneManager.sceneOpened += EditorSceneManager_sceneOpened;
EditorSceneManager.sceneSaved += EditorSceneManager_sceneSaved;
}
s_ScenesToUpdate.Add(scenePath);
}
s_ProcessScenes = processScenes;
}
internal static void ProcessActiveScene()
{
var activeScene = SceneManager.GetActiveScene();
if (s_ScenesToUpdate.Contains(activeScene.path) && s_ProcessScenes)
{
SceneOpened(activeScene);
}
}
internal static void ProcessScenes()
{
if (s_ScenesToUpdate.Count != 0)
{
s_CloseScenes = true;
var scenePath = s_ScenesToUpdate.First();
EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Additive);
}
else
{
s_CloseScenes = false;
EditorSceneManager.sceneSaved -= EditorSceneManager_sceneSaved;
EditorSceneManager.sceneOpened -= EditorSceneManager_sceneOpened;
AllScenesProcessed?.Invoke();
}
}
private static void FinishedProcessingScene(Scene scene, bool refreshed = false)
{
if (s_ScenesToUpdate.Contains(scene.path))
{
// Provide a log of all scenes that were modified to the user
if (refreshed)
{
Debug.Log($"Refreshed and saved updates to scene: {scene.name}");
}
s_ProcessScenes = false;
s_ScenesToUpdate.Remove(scene.path);
if (scene != SceneManager.GetActiveScene())
{
EditorSceneManager.CloseScene(scene, s_CloseScenes);
}
ProcessScenes();
}
}
private static void EditorSceneManager_sceneSaved(Scene scene)
{
FinishedProcessingScene(scene, true);
}
private static void SceneOpened(Scene scene)
{
if (s_ScenesToUpdate.Contains(scene.path))
{
if (s_ProcessScenes)
{
if (!EditorSceneManager.MarkSceneDirty(scene))
{
Debug.Log($"Scene {scene.name} did not get marked as dirty!");
FinishedProcessingScene(scene);
}
else
{
EditorSceneManager.SaveScene(scene);
}
}
else
{
FinishedProcessingScene(scene);
}
}
}
private static void EditorSceneManager_sceneOpened(Scene scene, OpenSceneMode mode)
{
SceneOpened(scene);
}
}
}
#endif // UNITY_EDITOR

View File

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

View File

@@ -0,0 +1,82 @@
using System;
namespace Unity.Netcode
{
/// <summary>
/// Marks a generic parameter in this class as a type that should be serialized through
/// <see cref="NetworkVariableSerialization{T}"/>. This enables the use of the following methods to support
/// serialization within a Network Variable type:
/// <br/>
/// <br/>
/// <see cref="NetworkVariableSerialization{T}"/>.<see cref="NetworkVariableSerialization{T}.Read"/>
/// <br/>
/// <see cref="NetworkVariableSerialization{T}"/>.<see cref="NetworkVariableSerialization{T}.Write"/>
/// <br/>
/// <see cref="NetworkVariableSerialization{T}"/>.<see cref="NetworkVariableSerialization{T}.AreEqual"/>
/// <br/>
/// <see cref="NetworkVariableSerialization{T}"/>.<see cref="NetworkVariableSerialization{T}.Duplicate"/>
/// <br/>
/// <br/>
/// The parameter is indicated by index (and is 0-indexed); for example:
/// <br/>
/// <code>
/// [SerializesGenericParameter(1)]
/// public class MyClass&lt;TTypeOne, TTypeTwo&gt;
/// {
/// }
/// </code>
/// <br/>
/// This tells the code generation for <see cref="NetworkVariableSerialization{T}"/> to generate
/// serialized code for <b>TTypeTwo</b> (generic parameter 1).
/// <br/>
/// <br/>
/// Note that this is primarily intended to support subtypes of <see cref="NetworkVariableBase"/>,
/// and as such, the type resolution is done by examining fields of <see cref="NetworkBehaviour"/>
/// subclasses. If your type is not used in a <see cref="NetworkBehaviour"/>, the codegen will
/// not find the types, even with this attribute.
/// <br/>
/// <br/>
/// This attribute is properly inherited by subclasses. For example:
/// <br/>
/// <code>
/// [SerializesGenericParameter(0)]
/// public class MyClass&lt;T&gt;
/// {
/// }
/// <br/>
/// public class MySubclass1 : MyClass&lt;Foo&gt;
/// {
/// }
/// <br/>
/// public class MySubclass2&lt;T&gt; : MyClass&lt;T&gt;
/// {
/// }
/// <br/>
/// [SerializesGenericParameter(1)]
/// public class MySubclass3&lt;TTypeOne, TTypeTwo&gt; : MyClass&lt;TTypeOne&gt;
/// {
/// }
/// <br/>
/// public class MyBehaviour : NetworkBehaviour
/// {
/// public MySubclass1 TheValue;
/// public MySubclass2&lt;Bar&gt; TheValue;
/// public MySubclass3&lt;Baz, Qux&gt; TheValue;
/// }
/// </code>
/// <br/>
/// The above code will trigger generation of serialization code for <b>Foo</b> (passed directly to the
/// base class), <b>Bar</b> (passed indirectly to the base class), <b>Baz</b> (passed indirectly to the base class),
/// and <b>Qux</b> (marked as serializable in the subclass).
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = true)]
public class GenerateSerializationForGenericParameterAttribute : Attribute
{
internal int ParameterIndex;
public GenerateSerializationForGenericParameterAttribute(int parameterIndex)
{
ParameterIndex = parameterIndex;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 18cdaa9c2f6446279b0c5948fcd34eec
timeCreated: 1694029524

View File

@@ -0,0 +1,26 @@
using System;
namespace Unity.Netcode
{
/// <summary>
/// Specifies a specific type that needs serialization to be generated by codegen.
/// This is only needed in special circumstances where manual serialization is being done.
/// If you are making a generic network variable-style class, use <see cref="GenerateSerializationForGenericParameterAttribute"/>.
/// <br />
/// <br />
/// This attribute can be attached to any class or method anywhere in the codebase and
/// will trigger codegen to generate serialization code for the provided type. It only needs
/// to be included once type per codebase, but including it multiple times for the same type
/// is safe.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method, AllowMultiple = true)]
public class GenerateSerializationForTypeAttribute : Attribute
{
internal Type Type;
public GenerateSerializationForTypeAttribute(Type type)
{
Type = type;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1bd80306706f4054b9ba514a72076df5
timeCreated: 1694103021

View File

@@ -1,4 +1,7 @@
using System.Collections.Generic;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace Unity.Netcode
{
@@ -13,5 +16,24 @@ namespace Unity.Netcode
{
return __network_message_types;
}
#if UNITY_EDITOR
[InitializeOnLoadMethod]
public static void NotifyOnPlayStateChange()
{
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
}
public static void OnPlayModeStateChanged(PlayModeStateChange change)
{
if (change == PlayModeStateChange.ExitingPlayMode)
{
// Clear out the network message types, because ILPP-generated RuntimeInitializeOnLoad code will
// run again and add more messages to it.
__network_message_types.Clear();
}
}
#endif
}
}

View File

@@ -60,6 +60,8 @@ namespace Unity.Netcode
}
}
networkObject.InvokeOwnershipChanged(originalOwner, OwnerClientId);
networkManager.NetworkMetrics.TrackOwnershipChangeReceived(context.SenderId, networkObject, context.MessageSize);
}
}

View File

@@ -37,6 +37,9 @@ namespace Unity.Netcode
BytePacker.WriteValueBitPacked(writer, NetworkTick);
uint sceneObjectCount = 0;
// When SpawnedObjectsList is not null then scene management is disabled. Provide a list of
// all observed and spawned NetworkObjects that the approved client needs to synchronize.
if (SpawnedObjectsList != null)
{
var pos = writer.Position;
@@ -45,7 +48,7 @@ namespace Unity.Netcode
// Serialize NetworkVariable data
foreach (var sobj in SpawnedObjectsList)
{
if (sobj.CheckObjectVisibility == null || sobj.CheckObjectVisibility(OwnerClientId))
if (sobj.SpawnWithObservers && (sobj.CheckObjectVisibility == null || sobj.CheckObjectVisibility(OwnerClientId)))
{
sobj.Observers.Add(OwnerClientId);
var sceneObject = sobj.GetMessageSceneObject(OwnerClientId);

View File

@@ -23,8 +23,12 @@ namespace Unity.Netcode
}
public void Handle(ref NetworkContext context)
{
var networkManager = (NetworkManager)context.SystemOwner;
if (!networkManager.ShutdownInProgress)
{
((NetworkManager)context.SystemOwner).CustomMessagingManager.InvokeNamedMessage(Hash, context.SenderId, m_ReceiveData, context.SerializedHeaderSize);
}
}
}
}

View File

@@ -105,7 +105,6 @@ namespace Unity.Netcode
{
networkVariable.WriteDelta(writer);
}
NetworkBehaviour.NetworkManager.NetworkMetrics.TrackNetworkVariableDeltaSent(
TargetClientId,
NetworkBehaviour.NetworkObject,
@@ -207,7 +206,6 @@ namespace Unity.Netcode
networkBehaviour.__getTypeName(),
context.MessageSize);
if (networkManager.NetworkConfig.EnsureNetworkVariableLengthSafety)
{
if (m_ReceivedNetworkVariableData.Position > (readStartPos + varSize))

View File

@@ -34,7 +34,7 @@ namespace Unity.Netcode
return false;
}
if (!NetworkManager.__rpc_func_table.ContainsKey(metadata.NetworkRpcMethodId))
if (!NetworkBehaviour.__rpc_func_table[networkBehaviour.GetType()].ContainsKey(metadata.NetworkRpcMethodId))
{
return false;
}
@@ -42,7 +42,7 @@ namespace Unity.Netcode
payload = new FastBufferReader(reader.GetUnsafePtrAtCurrentPosition(), Allocator.None, reader.Length - reader.Position);
#if DEVELOPMENT_BUILD || UNITY_EDITOR
if (NetworkManager.__rpc_name_table.TryGetValue(metadata.NetworkRpcMethodId, out var rpcMethodName))
if (NetworkBehaviour.__rpc_name_table[networkBehaviour.GetType()].TryGetValue(metadata.NetworkRpcMethodId, out var rpcMethodName))
{
networkManager.NetworkMetrics.TrackRpcReceived(
context.SenderId,
@@ -67,11 +67,19 @@ namespace Unity.Netcode
try
{
NetworkManager.__rpc_func_table[metadata.NetworkRpcMethodId](networkBehaviour, payload, rpcParams);
NetworkBehaviour.__rpc_func_table[networkBehaviour.GetType()][metadata.NetworkRpcMethodId](networkBehaviour, payload, rpcParams);
}
catch (Exception ex)
{
Debug.LogException(new Exception("Unhandled RPC exception!", ex));
if (networkManager.LogLevel == LogLevel.Developer)
{
Debug.Log($"RPC Table Contents");
foreach (var entry in NetworkBehaviour.__rpc_func_table[networkBehaviour.GetType()])
{
Debug.Log($"{entry.Key} | {entry.Value.Method.Name}");
}
}
}
}
}

View File

@@ -12,6 +12,11 @@ namespace Unity.Netcode
/// </summary>
public ushort Magic;
/// <summary>
/// Total number of messages in the batch.
/// </summary>
public ushort BatchCount;
/// <summary>
/// Total number of bytes in the batch.
/// </summary>
@@ -22,9 +27,5 @@ namespace Unity.Netcode
/// </summary>
public ulong BatchHash;
/// <summary>
/// Total number of messages in the batch.
/// </summary>
public ushort BatchCount;
}
}

View File

@@ -95,10 +95,12 @@ namespace Unity.Netcode
return m_MessageTypes[t];
}
public const int DefaultNonFragmentedMessageMaxSize = 1300;
public const int DefaultNonFragmentedMessageMaxSize = 1300 & ~7; // Round down to nearest word aligned size (1296)
public int NonFragmentedMessageMaxSize = DefaultNonFragmentedMessageMaxSize;
public int FragmentedMessageMaxSize = int.MaxValue;
public Dictionary<ulong, int> PeerMTUSizes = new Dictionary<ulong, int>();
internal struct MessageWithHandler
{
public Type MessageType;
@@ -497,6 +499,7 @@ namespace Unity.Netcode
m_SendQueues.Remove(clientId);
m_PerClientMessageVersions.Remove(clientId);
PeerMTUSizes.Remove(clientId);
}
internal void CleanupDisconnectedClients()
@@ -517,16 +520,19 @@ namespace Unity.Netcode
internal int GetMessageVersion(Type type, ulong clientId, bool forReceive = false)
{
if (!m_PerClientMessageVersions.TryGetValue(clientId, out var versionMap))
{
var networkManager = NetworkManager.Singleton;
if (networkManager != null && networkManager.LogLevel == LogLevel.Developer)
{
if (forReceive)
{
Debug.LogWarning($"Trying to receive {type.Name} from client {clientId} which is not in a connected state.");
NetworkLog.LogWarning($"Trying to receive {type.Name} from client {clientId} which is not in a connected state.");
}
else
{
Debug.LogWarning($"Trying to send {type.Name} to client {clientId} which is not in a connected state.");
NetworkLog.LogWarning($"Trying to send {type.Name} to client {clientId} which is not in a connected state.");
}
}
return -1;
}
@@ -675,6 +681,21 @@ namespace Unity.Netcode
continue;
}
var startSize = NonFragmentedMessageMaxSize;
if (delivery != NetworkDelivery.ReliableFragmentedSequenced)
{
if (PeerMTUSizes.TryGetValue(clientId, out var clientMaxSize))
{
maxSize = clientMaxSize;
}
startSize = maxSize;
if (tmpSerializer.Position >= maxSize)
{
Debug.LogError($"MTU size for {clientId} is too small to contain a message of type {typeof(TMessageType).FullName}");
continue;
}
}
for (var hookIdx = 0; hookIdx < m_Hooks.Count; ++hookIdx)
{
m_Hooks[hookIdx].OnBeforeSendMessage(clientId, ref message, delivery);
@@ -683,7 +704,7 @@ namespace Unity.Netcode
var sendQueueItem = m_SendQueues[clientId];
if (sendQueueItem.Length == 0)
{
sendQueueItem.Add(new SendQueueItem(delivery, NonFragmentedMessageMaxSize, Allocator.TempJob, maxSize));
sendQueueItem.Add(new SendQueueItem(delivery, startSize, Allocator.TempJob, maxSize));
sendQueueItem.ElementAt(0).Writer.Seek(sizeof(NetworkBatchHeader));
}
else
@@ -691,7 +712,7 @@ namespace Unity.Netcode
ref var lastQueueItem = ref sendQueueItem.ElementAt(sendQueueItem.Length - 1);
if (lastQueueItem.NetworkDelivery != delivery || lastQueueItem.Writer.MaxCapacity - lastQueueItem.Writer.Position < tmpSerializer.Length + headerSerializer.Length)
{
sendQueueItem.Add(new SendQueueItem(delivery, NonFragmentedMessageMaxSize, Allocator.TempJob, maxSize));
sendQueueItem.Add(new SendQueueItem(delivery, startSize, Allocator.TempJob, maxSize));
sendQueueItem.ElementAt(sendQueueItem.Length - 1).Writer.Seek(sizeof(NetworkBatchHeader));
}
}
@@ -826,11 +847,17 @@ namespace Unity.Netcode
// Skipping the Verify and sneaking the write mark in because we know it's fine.
queueItem.Writer.Handle->AllowedWriteMark = sizeof(NetworkBatchHeader);
#endif
queueItem.BatchHeader.BatchHash = XXHash.Hash64(queueItem.Writer.GetUnsafePtr() + sizeof(NetworkBatchHeader), queueItem.Writer.Length - sizeof(NetworkBatchHeader));
queueItem.BatchHeader.BatchSize = queueItem.Writer.Length;
var alignedLength = (queueItem.Writer.Length + 7) & ~7;
queueItem.Writer.TryBeginWrite(alignedLength);
queueItem.BatchHeader.BatchHash = XXHash.Hash64(queueItem.Writer.GetUnsafePtr() + sizeof(NetworkBatchHeader), alignedLength - sizeof(NetworkBatchHeader));
queueItem.BatchHeader.BatchSize = alignedLength;
queueItem.Writer.WriteValue(queueItem.BatchHeader);
queueItem.Writer.Seek(alignedLength);
try

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using Unity.Collections;
using UnityEngine;
namespace Unity.Netcode
{
@@ -9,6 +8,7 @@ namespace Unity.Netcode
/// Event based NetworkVariable container for syncing Lists
/// </summary>
/// <typeparam name="T">The type for the list</typeparam>
[GenerateSerializationForGenericParameter(0)]
public class NetworkList<T> : NetworkVariableBase where T : unmanaged, IEquatable<T>
{
private NativeList<T> m_List = new NativeList<T>(64, Allocator.Persistent);
@@ -68,14 +68,7 @@ namespace Unity.Netcode
internal void MarkNetworkObjectDirty()
{
if (m_NetworkBehaviour == null)
{
Debug.LogWarning($"NetworkList is written to, but doesn't know its NetworkBehaviour yet. " +
"Are you modifying a NetworkList before the NetworkObject is spawned?");
return;
}
m_NetworkBehaviour.NetworkManager.BehaviourUpdater.AddForUpdate(m_NetworkBehaviour.NetworkObject);
MarkNetworkBehaviourDirty();
}
/// <inheritdoc />

View File

@@ -8,6 +8,7 @@ namespace Unity.Netcode
/// </summary>
/// <typeparam name="T">the unmanaged type for <see cref="NetworkVariable{T}"/> </typeparam>
[Serializable]
[GenerateSerializationForGenericParameter(0)]
public class NetworkVariable<T> : NetworkVariableBase
{
/// <summary>
@@ -146,8 +147,11 @@ namespace Unity.Netcode
// Therefore, we set the m_PreviousValue field to a duplicate of the current
// field, so that our next dirty check is made against the current "not dirty"
// value.
if (!m_HasPreviousValue || !NetworkVariableSerialization<T>.AreEqual(ref m_InternalValue, ref m_PreviousValue))
{
m_HasPreviousValue = true;
NetworkVariableSerialization<T>.Serializer.Duplicate(m_InternalValue, ref m_PreviousValue);
NetworkVariableSerialization<T>.Duplicate(m_InternalValue, ref m_PreviousValue);
}
}
/// <summary>

View File

@@ -87,6 +87,12 @@ namespace Unity.Netcode
m_IsDirty = isDirty;
if (m_IsDirty)
{
MarkNetworkBehaviourDirty();
}
}
protected void MarkNetworkBehaviourDirty()
{
if (m_NetworkBehaviour == null)
{
@@ -97,7 +103,6 @@ namespace Unity.Netcode
m_NetworkBehaviour.NetworkManager.BehaviourUpdater.AddForUpdate(m_NetworkBehaviour.NetworkObject);
}
}
/// <summary>
/// Resets the dirty state and marks the variable as synced / clean

View File

@@ -514,7 +514,7 @@ namespace Unity.Netcode
public void Duplicate(in T value, ref T duplicatedValue)
{
using var writer = new FastBufferWriter(256, Allocator.Temp);
using var writer = new FastBufferWriter(256, Allocator.Temp, int.MaxValue);
var refValue = value;
Write(writer, ref refValue);
@@ -580,11 +580,16 @@ namespace Unity.Netcode
/// <typeparam name="T"></typeparam>
internal class FallbackSerializer<T> : INetworkVariableSerializer<T>
{
private void ThrowArgumentError()
{
throw new ArgumentException($"Serialization has not been generated for type {typeof(T).FullName}. This can be addressed by adding a [{nameof(GenerateSerializationForGenericParameterAttribute)}] to your generic class that serializes this value (if you are using one), adding [{nameof(GenerateSerializationForTypeAttribute)}(typeof({typeof(T).FullName})] to the class or method that is attempting to serialize it, or creating a field on a {nameof(NetworkBehaviour)} of type {nameof(NetworkVariable<T>)}. If this error continues to appear after doing one of those things and this is a type you can change, then either implement {nameof(INetworkSerializable)} or mark it as serializable by memcpy by adding {nameof(INetworkSerializeByMemcpy)} to its interface list to enable automatic serialization generation. If not, assign serialization code to {nameof(UserNetworkVariableSerialization<T>)}.{nameof(UserNetworkVariableSerialization<T>.WriteValue)}, {nameof(UserNetworkVariableSerialization<T>)}.{nameof(UserNetworkVariableSerialization<T>.ReadValue)}, and {nameof(UserNetworkVariableSerialization<T>)}.{nameof(UserNetworkVariableSerialization<T>.DuplicateValue)}, or if it's serializable by memcpy (contains no pointers), wrap it in {typeof(ForceNetworkSerializeByMemcpy<>).Name}.");
}
public void Write(FastBufferWriter writer, ref T value)
{
if (UserNetworkVariableSerialization<T>.ReadValue == null || UserNetworkVariableSerialization<T>.WriteValue == null || UserNetworkVariableSerialization<T>.DuplicateValue == null)
{
throw new ArgumentException($"Type {typeof(T).FullName} is not supported by {typeof(NetworkVariable<>).Name}. If this is a type you can change, then either implement {nameof(INetworkSerializable)} or mark it as serializable by memcpy by adding {nameof(INetworkSerializeByMemcpy)} to its interface list. If not, assign serialization code to {nameof(UserNetworkVariableSerialization<T>)}.{nameof(UserNetworkVariableSerialization<T>.WriteValue)}, {nameof(UserNetworkVariableSerialization<T>)}.{nameof(UserNetworkVariableSerialization<T>.ReadValue)}, and {nameof(UserNetworkVariableSerialization<T>)}.{nameof(UserNetworkVariableSerialization<T>.DuplicateValue)}, or if it's serializable by memcpy (contains no pointers), wrap it in {typeof(ForceNetworkSerializeByMemcpy<>).Name}.");
ThrowArgumentError();
}
UserNetworkVariableSerialization<T>.WriteValue(writer, value);
}
@@ -592,7 +597,7 @@ namespace Unity.Netcode
{
if (UserNetworkVariableSerialization<T>.ReadValue == null || UserNetworkVariableSerialization<T>.WriteValue == null || UserNetworkVariableSerialization<T>.DuplicateValue == null)
{
throw new ArgumentException($"Type {typeof(T).FullName} is not supported by {typeof(NetworkVariable<>).Name}. If this is a type you can change, then either implement {nameof(INetworkSerializable)} or mark it as serializable by memcpy by adding {nameof(INetworkSerializeByMemcpy)} to its interface list. If not, assign serialization code to {nameof(UserNetworkVariableSerialization<T>)}.{nameof(UserNetworkVariableSerialization<T>.WriteValue)}, {nameof(UserNetworkVariableSerialization<T>)}.{nameof(UserNetworkVariableSerialization<T>.ReadValue)}, and {nameof(UserNetworkVariableSerialization<T>)}.{nameof(UserNetworkVariableSerialization<T>.DuplicateValue)}, or if it's serializable by memcpy (contains no pointers), wrap it in {typeof(ForceNetworkSerializeByMemcpy<>).Name}.");
ThrowArgumentError();
}
UserNetworkVariableSerialization<T>.ReadValue(reader, out value);
}
@@ -606,7 +611,7 @@ namespace Unity.Netcode
{
if (UserNetworkVariableSerialization<T>.ReadValue == null || UserNetworkVariableSerialization<T>.WriteValue == null || UserNetworkVariableSerialization<T>.DuplicateValue == null)
{
throw new ArgumentException($"Type {typeof(T).FullName} is not supported by {typeof(NetworkVariable<>).Name}. If this is a type you can change, then either implement {nameof(INetworkSerializable)} or mark it as serializable by memcpy by adding {nameof(INetworkSerializeByMemcpy)} to its interface list. If not, assign serialization code to {nameof(UserNetworkVariableSerialization<T>)}.{nameof(UserNetworkVariableSerialization<T>.WriteValue)}, {nameof(UserNetworkVariableSerialization<T>)}.{nameof(UserNetworkVariableSerialization<T>.ReadValue)}, and {nameof(UserNetworkVariableSerialization<T>)}.{nameof(UserNetworkVariableSerialization<T>.DuplicateValue)}, or if it's serializable by memcpy (contains no pointers), wrap it in {typeof(ForceNetworkSerializeByMemcpy<>).Name}.");
ThrowArgumentError();
}
UserNetworkVariableSerialization<T>.DuplicateValue(value, ref duplicatedValue);
}
@@ -841,8 +846,95 @@ namespace Unity.Netcode
{
internal static INetworkVariableSerializer<T> Serializer = new FallbackSerializer<T>();
internal delegate bool EqualsDelegate(ref T a, ref T b);
internal static EqualsDelegate AreEqual;
/// <summary>
/// A callback to check if two values are equal.
/// </summary>
public delegate bool EqualsDelegate(ref T a, ref T b);
/// <summary>
/// Uses the most efficient mechanism for a given type to determine if two values are equal.
/// For types that implement <see cref="IEquatable{T}"/>, it will call the Equals() method.
/// For unmanaged types, it will do a bytewise memory comparison.
/// For other types, it will call the == operator.
/// <br/>
/// <br/>
/// Note: If you are using this in a custom generic class, please make sure your class is
/// decorated with <see cref="GenerateSerializationForGenericParameterAttribute"/> so that codegen can
/// initialize the serialization mechanisms correctly. If your class is NOT
/// generic, it is better to check their equality yourself.
/// </summary>
public static EqualsDelegate AreEqual { get; internal set; }
/// <summary>
/// Serialize a value using the best-known serialization method for a generic value.
/// Will reliably serialize any value that is passed to it correctly with no boxing.
/// <br/>
/// <br/>
/// Note: If you are using this in a custom generic class, please make sure your class is
/// decorated with <see cref="GenerateSerializationForGenericParameterAttribute"/> so that codegen can
/// initialize the serialization mechanisms correctly. If your class is NOT
/// generic, it is better to use FastBufferWriter directly.
/// <br/>
/// <br/>
/// If the codegen is unable to determine a serializer for a type,
/// <see cref="UserNetworkVariableSerialization{T}"/>.<see cref="UserNetworkVariableSerialization{T}.WriteValue"/> is called, which, by default,
/// will throw an exception, unless you have assigned a user serialization callback to it at runtime.
/// </summary>
/// <param name="writer"></param>
/// <param name="value"></param>
public static void Write(FastBufferWriter writer, ref T value)
{
Serializer.Write(writer, ref value);
}
/// <summary>
/// Deserialize a value using the best-known serialization method for a generic value.
/// Will reliably deserialize any value that is passed to it correctly with no boxing.
/// For types whose deserialization can be determined by codegen (which is most types),
/// GC will only be incurred if the type is a managed type and the ref value passed in is `null`,
/// in which case a new value is created; otherwise, it will be deserialized in-place.
/// <br/>
/// <br/>
/// Note: If you are using this in a custom generic class, please make sure your class is
/// decorated with <see cref="GenerateSerializationForGenericParameterAttribute"/> so that codegen can
/// initialize the serialization mechanisms correctly. If your class is NOT
/// generic, it is better to use FastBufferReader directly.
/// <br/>
/// <br/>
/// If the codegen is unable to determine a serializer for a type,
/// <see cref="UserNetworkVariableSerialization{T}"/>.<see cref="UserNetworkVariableSerialization{T}.ReadValue"/> is called, which, by default,
/// will throw an exception, unless you have assigned a user deserialization callback to it at runtime.
/// </summary>
/// <param name="reader"></param>
/// <param name="value"></param>
public static void Read(FastBufferReader reader, ref T value)
{
Serializer.Read(reader, ref value);
}
/// <summary>
/// Duplicates a value using the most efficient means of creating a complete copy.
/// For most types this is a simple assignment or memcpy.
/// For managed types, this is will serialize and then deserialize the value to ensure
/// a correct copy.
/// <br/>
/// <br/>
/// Note: If you are using this in a custom generic class, please make sure your class is
/// decorated with <see cref="GenerateSerializationForGenericParameterAttribute"/> so that codegen can
/// initialize the serialization mechanisms correctly. If your class is NOT
/// generic, it is better to duplicate it directly.
/// <br/>
/// <br/>
/// If the codegen is unable to determine a serializer for a type,
/// <see cref="UserNetworkVariableSerialization{T}"/>.<see cref="UserNetworkVariableSerialization{T}.DuplicateValue"/> is called, which, by default,
/// will throw an exception, unless you have assigned a user duplication callback to it at runtime.
/// </summary>
/// <param name="value"></param>
/// <param name="duplicatedValue"></param>
public static void Duplicate(in T value, ref T duplicatedValue)
{
Serializer.Duplicate(value, ref duplicatedValue);
}
// Compares two values of the same unmanaged type by underlying memory
// Ignoring any overridden value checks
@@ -1001,15 +1093,21 @@ namespace Unity.Netcode
{
return a == b;
}
internal static void Write(FastBufferWriter writer, ref T value)
{
Serializer.Write(writer, ref value);
}
internal static void Read(FastBufferReader reader, ref T value)
// RuntimeAccessModifiersILPP will make this `public`
// This is just pass-through to NetworkVariableSerialization<T> but is here becaues I could not get ILPP
// to generate code that would successfully call Type<T>.Method(T), but it has no problem calling Type.Method<T>(T)
internal class RpcFallbackSerialization
{
Serializer.Read(reader, ref value);
public static void Write<T>(FastBufferWriter writer, ref T value)
{
NetworkVariableSerialization<T>.Write(writer, ref value);
}
public static void Read<T>(FastBufferReader reader, ref T value)
{
NetworkVariableSerialization<T>.Read(reader, ref value);
}
}
}

View File

@@ -740,6 +740,14 @@ namespace Unity.Netcode
// Since NetworkManager is now always migrated to the DDOL we will use this to get the DDOL scene
DontDestroyOnLoadScene = networkManager.gameObject.scene;
// Since the server tracks loaded scenes, we need to add the currently active scene
// to the list of scenes that can be unloaded.
if (networkManager.IsServer)
{
var activeScene = SceneManager.GetActiveScene();
ScenesLoaded.Add(activeScene.handle, activeScene);
}
// Add to the server to client scene handle table
UpdateServerClientSceneHandle(DontDestroyOnLoadScene.handle, DontDestroyOnLoadScene.handle, DontDestroyOnLoadScene);
}
@@ -2191,6 +2199,10 @@ namespace Unity.Netcode
ClientId = clientId
});
// At this point the client is considered fully "connected"
NetworkManager.ConnectedClients[clientId].IsConnected = true;
// All scenes are synchronized, let the server know we are done synchronizing
OnSynchronizeComplete?.Invoke(clientId);
// At this time the client is fully synchronized with all loaded scenes and

View File

@@ -113,12 +113,6 @@ namespace Unity.Netcode
// Remove the previous owner's entry
OwnershipToObjectsTable[previousOwner].Remove(networkObject.NetworkObjectId);
// Server or Host alway invokes the lost ownership notification locally
if (NetworkManager.IsServer)
{
networkObject.InvokeBehaviourOnLostOwnership();
}
// If we are removing the entry (i.e. despawning or client lost ownership)
if (isRemoving)
{
@@ -143,12 +137,6 @@ namespace Unity.Netcode
{
// Add the new ownership entry
OwnershipToObjectsTable[newOwner].Add(networkObject.NetworkObjectId, networkObject);
// Server or Host always invokes the gained ownership notification locally
if (NetworkManager.IsServer)
{
networkObject.InvokeBehaviourOnGainedOwnership();
}
}
else if (isRemoving)
{
@@ -227,43 +215,6 @@ namespace Unity.Netcode
return null;
}
internal void RemoveOwnership(NetworkObject networkObject)
{
if (!NetworkManager.IsServer)
{
throw new NotServerException("Only the server can change ownership");
}
if (!networkObject.IsSpawned)
{
throw new SpawnStateException("Object is not spawned");
}
// If we made it here then we are the server and if the server is determined to already be the owner
// then ignore the RemoveOwnership invocation.
if (networkObject.OwnerClientId == NetworkManager.ServerClientId)
{
return;
}
networkObject.OwnerClientId = NetworkManager.ServerClientId;
// Server removes the entry and takes over ownership before notifying
UpdateOwnershipTable(networkObject, NetworkManager.ServerClientId, true);
var message = new ChangeOwnershipMessage
{
NetworkObjectId = networkObject.NetworkObjectId,
OwnerClientId = networkObject.OwnerClientId
};
var size = NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, NetworkManager.ConnectedClientsIds);
foreach (var client in NetworkManager.ConnectedClients)
{
NetworkManager.NetworkMetrics.TrackOwnershipChangeSent(client.Key, networkObject, size);
}
}
/// <summary>
/// Helper function to get a network client for a clientId from the NetworkManager.
/// On the server this will check the <see cref="NetworkManager.ConnectedClients"/> list.
@@ -289,6 +240,11 @@ namespace Unity.Netcode
return false;
}
internal void RemoveOwnership(NetworkObject networkObject)
{
ChangeOwnership(networkObject, NetworkManager.ServerClientId);
}
internal void ChangeOwnership(NetworkObject networkObject, ulong clientId)
{
if (!NetworkManager.IsServer)
@@ -301,14 +257,22 @@ namespace Unity.Netcode
throw new SpawnStateException("Object is not spawned");
}
var previous = networkObject.OwnerClientId;
// Assign the new owner
networkObject.OwnerClientId = clientId;
// Always notify locally on the server when ownership is lost
networkObject.InvokeBehaviourOnLostOwnership();
networkObject.MarkVariablesDirty(true);
NetworkManager.BehaviourUpdater.AddForUpdate(networkObject);
// Server adds entries for all client ownership
UpdateOwnershipTable(networkObject, networkObject.OwnerClientId);
// Always notify locally on the server when a new owner is assigned
networkObject.InvokeBehaviourOnGainedOwnership();
var message = new ChangeOwnershipMessage
{
NetworkObjectId = networkObject.NetworkObjectId,
@@ -323,6 +287,12 @@ namespace Unity.Netcode
NetworkManager.NetworkMetrics.TrackOwnershipChangeSent(client.Key, networkObject, size);
}
}
// After we have sent the change ownership message to all client observers, invoke the ownership changed notification.
/// !!Important!!
/// This gets called specifically *after* sending the ownership message so any additional messages that need to proceed an ownership
/// change can be sent from NetworkBehaviours that override the <see cref="NetworkBehaviour.OnOwnershipChanged"></see>
networkObject.InvokeOwnershipChanged(previous, clientId);
}
internal bool HasPrefab(NetworkObject.SceneObject sceneObject)
@@ -952,27 +922,35 @@ namespace Unity.Netcode
}
/// <summary>
/// Updates all spawned <see cref="NetworkObject.Observers"/> for the specified client
/// Updates all spawned <see cref="NetworkObject.Observers"/> for the specified newly connected client
/// Note: if the clientId is the server then it is observable to all spawned <see cref="NetworkObject"/>'s
/// </summary>
/// <remarks>
/// This method is to only to be used for newly connected clients in order to update the observers list for
/// each NetworkObject instance.
/// </remarks>
internal void UpdateObservedNetworkObjects(ulong clientId)
{
foreach (var sobj in SpawnedObjectsList)
{
// If the NetworkObject has no visibility check then prepare to add this client as an observer
if (sobj.CheckObjectVisibility == null)
{
if (!sobj.Observers.Contains(clientId))
// If the client is not part of the observers and spawn with observers is enabled on this instance or the clientId is the server
if (!sobj.Observers.Contains(clientId) && (sobj.SpawnWithObservers || clientId == NetworkManager.ServerClientId))
{
sobj.Observers.Add(clientId);
}
}
else
{
// CheckObject visibility overrides SpawnWithObservers under this condition
if (sobj.CheckObjectVisibility(clientId))
{
sobj.Observers.Add(clientId);
}
else if (sobj.Observers.Contains(clientId))
else // Otherwise, if the observers contains the clientId (shouldn't happen) then remove it since CheckObjectVisibility returned false
if (sobj.Observers.Contains(clientId))
{
sobj.Observers.Remove(clientId);
}

View File

@@ -9,9 +9,6 @@ namespace Unity.Netcode
/// </summary>
public class NetworkTimeSystem
{
/// <summary>
/// TODO 2023-Q2: Not sure if this just needs to go away, but there is nothing that ever replaces this
/// </summary>
/// <remarks>
/// This was the original comment when it lived in NetworkManager:
/// todo talk with UX/Product, find good default value for this

View File

@@ -199,7 +199,7 @@ namespace Unity.Netcode.Transports.UNET
public override bool StartClient()
{
m_ServerHostId = UnityEngine.Networking.NetworkTransport.AddHost(new HostTopology(GetConfig(), 1), 0, null);
m_ServerConnectionId = UnityEngine.Networking.NetworkTransport.Connect(m_ServerHostId, ConnectAddress, ConnectPort, 0, out byte error);
m_ServerConnectionId = UnityEngine.Networking.NetworkTransport.Connect(m_ServerHostId, ConnectAddress, GetConnectPort(), 0, out byte error);
return (NetworkError)error == NetworkError.Ok;
}
@@ -207,7 +207,7 @@ namespace Unity.Netcode.Transports.UNET
{
var topology = new HostTopology(GetConfig(), MaxConnections);
// Undocumented, but AddHost returns -1 in case of any type of failure. See UNET::NetLibraryManager::AddHost
return -1 != UnityEngine.Networking.NetworkTransport.AddHost(topology, ServerListenPort, null);
return -1 != UnityEngine.Networking.NetworkTransport.AddHost(topology, GetServerListenPort(), null);
}
public override void DisconnectRemoteClient(ulong clientId)
@@ -281,6 +281,26 @@ namespace Unity.Netcode.Transports.UNET
return connectionConfig;
}
private int GetConnectPort()
{
if (NetworkManager && NetworkManager.PortOverride.Overidden)
{
return NetworkManager.PortOverride.Value;
}
return ConnectPort;
}
private int GetServerListenPort()
{
if (NetworkManager && NetworkManager.PortOverride.Overidden)
{
return NetworkManager.PortOverride.Value;
}
return ServerListenPort;
}
}
}
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member

View File

@@ -242,7 +242,7 @@ namespace Unity.Netcode.Transports.UTP
/// Fill the given <see cref="DataStreamWriter"/> with as many bytes from the queue as
/// possible, disregarding message boundaries.
/// </summary>
///<remarks>
/// <remarks>
/// This does NOT actually consume anything from the queue. That is, calling this method
/// does not reduce the length of the queue. Callers are expected to call
/// <see cref="Consume"/> with the value returned by this method afterwards if the data can
@@ -252,15 +252,17 @@ namespace Unity.Netcode.Transports.UTP
/// this could lead to reading messages from a corrupted queue.
/// </remarks>
/// <param name="writer">The <see cref="DataStreamWriter"/> to write to.</param>
/// <param name="maxBytes">Max number of bytes to copy (0 means writer capacity).</param>
/// <returns>How many bytes were written to the writer.</returns>
public int FillWriterWithBytes(ref DataStreamWriter writer)
public int FillWriterWithBytes(ref DataStreamWriter writer, int maxBytes = 0)
{
if (!IsCreated || Length == 0)
{
return 0;
}
var copyLength = Math.Min(writer.Capacity, Length);
var maxLength = maxBytes == 0 ? writer.Capacity : Math.Min(maxBytes, writer.Capacity);
var copyLength = Math.Min(maxLength, Length);
unsafe
{

View File

@@ -402,6 +402,7 @@ namespace Unity.Netcode.Transports.UTP
/// - packet jitter (variances in latency, see: https://en.wikipedia.org/wiki/Jitter)
/// - packet drop rate (packet loss)
/// </summary>
#if UTP_TRANSPORT_2_0_ABOVE
[Obsolete("DebugSimulator is no longer supported and has no effect. Use Network Simulator from the Multiplayer Tools package.", false)]
#endif
@@ -685,9 +686,11 @@ namespace Unity.Netcode.Transports.UTP
/// <param name="packetDelay">Packet delay in milliseconds.</param>
/// <param name="packetJitter">Packet jitter in milliseconds.</param>
/// <param name="dropRate">Packet drop percentage.</param>
#if UTP_TRANSPORT_2_0_ABOVE
[Obsolete("SetDebugSimulatorParameters is no longer supported and has no effect. Use Network Simulator from the Multiplayer Tools package.", false)]
#endif
public void SetDebugSimulatorParameters(int packetDelay, int packetJitter, int dropRate)
{
if (m_Driver.IsCreated)
@@ -727,6 +730,7 @@ namespace Unity.Netcode.Transports.UTP
public SendTarget Target;
public BatchedSendQueue Queue;
public NetworkPipeline ReliablePipeline;
public int MTU;
public void Execute()
{
@@ -749,7 +753,7 @@ namespace Unity.Netcode.Transports.UTP
// in the stream (the send queue does that automatically) we are sure they'll be
// reassembled properly at the other end. This allows us to lift the limit of ~44KB
// on reliable payloads (because of the reliable window size).
var written = pipeline == ReliablePipeline ? Queue.FillWriterWithBytes(ref writer) : Queue.FillWriterWithMessages(ref writer);
var written = pipeline == ReliablePipeline ? Queue.FillWriterWithBytes(ref writer, MTU) : Queue.FillWriterWithMessages(ref writer);
result = Driver.EndSend(writer);
if (result == written)
@@ -783,12 +787,21 @@ namespace Unity.Netcode.Transports.UTP
{
return;
}
var mtu = 0;
if (NetworkManager)
{
var ngoClientId = NetworkManager.ConnectionManager.TransportIdToClientId(sendTarget.ClientId);
mtu = NetworkManager.GetPeerMTU(ngoClientId);
}
new SendBatchedMessagesJob
{
Driver = m_Driver.ToConcurrent(),
Target = sendTarget,
Queue = queue,
ReliablePipeline = m_ReliableSequencedPipeline
ReliablePipeline = m_ReliableSequencedPipeline,
MTU = mtu,
}.Run();
}
@@ -1199,6 +1212,11 @@ namespace Unity.Netcode.Transports.UTP
NetworkManager = networkManager;
if (NetworkManager && NetworkManager.PortOverride.Overidden)
{
ConnectionData.Port = NetworkManager.PortOverride.Value;
}
m_RealTimeProvider = NetworkManager ? NetworkManager.RealTimeProvider : new RealTimeProvider();
m_NetworkSettings = new NetworkSettings(Allocator.Persistent);
@@ -1560,6 +1578,21 @@ namespace Unity.Netcode.Transports.UTP
}
#endif
#if UTP_TRANSPORT_2_1_ABOVE
if (m_ProtocolType == ProtocolType.RelayUnityTransport)
{
if (m_UseWebSockets && m_RelayServerData.IsWebSocket == 0)
{
Debug.LogError("Transport is configured to use WebSockets, but Relay server data isn't. Be sure to use \"wss\" as the connection type when creating the server data (instead of \"dtls\" or \"udp\").");
}
if (!m_UseWebSockets && m_RelayServerData.IsWebSocket != 0)
{
Debug.LogError("Relay server data indicates usage of WebSockets, but \"Use WebSockets\" checkbox isn't checked under \"Unity Transport\" component.");
}
}
#endif
#if UTP_TRANSPORT_2_0_ABOVE
if (m_UseWebSockets)
{
@@ -1567,7 +1600,7 @@ namespace Unity.Netcode.Transports.UTP
}
else
{
#if UNITY_WEBGL
#if UNITY_WEBGL && !UNITY_EDITOR
Debug.LogWarning($"WebSockets were used even though they're not selected in NetworkManager. You should check {nameof(UseWebSockets)}', on the Unity Transport component, to silence this warning.");
driver = NetworkDriver.Create(new WebSocketNetworkInterface(), m_NetworkSettings);
#else

View File

@@ -36,6 +36,16 @@
"name": "com.unity.transport",
"expression": "2.0.0-exp",
"define": "UTP_TRANSPORT_2_0_ABOVE"
},
{
"name": "com.unity.transport",
"expression": "2.1.0",
"define": "UTP_TRANSPORT_2_1_ABOVE"
},
{
"name": "Unity",
"expression": "2023",
"define": "UNITY_DEDICATED_SERVER_ARGUMENTS_PRESENT"
}
]
}

View File

@@ -913,6 +913,9 @@ namespace Unity.Netcode.TestHelpers.Runtime
if (CoroutineRunner == null)
{
CoroutineRunner = new GameObject("UnitTestSceneHandlerCoroutine").AddComponent<CoroutineRunner>();
// Move the CoroutineRunner into the DDOL in case we unload the scene it was instantiated in.
// (which if that gets destroyed then it basically stops all integration test queue processing)
Object.DontDestroyOnLoad(CoroutineRunner);
}
}

View File

@@ -886,7 +886,6 @@ namespace Unity.Netcode.TestHelpers.Runtime
{
IntegrationTestSceneHandler.CanClientsLoad += ClientSceneHandler_CanClientsLoad;
IntegrationTestSceneHandler.CanClientsUnload += ClientSceneHandler_CanClientsUnload;
NetcodeIntegrationTestHelpers.RegisterSceneManagerHandler(m_ServerNetworkManager, true);
}
private bool ClientSceneHandler_CanClientsUnload()

View File

@@ -165,7 +165,8 @@ namespace Unity.Netcode.TestHelpers.Runtime
if (!networkManager.IsServer || networkManager.IsServer && serverSideSceneManager)
{
RegisterSceneManagerHandler(networkManager);
// Pass along the serverSideSceneManager property (otherwise the server won't register properly)
RegisterSceneManagerHandler(networkManager, serverSideSceneManager);
}
}
@@ -405,7 +406,10 @@ namespace Unity.Netcode.TestHelpers.Runtime
// scene to synchronize NetworkObjects. Next, add the currently active test runner scene to the scenes
// loaded and register the server to client scene handle since host-server shares the test runner scene
// with the clients.
networkManager.SceneManager.GetAndAddNewlyLoadedSceneByName(scene.name);
if (!networkManager.SceneManager.ScenesLoaded.ContainsKey(scene.handle))
{
networkManager.SceneManager.ScenesLoaded.Add(scene.handle, scene);
}
networkManager.SceneManager.ServerSceneHandleToClientSceneHandle.Add(scene.handle, scene.handle);
}
}
@@ -443,8 +447,8 @@ namespace Unity.Netcode.TestHelpers.Runtime
server.ConnectionManager.MessageManager.Hook(hooks);
s_Hooks[server] = hooks;
// if set, then invoke this for the server
RegisterHandlers(server);
// Register the server side handler (always pass true for server)
RegisterHandlers(server, true);
callback?.Invoke();
@@ -919,6 +923,24 @@ namespace Unity.Netcode.TestHelpers.Runtime
var res = check.Result;
result.Result = res;
}
public static uint GetGlobalObjectIdHash(NetworkObject networkObject)
{
return networkObject.GlobalObjectIdHash;
}
#if UNITY_EDITOR
public static void SetRefreshAllPrefabsCallback(Action scenesProcessed)
{
NetworkObjectRefreshTool.AllScenesProcessed = scenesProcessed;
}
public static void RefreshAllPrefabInstances(NetworkObject networkObject, Action scenesProcessed)
{
NetworkObjectRefreshTool.AllScenesProcessed = scenesProcessed;
networkObject.RefreshAllPrefabInstances();
}
#endif
}
// Empty MonoBehaviour that is a holder of coroutine

View File

@@ -181,6 +181,64 @@ namespace Unity.Netcode.EditorTests
Assert.AreEqual(2, m_MessageSender.MessageQueue.Count);
}
[Test]
public void WhenExceedingPerClientBatchSizeLessThanDefault_NewBatchesAreCreated([Values(500, 1000, 1300, 2000)] int maxMessageSize)
{
var message = GetMessage();
m_MessageManager.NonFragmentedMessageMaxSize = maxMessageSize * 5;
var clients = new ulong[] { 0, 1, 2 };
m_MessageManager.ClientConnected(1);
m_MessageManager.ClientConnected(2);
m_MessageManager.SetVersion(1, XXHash.Hash32(typeof(TestMessage).FullName), 0);
m_MessageManager.SetVersion(2, XXHash.Hash32(typeof(TestMessage).FullName), 0);
for (var i = 0; i < clients.Length; ++i)
{
m_MessageManager.PeerMTUSizes[clients[i]] = maxMessageSize * (i + 1);
}
var size = UnsafeUtility.SizeOf<TestMessage>() + 2; // MessageHeader packed with this message will be 2 bytes
for (var i = 0; i < clients.Length; ++i)
{
for (var j = 0; j < ((m_MessageManager.PeerMTUSizes[clients[i]] - UnsafeUtility.SizeOf<NetworkBatchHeader>()) / size) + 1; ++j)
{
m_MessageManager.SendMessage(ref message, NetworkDelivery.Reliable, clients[i]);
}
}
m_MessageManager.ProcessSendQueues();
Assert.AreEqual(2 * clients.Length, m_MessageSender.MessageQueue.Count);
}
[Test]
public void WhenExceedingPerClientBatchSizeGreaterThanDefault_OnlyOneNewBatcheIsCreated([Values(500, 1000, 1300, 2000)] int maxMessageSize)
{
var message = GetMessage();
m_MessageManager.NonFragmentedMessageMaxSize = 128;
var clients = new ulong[] { 0, 1, 2 };
m_MessageManager.ClientConnected(1);
m_MessageManager.ClientConnected(2);
m_MessageManager.SetVersion(1, XXHash.Hash32(typeof(TestMessage).FullName), 0);
m_MessageManager.SetVersion(2, XXHash.Hash32(typeof(TestMessage).FullName), 0);
for (var i = 0; i < clients.Length; ++i)
{
m_MessageManager.PeerMTUSizes[clients[i]] = maxMessageSize * (i + 1);
}
var size = UnsafeUtility.SizeOf<TestMessage>() + 2; // MessageHeader packed with this message will be 2 bytes
for (var i = 0; i < clients.Length; ++i)
{
for (var j = 0; j < ((m_MessageManager.PeerMTUSizes[clients[i]] - UnsafeUtility.SizeOf<NetworkBatchHeader>()) / size) + 1; ++j)
{
m_MessageManager.SendMessage(ref message, NetworkDelivery.Reliable, clients[i]);
}
}
m_MessageManager.ProcessSendQueues();
Assert.AreEqual(2 * clients.Length, m_MessageSender.MessageQueue.Count);
}
[Test]
public void WhenExceedingMTUSizeWithFragmentedDelivery_NewBatchesAreNotCreated([Values(500, 1000, 1300, 2000)] int maxMessageSize)
{

View File

@@ -173,6 +173,8 @@ namespace Unity.Netcode.EditorTests
NetworkTransport = networkManager.gameObject.AddComponent<UnityTransport>()
};
try
{
var object1 = new GameObject("Object 1").AddComponent<NetworkObject>();
var object2 = new GameObject("Object 2").AddComponent<NetworkObject>();
@@ -205,6 +207,17 @@ namespace Unity.Netcode.EditorTests
Assert.IsFalse(sharedList.Contains(object2.gameObject));
Assert.IsFalse(sharedList.Contains(object3.gameObject));
}
finally
{
networkManager.ShutdownInternal();
networkManager2.ShutdownInternal();
// Shutdown doesn't get called correctly because we called Initialize()
// instead of calling StartHost/StartClient/StartServer. See MTT-860 for
// why.
networkManager.NetworkConfig?.NetworkTransport.Shutdown();
networkManager2.NetworkConfig?.NetworkTransport.Shutdown();
}
}
[Test]
public void WhenModifyingPrefabListUsingPrefabsAPI_ModificationIsLocal()
@@ -224,6 +237,8 @@ namespace Unity.Netcode.EditorTests
NetworkTransport = networkManager.gameObject.AddComponent<UnityTransport>()
};
try
{
var object1 = new GameObject("Object 1").AddComponent<NetworkObject>();
var object2 = new GameObject("Object 2").AddComponent<NetworkObject>();
var object3 = new GameObject("Object 3").AddComponent<NetworkObject>();
@@ -255,6 +270,17 @@ namespace Unity.Netcode.EditorTests
Assert.IsFalse(sharedList.Contains(object2.gameObject));
Assert.IsFalse(sharedList.Contains(object3.gameObject));
}
finally
{
networkManager.ShutdownInternal();
networkManager2.ShutdownInternal();
// Shutdown doesn't get called correctly because we called Initialize()
// instead of calling StartHost/StartClient/StartServer. See MTT-860 for
// why.
networkManager.NetworkConfig?.NetworkTransport.Shutdown();
networkManager2.NetworkConfig?.NetworkTransport.Shutdown();
}
}
[Test]
public void WhenModifyingPrefabListUsingPrefabsListAPI_ModificationIsShared()
@@ -274,6 +300,8 @@ namespace Unity.Netcode.EditorTests
NetworkTransport = networkManager.gameObject.AddComponent<UnityTransport>()
};
try
{
var object1 = new GameObject("Object 1").AddComponent<NetworkObject>();
var object2 = new GameObject("Object 2").AddComponent<NetworkObject>();
var object3 = new GameObject("Object 3").AddComponent<NetworkObject>();
@@ -305,6 +333,17 @@ namespace Unity.Netcode.EditorTests
Assert.IsTrue(sharedList.Contains(object2.gameObject));
Assert.IsTrue(sharedList.Contains(object3.gameObject));
}
finally
{
networkManager.ShutdownInternal();
networkManager2.ShutdownInternal();
// Shutdown doesn't get called correctly because we called Initialize()
// instead of calling StartHost/StartClient/StartServer. See MTT-860 for
// why.
networkManager.NetworkConfig?.NetworkTransport.Shutdown();
networkManager2.NetworkConfig?.NetworkTransport.Shutdown();
}
}
[Test]
public void WhenCallingInitializeAfterAddingAPrefabUsingPrefabsAPI_ThePrefabStillExists()
@@ -324,6 +363,8 @@ namespace Unity.Netcode.EditorTests
NetworkTransport = networkManager.gameObject.AddComponent<UnityTransport>()
};
try
{
var object1 = new GameObject("Object 1").AddComponent<NetworkObject>();
var object2 = new GameObject("Object 2").AddComponent<NetworkObject>();
var object3 = new GameObject("Object 3").AddComponent<NetworkObject>();
@@ -355,6 +396,17 @@ namespace Unity.Netcode.EditorTests
Assert.IsFalse(sharedList.Contains(object2.gameObject));
Assert.IsFalse(sharedList.Contains(object3.gameObject));
}
finally
{
networkManager.ShutdownInternal();
networkManager2.ShutdownInternal();
// Shutdown doesn't get called correctly because we called Initialize()
// instead of calling StartHost/StartClient/StartServer. See MTT-860 for
// why.
networkManager.NetworkConfig?.NetworkTransport.Shutdown();
networkManager2.NetworkConfig?.NetworkTransport.Shutdown();
}
}
[Test]
public void WhenShuttingDownAndReinitializingPrefabs_RuntimeAddedPrefabsStillExists()
@@ -374,6 +426,8 @@ namespace Unity.Netcode.EditorTests
NetworkTransport = networkManager.gameObject.AddComponent<UnityTransport>()
};
try
{
var object1 = new GameObject("Object 1").AddComponent<NetworkObject>();
var object2 = new GameObject("Object 2").AddComponent<NetworkObject>();
var object3 = new GameObject("Object 3").AddComponent<NetworkObject>();
@@ -396,6 +450,11 @@ namespace Unity.Netcode.EditorTests
networkManager.ShutdownInternal();
networkManager2.ShutdownInternal();
// Shutdown doesn't get called correctly because we called Initialize()
// instead of calling StartHost/StartClient/StartServer. See MTT-860 for
// why.
networkManager.NetworkConfig?.NetworkTransport.Shutdown();
networkManager2.NetworkConfig?.NetworkTransport.Shutdown();
networkManager.Initialize(true);
networkManager2.Initialize(false);
@@ -411,6 +470,17 @@ namespace Unity.Netcode.EditorTests
Assert.IsFalse(sharedList.Contains(object2.gameObject));
Assert.IsFalse(sharedList.Contains(object3.gameObject));
}
finally
{
networkManager.ShutdownInternal();
networkManager2.ShutdownInternal();
// Shutdown doesn't get called correctly because we called Initialize()
// instead of calling StartHost/StartClient/StartServer. See MTT-860 for
// why.
networkManager.NetworkConfig?.NetworkTransport.Shutdown();
networkManager2.NetworkConfig?.NetworkTransport.Shutdown();
}
}
[Test]
public void WhenCallingInitializeMultipleTimes_NothingBreaks()
@@ -430,6 +500,8 @@ namespace Unity.Netcode.EditorTests
NetworkTransport = networkManager.gameObject.AddComponent<UnityTransport>()
};
try
{
var object1 = new GameObject("Object 1").AddComponent<NetworkObject>();
var object2 = new GameObject("Object 2").AddComponent<NetworkObject>();
var object3 = new GameObject("Object 3").AddComponent<NetworkObject>();
@@ -464,5 +536,16 @@ namespace Unity.Netcode.EditorTests
Assert.IsFalse(sharedList.Contains(object2.gameObject));
Assert.IsFalse(sharedList.Contains(object3.gameObject));
}
finally
{
networkManager.ShutdownInternal();
networkManager2.ShutdownInternal();
// Shutdown doesn't get called correctly because we called Initialize()
// instead of calling StartHost/StartClient/StartServer. See MTT-860 for
// why.
networkManager.NetworkConfig?.NetworkTransport.Shutdown();
networkManager2.NetworkConfig?.NetworkTransport.Shutdown();
}
}
}
}

View File

@@ -1,5 +1,6 @@
using System.Text.RegularExpressions;
using NUnit.Framework;
using Unity.Netcode.Editor;
using UnityEngine;
using UnityEngine.TestTools;
@@ -64,9 +65,95 @@ namespace Unity.Netcode.EditorTests
Object.DestroyImmediate(gameObject);
}
/// <summary>
/// Verifies that a NetworkObject component that is positioned after a NetworkBehaviour component will
/// be migrated to a component index value that is before the lowest NetworkBehaviour component index value.
/// (The lowest NetworkBehaviour component's index value will also change when this happens)
/// </summary>
[Test]
public void NetworkObjectComponentOrder()
{
var gameObject = new GameObject(nameof(GetBehaviourIndexOne));
// Add the Networkbehaviour first
var networkBehaviour = gameObject.AddComponent<EmptyNetworkBehaviour>();
// Add an empty MonoBehaviour inbetween the NetworkBehaviour and NetworkObject
gameObject.AddComponent<EmptyMonoBehaviour>();
// Add the NetworkObject
var networkObject = gameObject.AddComponent<NetworkObject>();
var componentIndices = GetIndices(gameObject);
// Verify the NetworkObject procedes the NetworkBehaviour
Assert.True(componentIndices.NetworkObjectIndex > componentIndices.NetworkBehaviourIndex, $"[Initial Setup] NetworkObject index ({componentIndices.NetworkObjectIndex}) is not greater than the NetworkBehaviour index ({componentIndices.NetworkBehaviourIndex})!");
// Force-Invoke the CheckForNetworkObject method in order to verify the NetworkObject is moved
NetworkBehaviourEditor.CheckForNetworkObject(gameObject);
var adjustedIndices = GetIndices(gameObject);
Assert.True(ValidateComponentIndices(componentIndices, GetIndices(gameObject)), "NetworkObject did not get migrated below the NetworkBehaviour!");
// Cleanup
Object.DestroyImmediate(gameObject);
}
private bool ValidateComponentIndices(ComponentIndices previous, ComponentIndices current)
{
if (previous.NetworkObjectIndex != current.NetworkObjectIndex && previous.NetworkBehaviourIndex != current.NetworkBehaviourIndex)
{
if (current.NetworkObjectIndex < previous.NetworkObjectIndex && current.NetworkObjectIndex < current.NetworkBehaviourIndex)
{
return true;
}
}
return false;
}
private ComponentIndices GetIndices(GameObject gameObject)
{
// Get the index/order values for the added NetworkBehaviour and NetworkObject
var components = gameObject.GetComponents<MonoBehaviour>();
var componentIndices = new ComponentIndices()
{
NetworkObjectIndex = -1,
NetworkBehaviourIndex = -1
};
for (int i = 0; i < components.Length; i++)
{
if (componentIndices.NetworkObjectIndex != -1 && componentIndices.NetworkBehaviourIndex != -1)
{
break;
}
var component = components[i];
var networkObjectComponent = component as NetworkObject;
if (networkObjectComponent != null)
{
componentIndices.NetworkObjectIndex = i;
continue;
}
var networkBehaviourComponent = component as EmptyNetworkBehaviour;
if (networkBehaviourComponent != null)
{
componentIndices.NetworkBehaviourIndex = i;
continue;
}
}
return componentIndices;
}
private struct ComponentIndices
{
public int NetworkObjectIndex;
public int NetworkBehaviourIndex;
}
public class EmptyNetworkBehaviour : NetworkBehaviour
{
}
public class EmptyMonoBehaviour : MonoBehaviour
{
}
}
}

View File

@@ -293,6 +293,23 @@ namespace Unity.Netcode.EditorTests
AssertIsTestMessage(data);
}
[Test]
public void BatchedSendQueue_FillWriterWithBytes_MaxBytesGreaterThanCapacity()
{
var dataLength = k_TestMessageSize + BatchedSendQueue.PerMessageOverhead;
using var q = new BatchedSendQueue(k_TestQueueCapacity);
using var data = new NativeArray<byte>(dataLength, Allocator.Temp);
q.PushMessage(m_TestMessage);
q.PushMessage(m_TestMessage);
var writer = new DataStreamWriter(data);
Assert.AreEqual(dataLength, q.FillWriterWithBytes(ref writer, dataLength * 2));
AssertIsTestMessage(data);
Assert.False(writer.HasFailedWrites);
}
[Test]
public void BatchedSendQueue_Consume_LessThanLength()
{

View File

@@ -121,9 +121,7 @@ namespace Unity.Netcode.EditorTests
LogAssert.Expect(LogType.Error, "Invalid network endpoint: 127.0.0.:4242.");
LogAssert.Expect(LogType.Error, "Network listen address (127.0.0.) is Invalid!");
#if UTP_TRANSPORT_2_0_ABOVE
LogAssert.Expect(LogType.Error, "Socket creation failed (error Unity.Baselib.LowLevel.Binding+Baselib_ErrorState: Invalid argument (0x01000003) <argument name stripped>");
#endif
transport.SetConnectionData("127.0.0.1", 4242, "127.0.0.1");
Assert.True(transport.StartServer());

View File

@@ -41,7 +41,7 @@ namespace Unity.Netcode.RuntimeTests.Metrics
}
Assert.True(observer.Found);
Assert.AreEqual(FastBufferWriter.GetWriteSize(messageName) + k_MessageOverhead, observer.Value);
Assert.AreEqual(((FastBufferWriter.GetWriteSize(messageName) + k_MessageOverhead) + 7) & ~7, observer.Value);
}
[UnityTest]
@@ -61,8 +61,6 @@ namespace Unity.Netcode.RuntimeTests.Metrics
writer.Dispose();
}
var nbFrames = 0;
while (!observer.Found || nbFrames < 10)
{
@@ -71,7 +69,7 @@ namespace Unity.Netcode.RuntimeTests.Metrics
}
Assert.True(observer.Found);
Assert.AreEqual(FastBufferWriter.GetWriteSize(messageName) + k_MessageOverhead, observer.Value);
Assert.AreEqual(((FastBufferWriter.GetWriteSize(messageName) + k_MessageOverhead) + 7) & ~7, observer.Value);
}
private class TotalBytesObserver : IMetricObserver
@@ -89,12 +87,22 @@ namespace Unity.Netcode.RuntimeTests.Metrics
public long Value { get; private set; }
private int m_BytesFoundCounter;
private long m_TotalBytes;
public void Observe(MetricCollection collection)
{
if (collection.TryGetCounter(m_MetricInfo.Id, out var counter) && counter.Value > 0)
{
// Don't assign another observed value once one is already observed
if (!Found)
{
Found = true;
Value = counter.Value;
m_TotalBytes += ((counter.Value + 7) & ~7);
m_BytesFoundCounter++;
UnityEngine.Debug.Log($"[{m_BytesFoundCounter}] Bytes Observed {counter.Value} | Total Bytes Observed: {m_TotalBytes}");
}
}
}
}

View File

@@ -67,7 +67,6 @@ namespace Unity.Netcode.RuntimeTests
// Set the child object to be inactive in the hierarchy
childObject.SetActive(false);
LogAssert.Expect(LogType.Warning, $"{childObject.name} is disabled! Netcode for GameObjects does not support disabled NetworkBehaviours! The {childBehaviour.GetType().Name} component was skipped during ownership assignment!");
LogAssert.Expect(LogType.Warning, $"{childObject.name} is disabled! Netcode for GameObjects does not support spawning disabled NetworkBehaviours! The {childBehaviour.GetType().Name} component was skipped during spawn!");
parentNetworkObject.Spawn();

View File

@@ -61,11 +61,68 @@ namespace Unity.Netcode.RuntimeTests
return true;
}
[UnityTest]
public IEnumerator ObserverSpawnTests([Values] ObserverTestTypes observerTestTypes)
/// <summary>
/// Assures the <see cref="ObserverSpawnTests"/> late joining client has all
/// NetworkPrefabs required to connect.
/// </summary>
protected override void OnNewClientCreated(NetworkManager networkManager)
{
foreach (var networkPrefab in m_ServerNetworkManager.NetworkConfig.Prefabs.Prefabs)
{
if (!networkManager.NetworkConfig.Prefabs.Contains(networkPrefab.Prefab))
{
networkManager.NetworkConfig.Prefabs.Add(networkPrefab);
}
}
networkManager.NetworkConfig.EnableSceneManagement = m_ServerNetworkManager.NetworkConfig.EnableSceneManagement;
base.OnNewClientCreated(networkManager);
}
/// <summary>
/// This test validates <see cref="NetworkObject.SpawnWithObservers"/> property
/// </summary>
/// <param name="observerTestTypes">whether to spawn with or without observers</param>
[UnityTest]
public IEnumerator ObserverSpawnTests([Values] ObserverTestTypes observerTestTypes, [Values] bool sceneManagement)
{
if (!sceneManagement)
{
// Disable prefabs to prevent them from being destroyed
foreach (var networkPrefab in m_ServerNetworkManager.NetworkConfig.Prefabs.Prefabs)
{
networkPrefab.Prefab.SetActive(false);
}
// Shutdown and clean up the current client NetworkManager instances
foreach (var networkManager in m_ClientNetworkManagers)
{
m_PlayerNetworkObjects[networkManager.LocalClientId].Clear();
m_PlayerNetworkObjects.Remove(networkManager.LocalClientId);
yield return StopOneClient(networkManager, true);
}
// Shutdown and clean up the server NetworkManager instance
m_PlayerNetworkObjects[m_ServerNetworkManager.LocalClientId].Clear();
yield return StopOneClient(m_ServerNetworkManager);
// Set the prefabs to active again
foreach (var networkPrefab in m_ServerNetworkManager.NetworkConfig.Prefabs.Prefabs)
{
networkPrefab.Prefab.SetActive(true);
}
// Disable scene management and start the host
m_ServerNetworkManager.NetworkConfig.EnableSceneManagement = false;
m_ServerNetworkManager.StartHost();
yield return s_DefaultWaitForTick;
// Create 2 new clients and connect them
for (int i = 0; i < NumberOfClients; i++)
{
yield return CreateAndStartNewClient();
}
}
m_ObserverTestType = observerTestTypes;
var prefabNetworkObject = m_ObserverPrefab.GetComponent<NetworkObject>();
prefabNetworkObject.SpawnWithObservers = observerTestTypes == ObserverTestTypes.WithObservers;
@@ -92,8 +149,26 @@ namespace Unity.Netcode.RuntimeTests
m_ObserverTestType = ObserverTestTypes.WithObservers;
yield return WaitForConditionOrTimeOut(CheckClientsSideObserverTestObj);
AssertOnTimeout($"{k_WithObserversError} {k_ObserverTestObjName} object!");
// Validate that a late joining client does not see the NetworkObject when it spawns
yield return CreateAndStartNewClient();
m_ObserverTestType = ObserverTestTypes.WithoutObservers;
// Just give a little time to make sure nothing spawned
yield return s_DefaultWaitForTick;
yield return WaitForConditionOrTimeOut(CheckClientsSideObserverTestObj);
AssertOnTimeout($"{(withoutObservers ? k_WithoutObserversError : k_WithObserversError)} {k_ObserverTestObjName} object!");
// Now validate that we can make the NetworkObject visible to the newly joined client
m_ObserverTestNetworkObject.NetworkShow(m_ClientNetworkManagers[NumberOfClients].LocalClientId);
// Validate the NetworkObject is visible to all connected clients (including the recently joined client)
m_ObserverTestType = ObserverTestTypes.WithObservers;
yield return WaitForConditionOrTimeOut(CheckClientsSideObserverTestObj);
AssertOnTimeout($"{k_WithObserversError} {k_ObserverTestObjName} object!");
}
}
/// <summary>
/// Tests that instantiating a <see cref="NetworkObject"/> and destroying without spawning it
/// does not run <see cref="NetworkBehaviour.OnNetworkSpawn"/> or <see cref="NetworkBehaviour.OnNetworkSpawn"/>.

View File

@@ -42,6 +42,12 @@ namespace Unity.Netcode.RuntimeTests
public NetworkObjectOwnershipTests(HostOrServer hostOrServer) : base(hostOrServer) { }
public enum OwnershipChecks
{
Change,
Remove
}
protected override void OnServerAndClientsCreated()
{
m_OwnershipPrefab = CreateNetworkObjectPrefab("OnwershipPrefab");
@@ -62,7 +68,7 @@ namespace Unity.Netcode.RuntimeTests
}
[UnityTest]
public IEnumerator TestOwnershipCallbacks()
public IEnumerator TestOwnershipCallbacks([Values] OwnershipChecks ownershipChecks)
{
m_OwnershipObject = SpawnObject(m_OwnershipPrefab, m_ServerNetworkManager);
m_OwnershipNetworkObject = m_OwnershipObject.GetComponent<NetworkObject>();
@@ -109,7 +115,17 @@ namespace Unity.Netcode.RuntimeTests
serverComponent.ResetFlags();
clientComponent.ResetFlags();
if (ownershipChecks == OwnershipChecks.Change)
{
// Validates that when ownership is changed back to the server it will get an OnGainedOwnership notification
serverObject.ChangeOwnership(NetworkManager.ServerClientId);
}
else
{
// Validates that when ownership is removed the server gets an OnGainedOwnership notification
serverObject.RemoveOwnership();
}
yield return s_DefaultWaitForTick;
Assert.That(serverComponent.OnGainedOwnershipFired);
@@ -125,7 +141,7 @@ namespace Unity.Netcode.RuntimeTests
/// Verifies that switching ownership between several clients works properly
/// </summary>
[UnityTest]
public IEnumerator TestOwnershipCallbacksSeveralClients()
public IEnumerator TestOwnershipCallbacksSeveralClients([Values] OwnershipChecks ownershipChecks)
{
// Build our message hook entries tables so we can determine if all clients received spawn or ownership messages
var messageHookEntriesForSpawn = new List<MessageHookEntry>();
@@ -247,8 +263,17 @@ namespace Unity.Netcode.RuntimeTests
previousClientComponent = currentClientComponent;
}
// Now change ownership back to the server
if (ownershipChecks == OwnershipChecks.Change)
{
// Validates that when ownership is changed back to the server it will get an OnGainedOwnership notification
serverObject.ChangeOwnership(NetworkManager.ServerClientId);
}
else
{
// Validates that when ownership is removed the server gets an OnGainedOwnership notification
serverObject.RemoveOwnership();
}
yield return WaitForConditionOrTimeOut(ownershipMessageHooks);
Assert.False(s_GlobalTimeoutHelper.TimedOut, $"Timed out waiting for all clients to receive the {nameof(ChangeOwnershipMessage)} message (back to server).");
@@ -269,5 +294,69 @@ namespace Unity.Netcode.RuntimeTests
}
serverComponent.ResetFlags();
}
private const int k_NumberOfSpawnedObjects = 5;
private bool AllClientsHaveCorrectObjectCount()
{
foreach (var clientNetworkManager in m_ClientNetworkManagers)
{
if (clientNetworkManager.LocalClient.OwnedObjects.Count < k_NumberOfSpawnedObjects)
{
return false;
}
}
return true;
}
private bool ServerHasCorrectClientOwnedObjectCount()
{
// Only check when we are the host
if (m_ServerNetworkManager.IsHost)
{
if (m_ServerNetworkManager.LocalClient.OwnedObjects.Count < k_NumberOfSpawnedObjects)
{
return false;
}
}
foreach (var connectedClient in m_ServerNetworkManager.ConnectedClients)
{
if (connectedClient.Value.OwnedObjects.Count < k_NumberOfSpawnedObjects)
{
return false;
}
}
return true;
}
[UnityTest]
public IEnumerator TestOwnedObjectCounts()
{
if (m_ServerNetworkManager.IsHost)
{
for (int i = 0; i < 5; i++)
{
SpawnObject(m_OwnershipPrefab, m_ServerNetworkManager);
}
}
foreach (var clientNetworkManager in m_ClientNetworkManagers)
{
for (int i = 0; i < 5; i++)
{
SpawnObject(m_OwnershipPrefab, clientNetworkManager);
}
}
yield return WaitForConditionOrTimeOut(AllClientsHaveCorrectObjectCount);
AssertOnTimeout($"Not all clients spawned {k_NumberOfSpawnedObjects} {nameof(NetworkObject)}s!");
yield return WaitForConditionOrTimeOut(ServerHasCorrectClientOwnedObjectCount);
AssertOnTimeout($"Server does not have the correct count for all clients spawned {k_NumberOfSpawnedObjects} {nameof(NetworkObject)}s!");
}
}
}

View File

@@ -46,6 +46,93 @@ namespace Unity.Netcode.RuntimeTests
base.OnServerAndClientsCreated();
}
/// <summary>
/// Clients created during a test need to have their prefabs list updated to
/// match the server's prefab list.
/// </summary>
protected override void OnNewClientCreated(NetworkManager networkManager)
{
foreach (var networkPrefab in m_ServerNetworkManager.NetworkConfig.Prefabs.Prefabs)
{
networkManager.NetworkConfig.Prefabs.Add(networkPrefab);
}
base.OnNewClientCreated(networkManager);
}
private bool ClientIsOwner()
{
var clientId = m_ClientNetworkManagers[0].LocalClientId;
if (!VerifyObjectIsSpawnedOnClient.GetClientsThatSpawnedThisPrefab().Contains(clientId))
{
return false;
}
if (VerifyObjectIsSpawnedOnClient.GetClientInstance(clientId).OwnerClientId != clientId)
{
return false;
}
return true;
}
/// <summary>
/// This test verifies a late joining client cannot change the transform when:
/// - A NetworkObject is spawned with a host and one or more connected clients
/// - The NetworkTransform is owner authoritative and spawned with the host as the owner
/// - The host does not change the transform values
/// - One of the already connected clients gains ownership of the spawned NetworkObject
/// - The new client owner does not change the transform values
/// - A new late joining client connects and is synchronized
/// - The newly connected late joining client tries to change the transform of the NetworkObject
/// it does not own
/// </summary>
[UnityTest]
public IEnumerator LateJoinedNonOwnerClientCannotChangeTransform()
{
// Spawn the m_ClientNetworkTransformPrefab with the host starting as the owner
var hostInstance = SpawnObject(m_ClientNetworkTransformPrefab, m_ServerNetworkManager);
// Wait for the client to spawn it
yield return WaitForConditionOrTimeOut(() => VerifyObjectIsSpawnedOnClient.GetClientsThatSpawnedThisPrefab().Contains(m_ClientNetworkManagers[0].LocalClientId));
// Change the ownership to the connectd client
hostInstance.GetComponent<NetworkObject>().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId);
// Wait until the client gains ownership
yield return WaitForConditionOrTimeOut(ClientIsOwner);
// Spawn a new client
yield return CreateAndStartNewClient();
// Get the instance of the object relative to the newly joined client
var newClientObjectInstance = VerifyObjectIsSpawnedOnClient.GetClientInstance(m_ClientNetworkManagers[1].LocalClientId);
// Attempt to change the transform values
var currentPosition = newClientObjectInstance.transform.position;
newClientObjectInstance.transform.position = GetRandomVector3(0.5f, 10.0f);
var rotation = newClientObjectInstance.transform.rotation;
var currentRotation = rotation.eulerAngles;
rotation.eulerAngles = GetRandomVector3(1.0f, 180.0f);
var currentScale = newClientObjectInstance.transform.localScale;
newClientObjectInstance.transform.localScale = GetRandomVector3(0.25f, 4.0f);
// Wait one frame so the NetworkTransform can apply the owner's last state received on the late joining client side
// (i.e. prevent the non-owner from changing the transform)
yield return null;
// Get the owner instance
var ownerInstance = VerifyObjectIsSpawnedOnClient.GetClientInstance(m_ClientNetworkManagers[0].LocalClientId);
// Verify that the non-owner instance transform values are the same before they were changed last frame
Assert.True(Approximately(currentPosition, newClientObjectInstance.transform.position), $"Non-owner instance was able to change the position!");
Assert.True(Approximately(currentRotation, newClientObjectInstance.transform.rotation.eulerAngles), $"Non-owner instance was able to change the rotation!");
Assert.True(Approximately(currentScale, newClientObjectInstance.transform.localScale), $"Non-owner instance was able to change the scale!");
// Verify that the non-owner instance transform is still the same as the owner instance transform
Assert.True(Approximately(ownerInstance.transform.position, newClientObjectInstance.transform.position), "Non-owner and owner instance position values are not the same!");
Assert.True(Approximately(ownerInstance.transform.rotation.eulerAngles, newClientObjectInstance.transform.rotation.eulerAngles), "Non-owner and owner instance rotation values are not the same!");
Assert.True(Approximately(ownerInstance.transform.localScale, newClientObjectInstance.transform.localScale), "Non-owner and owner instance scale values are not the same!");
}
public enum StartingOwnership
{
HostStartsAsOwner,

View File

@@ -2,6 +2,7 @@ using NUnit.Framework;
using Unity.Netcode.Components;
using UnityEngine;
namespace Unity.Netcode.RuntimeTests
{
@@ -89,6 +90,125 @@ namespace Unity.Netcode.RuntimeTests
networkTransform.SyncPositionX || networkTransform.SyncPositionY || networkTransform.SyncPositionZ;
}
[Test]
public void NetworkTransformStateFlags()
{
var indexValues = new System.Collections.Generic.List<uint>();
var currentFlag = (uint)0x00000001;
for (int j = 0; j < 18; j++)
{
indexValues.Add(currentFlag);
currentFlag = currentFlag << 1;
}
// TrackByStateId is unique
indexValues.Add(0x10000000);
var boolSet = new System.Collections.Generic.List<bool>();
var transformState = new NetworkTransform.NetworkTransformState();
// Test setting one at a time.
for (int j = 0; j < 19; j++)
{
boolSet = new System.Collections.Generic.List<bool>();
for (int i = 0; i < 19; i++)
{
if (i == j)
{
boolSet.Add(true);
}
else
{
boolSet.Add(false);
}
}
transformState = new NetworkTransform.NetworkTransformState()
{
InLocalSpace = boolSet[0],
HasPositionX = boolSet[1],
HasPositionY = boolSet[2],
HasPositionZ = boolSet[3],
HasRotAngleX = boolSet[4],
HasRotAngleY = boolSet[5],
HasRotAngleZ = boolSet[6],
HasScaleX = boolSet[7],
HasScaleY = boolSet[8],
HasScaleZ = boolSet[9],
IsTeleportingNextFrame = boolSet[10],
UseInterpolation = boolSet[11],
QuaternionSync = boolSet[12],
QuaternionCompression = boolSet[13],
UseHalfFloatPrecision = boolSet[14],
IsSynchronizing = boolSet[15],
UsePositionSlerp = boolSet[16],
IsParented = boolSet[17],
TrackByStateId = boolSet[18],
};
Assert.True((transformState.BitSet & indexValues[j]) == indexValues[j], $"[FlagTest][Individual] Set flag value {indexValues[j]} at index {j}, but BitSet value did not match!");
}
// Test setting all flag values
boolSet = new System.Collections.Generic.List<bool>();
for (int i = 0; i < 19; i++)
{
boolSet.Add(true);
}
transformState = new NetworkTransform.NetworkTransformState()
{
InLocalSpace = boolSet[0],
HasPositionX = boolSet[1],
HasPositionY = boolSet[2],
HasPositionZ = boolSet[3],
HasRotAngleX = boolSet[4],
HasRotAngleY = boolSet[5],
HasRotAngleZ = boolSet[6],
HasScaleX = boolSet[7],
HasScaleY = boolSet[8],
HasScaleZ = boolSet[9],
IsTeleportingNextFrame = boolSet[10],
UseInterpolation = boolSet[11],
QuaternionSync = boolSet[12],
QuaternionCompression = boolSet[13],
UseHalfFloatPrecision = boolSet[14],
IsSynchronizing = boolSet[15],
UsePositionSlerp = boolSet[16],
IsParented = boolSet[17],
TrackByStateId = boolSet[18],
};
for (int j = 0; j < 19; j++)
{
Assert.True((transformState.BitSet & indexValues[j]) == indexValues[j], $"[FlagTest][All] All flag values are set but failed to detect flag value {indexValues[j]}!");
}
// Test getting all flag values
transformState = new NetworkTransform.NetworkTransformState();
for (int i = 0; i < 19; i++)
{
transformState.BitSet |= indexValues[i];
}
Assert.True(transformState.InLocalSpace, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.InLocalSpace)}!");
Assert.True(transformState.HasPositionX, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.HasPositionX)}!");
Assert.True(transformState.HasPositionY, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.HasPositionY)}!");
Assert.True(transformState.HasPositionZ, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.HasPositionZ)}!");
Assert.True(transformState.HasRotAngleX, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.HasRotAngleX)}!");
Assert.True(transformState.HasRotAngleY, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.HasRotAngleY)}!");
Assert.True(transformState.HasRotAngleZ, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.HasRotAngleZ)}!");
Assert.True(transformState.HasScaleX, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.HasScaleX)}!");
Assert.True(transformState.HasScaleY, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.HasScaleY)}!");
Assert.True(transformState.HasScaleZ, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.HasScaleZ)}!");
Assert.True(transformState.IsTeleportingNextFrame, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.IsTeleportingNextFrame)}!");
Assert.True(transformState.UseInterpolation, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.UseInterpolation)}!");
Assert.True(transformState.QuaternionSync, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.QuaternionSync)}!");
Assert.True(transformState.QuaternionCompression, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.QuaternionCompression)}!");
Assert.True(transformState.UseHalfFloatPrecision, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.UseHalfFloatPrecision)}!");
Assert.True(transformState.IsSynchronizing, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.IsSynchronizing)}!");
Assert.True(transformState.UsePositionSlerp, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.UsePositionSlerp)}!");
Assert.True(transformState.IsParented, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.IsParented)}!");
Assert.True(transformState.TrackByStateId, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.TrackByStateId)}!");
}
[Test]
public void TestSyncAxes([Values] SynchronizationType synchronizationType, [Values] SyncAxis syncAxis)

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Text;
using NUnit.Framework;
using Unity.Netcode.Components;
using Unity.Netcode.TestHelpers.Runtime;
@@ -63,24 +64,50 @@ namespace Unity.Netcode.RuntimeTests
}
}
/// <summary>
/// Helper component for NetworkTransform parenting tests when
/// a child is a parent of another child (i.e. "sub child")
/// </summary>
public class SubChildObjectComponent : ChildObjectComponent
{
protected override bool IsSubChild()
{
return true;
}
}
/// <summary>
/// Helper component for NetworkTransform parenting tests
/// </summary>
public class ChildObjectComponent : NetworkTransform
{
public static readonly List<ChildObjectComponent> Instances = new List<ChildObjectComponent>();
public static readonly List<ChildObjectComponent> SubInstances = new List<ChildObjectComponent>();
public static ChildObjectComponent AuthorityInstance { get; internal set; }
public static ChildObjectComponent AuthoritySubInstance { get; internal set; }
public static readonly Dictionary<ulong, NetworkObject> ClientInstances = new Dictionary<ulong, NetworkObject>();
public static readonly Dictionary<ulong, NetworkObject> ClientSubChildInstances = new Dictionary<ulong, NetworkObject>();
public static bool HasSubChild;
public static void Reset()
{
AuthorityInstance = null;
AuthoritySubInstance = null;
HasSubChild = false;
ClientInstances.Clear();
ClientSubChildInstances.Clear();
Instances.Clear();
SubInstances.Clear();
}
public bool ServerAuthority;
protected virtual bool IsSubChild()
{
return false;
}
protected override bool OnIsServerAuthoritative()
{
return ServerAuthority;
@@ -90,16 +117,37 @@ namespace Unity.Netcode.RuntimeTests
{
base.OnNetworkSpawn();
if (CanCommitToTransform)
{
if (!IsSubChild())
{
AuthorityInstance = this;
}
else
{
AuthoritySubInstance = this;
}
}
else
{
if (!IsSubChild())
{
Instances.Add(this);
}
else
{
SubInstances.Add(this);
}
}
if (HasSubChild && IsSubChild())
{
ClientSubChildInstances.Add(NetworkManager.LocalClientId, NetworkObject);
}
else
{
ClientInstances.Add(NetworkManager.LocalClientId, NetworkObject);
}
}
}
/// <summary>
/// Integration tests for NetworkTransform that will test both
@@ -116,6 +164,7 @@ namespace Unity.Netcode.RuntimeTests
private NetworkObject m_AuthoritativePlayer;
private NetworkObject m_NonAuthoritativePlayer;
private NetworkObject m_ChildObject;
private NetworkObject m_SubChildObject;
private NetworkObject m_ParentObject;
private NetworkTransformTestComponent m_AuthoritativeTransform;
@@ -231,6 +280,11 @@ namespace Unity.Netcode.RuntimeTests
protected override void OnServerAndClientsCreated()
{
var subChildObject = CreateNetworkObjectPrefab("SubChildObject");
var subChildNetworkTransform = subChildObject.AddComponent<SubChildObjectComponent>();
subChildNetworkTransform.ServerAuthority = m_Authority == Authority.ServerAuthority;
m_SubChildObject = subChildObject.GetComponent<NetworkObject>();
var childObject = CreateNetworkObjectPrefab("ChildObject");
var childNetworkTransform = childObject.AddComponent<ChildObjectComponent>();
childNetworkTransform.ServerAuthority = m_Authority == Authority.ServerAuthority;
@@ -241,13 +295,19 @@ namespace Unity.Netcode.RuntimeTests
parentNetworkTransform.ServerAuthority = m_Authority == Authority.ServerAuthority;
m_ParentObject = parentObject.GetComponent<NetworkObject>();
// Now apply local transform values
m_ChildObject.transform.position = m_ChildObjectLocalPosition;
var childRotation = m_ChildObject.transform.rotation;
childRotation.eulerAngles = m_ChildObjectLocalRotation;
m_ChildObject.transform.rotation = childRotation;
m_ChildObject.transform.localScale = m_ChildObjectLocalScale;
m_SubChildObject.transform.position = m_SubChildObjectLocalPosition;
var subChildRotation = m_SubChildObject.transform.rotation;
subChildRotation.eulerAngles = m_SubChildObjectLocalRotation;
m_SubChildObject.transform.rotation = childRotation;
m_SubChildObject.transform.localScale = m_SubChildObjectLocalScale;
if (m_EnableVerboseDebug)
{
m_ServerNetworkManager.LogLevel = LogLevel.Developer;
@@ -301,6 +361,11 @@ namespace Unity.Netcode.RuntimeTests
return false;
}
if (ChildObjectComponent.HasSubChild && ChildObjectComponent.AuthoritySubInstance == null)
{
return false;
}
foreach (var clientNetworkManager in m_ClientNetworkManagers)
{
if (!ChildObjectComponent.ClientInstances.ContainsKey(clientNetworkManager.LocalClientId))
@@ -320,6 +385,16 @@ namespace Unity.Netcode.RuntimeTests
return false;
}
}
if (ChildObjectComponent.HasSubChild)
{
foreach (var instance in ChildObjectComponent.ClientSubChildInstances.Values)
{
if (instance.transform.parent == null)
{
return false;
}
}
}
return true;
}
@@ -327,6 +402,10 @@ namespace Unity.Netcode.RuntimeTests
private Vector3 m_ChildObjectLocalPosition = new Vector3(5.0f, 0.0f, -5.0f);
private Vector3 m_ChildObjectLocalRotation = new Vector3(-35.0f, 90.0f, 270.0f);
private Vector3 m_ChildObjectLocalScale = new Vector3(0.1f, 0.5f, 0.4f);
private Vector3 m_SubChildObjectLocalPosition = new Vector3(2.0f, 1.0f, -1.0f);
private Vector3 m_SubChildObjectLocalRotation = new Vector3(5.0f, 15.0f, 124.0f);
private Vector3 m_SubChildObjectLocalScale = new Vector3(1.0f, 0.15f, 0.75f);
/// <summary>
/// A wait condition specific method that assures the local space coordinates
@@ -374,17 +453,17 @@ namespace Unity.Netcode.RuntimeTests
/// If not, it generates a message containing the axial values that did not match
/// the target/start local space values.
/// </summary>
private void AllChildrenLocalTransformValuesMatch()
private void AllChildrenLocalTransformValuesMatch(bool useSubChild)
{
var success = WaitForConditionOrTimeOutWithTimeTravel(AllInstancesKeptLocalTransformValues);
//TimeTravelToNextTick();
var infoMessage = new System.Text.StringBuilder($"Timed out waiting for all children to have the correct local space values:\n");
var authorityObjectLocalPosition = m_AuthorityChildObject.transform.localPosition;
var authorityObjectLocalRotation = m_AuthorityChildObject.transform.localRotation.eulerAngles;
var authorityObjectLocalScale = m_AuthorityChildObject.transform.localScale;
var infoMessage = new StringBuilder($"Timed out waiting for all children to have the correct local space values:\n");
var authorityObjectLocalPosition = useSubChild ? m_AuthoritySubChildObject.transform.localPosition : m_AuthorityChildObject.transform.localPosition;
var authorityObjectLocalRotation = useSubChild ? m_AuthoritySubChildObject.transform.localRotation.eulerAngles : m_AuthorityChildObject.transform.localRotation.eulerAngles;
var authorityObjectLocalScale = useSubChild ? m_AuthoritySubChildObject.transform.localScale : m_AuthorityChildObject.transform.localScale;
if (s_GlobalTimeoutHelper.TimedOut || !success)
{
var instances = useSubChild ? ChildObjectComponent.SubInstances : ChildObjectComponent.Instances;
foreach (var childInstance in ChildObjectComponent.Instances)
{
var childLocalPosition = childInstance.transform.localPosition;
@@ -427,8 +506,11 @@ namespace Unity.Netcode.RuntimeTests
private NetworkObject m_AuthorityParentObject;
private NetworkTransformTestComponent m_AuthorityParentNetworkTransform;
private NetworkObject m_AuthorityChildObject;
private NetworkObject m_AuthoritySubChildObject;
private ChildObjectComponent m_AuthorityChildNetworkTransform;
private ChildObjectComponent m_AuthoritySubChildNetworkTransform;
/// <summary>
/// Validates that transform values remain the same when a NetworkTransform is
/// parented under another NetworkTransform under all of the possible axial conditions
@@ -450,9 +532,11 @@ namespace Unity.Netcode.RuntimeTests
authorityNetworkManager = m_ClientNetworkManagers[0];
}
// Spawn a parent and child object
// Spawn a parent and children
ChildObjectComponent.HasSubChild = true;
var serverSideParent = SpawnObject(m_ParentObject.gameObject, authorityNetworkManager).GetComponent<NetworkObject>();
var serverSideChild = SpawnObject(m_ChildObject.gameObject, authorityNetworkManager).GetComponent<NetworkObject>();
var serverSideSubChild = SpawnObject(m_SubChildObject.gameObject, authorityNetworkManager).GetComponent<NetworkObject>();
// Assure all of the child object instances are spawned before proceeding to parenting
var success = WaitForConditionOrTimeOutWithTimeTravel(AllChildObjectInstancesAreSpawned);
@@ -461,6 +545,7 @@ namespace Unity.Netcode.RuntimeTests
// Get the authority parent and child instances
m_AuthorityParentObject = NetworkTransformTestComponent.AuthorityInstance.NetworkObject;
m_AuthorityChildObject = ChildObjectComponent.AuthorityInstance.NetworkObject;
m_AuthoritySubChildObject = ChildObjectComponent.AuthoritySubInstance.NetworkObject;
// The child NetworkTransform will use world space when world position stays and
// local space when world position does not stay when parenting.
@@ -469,15 +554,26 @@ namespace Unity.Netcode.RuntimeTests
ChildObjectComponent.AuthorityInstance.UseQuaternionSynchronization = rotation == Rotation.Quaternion;
ChildObjectComponent.AuthorityInstance.UseQuaternionCompression = rotationCompression == RotationCompression.QuaternionCompress;
ChildObjectComponent.AuthoritySubInstance.InLocalSpace = !worldPositionStays;
ChildObjectComponent.AuthoritySubInstance.UseHalfFloatPrecision = precision == Precision.Half;
ChildObjectComponent.AuthoritySubInstance.UseQuaternionSynchronization = rotation == Rotation.Quaternion;
ChildObjectComponent.AuthoritySubInstance.UseQuaternionCompression = rotationCompression == RotationCompression.QuaternionCompress;
// Set whether we are interpolating or not
m_AuthorityParentNetworkTransform = m_AuthorityParentObject.GetComponent<NetworkTransformTestComponent>();
m_AuthorityParentNetworkTransform.Interpolate = interpolation == Interpolation.EnableInterpolate;
m_AuthorityChildNetworkTransform = m_AuthorityChildObject.GetComponent<ChildObjectComponent>();
m_AuthorityChildNetworkTransform.Interpolate = interpolation == Interpolation.EnableInterpolate;
m_AuthoritySubChildNetworkTransform = m_AuthoritySubChildObject.GetComponent<ChildObjectComponent>();
m_AuthoritySubChildNetworkTransform.Interpolate = interpolation == Interpolation.EnableInterpolate;
// Apply a scale to the parent object to make sure the scale on the child is properly updated on
// non-authority instances.
m_AuthorityParentObject.transform.localScale = new Vector3(scale, scale, scale);
var halfScale = scale * 0.5f;
m_AuthorityParentObject.transform.localScale = GetRandomVector3(scale - halfScale, scale + halfScale);
m_AuthorityChildObject.transform.localScale = GetRandomVector3(scale - halfScale, scale + halfScale);
m_AuthoritySubChildObject.transform.localScale = GetRandomVector3(scale - halfScale, scale + halfScale);
// Allow one tick for authority to update these changes
TimeTravelToNextTick();
@@ -485,12 +581,19 @@ namespace Unity.Netcode.RuntimeTests
// Parent the child under the parent with the current world position stays setting
Assert.True(serverSideChild.TrySetParent(serverSideParent.transform, worldPositionStays), "[Server-Side Child] Failed to set child's parent!");
// Parent the sub-child under the child with the current world position stays setting
Assert.True(serverSideSubChild.TrySetParent(serverSideChild.transform, worldPositionStays), "[Server-Side SubChild] Failed to set sub-child's parent!");
// This waits for all child instances to be parented
success = WaitForConditionOrTimeOutWithTimeTravel(AllChildObjectInstancesHaveChild);
Assert.True(success, "Timed out waiting for all instances to have parented a child!");
TimeTravelToNextTick();
// This validates each child instance has preserved their local space values
AllChildrenLocalTransformValuesMatch();
AllChildrenLocalTransformValuesMatch(false);
// This validates each sub-child instance has preserved their local space values
AllChildrenLocalTransformValuesMatch(true);
// Verify that a late joining client will synchronize to the parented NetworkObjects properly
CreateAndStartNewClientWithTimeTravel();
@@ -499,8 +602,15 @@ namespace Unity.Netcode.RuntimeTests
success = WaitForConditionOrTimeOutWithTimeTravel(AllChildObjectInstancesAreSpawned);
Assert.True(success, "Timed out waiting for all child instances to be spawned!");
// Assure the newly connected client's child object's transform values are correct
AllChildrenLocalTransformValuesMatch();
// This waits for all child instances to be parented
success = WaitForConditionOrTimeOutWithTimeTravel(AllChildObjectInstancesHaveChild);
Assert.True(success, "Timed out waiting for all instances to have parented a child!");
// This validates each child instance has preserved their local space values
AllChildrenLocalTransformValuesMatch(false);
// This validates each sub-child instance has preserved their local space values
AllChildrenLocalTransformValuesMatch(true);
}
/// <summary>
@@ -568,8 +678,8 @@ namespace Unity.Netcode.RuntimeTests
}
// The number of iterations to change position, rotation, and scale for NetworkTransformMultipleChangesOverTime
// Note: this was reduced from 8 iterations to 3 due to the number of tests based on all of the various parameter combinations
private const int k_PositionRotationScaleIterations = 3;
private const int k_PositionRotationScaleIterations3Axis = 8;
protected override void OnNewClientCreated(NetworkManager networkManager)
{
@@ -580,7 +690,7 @@ namespace Unity.Netcode.RuntimeTests
private Precision m_Precision = Precision.Full;
private float m_CurrentHalfPrecision = 0.0f;
private const float k_HalfPrecisionPosScale = 0.03f;
private const float k_HalfPrecisionPosScale = 0.041f;
private const float k_HalfPrecisionRot = 0.725f;
protected override float GetDeltaVarianceThreshold()
@@ -594,22 +704,69 @@ namespace Unity.Netcode.RuntimeTests
private Axis m_CurrentAxis;
private bool m_AxisExcluded;
/// <summary>
/// Randomly determine if an axis should be excluded.
/// If so, then randomly pick one of the axis to be excluded.
/// </summary>
private Vector3 RandomlyExcludeAxis(Vector3 delta)
{
if (Random.Range(0.0f, 1.0f) >= 0.5f)
{
m_AxisExcluded = true;
var axisToIgnore = Random.Range(0, 2);
switch (axisToIgnore)
{
case 0:
{
delta.x = 0;
break;
}
case 1:
{
delta.y = 0;
break;
}
case 2:
{
delta.z = 0;
break;
}
}
}
return delta;
}
/// <summary>
/// This validates that multiple changes can occur within the same tick or over
/// several ticks while still keeping non-authoritative instances synchronized.
/// </summary>
/// <remarks>
/// When testing < 3 axis: Interpolation is disabled and only 3 delta updates are applied per unique test
/// When testing 3 axis: Interpolation is enabled, sometimes an axis is intentionally excluded during a
/// delta update, and it runs through 8 delta updates per unique test.
/// </remarks>
[Test]
public void NetworkTransformMultipleChangesOverTime([Values] TransformSpace testLocalTransform, [Values] OverrideState overideState,
[Values] Precision precision, [Values] Rotation rotationSynch, [Values] Axis axis)
{
// In the name of reducing the very long time it takes to interpolate and run all of the possible combinations,
// we only interpolate when the second client joins
m_AuthoritativeTransform.Interpolate = false;
m_AuthoritativeTransform.InLocalSpace = testLocalTransform == TransformSpace.Local;
bool axisX = axis == Axis.X || axis == Axis.XY || axis == Axis.XZ || axis == Axis.XYZ;
bool axisY = axis == Axis.Y || axis == Axis.XY || axis == Axis.YZ || axis == Axis.XYZ;
bool axisZ = axis == Axis.Z || axis == Axis.XZ || axis == Axis.YZ || axis == Axis.XYZ;
var axisCount = axisX ? 1 : 0;
axisCount += axisY ? 1 : 0;
axisCount += axisZ ? 1 : 0;
// Enable interpolation when all 3 axis are selected to make sure we are synchronizing properly
// when interpolation is enabled.
m_AuthoritativeTransform.Interpolate = axisCount == 3 ? true : false;
m_CurrentAxis = axis;
// Authority dictates what is synchronized and what the precision is going to be
// so we only need to set this on the authoritative side.
m_AuthoritativeTransform.UseHalfFloatPrecision = precision == Precision.Half;
@@ -640,29 +797,49 @@ namespace Unity.Netcode.RuntimeTests
m_AuthoritativeTransform.SyncScaleY = axisY;
m_AuthoritativeTransform.SyncScaleZ = axisZ;
var positionStart = GetRandomVector3(0.25f, 1.75f);
var rotationStart = GetRandomVector3(1f, 15f);
var scaleStart = GetRandomVector3(0.25f, 2.0f);
var position = positionStart;
var rotation = rotationStart;
var scale = scaleStart;
var success = false;
m_AuthoritativeTransform.StatePushed = false;
// Wait for the deltas to be pushed
WaitForConditionOrTimeOutWithTimeTravel(() => m_AuthoritativeTransform.StatePushed);
// Allow the precision settings to propagate first as changing precision
// causes a teleport event to occur
WaitForNextTick();
var iterations = axisCount == 3 ? k_PositionRotationScaleIterations3Axis : k_PositionRotationScaleIterations;
// Move and rotate within the same tick, validate the non-authoritative instance updates
// to each set of changes. Repeat several times.
for (int i = 0; i < k_PositionRotationScaleIterations; i++)
for (int i = 0; i < iterations; i++)
{
// Always reset this per delta update pass
m_AxisExcluded = false;
var deltaPositionDelta = GetRandomVector3(-1.5f, 1.5f);
var deltaRotationDelta = GetRandomVector3(-3.5f, 3.5f);
var deltaScaleDelta = GetRandomVector3(-0.5f, 0.5f);
m_NonAuthoritativeTransform.StateUpdated = false;
m_AuthoritativeTransform.StatePushed = false;
position = positionStart * i;
rotation = rotationStart * i;
scale = scaleStart * i;
// With two or more axis, excluding one of them while chaging another will validate that
// full precision updates are maintaining their target state value(s) to interpolate towards
if (axisCount == 3)
{
position += RandomlyExcludeAxis(deltaPositionDelta);
rotation += RandomlyExcludeAxis(deltaRotationDelta);
scale += RandomlyExcludeAxis(deltaScaleDelta);
}
else
{
position += deltaPositionDelta;
rotation += deltaRotationDelta;
scale += deltaScaleDelta;
}
// Apply delta between ticks
MoveRotateAndScaleAuthority(position, rotation, scale, overideState);
@@ -670,54 +847,37 @@ namespace Unity.Netcode.RuntimeTests
// Wait for the deltas to be pushed
Assert.True(WaitForConditionOrTimeOutWithTimeTravel(() => m_AuthoritativeTransform.StatePushed && m_NonAuthoritativeTransform.StateUpdated), $"[Non-Interpolate {i}] Timed out waiting for state to be pushed ({m_AuthoritativeTransform.StatePushed}) or state to be updated ({m_NonAuthoritativeTransform.StateUpdated})!");
// For 3 axis, we will skip validating that the non-authority interpolates to its target point at least once.
// This will validate that non-authoritative updates are maintaining their target state axis values if only 2
// of the axis are being updated to assure interpolation maintains the targeted axial value per axis.
// For 2 and 1 axis tests we always validate per delta update
if (m_AxisExcluded || axisCount < 3)
{
// Wait for deltas to synchronize on non-authoritative side
var success = WaitForConditionOrTimeOutWithTimeTravel(PositionRotationScaleMatches);
success = WaitForConditionOrTimeOutWithTimeTravel(PositionRotationScaleMatches);
// Provide additional debug info about what failed (if it fails)
if (!success)
{
m_EnableVerboseDebug = true;
PositionRotationScaleMatches();
success = PositionRotationScaleMatches();
m_EnableVerboseDebug = false;
}
Assert.True(success, $"[Non-Interpolate {i}] Timed out waiting for non-authority to match authority's position or rotation");
}
}
// Only enable interpolation when all axis are set (to reduce the test times)
if (axis == Axis.XYZ)
if (axisCount == 3)
{
// Now, enable interpolation
m_AuthoritativeTransform.Interpolate = true;
m_NonAuthoritativeTransform.StateUpdated = false;
m_AuthoritativeTransform.StatePushed = false;
// Wait for the delta (change in interpolation) to be pushed
var success = WaitForConditionOrTimeOutWithTimeTravel(() => m_AuthoritativeTransform.StatePushed && m_NonAuthoritativeTransform.StateUpdated);
Assert.True(success, $"[Interpolation Enable] Timed out waiting for state to be pushed ({m_AuthoritativeTransform.StatePushed}) or state to be updated ({m_NonAuthoritativeTransform.StateUpdated})!");
// Continue for one more update with interpolation enabled
// Note: We are just verifying one update with interpolation enabled due to the number of tests this integration test has to run
// and since the NestedNetworkTransformTests already tests interpolation under the same number of conditions (excluding Axis).
// This is just to verify selecting specific axis doesn't cause issues when interpolating as well.
m_NonAuthoritativeTransform.StateUpdated = false;
m_AuthoritativeTransform.StatePushed = false;
position = positionStart * k_PositionRotationScaleIterations;
rotation = rotationStart * k_PositionRotationScaleIterations;
scale = scaleStart * k_PositionRotationScaleIterations;
MoveRotateAndScaleAuthority(position, rotation, scale, overideState);
// Wait for the deltas to be pushed and updated
success = WaitForConditionOrTimeOutWithTimeTravel(() => m_AuthoritativeTransform.StatePushed && m_NonAuthoritativeTransform.StateUpdated);
Assert.True(success, $"[Interpolation {k_PositionRotationScaleIterations}] Timed out waiting for state to be pushed ({m_AuthoritativeTransform.StatePushed}) or state to be updated ({m_NonAuthoritativeTransform.StateUpdated})!");
success = WaitForConditionOrTimeOutWithTimeTravel(PositionRotationScaleMatches, 120);
// As a final test, wait for deltas to synchronize on non-authoritative side to assure it interpolates to th
success = WaitForConditionOrTimeOutWithTimeTravel(PositionRotationScaleMatches);
// Provide additional debug info about what failed (if it fails)
if (!success)
{
m_EnableVerboseDebug = true;
PositionRotationScaleMatches();
success = PositionRotationScaleMatches();
m_EnableVerboseDebug = false;
}
Assert.True(success, $"[Interpolation {k_PositionRotationScaleIterations}] Timed out waiting for non-authority to match authority's position or rotation");
Assert.True(success, $"Timed out waiting for non-authority to match authority's position or rotation");
}
}
@@ -1003,7 +1163,7 @@ namespace Unity.Netcode.RuntimeTests
m_AuthoritativeTransform.transform.rotation = Quaternion.Euler(1, 2, 3);
var serverLastSentState = m_AuthoritativeTransform.AuthorityLastSentState;
var clientReplicatedState = m_NonAuthoritativeTransform.ReplicatedNetworkState.Value;
var clientReplicatedState = m_NonAuthoritativeTransform.LocalAuthoritativeNetworkState;
var success = WaitForConditionOrTimeOutWithTimeTravel(() => ValidateBitSetValues(serverLastSentState, clientReplicatedState));
Assert.True(success, $"Timed out waiting for Authoritative Bitset state to equal NonAuthoritative replicated Bitset state!");

View File

@@ -50,6 +50,85 @@ namespace Unity.Netcode.RuntimeTests
}
}
internal struct TypeReferencedOnlyInCustomSerialization1 : INetworkSerializeByMemcpy
{
public int I;
}
internal struct TypeReferencedOnlyInCustomSerialization2 : INetworkSerializeByMemcpy
{
public int I;
}
internal struct TypeReferencedOnlyInCustomSerialization3 : INetworkSerializeByMemcpy
{
public int I;
}
internal struct TypeReferencedOnlyInCustomSerialization4 : INetworkSerializeByMemcpy
{
public int I;
}
internal struct TypeReferencedOnlyInCustomSerialization5 : INetworkSerializeByMemcpy
{
public int I;
}
internal struct TypeReferencedOnlyInCustomSerialization6 : INetworkSerializeByMemcpy
{
public int I;
}
// Both T and U are serializable
[GenerateSerializationForGenericParameter(0)]
[GenerateSerializationForGenericParameter(1)]
internal class CustomSerializableClass<TSerializableType1, TSerializableType2>
{
}
// Only U is serializable
[GenerateSerializationForGenericParameter(1)]
internal class CustomSerializableBaseClass<TUnserializableType, TSerializableType>
{
}
// T is serializable, passes TypeReferencedOnlyInCustomSerialization3 as U to the subclass, making it serializable
[GenerateSerializationForGenericParameter(0)]
internal class CustomSerializableSubclass<TSerializableType> : CustomSerializableBaseClass<TSerializableType, TypeReferencedOnlyInCustomSerialization3>
{
}
// T is serializable, passes TypeReferencedOnlyInCustomSerialization3 as U to the subclass, making it serializable
[GenerateSerializationForGenericParameter(0)]
internal class CustomSerializableSubclassWithNativeArray<TSerializableType> : CustomSerializableBaseClass<TSerializableType, NativeArray<TypeReferencedOnlyInCustomSerialization3>>
{
}
internal class CustomGenericSerializationTestBehaviour : NetworkBehaviour
{
public CustomSerializableClass<TypeReferencedOnlyInCustomSerialization1, TypeReferencedOnlyInCustomSerialization2> Value1;
public CustomSerializableClass<NativeArray<TypeReferencedOnlyInCustomSerialization1>, NativeArray<TypeReferencedOnlyInCustomSerialization2>> Value2;
public CustomSerializableSubclass<TypeReferencedOnlyInCustomSerialization4> Value3;
public CustomSerializableSubclassWithNativeArray<NativeArray<TypeReferencedOnlyInCustomSerialization4>> Value4;
}
[GenerateSerializationForType(typeof(TypeReferencedOnlyInCustomSerialization5))]
[GenerateSerializationForType(typeof(NativeArray<TypeReferencedOnlyInCustomSerialization5>))]
internal struct SomeRandomStruct
{
[GenerateSerializationForType(typeof(TypeReferencedOnlyInCustomSerialization6))]
[GenerateSerializationForType(typeof(NativeArray<TypeReferencedOnlyInCustomSerialization6>))]
public void Foo()
{
}
}
public struct TemplatedValueOnlyReferencedByNetworkVariableSubclass<T> : INetworkSerializeByMemcpy
where T : unmanaged
{
@@ -1329,6 +1408,39 @@ namespace Unity.Netcode.RuntimeTests
Assert.True(WaitForConditionOrTimeOutWithTimeTravel(VerifyCallback));
}
[Test]
public void TestCustomGenericSerialization()
{
// Just verifies that the ILPP codegen initialized these values for this type.
Assert.AreEqual(typeof(UnmanagedTypeSerializer<TypeReferencedOnlyInCustomSerialization1>), NetworkVariableSerialization<TypeReferencedOnlyInCustomSerialization1>.Serializer.GetType());
Assert.AreEqual(typeof(UnmanagedTypeSerializer<TypeReferencedOnlyInCustomSerialization2>), NetworkVariableSerialization<TypeReferencedOnlyInCustomSerialization2>.Serializer.GetType());
Assert.AreEqual(typeof(UnmanagedTypeSerializer<TypeReferencedOnlyInCustomSerialization3>), NetworkVariableSerialization<TypeReferencedOnlyInCustomSerialization3>.Serializer.GetType());
Assert.AreEqual(typeof(UnmanagedTypeSerializer<TypeReferencedOnlyInCustomSerialization4>), NetworkVariableSerialization<TypeReferencedOnlyInCustomSerialization4>.Serializer.GetType());
Assert.AreEqual(typeof(UnmanagedTypeSerializer<TypeReferencedOnlyInCustomSerialization5>), NetworkVariableSerialization<TypeReferencedOnlyInCustomSerialization5>.Serializer.GetType());
Assert.AreEqual(typeof(UnmanagedTypeSerializer<TypeReferencedOnlyInCustomSerialization6>), NetworkVariableSerialization<TypeReferencedOnlyInCustomSerialization6>.Serializer.GetType());
Assert.IsNotNull(NetworkVariableSerialization<TypeReferencedOnlyInCustomSerialization1>.AreEqual);
Assert.IsNotNull(NetworkVariableSerialization<TypeReferencedOnlyInCustomSerialization2>.AreEqual);
Assert.IsNotNull(NetworkVariableSerialization<TypeReferencedOnlyInCustomSerialization3>.AreEqual);
Assert.IsNotNull(NetworkVariableSerialization<TypeReferencedOnlyInCustomSerialization4>.AreEqual);
Assert.IsNotNull(NetworkVariableSerialization<TypeReferencedOnlyInCustomSerialization5>.AreEqual);
Assert.IsNotNull(NetworkVariableSerialization<TypeReferencedOnlyInCustomSerialization6>.AreEqual);
// Verify no issues with generic values...
Assert.AreEqual(typeof(UnmanagedArraySerializer<TypeReferencedOnlyInCustomSerialization1>), NetworkVariableSerialization<NativeArray<TypeReferencedOnlyInCustomSerialization1>>.Serializer.GetType());
Assert.AreEqual(typeof(UnmanagedArraySerializer<TypeReferencedOnlyInCustomSerialization2>), NetworkVariableSerialization<NativeArray<TypeReferencedOnlyInCustomSerialization2>>.Serializer.GetType());
Assert.AreEqual(typeof(UnmanagedArraySerializer<TypeReferencedOnlyInCustomSerialization3>), NetworkVariableSerialization<NativeArray<TypeReferencedOnlyInCustomSerialization3>>.Serializer.GetType());
Assert.AreEqual(typeof(UnmanagedArraySerializer<TypeReferencedOnlyInCustomSerialization4>), NetworkVariableSerialization<NativeArray<TypeReferencedOnlyInCustomSerialization4>>.Serializer.GetType());
Assert.AreEqual(typeof(UnmanagedArraySerializer<TypeReferencedOnlyInCustomSerialization5>), NetworkVariableSerialization<NativeArray<TypeReferencedOnlyInCustomSerialization5>>.Serializer.GetType());
Assert.AreEqual(typeof(UnmanagedArraySerializer<TypeReferencedOnlyInCustomSerialization6>), NetworkVariableSerialization<NativeArray<TypeReferencedOnlyInCustomSerialization6>>.Serializer.GetType());
Assert.IsNotNull(NetworkVariableSerialization<NativeArray<TypeReferencedOnlyInCustomSerialization1>>.AreEqual);
Assert.IsNotNull(NetworkVariableSerialization<NativeArray<TypeReferencedOnlyInCustomSerialization2>>.AreEqual);
Assert.IsNotNull(NetworkVariableSerialization<NativeArray<TypeReferencedOnlyInCustomSerialization3>>.AreEqual);
Assert.IsNotNull(NetworkVariableSerialization<NativeArray<TypeReferencedOnlyInCustomSerialization4>>.AreEqual);
Assert.IsNotNull(NetworkVariableSerialization<NativeArray<TypeReferencedOnlyInCustomSerialization5>>.AreEqual);
Assert.IsNotNull(NetworkVariableSerialization<NativeArray<TypeReferencedOnlyInCustomSerialization6>>.AreEqual);
}
[Test]
public void TestUnsupportedManagedTypesThrowExceptions()
{

View File

@@ -12,9 +12,37 @@ namespace Unity.Netcode.RuntimeTests
{
public class RpcTests : NetcodeIntegrationTest
{
public class RpcTestNB : NetworkBehaviour
public class CompileTimeNoRpcsBaseClassTest : NetworkBehaviour
{
}
public class CompileTimeHasRpcsChildClassDerivedFromNoRpcsBaseClassTest : CompileTimeNoRpcsBaseClassTest
{
[ServerRpc]
public void SomeDummyServerRpc()
{
}
}
public class GenericRpcTestNB<T> : NetworkBehaviour where T : unmanaged
{
public event Action<T, ServerRpcParams> OnServer_Rpc;
[ServerRpc]
public void MyServerRpc(T clientId, ServerRpcParams param = default)
{
OnServer_Rpc(clientId, param);
}
}
public class RpcTestNBFloat : GenericRpcTestNB<float>
{
}
public class RpcTestNB : GenericRpcTestNB<ulong>
{
public event Action<ulong, ServerRpcParams> OnServer_Rpc;
#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT
public event Action<NativeList<ulong>, ServerRpcParams> OnNativeListServer_Rpc;
#endif
@@ -26,12 +54,6 @@ namespace Unity.Netcode.RuntimeTests
public event Action OnClient_Rpc;
[ServerRpc]
public void MyServerRpc(ulong clientId, ServerRpcParams param = default)
{
OnServer_Rpc(clientId, param);
}
#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT
[ServerRpc]
public void MyNativeListServerRpc(NativeList<ulong> clientId, ServerRpcParams param = default)
@@ -67,6 +89,7 @@ namespace Unity.Netcode.RuntimeTests
protected override void OnCreatePlayerPrefab()
{
m_PlayerPrefab.AddComponent<RpcTestNB>();
m_PlayerPrefab.AddComponent<RpcTestNBFloat>();
}
[UnityTest]
@@ -74,12 +97,15 @@ namespace Unity.Netcode.RuntimeTests
{
// This is the *SERVER VERSION* of the *CLIENT PLAYER* RpcTestNB component
var serverClientRpcTestNB = m_PlayerNetworkObjects[m_ServerNetworkManager.LocalClientId][m_ClientNetworkManagers[0].LocalClientId].GetComponent<RpcTestNB>();
var serverClientRpcTestNBFloat = m_PlayerNetworkObjects[m_ServerNetworkManager.LocalClientId][m_ClientNetworkManagers[0].LocalClientId].GetComponent<RpcTestNBFloat>();
// This is the *CLIENT VERSION* of the *CLIENT PLAYER* RpcTestNB component
var localClienRpcTestNB = m_PlayerNetworkObjects[m_ClientNetworkManagers[0].LocalClientId][m_ClientNetworkManagers[0].LocalClientId].GetComponent<RpcTestNB>();
var localClienRpcTestNBFloat = m_PlayerNetworkObjects[m_ClientNetworkManagers[0].LocalClientId][m_ClientNetworkManagers[0].LocalClientId].GetComponent<RpcTestNBFloat>();
// Setup state
bool hasReceivedServerRpc = false;
bool hasReceivedFloatServerRpc = false;
bool hasReceivedTypedServerRpc = false;
bool hasReceivedClientRpcRemotely = false;
bool hasReceivedClientRpcLocally = false;
@@ -106,6 +132,12 @@ namespace Unity.Netcode.RuntimeTests
Assert.Fail("ServerRpc invoked locally. Weaver failure?");
};
localClienRpcTestNBFloat.OnServer_Rpc += (clientId, param) =>
{
// The RPC invoked locally. (Weaver failure?)
Assert.Fail("ServerRpc (float) invoked locally. Weaver failure?");
};
serverClientRpcTestNB.OnServer_Rpc += (clientId, param) =>
{
Debug.Log("ServerRpc received on server object");
@@ -113,6 +145,13 @@ namespace Unity.Netcode.RuntimeTests
hasReceivedServerRpc = true;
};
serverClientRpcTestNBFloat.OnServer_Rpc += (clientId, param) =>
{
Debug.Log("ServerRpc (float) received on server object");
Assert.True(param.Receive.SenderClientId == clientId);
hasReceivedFloatServerRpc = true;
};
serverClientRpcTestNB.OnClient_Rpc += () =>
{
// The RPC invoked locally. (Weaver failure?)
@@ -145,6 +184,7 @@ namespace Unity.Netcode.RuntimeTests
// Send ServerRpc
localClienRpcTestNB.MyServerRpc(m_ClientNetworkManagers[0].LocalClientId);
localClienRpcTestNBFloat.MyServerRpc(m_ClientNetworkManagers[0].LocalClientId);
// Send TypedServerRpc
localClienRpcTestNB.MyTypedServerRpc(vector3, vector3s,
@@ -181,6 +221,7 @@ namespace Unity.Netcode.RuntimeTests
yield return WaitForConditionOrTimeOut(() => hasReceivedServerRpc && hasReceivedClientRpcLocally && hasReceivedClientRpcRemotely && hasReceivedTypedServerRpc);
Assert.True(hasReceivedServerRpc, "ServerRpc was not received");
Assert.True(hasReceivedFloatServerRpc, "ServerRpc was not received");
Assert.True(hasReceivedTypedServerRpc, "TypedServerRpc was not received");
Assert.True(hasReceivedClientRpcLocally, "ClientRpc was not locally received on the server");
Assert.True(hasReceivedClientRpcRemotely, "ClientRpc was not remotely received on the client");

View File

@@ -37,7 +37,7 @@ namespace Unity.Netcode.RuntimeTests
return TestComplete;
}
protected override void OnInitialize(ref NetworkVariable<NetworkTransformState> replicatedState)
protected override void OnInitialize(ref NetworkTransformState replicatedState)
{
m_LocalSpaceToggles = 0;
m_FrameRateFractional = 1.0f / Application.targetFrameRate;
@@ -59,6 +59,9 @@ namespace Unity.Netcode.RuntimeTests
IsMoving = false;
}
private const int k_MaxThresholdFailures = 4;
private int m_ExceededThresholdCount;
protected override void Update()
{
base.Update();
@@ -72,10 +75,22 @@ namespace Unity.Netcode.RuntimeTests
if (CheckPosition)
{
if (transform.position.y < -MinThreshold || transform.position.y > Application.targetFrameRate + MinThreshold)
{
// Temporary work around for this test.
// Really, this test needs to be completely re-written.
m_ExceededThresholdCount++;
// If we haven't corrected ourselves within the maximum number of updates then throw an error.
if (m_ExceededThresholdCount > k_MaxThresholdFailures)
{
Debug.LogError($"Interpolation failure. transform.position.y is {transform.position.y}. Should be between 0.0 and 100.0. Current threshold is [+/- {MinThreshold}].");
}
}
else
{
// If corrected, then reset our count
m_ExceededThresholdCount = 0;
}
}
// Move the nested object on the server
if (IsMoving)

10
ValidationExceptions.json Normal file
View File

@@ -0,0 +1,10 @@
{
"ErrorExceptions": [
{
"ValidationTest": "API Validation",
"ExceptionMessage": "Additions require a new minor or major version.",
"PackageVersion": "1.5.2"
}
],
"WarningExceptions": []
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 2a43005be301c9043aab7034757d4868
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -2,23 +2,23 @@
"name": "com.unity.netcode.gameobjects",
"displayName": "Netcode for GameObjects",
"description": "Netcode for GameObjects is a high-level netcode SDK that provides networking capabilities to GameObject/MonoBehaviour workflows within Unity and sits on top of underlying transport layer.",
"version": "1.5.1",
"version": "1.7.1",
"unity": "2020.3",
"dependencies": {
"com.unity.nuget.mono-cecil": "1.10.1",
"com.unity.transport": "1.3.4"
"com.unity.transport": "1.4.0"
},
"_upm": {
"changelog": "### Added\n\n- Added support for serializing `NativeArray<>` and `NativeList<>` in `FastBufferReader`/`FastBufferWriter`, `BufferSerializer`, `NetworkVariable`, and RPCs. (To use `NativeList<>`, add `UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT` to your Scripting Define Symbols in `Project Settings > Player`) (#2375)\n- The location of the automatically-created default network prefab list can now be configured (#2544)\n- Added: Message size limits (max single message and max fragmented message) can now be set using NetworkManager.MaximumTransmissionUnitSize and NetworkManager.MaximumFragmentedMessageSize for transports that don't work with the default values (#2530)\n- Added `NetworkObject.SpawnWithObservers` property (default is true) that when set to false will spawn a `NetworkObject` with no observers and will not be spawned on any client until `NetworkObject.NetworkShow` is invoked. (#2568)\n\n### Fixed\n\n- Fixed: Fixed a null reference in codegen in some projects (#2581)\n- Fixed issue where the `OnClientDisconnected` client identifier was incorrect after a pending client connection was denied. (#2569)\n- Fixed warning \"Runtime Network Prefabs was not empty at initialization time.\" being erroneously logged when no runtime network prefabs had been added (#2565)\n- Fixed issue where some temporary debug console logging was left in a merged PR. (#2562)\n- Fixed the \"Generate Default Network Prefabs List\" setting not loading correctly and always reverting to being checked. (#2545)\n- Fixed issue where users could not use NetworkSceneManager.VerifySceneBeforeLoading to exclude runtime generated scenes from client synchronization. (#2550)\n- Fixed missing value on `NetworkListEvent` for `EventType.RemoveAt` events. (#2542,#2543)\n- Fixed issue where parenting a NetworkTransform under a transform with a scale other than Vector3.one would result in incorrect values on non-authoritative instances. (#2538)\n- Fixed issue where a server would include scene migrated and then despawned NetworkObjects to a client that was being synchronized. (#2532)\n- Fixed the inspector throwing exceptions when attempting to render `NetworkVariable`s of enum types. (#2529)\n- Making a `NetworkVariable` with an `INetworkSerializable` type that doesn't meet the `new()` constraint will now create a compile-time error instead of an editor crash (#2528)\n- Fixed Multiplayer Tools package installation docs page link on the NetworkManager popup. (#2526)\n- Fixed an exception and error logging when two different objects are shown and hidden on the same frame (#2524)\n- Fixed a memory leak in `UnityTransport` that occurred if `StartClient` failed. (#2518)\n- Fixed issue where a client could throw an exception if abruptly disconnected from a network session with one or more spawned `NetworkObject`(s). (#2510)\n- Fixed issue where invalid endpoint addresses were not being detected and returning false from NGO UnityTransport. (#2496)\n- Fixed some errors that could occur if a connection is lost and the loss is detected when attempting to write to the socket. (#2495)\n\n## Changed\n\n- Adding network prefabs before NetworkManager initialization is now supported. (#2565)\n- Connecting clients being synchronized now switch to the server's active scene before spawning and synchronizing NetworkObjects. (#2532)\n- Updated `UnityTransport` dependency on `com.unity.transport` to 1.3.4. (#2533)\n- Improved performance of NetworkBehaviour initialization by replacing reflection when initializing NetworkVariables with compile-time code generation, which should help reduce hitching during additive scene loads. (#2522)"
"changelog": "### Added\n\n### Fixed\n\n- Fixed a bug where having a class with Rpcs that inherits from a class without Rpcs that inherits from NetworkVariable would cause a compile error. (#2751)\n- Fixed issue where `NetworkBehaviour.Synchronize` was not truncating the write buffer if nothing was serialized during `NetworkBehaviour.OnSynchronize` causing an additional 6 bytes to be written per `NetworkBehaviour` component instance. (#2749)\n\n### Changed"
},
"upmCi": {
"footprint": "35c5325acc3edf18c37ef8c9d19e0944fae0d42a"
"footprint": "0aa1a9720f4e4850c481cc1bb159b646494808e1"
},
"documentationUrl": "https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@1.5/manual/index.html",
"documentationUrl": "https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@1.7/manual/index.html",
"repository": {
"url": "https://github.com/Unity-Technologies/com.unity.netcode.gameobjects.git",
"type": "git",
"revision": "7a969f89d6dda65ac373ce552c0c997c9116f21a"
"revision": "5df824c7588b43c29238a927d14642d5f94129ff"
},
"samples": [
{