diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md
index a226fda36e..f0946b5388 100644
--- a/com.unity.netcode.gameobjects/CHANGELOG.md
+++ b/com.unity.netcode.gameobjects/CHANGELOG.md
@@ -12,6 +12,19 @@ Additional documentation and release notes are available at [Multiplayer Documen
### Fixed
+- 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)
diff --git a/com.unity.netcode.gameobjects/Components/HalfVector3.cs b/com.unity.netcode.gameobjects/Components/HalfVector3.cs
index 5ceca1267d..679496452c 100644
--- a/com.unity.netcode.gameobjects/Components/HalfVector3.cs
+++ b/com.unity.netcode.gameobjects/Components/HalfVector3.cs
@@ -27,7 +27,7 @@ public struct HalfVector3 : INetworkSerializable
///
/// The half float precision value of the z-axis as a .
///
- public half Z => Axis.x;
+ public half Z => Axis.z;
///
/// Used to store the half float precision values as a
@@ -39,6 +39,17 @@ public struct HalfVector3 : INetworkSerializable
///
public bool3 AxisToSynchronize;
+ ///
+ /// Directly sets each axial value to the passed in full precision values
+ /// that are converted to half precision
+ ///
+ 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++)
diff --git a/com.unity.netcode.gameobjects/Components/NetworkTransform.cs b/com.unity.netcode.gameobjects/Components/NetworkTransform.cs
index e1e9667d23..ab88ddef9a 100644
--- a/com.unity.netcode.gameobjects/Components/NetworkTransform.cs
+++ b/com.unity.netcode.gameobjects/Components/NetworkTransform.cs
@@ -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 @@ public class NetworkTransform : NetworkBehaviour
///
public OnClientRequestChangeDelegate OnClientRequestChange;
+ ///
+ /// When set each state update will contain a state identifier
+ ///
+ internal static bool TrackByStateId;
+
///
/// Data structure used to synchronize the
///
@@ -71,9 +77,16 @@ public struct NetworkTransformState : INetworkSerializable
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 @@ public struct NetworkTransformState : INetworkSerializable
// 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 @@ public struct NetworkTransformState : INetworkSerializable
internal int NetworkTick;
// Used when tracking by state ID is enabled
- internal bool TrackByStateId;
internal int StateId;
// Used during serialization
@@ -416,6 +429,24 @@ internal set
}
}
+ 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)
{
@@ -534,6 +565,8 @@ public int GetNetworkTick()
return NetworkTick;
}
+ internal HalfVector3 HalfEulerRotation;
+
///
/// Serializes this
///
@@ -553,23 +586,6 @@ public void NetworkSerialize(BufferSerializer serializer) where T : IReade
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 +605,22 @@ public void NetworkSerialize(BufferSerializer serializer) where T : IReade
}
}
+ // 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 +631,7 @@ public void NetworkSerialize(BufferSerializer serializer) where T : IReade
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 +644,7 @@ public void NetworkSerialize(BufferSerializer serializer) where T : IReade
{
if (!isWriting)
{
- NetworkDeltaPosition = new NetworkDeltaPosition(Vector3.zero, 0, math.bool3(HasPositionX, HasPositionY, HasPositionZ));
+ NetworkDeltaPosition.NetworkTick = NetworkTick;
NetworkDeltaPosition.NetworkSerialize(serializer);
}
else
@@ -626,9 +653,8 @@ public void NetworkSerialize(BufferSerializer serializer) where T : IReade
}
}
}
- else // Legacy Position Synchronization
+ else // Full precision axis specific position synchronization
{
- // Position Values
if (HasPositionX)
{
serializer.SerializeValue(ref PositionX);
@@ -703,11 +729,21 @@ public void NetworkSerialize(BufferSerializer serializer) where T : IReade
{
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 +785,12 @@ public void NetworkSerialize(BufferSerializer serializer) where T : IReade
// 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 +800,19 @@ public void NetworkSerialize(BufferSerializer serializer) where T : IReade
}
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 +1081,6 @@ private bool SynchronizeScale
///
protected NetworkManager m_CachedNetworkManager; // Note: we no longer use this and are only keeping it until we decide to deprecate it
- ///
- /// We have two internal NetworkVariables.
- /// One for server authoritative and one for "client/owner" authoritative.
- ///
- private readonly NetworkVariable m_ReplicatedNetworkStateServer = new NetworkVariable(new NetworkTransformState(), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
- private readonly NetworkVariable m_ReplicatedNetworkStateOwner = new NetworkVariable(new NetworkTransformState(), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
-
- internal NetworkVariable ReplicatedNetworkState
- {
- get
- {
- if (!IsServerAuthoritative())
- {
- return m_ReplicatedNetworkStateOwner;
- }
-
- return m_ReplicatedNetworkStateServer;
- }
- }
-
///
/// Helper method that returns the space relative position of the transform.
///
@@ -1149,6 +1181,8 @@ public Vector3 GetScale(bool getCurrentState = false)
// 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 m_ClientIds = new List() { 0 };
@@ -1165,6 +1199,9 @@ public Vector3 GetScale(bool getCurrentState = false)
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)]
internal void UpdatePositionInterpolator(Vector3 position, double time, bool resetInterpolator = false)
@@ -1258,7 +1295,13 @@ private bool ShouldSynchronizeHalfFloat(ulong targetClientId)
protected override void OnSynchronize(ref BufferSerializer 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)
{
@@ -1286,6 +1329,7 @@ protected override void OnSynchronize(ref BufferSerializer serializer)
m_LocalAuthoritativeNetworkState = synchronizationState;
m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = false;
+ m_LocalAuthoritativeNetworkState.IsSynchronizing = false;
}
}
@@ -1366,11 +1410,11 @@ private void TryCommitTransform(ref Transform transformToCommit, bool synchroniz
// 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;
OnAuthorityPushTransformState(ref m_LocalAuthoritativeNetworkState);
- // "push"/commit the state
- ReplicatedNetworkState.Value = m_LocalAuthoritativeNetworkState;
+ // Update the state
+ UpdateTransformState();
m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = false;
}
@@ -1651,7 +1695,43 @@ private bool ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState netw
}
}
- // 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();
+
+ // 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)
@@ -1682,7 +1762,7 @@ private bool ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState netw
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];
@@ -1691,46 +1771,18 @@ private bool ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState netw
}
}
}
- 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();
-
- // 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;
@@ -2027,28 +2079,41 @@ private void ApplyTeleportingState(NetworkTransformState newState)
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;
+ 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;
@@ -2112,7 +2177,7 @@ private void ApplyTeleportingState(NetworkTransformState newState)
///
/// Only non-authoritative instances should invoke this
///
- private void UpdateState(NetworkTransformState oldState, NetworkTransformState newState)
+ private void ApplyUpdatedState(NetworkTransformState newState)
{
// Set the transforms's synchronization modes
InLocalSpace = newState.InLocalSpace;
@@ -2142,8 +2207,9 @@ private void UpdateState(NetworkTransformState oldState, NetworkTransformState n
{
// 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.CurrentPosition = m_TargetPosition;
}
if (!Interpolate)
@@ -2154,14 +2220,9 @@ private void UpdateState(NetworkTransformState oldState, NetworkTransformState n
// 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)
- {
- UpdatePositionInterpolator(m_LocalAuthoritativeNetworkState.CurrentPosition, sentTime);
- }
- else
+ if (!m_LocalAuthoritativeNetworkState.UseHalfFloatPrecision)
{
var newTargetPosition = m_TargetPosition;
if (m_LocalAuthoritativeNetworkState.HasPositionX)
@@ -2178,9 +2239,9 @@ private void UpdateState(NetworkTransformState oldState, NetworkTransformState n
{
newTargetPosition.z = m_LocalAuthoritativeNetworkState.PositionZ;
}
- UpdatePositionInterpolator(newTargetPosition, sentTime);
m_TargetPosition = newTargetPosition;
}
+ UpdatePositionInterpolator(m_TargetPosition, sentTime);
}
if (m_LocalAuthoritativeNetworkState.HasScaleChange)
@@ -2263,6 +2324,8 @@ protected virtual void OnNetworkTransformStateUpdated(ref NetworkTransformState
}
+ private NetworkTransformState m_OldState = new NetworkTransformState();
+
///
/// Only non-authoritative instances should invoke this method
///
@@ -2276,8 +2339,8 @@ private void OnNetworkStateChanged(NetworkTransformState oldState, NetworkTransf
// 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);
@@ -2360,10 +2423,19 @@ private void AxisChangedDeltaPositionCheck()
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();
@@ -2394,20 +2466,31 @@ private void NetworkTickSystem_Tick()
}
}
+
+
+
///
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();
}
///
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)
{
@@ -2424,9 +2507,6 @@ public override void OnDestroy()
}
CanCommitToTransform = false;
base.OnDestroy();
- m_ReplicatedNetworkStateServer.Dispose();
- m_ReplicatedNetworkStateOwner.Dispose();
-
}
///
@@ -2453,7 +2533,20 @@ public override void OnLostOwnership()
///
/// Invoked when first spawned and when ownership changes.
///
- /// the replicated
+ /// the current after initializing
+ protected virtual void OnInitialize(ref NetworkTransformState replicatedState)
+ {
+ }
+
+ ///
+ /// An owner read and owner write NetworkVariable so it doesn't generate any messages
+ ///
+ private NetworkVariable m_InternalStatNetVar = new NetworkVariable(default, NetworkVariableReadPermission.Owner, NetworkVariableWritePermission.Owner);
+ ///
+ /// This method is only invoked by the owner
+ /// Use: OnInitialize(ref NetworkTransformState replicatedState) to be notified on all instances
+ ///
+ ///
protected virtual void OnInitialize(ref NetworkVariable replicatedState)
{
@@ -2470,7 +2563,6 @@ protected void Initialize()
}
CanCommitToTransform = IsServerAuthoritative() ? IsServer : IsOwner;
- var replicatedState = ReplicatedNetworkState;
var currentPosition = GetSpaceRelativePosition();
var currentRotation = GetSpaceRelativeRotation();
@@ -2490,14 +2582,12 @@ protected void Initialize()
}
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;
@@ -2506,8 +2596,13 @@ protected void Initialize()
m_TargetRotation = currentRotation.eulerAngles;
}
+ OnInitialize(ref m_LocalAuthoritativeNetworkState);
- OnInitialize(ref replicatedState);
+ if (IsOwner)
+ {
+ m_InternalStatNetVar.Value = m_LocalAuthoritativeNetworkState;
+ OnInitialize(ref m_InternalStatNetVar);
+ }
}
///
@@ -2690,12 +2785,6 @@ protected virtual void Update()
}
}
- // 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();
}
@@ -2738,6 +2827,103 @@ public bool IsServerAuthoritative()
{
return OnIsServerAuthoritative();
}
+
+ ///
+ /// Receives the named message updates
+ ///
+ /// authority of the transform
+ /// serialzied
+ private void TransformStateUpdate(ulong senderId, FastBufferReader messagePayload)
+ {
+ // 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);
+ }
+
+ ///
+ /// Forwards owner authoritative state updates when received by the server
+ ///
+ /// the owner state message payload
+ private unsafe void ForwardStateUpdateMessage(FastBufferReader messagePayload)
+ {
+ 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 (!OnIsServerAuthoritative() && (NetworkManager.ServerClientId == clientId || clientId == OwnerClientId))
+ {
+ continue;
+ }
+ NetworkManager.CustomMessagingManager.SendNamedMessage(m_MessageName, clientId, writer);
+ }
+ }
+ messagePayload.Seek(currentPosition);
+ }
+
+ ///
+ /// Sends named message updates by the authority of the transform
+ ///
+ 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
diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/NamedMessage.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/NamedMessage.cs
index 87ac914c99..61773660e6 100644
--- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/NamedMessage.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/NamedMessage.cs
@@ -24,7 +24,11 @@ public bool Deserialize(FastBufferReader reader, ref NetworkContext context, int
public void Handle(ref NetworkContext context)
{
- ((NetworkManager)context.SystemOwner).CustomMessagingManager.InvokeNamedMessage(Hash, context.SenderId, m_ReceiveData, context.SerializedHeaderSize);
+ var networkManager = (NetworkManager)context.SystemOwner;
+ if (!networkManager.ShutdownInProgress)
+ {
+ ((NetworkManager)context.SystemOwner).CustomMessagingManager.InvokeNamedMessage(Hash, context.SenderId, m_ReceiveData, context.SerializedHeaderSize);
+ }
}
}
}
diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/NetworkMessageManager.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/NetworkMessageManager.cs
index 4722ea160f..dcef1dc378 100644
--- a/com.unity.netcode.gameobjects/Runtime/Messaging/NetworkMessageManager.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Messaging/NetworkMessageManager.cs
@@ -518,15 +518,18 @@ internal int GetMessageVersion(Type type, ulong clientId, bool forReceive = fals
{
if (!m_PerClientMessageVersions.TryGetValue(clientId, out var versionMap))
{
- if (forReceive)
+ var networkManager = NetworkManager.Singleton;
+ if (networkManager != null && networkManager.LogLevel == LogLevel.Developer)
{
- Debug.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.");
+ if (forReceive)
+ {
+ NetworkLog.LogWarning($"Trying to receive {type.Name} from client {clientId} which is not in a connected state.");
+ }
+ else
+ {
+ NetworkLog.LogWarning($"Trying to send {type.Name} to client {clientId} which is not in a connected state.");
+ }
}
-
return -1;
}
diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformOwnershipTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformOwnershipTests.cs
index 6efc10b2a2..1bcd7a9c0a 100644
--- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformOwnershipTests.cs
+++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformOwnershipTests.cs
@@ -46,6 +46,93 @@ protected override void OnServerAndClientsCreated()
base.OnServerAndClientsCreated();
}
+ ///
+ /// Clients created during a test need to have their prefabs list updated to
+ /// match the server's prefab list.
+ ///
+ 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;
+ }
+
+ ///
+ /// 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
+ ///
+ [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().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,
diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformStateTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformStateTests.cs
index ac85b191ca..2669c233a9 100644
--- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformStateTests.cs
+++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformStateTests.cs
@@ -2,6 +2,7 @@
using Unity.Netcode.Components;
using UnityEngine;
+
namespace Unity.Netcode.RuntimeTests
{
@@ -89,6 +90,125 @@ private bool WillAnAxisBeSynchronized(ref NetworkTransform networkTransform)
networkTransform.SyncPositionX || networkTransform.SyncPositionY || networkTransform.SyncPositionZ;
}
+ [Test]
+ public void NetworkTransformStateFlags()
+ {
+ var indexValues = new System.Collections.Generic.List();
+ 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();
+ var transformState = new NetworkTransform.NetworkTransformState();
+ // Test setting one at a time.
+ for (int j = 0; j < 19; j++)
+ {
+ boolSet = new System.Collections.Generic.List();
+ 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();
+ 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)
diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs
index abd0c42698..321f9137df 100644
--- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs
+++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs
@@ -64,24 +64,50 @@ public void CommitToTransform()
}
}
+ ///
+ /// Helper component for NetworkTransform parenting tests when
+ /// a child is a parent of another child (i.e. "sub child")
+ ///
+ public class SubChildObjectComponent : ChildObjectComponent
+ {
+ protected override bool IsSubChild()
+ {
+ return true;
+ }
+ }
+
///
/// Helper component for NetworkTransform parenting tests
///
public class ChildObjectComponent : NetworkTransform
{
public static readonly List Instances = new List();
+ public static readonly List SubInstances = new List();
public static ChildObjectComponent AuthorityInstance { get; internal set; }
+ public static ChildObjectComponent AuthoritySubInstance { get; internal set; }
public static readonly Dictionary ClientInstances = new Dictionary();
+ public static readonly Dictionary ClientSubChildInstances = new Dictionary();
+
+ 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;
@@ -92,13 +118,34 @@ public override void OnNetworkSpawn()
base.OnNetworkSpawn();
if (CanCommitToTransform)
{
- AuthorityInstance = this;
+ 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
{
- Instances.Add(this);
+ ClientInstances.Add(NetworkManager.LocalClientId, NetworkObject);
}
- ClientInstances.Add(NetworkManager.LocalClientId, NetworkObject);
}
}
@@ -117,6 +164,7 @@ public class NetworkTransformTests : IntegrationTestWithApproximation
private NetworkObject m_AuthoritativePlayer;
private NetworkObject m_NonAuthoritativePlayer;
private NetworkObject m_ChildObject;
+ private NetworkObject m_SubChildObject;
private NetworkObject m_ParentObject;
private NetworkTransformTestComponent m_AuthoritativeTransform;
@@ -232,6 +280,11 @@ protected override void OnCreatePlayerPrefab()
protected override void OnServerAndClientsCreated()
{
+ var subChildObject = CreateNetworkObjectPrefab("SubChildObject");
+ var subChildNetworkTransform = subChildObject.AddComponent();
+ subChildNetworkTransform.ServerAuthority = m_Authority == Authority.ServerAuthority;
+ m_SubChildObject = subChildObject.GetComponent();
+
var childObject = CreateNetworkObjectPrefab("ChildObject");
var childNetworkTransform = childObject.AddComponent();
childNetworkTransform.ServerAuthority = m_Authority == Authority.ServerAuthority;
@@ -242,13 +295,19 @@ protected override void OnServerAndClientsCreated()
parentNetworkTransform.ServerAuthority = m_Authority == Authority.ServerAuthority;
m_ParentObject = parentObject.GetComponent();
-
// 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;
@@ -302,6 +361,11 @@ private bool AllChildObjectInstancesAreSpawned()
return false;
}
+ if (ChildObjectComponent.HasSubChild && ChildObjectComponent.AuthoritySubInstance == null)
+ {
+ return false;
+ }
+
foreach (var clientNetworkManager in m_ClientNetworkManagers)
{
if (!ChildObjectComponent.ClientInstances.ContainsKey(clientNetworkManager.LocalClientId))
@@ -321,6 +385,16 @@ private bool AllChildObjectInstancesHaveChild()
return false;
}
}
+ if (ChildObjectComponent.HasSubChild)
+ {
+ foreach (var instance in ChildObjectComponent.ClientSubChildInstances.Values)
+ {
+ if (instance.transform.parent == null)
+ {
+ return false;
+ }
+ }
+ }
return true;
}
@@ -328,6 +402,10 @@ private bool AllChildObjectInstancesHaveChild()
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);
+
///
/// A wait condition specific method that assures the local space coordinates
@@ -375,17 +453,17 @@ private bool AllInstancesKeptLocalTransformValues()
/// If not, it generates a message containing the axial values that did not match
/// the target/start local space values.
///
- private void AllChildrenLocalTransformValuesMatch()
+ private void AllChildrenLocalTransformValuesMatch(bool useSubChild)
{
var success = WaitForConditionOrTimeOutWithTimeTravel(AllInstancesKeptLocalTransformValues);
- //TimeTravelToNextTick();
var infoMessage = new 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 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;
@@ -428,8 +506,11 @@ private void AllChildrenLocalTransformValuesMatch()
private NetworkObject m_AuthorityParentObject;
private NetworkTransformTestComponent m_AuthorityParentNetworkTransform;
private NetworkObject m_AuthorityChildObject;
+ private NetworkObject m_AuthoritySubChildObject;
private ChildObjectComponent m_AuthorityChildNetworkTransform;
+ private ChildObjectComponent m_AuthoritySubChildNetworkTransform;
+
///
/// Validates that transform values remain the same when a NetworkTransform is
/// parented under another NetworkTransform under all of the possible axial conditions
@@ -451,9 +532,11 @@ public void ParentedNetworkTransformTest([Values] Precision precision, [Values]
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();
var serverSideChild = SpawnObject(m_ChildObject.gameObject, authorityNetworkManager).GetComponent();
+ var serverSideSubChild = SpawnObject(m_SubChildObject.gameObject, authorityNetworkManager).GetComponent();
// Assure all of the child object instances are spawned before proceeding to parenting
var success = WaitForConditionOrTimeOutWithTimeTravel(AllChildObjectInstancesAreSpawned);
@@ -462,6 +545,7 @@ public void ParentedNetworkTransformTest([Values] Precision precision, [Values]
// 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.
@@ -470,15 +554,26 @@ public void ParentedNetworkTransformTest([Values] Precision precision, [Values]
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();
m_AuthorityParentNetworkTransform.Interpolate = interpolation == Interpolation.EnableInterpolate;
m_AuthorityChildNetworkTransform = m_AuthorityChildObject.GetComponent();
m_AuthorityChildNetworkTransform.Interpolate = interpolation == Interpolation.EnableInterpolate;
+ m_AuthoritySubChildNetworkTransform = m_AuthoritySubChildObject.GetComponent();
+ 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();
@@ -486,12 +581,18 @@ public void ParentedNetworkTransformTest([Values] Precision precision, [Values]
// 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!");
// 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();
@@ -500,8 +601,15 @@ public void ParentedNetworkTransformTest([Values] Precision precision, [Values]
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);
}
///
@@ -1054,7 +1162,7 @@ public void TestBitsetValue([Values] Interpolation interpolation)
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!");
diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/TransformInterpolationTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/TransformInterpolationTests.cs
index 67c0d66d62..9ef656caca 100644
--- a/com.unity.netcode.gameobjects/Tests/Runtime/TransformInterpolationTests.cs
+++ b/com.unity.netcode.gameobjects/Tests/Runtime/TransformInterpolationTests.cs
@@ -37,7 +37,7 @@ public bool ReachedTargetLocalSpaceTransitionCount()
return TestComplete;
}
- protected override void OnInitialize(ref NetworkVariable replicatedState)
+ protected override void OnInitialize(ref NetworkTransformState replicatedState)
{
m_LocalSpaceToggles = 0;
m_FrameRateFractional = 1.0f / Application.targetFrameRate;
diff --git a/testproject/Assets/Tests/Manual/Scripts/IntegrationNetworkTransform.cs b/testproject/Assets/Tests/Manual/Scripts/IntegrationNetworkTransform.cs
index 4aeb42a238..1146c1969e 100644
--- a/testproject/Assets/Tests/Manual/Scripts/IntegrationNetworkTransform.cs
+++ b/testproject/Assets/Tests/Manual/Scripts/IntegrationNetworkTransform.cs
@@ -318,48 +318,7 @@ private void DebugTransformStateUpdate(NetworkTransformState oldState, NetworkTr
OnNetworkTransformStateUpdate(ref m_NetworkTransformStateUpdate);
}
-#endif
-
- private NetworkVariable m_CurrentReplicatedState = new NetworkVariable();
-
- protected override void OnInitialize(ref NetworkVariable replicatedState)
- {
- if (CanCommitToTransform && DebugTransform)
- {
- m_CurrentReplicatedState = replicatedState;
- // Sanity check to assure we only subscribe to OnValueChanged once
- replicatedState.OnValueChanged -= OnStateUpdate;
- replicatedState.OnValueChanged += OnStateUpdate;
- }
- }
-
- public override void OnNetworkDespawn()
- {
- if (DebugTransform)
- {
- m_CurrentReplicatedState.OnValueChanged -= OnStateUpdate;
- }
-
- base.OnNetworkDespawn();
- }
-
- ///
- /// Authoritative State Update
- ///
- private void OnStateUpdate(NetworkTransformState oldState, NetworkTransformState newState)
- {
- if (DebugTransform)
- {
- if (IsOwner && !IsServerAuthoritative() && !m_StopLoggingStates)
- {
- if (IsServer && NetworkManager.ConnectedClientsIds.Count < 2)
- {
- return;
- }
- InternalAddLogEntry(ref newState, OwnerClientId);
- }
- }
- }
+#endif
#if DEBUG_NETWORKTRANSFORM || UNITY_INCLUDE_TESTS
///