diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs
index b0335356eb..b62aaddf13 100644
--- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs
@@ -17,7 +17,7 @@ namespace Unity.Netcode.Components
[DisallowMultipleComponent]
[AddComponentMenu("Netcode/Network Transform")]
[HelpURL(HelpUrls.NetworkTransform)]
- public class NetworkTransform : NetworkBehaviour
+ public partial class NetworkTransform : NetworkBehaviour
{
#if UNITY_EDITOR
internal virtual bool HideInterpolateValue => false;
@@ -2147,84 +2147,115 @@ internal bool ApplyTransformToNetworkState(ref NetworkTransformState networkStat
}
///
- /// Applies the transform to the specified.
+ /// Authority:
+ /// Gets the instance's configuration for a delta check.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private bool CheckForStateChange(ref NetworkTransformState networkState, bool isSynchronization = false, ulong targetClientId = 0, bool forceState = false)
+ private TransformDeltaConfig GetTransformDeltaConfig()
{
- var flagStates = networkState.FlagStates;
+ return new TransformDeltaConfig()
+ {
+ PositionThreshold = PositionThreshold,
+ RotAngleThreshold = RotAngleThreshold,
+ ScaleThreshold = ScaleThreshold,
+ SyncPositionX = SyncPositionX,
+ SyncPositionY = SyncPositionY,
+ SyncPositionZ = SyncPositionZ,
+ SyncRotAngleX = SyncRotAngleX,
+ SyncRotAngleY = SyncRotAngleY,
+ SyncRotAngleZ = SyncRotAngleZ,
+ SyncScaleX = SyncScaleX,
+ SyncScaleY = SyncScaleY,
+ SyncScaleZ = SyncScaleZ,
+ UseQuaternionSynchronization = UseQuaternionSynchronization,
+ UseQuaternionCompression = UseQuaternionCompression,
+ UseHalfFloatPrecision = UseHalfFloatPrecision,
+ SlerpPosition = SlerpPosition,
+ Interpolate = Interpolate,
+ UseUnreliableDeltas = UseUnreliableDeltas,
+ SwitchTransformSpaceWhenParented = SwitchTransformSpaceWhenParented,
+#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D
+ UseRigidbodyForMotion = m_UseRigidbodyForMotion,
+#else
+ UseRigidbodyForMotion = false,
+#endif
+ InLocalSpace = InLocalSpace,
+ CurrentTick = CurrentTick,
+ CachedTickRate = m_CachedTickRate,
+ HalfFloatTargetTickOwnership = m_HalfFloatTargetTickOwnership,
+ NextTickSync = m_NextTickSync,
+ DeltaSynch = m_DeltaSynch,
+ Enabled = enabled,
+ };
+ }
- // As long as we are not doing our first synchronization and we are sending unreliable deltas, each
- // NetworkTransform will stagger its full transfom synchronization over a 1 second period based on the
- // assigned tick slot (m_TickSync).
- // More about m_DeltaSynch:
- // If we have not sent any deltas since our last frame synch, then this will prevent us from sending
- // frame synch's when the object is at rest. If this is false and a state update is detected and sent,
- // then it will be set to true and each subsequent tick will do this check to determine if it should
- // send a full frame synch.
- var isAxisSync = false;
- // We compare against the NetworkTickSystem version since ServerTime is set when updating ticks
- if (UseUnreliableDeltas && !isSynchronization && m_DeltaSynch && m_NextTickSync <= CurrentTick)
+ ///
+ /// Applies anything the delta check updated back onto this instance.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void ApplyTransformDeltaConfig(in TransformDeltaConfig config)
+ {
+ InLocalSpace = config.InLocalSpace;
+ m_NextTickSync = config.NextTickSync;
+ m_DeltaSynch = config.DeltaSynch;
+ }
+
+ ///
+ /// Determines whether the associated should be treated as parented.
+ ///
+ ///
+ /// Needs a component lookup, so it is resolved here and handed to the delta check as a value. Only
+ /// relevant while synchronizing, teleporting, or forcing a full state update.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private bool HasParentNetworkObject()
+ {
+ // This all has to do with complex nested hierarchies and how it impacts scale
+ // when set for the first time or teleporting and depends upon whether the
+ // NetworkObject is parented (or "de-parented") at the same time any scale
+ // values are applied.
+ // 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)
{
- // Increment to the next frame synch tick position for this instance
- m_NextTickSync += m_CachedTickRate;
- // If we are teleporting, we do not need to send a frame synch for this tick slot
- // as a "frame synch" really is effectively just a teleport.
- isAxisSync = !flagStates.IsTeleportingNextFrame;
- // Reset our delta synch trigger so we don't send another frame synch until we
- // send at least 1 unreliable state update after this fame synch or teleport
- m_DeltaSynch = false;
+ return false;
}
- // This is used to determine if we need to send the state update reliably (if we are doing an axial sync)
- flagStates.UnreliableFrameSync = isAxisSync;
-
- var isTeleportingAndNotSynchronizing = flagStates.IsTeleportingNextFrame && !isSynchronization;
- var isDirty = false;
- var isPositionDirty = isTeleportingAndNotSynchronizing ? flagStates.HasPositionChange : false;
- var isRotationDirty = isTeleportingAndNotSynchronizing ? flagStates.HasRotAngleChange : false;
- var isScaleDirty = isTeleportingAndNotSynchronizing ? flagStates.HasScaleChange : false;
-
- flagStates.SwitchTransformSpaceWhenParented = SwitchTransformSpaceWhenParented;
-
+ var parentNetworkObject = NetworkObject.transform.parent.GetComponent();
-
- // All of the checks below, up to the delta position checking portion, are to determine if the
- // authority changed a property during runtime that requires a full synchronizing.
-#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D
- if ((InLocalSpace != flagStates.InLocalSpace || isSynchronization) && !m_UseRigidbodyForMotion)
-#else
- if (InLocalSpace != flagStates.InLocalSpace)
-#endif
+ // In-scene placed NetworkObjects parented under a GameObject with no
+ // NetworkObject preserve their lossyScale when synchronizing.
+ if (parentNetworkObject == null && NetworkObject.InScenePlaced)
{
- // When SwitchTransformSpaceWhenParented is set we automatically set our local space based on whether
- // we are parented or not.
- flagStates.InLocalSpace = SwitchTransformSpaceWhenParented ? transform.parent != null : InLocalSpace;
- if (SwitchTransformSpaceWhenParented)
- {
- InLocalSpace = flagStates.InLocalSpace;
- }
- isDirty = true;
+ return true;
+ }
- // If we are already teleporting preserve the teleport flag.
- // If we don't have SwitchTransformSpaceWhenParented set or we are synchronizing,
- // then set the teleport flag.
- flagStates.IsTeleportingNextFrame |= !SwitchTransformSpaceWhenParented || isSynchronization;
+ // Or if the relative NetworkObject has a parent NetworkObject
+ return parentNetworkObject != null;
+ }
- // Otherwise, if SwitchTransformSpaceWhenParented is set we force a full state update.
- // If interpolation is enabled, then any non-authority instance will update any pending
- // buffered values to the correct world or local space values.
- forceState = SwitchTransformSpaceWhenParented;
- }
+ ///
+ /// Applies the transform to the .
+ ///
+ ///
+ /// Splits out to be job friendly.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private bool CheckForStateChange(ref NetworkTransformState networkState, bool isSynchronization = false, ulong targetClientId = 0, bool forceState = false)
+ {
+ var config = GetTransformDeltaConfig();
+ var flagStates = networkState.FlagStates;
+ // Resolve which transform space is being compared before sampling, otherwise the wrong set of
+ // values would be read.
+ var transformSpaceChanged = ResolveTransformSpace(ref config, ref flagStates, transform.parent != null, isSynchronization, ref forceState);
+ networkState.FlagStates = flagStates;
+ InLocalSpace = config.InLocalSpace;
#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D
var position = m_UseRigidbodyForMotion ? m_NetworkRigidbodyInternal.GetPosition() : InLocalSpace ? CachedTransform.localPosition : CachedTransform.position;
var rotation = m_UseRigidbodyForMotion ? m_NetworkRigidbodyInternal.GetRotation() : InLocalSpace ? CachedTransform.localRotation : CachedTransform.rotation;
- var positionThreshold = Vector3.one * PositionThreshold;
- var rotationThreshold = Vector3.one * RotAngleThreshold;
-
// NSS: Disabling this for the time being
// TODO: Determine if we actually need this and if not remove this from NetworkRigidBodyBase
//if (m_UseRigidbodyForMotion)
@@ -2235,353 +2266,46 @@ private bool CheckForStateChange(ref NetworkTransformState networkState, bool is
#else
var position = InLocalSpace ? CachedTransform.localPosition : CachedTransform.position;
var rotation = InLocalSpace ? CachedTransform.localRotation : CachedTransform.rotation;
- var positionThreshold = Vector3.one * PositionThreshold;
- var rotationThreshold = Vector3.one * RotAngleThreshold;
#endif
- var rotAngles = rotation.eulerAngles;
- var scale = CachedTransform.localScale;
- flagStates.IsSynchronizing = isSynchronization;
-
- // Check for parenting when synchronizing and/or teleporting
- if (isSynchronization || flagStates.IsTeleportingNextFrame || forceState)
- {
- // This all has to do with complex nested hierarchies and how it impacts scale
- // when set for the first time or teleporting and depends upon whether the
- // NetworkObject is parented (or "de-parented") at the same time any 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.InScenePlaced)
- {
- hasParentNetworkObject = true;
- }
- else
- {
- // Or if the relative NetworkObject has a parent NetworkObject
- hasParentNetworkObject = parentNetworkObject != null;
- }
- }
-
- flagStates.IsParented = hasParentNetworkObject;
- }
-
- if (Interpolate != flagStates.UseInterpolation)
- {
- flagStates.UseInterpolation = Interpolate;
- isDirty = true;
- // When we change from interpolating to not interpolating (or vice versa) we need to synchronize/reset everything
- flagStates.IsTeleportingNextFrame = true;
- }
-
- if (UseQuaternionSynchronization != flagStates.QuaternionSync)
- {
- flagStates.QuaternionSync = UseQuaternionSynchronization;
- isDirty = true;
- flagStates.IsTeleportingNextFrame = true;
- }
-
- if (UseQuaternionCompression != flagStates.QuaternionCompression)
- {
- flagStates.QuaternionCompression = UseQuaternionCompression;
- isDirty = true;
- flagStates.IsTeleportingNextFrame = true;
- }
-
- if (UseHalfFloatPrecision != flagStates.UseHalfFloatPrecision)
- {
- flagStates.UseHalfFloatPrecision = UseHalfFloatPrecision;
- isDirty = true;
- flagStates.IsTeleportingNextFrame = true;
- }
-
- if (SlerpPosition != flagStates.UsePositionSlerp)
- {
- flagStates.UsePositionSlerp = SlerpPosition;
- isDirty = true;
- flagStates.IsTeleportingNextFrame = true;
- }
-
- if (UseUnreliableDeltas != flagStates.UseUnreliableDeltas)
+ var sample = new TransformSample()
{
- flagStates.UseUnreliableDeltas = UseUnreliableDeltas;
- isDirty = true;
- flagStates.IsTeleportingNextFrame = true;
- }
-
- // Begin delta checks against last sent state update
- if (!UseHalfFloatPrecision)
- {
- if (SyncPositionX && (Mathf.Abs(networkState.PositionX - position.x) >= positionThreshold.x || flagStates.IsTeleportingNextFrame || isAxisSync || forceState))
- {
- networkState.PositionX = position.x;
- flagStates.SetHasPosition(Axis.X, true);
- isPositionDirty = true;
- }
-
- if (SyncPositionY && (Mathf.Abs(networkState.PositionY - position.y) >= positionThreshold.y || flagStates.IsTeleportingNextFrame || isAxisSync || forceState))
- {
- networkState.PositionY = position.y;
- flagStates.SetHasPosition(Axis.Y, true);
- isPositionDirty = true;
- }
-
- if (SyncPositionZ && (Mathf.Abs(networkState.PositionZ - position.z) >= positionThreshold.z || flagStates.IsTeleportingNextFrame || isAxisSync || forceState))
- {
- networkState.PositionZ = position.z;
- flagStates.SetHasPosition(Axis.Z, true);
- isPositionDirty = true;
- }
- }
- else if (SynchronizePosition)
- {
- // If we are teleporting then we can skip the delta threshold check
- isPositionDirty = flagStates.IsTeleportingNextFrame || isAxisSync || forceState;
- if (m_HalfFloatTargetTickOwnership > CurrentTick)
- {
- isPositionDirty = true;
- }
-
- // For NetworkDeltaPosition, if any axial value is dirty then we always send a full update
- if (!isPositionDirty)
- {
- for (int i = 0; i < 3; i++)
- {
- if (Math.Abs(position[i] - m_HalfPositionState.PreviousPosition[i]) >= positionThreshold[i])
- {
- isPositionDirty = i == 0 ? SyncPositionX : i == 1 ? SyncPositionY : SyncPositionZ;
- if (!isPositionDirty)
- {
- continue;
- }
- break;
- }
- }
- }
-
- // If the position is dirty or we are teleporting (which includes synchronization)
- // then determine what parts of the NetworkDeltaPosition should be updated
- if (isPositionDirty)
- {
- // If we are not synchronizing the transform state for the first time
- if (!isSynchronization)
- {
- // With global teleporting (broadcast to all non-authority instances)
- // we re-initialize authority's NetworkDeltaPosition and synchronize all
- // non-authority instances with the new full precision position
- if (flagStates.IsTeleportingNextFrame)
- {
- m_HalfPositionState = new NetworkDeltaPosition(position, networkState.NetworkTick, math.bool3(SyncPositionX, SyncPositionY, SyncPositionZ));
- networkState.CurrentPosition = position;
- }
- else // Otherwise, just synchronize the delta position value
- {
- m_HalfPositionState.HalfVector3.AxisToSynchronize = math.bool3(SyncPositionX, SyncPositionY, SyncPositionZ);
- m_HalfPositionState.UpdateFrom(ref position, networkState.NetworkTick);
- }
-
- networkState.NetworkDeltaPosition = m_HalfPositionState;
-
- // If ownership offset is greater or we are doing an axial synchronization then synchronize the base position
- if ((m_HalfFloatTargetTickOwnership > CurrentTick || isAxisSync) && !flagStates.IsTeleportingNextFrame)
- {
- flagStates.SynchronizeBaseHalfFloat = true;
- }
- else
- {
- flagStates.SynchronizeBaseHalfFloat = UseUnreliableDeltas ? m_HalfPositionState.CollapsedDeltaIntoBase : false;
- }
- }
- else // If synchronizing is set, then use the current full position value on the server side
- {
- if (ShouldSynchronizeHalfFloat(targetClientId))
- {
- // If we have a NetworkDeltaPosition that has a state applied, then we want to determine
- // what needs to be synchronized. For owner authoritative mode, the server side
- // will have no valid state yet.
- if (m_HalfPositionState.NetworkTick > 0)
- {
- // Always synchronize the base position and the ushort values of the
- // current m_HalfPositionState
- networkState.CurrentPosition = m_HalfPositionState.CurrentBasePosition;
- networkState.NetworkDeltaPosition = m_HalfPositionState;
- // If the server is the owner, in both server and owner authoritative modes,
- // or we are running in server authoritative mode, then we use the
- // HalfDeltaConvertedBack value as the delta position
- if (NetworkObject.IsOwnedByServer || IsServerAuthoritative())
- {
- networkState.DeltaPosition = m_HalfPositionState.HalfDeltaConvertedBack;
- }
- else
- {
- // Otherwise, we are in owner authoritative mode and the server's NetworkDeltaPosition
- // state is "non-authoritative" relative so we use the DeltaPosition.
- networkState.DeltaPosition = m_HalfPositionState.DeltaPosition;
- }
- }
- else // Reset everything and just send the current position
- {
- networkState.NetworkDeltaPosition = new NetworkDeltaPosition(Vector3.zero, 0, math.bool3(SyncPositionX, SyncPositionY, SyncPositionZ));
- networkState.DeltaPosition = Vector3.zero;
- networkState.CurrentPosition = position;
- }
- }
- else
- {
- networkState.NetworkDeltaPosition = new NetworkDeltaPosition(Vector3.zero, 0, math.bool3(SyncPositionX, SyncPositionY, SyncPositionZ));
- networkState.CurrentPosition = position;
- }
- // Add log entry for this update relative to the client being synchronized
- AddLogEntry(ref networkState, targetClientId, true);
- }
- flagStates.HasPositionX = SyncPositionX;
- flagStates.HasPositionY = SyncPositionY;
- flagStates.HasPositionZ = SyncPositionZ;
- flagStates.HasPositionChange = SyncPositionX || SyncPositionY || SyncPositionZ;
- }
- }
+ Position = position,
+ Rotation = rotation,
+ RotAngles = NetworkTransformMath.EulerAngles(rotation),
+ Scale = CachedTransform.localScale,
+ };
- if (!UseQuaternionSynchronization)
+ // Only resolved when it can actually be consumed, since both of these are lookups.
+ if (isSynchronization || networkState.FlagStates.IsTeleportingNextFrame || forceState)
{
- if (SyncRotAngleX && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleX, rotAngles.x)) >= rotationThreshold.x || flagStates.IsTeleportingNextFrame || isAxisSync || forceState))
- {
- networkState.RotAngleX = rotAngles.x;
- flagStates.SetHasRotation(Axis.X, true);
- isRotationDirty = true;
- }
-
- if (SyncRotAngleY && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleY, rotAngles.y)) >= rotationThreshold.y || flagStates.IsTeleportingNextFrame || isAxisSync || forceState))
- {
- networkState.RotAngleY = rotAngles.y;
- flagStates.SetHasRotation(Axis.Y, true);
- isRotationDirty = true;
- }
-
- if (SyncRotAngleZ && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleZ, rotAngles.z)) >= rotationThreshold.z || flagStates.IsTeleportingNextFrame || isAxisSync || forceState))
- {
- networkState.RotAngleZ = rotAngles.z;
- flagStates.SetHasRotation(Axis.Z, true);
- isRotationDirty = true;
- }
+ sample.HasParentNetworkObject = HasParentNetworkObject();
+ sample.LossyScale = CachedTransform.lossyScale;
}
- else if (SynchronizeRotation)
+ else if (networkState.FlagStates.IsParented)
{
- // If we are teleporting then we can skip the delta threshold check
- isRotationDirty = flagStates.IsTeleportingNextFrame || isAxisSync || forceState;
- // For quaternion synchronization, if one angle is dirty we send a full update
- if (!isRotationDirty)
- {
- var previousRotation = networkState.Rotation.eulerAngles;
- for (int i = 0; i < 3; i++)
- {
- if (Mathf.Abs(Mathf.DeltaAngle(previousRotation[i], rotAngles[i])) >= rotationThreshold[i])
- {
- isRotationDirty = true;
- break;
- }
- }
- }
- if (isRotationDirty)
- {
- networkState.Rotation = rotation;
- flagStates.MarkChanged(AxialType.Rotation, true);
- }
+ // IsParented can still be set from a previous state update while none of the conditions above
+ // are met, and the delta check itself can raise the teleport flag after this point (a change to
+ // any of the interpolation or precision settings does so). Both together are what makes the
+ // lossy scale get written, so it has to be sampled here as well.
+ sample.LossyScale = CachedTransform.lossyScale;
}
- // For scale, we need to check for parenting when synchronizing and/or teleporting (synchronization is always teleporting)
- if (flagStates.IsTeleportingNextFrame)
+ if (isSynchronization)
{
- // 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 (flagStates.IsParented)
- {
- networkState.LossyScale = CachedTransform.lossyScale;
- }
+ sample.ShouldSynchronizeHalfFloat = ShouldSynchronizeHalfFloat(targetClientId);
+ sample.UseHalfDeltaConvertedBack = NetworkObject.IsOwnedByServer || IsServerAuthoritative();
}
- // Checking scale deltas when not synchronizing
- if (!isSynchronization)
- {
- if (!UseHalfFloatPrecision)
- {
- if (SyncScaleX && (Mathf.Abs(networkState.ScaleX - scale.x) >= ScaleThreshold || flagStates.IsTeleportingNextFrame || isAxisSync || forceState))
- {
- networkState.ScaleX = scale.x;
- flagStates.SetHasScale(Axis.X, true);
- isScaleDirty = true;
- }
+ var isDirty = CheckForStateChange(ref networkState, ref m_HalfPositionState, ref config, sample, isSynchronization, forceState, transformSpaceChanged);
- if (SyncScaleY && (Mathf.Abs(networkState.ScaleY - scale.y) >= ScaleThreshold || flagStates.IsTeleportingNextFrame || isAxisSync || forceState))
- {
- networkState.ScaleY = scale.y;
- flagStates.SetHasScale(Axis.Y, true);
- isScaleDirty = true;
- }
+ ApplyTransformDeltaConfig(config);
- if (SyncScaleZ && (Mathf.Abs(networkState.ScaleZ - scale.z) >= ScaleThreshold || flagStates.IsTeleportingNextFrame || isAxisSync || forceState))
- {
- networkState.ScaleZ = scale.z;
- flagStates.SetHasScale(Axis.Z, true);
- isScaleDirty = true;
- }
- }
- else if (SynchronizeScale)
- {
- var previousScale = networkState.Scale;
- for (int i = 0; i < 3; i++)
- {
- if (Mathf.Abs(scale[i] - previousScale[i]) >= ScaleThreshold || flagStates.IsTeleportingNextFrame || isAxisSync || forceState)
- {
- isScaleDirty = true;
- networkState.Scale[i] = scale[i];
- flagStates.SetHasScale((Axis)i, i == 0 ? SyncScaleX : i == 1 ? SyncScaleY : SyncScaleZ);
- }
- }
- }
- }
- // Just apply the full local scale when synchronizing
- else if (SynchronizeScale)
- {
- var localScale = CachedTransform.localScale;
- if (!UseHalfFloatPrecision)
- {
-
- networkState.ScaleX = localScale.x;
- networkState.ScaleY = localScale.y;
- networkState.ScaleZ = localScale.z;
- }
- else
- {
- networkState.Scale = localScale;
- }
- flagStates.MarkChanged(AxialType.Scale, true);
- isScaleDirty = true;
- }
- isDirty |= isPositionDirty || isRotationDirty || isScaleDirty;
-
- if (isDirty)
+ if (config.LogSynchronizationEntry)
{
- // Some integration/unit tests disable the NetworkTransform and there is no
- // NetworkManager
- if (enabled)
- {
- // We use the NetworkTickSystem version since ServerTime is set when updating ticks
- networkState.NetworkTick = CurrentTick;
- }
+ // Add log entry for this update relative to the client being synchronized
+ AddLogEntry(ref networkState, targetClientId, true);
}
- // Mark the state dirty for the next network tick update to clear out the bitset values
- flagStates.IsDirty |= isDirty;
-
- // Apply any flag state changes
- networkState.FlagStates = flagStates;
return isDirty;
}
@@ -2614,7 +2338,7 @@ private void OnNetworkTick(bool isCalledFromParent = false)
#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D
// Let the parent handle the updating of this to keep the two synchronized
- if (!isCalledFromParent && m_UseRigidbodyForMotion && m_NetworkRigidbodyInternal.ParentBody != null && !m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame)
+ if (!isCalledFromParent && m_UseRigidbodyForMotion && m_NetworkRigidbodyInternal.ParentBody != null && !m_LocalAuthoritativeNetworkState.FlagStates.IsTeleportingNextFrame)
{
return;
}
diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransformDeltaCheck.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransformDeltaCheck.cs
new file mode 100644
index 0000000000..e541400265
--- /dev/null
+++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransformDeltaCheck.cs
@@ -0,0 +1,605 @@
+using System.Runtime.CompilerServices;
+using Unity.Mathematics;
+using UnityEngine;
+
+namespace Unity.Netcode.Components
+{
+ public partial class NetworkTransform
+ {
+ ///
+ /// Abstraction layer config:
+ /// Everything needs from the instance it is checking.
+ ///
+ ///
+ /// Separates the delta check from itself, which is what lets it run both
+ /// on the main thread and from within a job.
+ /// A few members are read/write: the check can change the transform space it operates in and it
+ /// advances the axial frame synchronization bookkeeping, both of which have to make it back to the
+ /// instance.
+ ///
+ internal struct TransformDeltaConfig
+ {
+ internal float PositionThreshold;
+ internal float RotAngleThreshold;
+ internal float ScaleThreshold;
+
+ internal bool SyncPositionX;
+ internal bool SyncPositionY;
+ internal bool SyncPositionZ;
+ internal bool SyncRotAngleX;
+ internal bool SyncRotAngleY;
+ internal bool SyncRotAngleZ;
+ internal bool SyncScaleX;
+ internal bool SyncScaleY;
+ internal bool SyncScaleZ;
+
+ internal bool UseQuaternionSynchronization;
+ internal bool UseQuaternionCompression;
+ internal bool UseHalfFloatPrecision;
+ internal bool SlerpPosition;
+ internal bool Interpolate;
+ internal bool UseUnreliableDeltas;
+ internal bool SwitchTransformSpaceWhenParented;
+ internal bool UseRigidbodyForMotion;
+
+ ///
+ /// Read/write. can change this when
+ /// is enabled.
+ ///
+ internal bool InLocalSpace;
+
+ internal int CurrentTick;
+ internal int CachedTickRate;
+ internal int HalfFloatTargetTickOwnership;
+
+ ///
+ /// Read/write. The next tick to send an axial frame synchronization, if a delta has been sent
+ /// since the last one.
+ ///
+ internal int NextTickSync;
+
+ ///
+ /// Read/write. Whether a delta has been sent since the last axial frame synchronization.
+ ///
+ internal bool DeltaSynch;
+
+ ///
+ /// Some integration and unit tests disable the , in which case the
+ /// network tick is not applied to the state.
+ ///
+ internal bool Enabled;
+
+ ///
+ /// Write only. Set when the synchronization path produced a state that the (debug only) log entry
+ /// handler should be given. Reported back as opposed to being invoked inline so that the delta
+ /// check itself stays free of anything that cannot run within a job.
+ ///
+ internal bool LogSynchronizationEntry;
+ }
+
+ ///
+ /// Abstraction layer struct:
+ /// The transform values compares against, along with the handful of
+ /// lookups that can only be resolved on the main thread.
+ ///
+ ///
+ /// The position and rotation are already resolved for local versus world space and for whether a
+ /// rigidbody is driving the motion, so the delta check never has to touch a
+ /// or a rigidbody itself.
+ ///
+ internal struct TransformSample
+ {
+ internal Vector3 Position;
+ internal Quaternion Rotation;
+ internal Vector3 RotAngles;
+ internal Vector3 Scale;
+ internal Vector3 LossyScale;
+
+ ///
+ /// Whether the associated is considered parented. Resolving this needs
+ /// a lookup, so it is passed in already resolved.
+ ///
+ internal bool HasParentNetworkObject;
+
+ ///
+ /// Synchronization only. The result of for the client
+ /// being synchronized.
+ ///
+ internal bool ShouldSynchronizeHalfFloat;
+
+ ///
+ /// Synchronization only. When set, the half float delta uses the converted back value as opposed
+ /// to the full precision delta position.
+ ///
+ internal bool UseHalfDeltaConvertedBack;
+ }
+
+ ///
+ /// Abstraction layer struct:
+ /// Everything the batched delta check reads and writes for a single .
+ ///
+ ///
+ /// Held as one struct (as opposed to several parallel native arrays) so that adding or removing an
+ /// instance only ever has to keep three collections in step rather than a growing number of them.
+ ///
+ internal struct TransformDeltaEntry
+ {
+ ///
+ /// The last sent state, updated in place by the delta check.
+ ///
+ internal NetworkTransformState State;
+
+ ///
+ /// The instance's , updated in place by the delta check.
+ ///
+ internal NetworkDeltaPosition HalfPositionState;
+
+ internal TransformDeltaConfig Config;
+
+ ///
+ /// The parts of the sample that can only be resolved on the main thread. The job fills in the
+ /// transform values it reads through the .
+ ///
+ internal TransformSample Sample;
+
+ ///
+ /// Whether the transform currently has a parent, resolved on the main thread since a job cannot
+ /// walk the hierarchy.
+ ///
+ internal bool TransformHasParent;
+
+ ///
+ /// Result. Set by the job when there is a state update to send.
+ ///
+ internal bool IsDirty;
+ }
+
+ ///
+ /// Abstraction Layer Method:
+ /// Resolves the transform space for both per-instance and batched mode delta checks.
+ ///
+ ///
+ /// Runs before the transform is sampled because it determines whether the local or the world values
+ /// are the ones being compared. Kept separate (as opposed to being folded into
+ /// ) so that neither caller has to sample both transform spaces.
+ ///
+ /// The instance configuration. may be updated.
+ /// The state flags being updated.
+ /// Whether the transform currently has a parent.
+ /// Whether this is the initial synchronization of the state.
+ /// Set when the resulting state update has to be a full one.
+ /// true when the transform space changed, which makes the state dirty on its own.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static bool ResolveTransformSpace(ref TransformDeltaConfig config, ref FlagStates flagStates, bool transformHasParent, bool isSynchronization, ref bool forceState)
+ {
+ // All of the checks below, up to the delta position checking portion, are to determine if the
+ // authority changed a property during runtime that requires a full synchronizing.
+#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D
+ if (config.UseRigidbodyForMotion || (config.InLocalSpace == flagStates.InLocalSpace && !isSynchronization))
+ {
+ return false;
+ }
+#else
+ if (config.InLocalSpace == flagStates.InLocalSpace)
+ {
+ return false;
+ }
+#endif
+
+ // When SwitchTransformSpaceWhenParented is set we automatically set our local space based on whether
+ // we are parented or not.
+ flagStates.InLocalSpace = config.SwitchTransformSpaceWhenParented ? transformHasParent : config.InLocalSpace;
+ if (config.SwitchTransformSpaceWhenParented)
+ {
+ config.InLocalSpace = flagStates.InLocalSpace;
+ }
+
+ // If we are already teleporting preserve the teleport flag.
+ // If we don't have SwitchTransformSpaceWhenParented set or we are synchronizing,
+ // then set the teleport flag.
+ flagStates.IsTeleportingNextFrame |= !config.SwitchTransformSpaceWhenParented || isSynchronization;
+
+ // Otherwise, if SwitchTransformSpaceWhenParented is set we force a full state update.
+ // If interpolation is enabled, then any non-authority instance will update any pending
+ // buffered values to the correct world or local space values.
+ forceState = config.SwitchTransformSpaceWhenParented;
+ return true;
+ }
+
+ ///
+ /// Abstraction Layer Method:
+ /// Determines whether the sampled transform differs from the last state that was sent.
+ ///
+ ///
+ /// This is primary delta check implementation.
+ /// When running in per-instance mode, invokes this on a per instance basis.
+ /// When running in batched synchronization mode, this is invoked from within the job.
+ ///
+ /// The last sent state, updated in place with any changes.
+ /// The instance's , updated in place.
+ /// The instance configuration, some of which is updated in place.
+ /// The sampled transform values.
+ /// Whether this is the initial synchronization of the state.
+ /// Whether a full state update is being forced.
+ /// The result of .
+ /// true when there is a state update to send.
+ internal static bool CheckForStateChange(ref NetworkTransformState networkState, ref NetworkDeltaPosition halfPositionState,
+ ref TransformDeltaConfig config, in TransformSample sample, bool isSynchronization, bool forceState, bool transformSpaceChanged)
+ {
+ var flagStates = networkState.FlagStates;
+
+ // As long as we are not doing our first synchronization and we are sending unreliable deltas, each
+ // NetworkTransform will stagger its full transform synchronization over a 1 second period based on the
+ // assigned tick slot (m_TickSync).
+ // More about DeltaSynch:
+ // If we have not sent any deltas since our last frame synch, then this will prevent us from sending
+ // frame synch's when the object is at rest. If this is false and a state update is detected and sent,
+ // then it will be set to true and each subsequent tick will do this check to determine if it should
+ // send a full frame synch.
+ var isAxisSync = false;
+ // We compare against the NetworkTickSystem version since ServerTime is set when updating ticks
+ if (config.UseUnreliableDeltas && !isSynchronization && config.DeltaSynch && config.NextTickSync <= config.CurrentTick)
+ {
+ // Increment to the next frame synch tick position for this instance
+ config.NextTickSync += config.CachedTickRate;
+ // If we are teleporting, we do not need to send a frame synch for this tick slot
+ // as a "frame synch" really is effectively just a teleport.
+ isAxisSync = !flagStates.IsTeleportingNextFrame;
+ // Reset our delta synch trigger so we don't send another frame synch until we
+ // send at least 1 unreliable state update after this frame synch or teleport
+ config.DeltaSynch = false;
+ }
+
+ // This is used to determine if we need to send the state update reliably (if we are doing an axial sync)
+ flagStates.UnreliableFrameSync = isAxisSync;
+
+ var isTeleportingAndNotSynchronizing = flagStates.IsTeleportingNextFrame && !isSynchronization;
+ // The transform space changing is a state change on its own.
+ var isDirty = transformSpaceChanged;
+ var isPositionDirty = isTeleportingAndNotSynchronizing ? flagStates.HasPositionChange : false;
+ var isRotationDirty = isTeleportingAndNotSynchronizing ? flagStates.HasRotAngleChange : false;
+ var isScaleDirty = isTeleportingAndNotSynchronizing ? flagStates.HasScaleChange : false;
+
+ flagStates.SwitchTransformSpaceWhenParented = config.SwitchTransformSpaceWhenParented;
+
+ var position = sample.Position;
+ var rotation = sample.Rotation;
+ var rotAngles = sample.RotAngles;
+ var scale = sample.Scale;
+ var positionThreshold = config.PositionThreshold;
+ var rotationThreshold = config.RotAngleThreshold;
+
+ var synchronizePosition = config.SyncPositionX || config.SyncPositionY || config.SyncPositionZ;
+ var synchronizeRotation = config.SyncRotAngleX || config.SyncRotAngleY || config.SyncRotAngleZ;
+ var synchronizeScale = config.SyncScaleX || config.SyncScaleY || config.SyncScaleZ;
+
+ flagStates.IsSynchronizing = isSynchronization;
+
+ // Check for parenting when synchronizing and/or teleporting
+ if (isSynchronization || flagStates.IsTeleportingNextFrame || forceState)
+ {
+ // This all has to do with complex nested hierarchies and how it impacts scale
+ // when set for the first time or teleporting and depends upon whether the
+ // NetworkObject is parented (or "de-parented") at the same time any scale
+ // values are applied.
+ flagStates.IsParented = sample.HasParentNetworkObject;
+ }
+
+ if (config.Interpolate != flagStates.UseInterpolation)
+ {
+ flagStates.UseInterpolation = config.Interpolate;
+ isDirty = true;
+ // When we change from interpolating to not interpolating (or vice versa) we need to synchronize/reset everything
+ flagStates.IsTeleportingNextFrame = true;
+ }
+
+ if (config.UseQuaternionSynchronization != flagStates.QuaternionSync)
+ {
+ flagStates.QuaternionSync = config.UseQuaternionSynchronization;
+ isDirty = true;
+ flagStates.IsTeleportingNextFrame = true;
+ }
+
+ if (config.UseQuaternionCompression != flagStates.QuaternionCompression)
+ {
+ flagStates.QuaternionCompression = config.UseQuaternionCompression;
+ isDirty = true;
+ flagStates.IsTeleportingNextFrame = true;
+ }
+
+ if (config.UseHalfFloatPrecision != flagStates.UseHalfFloatPrecision)
+ {
+ flagStates.UseHalfFloatPrecision = config.UseHalfFloatPrecision;
+ isDirty = true;
+ flagStates.IsTeleportingNextFrame = true;
+ }
+
+ if (config.SlerpPosition != flagStates.UsePositionSlerp)
+ {
+ flagStates.UsePositionSlerp = config.SlerpPosition;
+ isDirty = true;
+ flagStates.IsTeleportingNextFrame = true;
+ }
+
+ if (config.UseUnreliableDeltas != flagStates.UseUnreliableDeltas)
+ {
+ flagStates.UseUnreliableDeltas = config.UseUnreliableDeltas;
+ isDirty = true;
+ flagStates.IsTeleportingNextFrame = true;
+ }
+
+ // Begin delta checks against last sent state update
+ if (!config.UseHalfFloatPrecision)
+ {
+ if (config.SyncPositionX && (math.abs(networkState.PositionX - position.x) >= positionThreshold || flagStates.IsTeleportingNextFrame || isAxisSync || forceState))
+ {
+ networkState.PositionX = position.x;
+ flagStates.SetHasPosition(Axis.X, true);
+ isPositionDirty = true;
+ }
+
+ if (config.SyncPositionY && (math.abs(networkState.PositionY - position.y) >= positionThreshold || flagStates.IsTeleportingNextFrame || isAxisSync || forceState))
+ {
+ networkState.PositionY = position.y;
+ flagStates.SetHasPosition(Axis.Y, true);
+ isPositionDirty = true;
+ }
+
+ if (config.SyncPositionZ && (math.abs(networkState.PositionZ - position.z) >= positionThreshold || flagStates.IsTeleportingNextFrame || isAxisSync || forceState))
+ {
+ networkState.PositionZ = position.z;
+ flagStates.SetHasPosition(Axis.Z, true);
+ isPositionDirty = true;
+ }
+ }
+ else if (synchronizePosition)
+ {
+ // If we are teleporting then we can skip the delta threshold check
+ isPositionDirty = flagStates.IsTeleportingNextFrame || isAxisSync || forceState;
+ if (config.HalfFloatTargetTickOwnership > config.CurrentTick)
+ {
+ isPositionDirty = true;
+ }
+
+ // For NetworkDeltaPosition, if any axial value is dirty then we always send a full update.
+ // Unrolled (as opposed to indexing into the Vector3s) since the indexer is a bounds checked
+ // property as opposed to a direct field access.
+ if (!isPositionDirty)
+ {
+ var previousPosition = halfPositionState.PreviousPosition;
+ isPositionDirty = (config.SyncPositionX && math.abs(position.x - previousPosition.x) >= positionThreshold)
+ || (config.SyncPositionY && math.abs(position.y - previousPosition.y) >= positionThreshold)
+ || (config.SyncPositionZ && math.abs(position.z - previousPosition.z) >= positionThreshold);
+ }
+
+ // If the position is dirty or we are teleporting (which includes synchronization)
+ // then determine what parts of the NetworkDeltaPosition should be updated
+ if (isPositionDirty)
+ {
+ var axisToSynchronize = math.bool3(config.SyncPositionX, config.SyncPositionY, config.SyncPositionZ);
+
+ // If we are not synchronizing the transform state for the first time
+ if (!isSynchronization)
+ {
+ // With global teleporting (broadcast to all non-authority instances)
+ // we re-initialize authority's NetworkDeltaPosition and synchronize all
+ // non-authority instances with the new full precision position
+ if (flagStates.IsTeleportingNextFrame)
+ {
+ halfPositionState = new NetworkDeltaPosition(position, networkState.NetworkTick, axisToSynchronize);
+ networkState.CurrentPosition = position;
+ }
+ else // Otherwise, just synchronize the delta position value
+ {
+ halfPositionState.HalfVector3.AxisToSynchronize = axisToSynchronize;
+ halfPositionState.UpdateFrom(ref position, networkState.NetworkTick);
+ }
+
+ networkState.NetworkDeltaPosition = halfPositionState;
+
+ // If ownership offset is greater or we are doing an axial synchronization then synchronize the base position
+ if ((config.HalfFloatTargetTickOwnership > config.CurrentTick || isAxisSync) && !flagStates.IsTeleportingNextFrame)
+ {
+ flagStates.SynchronizeBaseHalfFloat = true;
+ }
+ else
+ {
+ flagStates.SynchronizeBaseHalfFloat = config.UseUnreliableDeltas ? halfPositionState.CollapsedDeltaIntoBase : false;
+ }
+ }
+ else // If synchronizing is set, then use the current full position value on the server side
+ {
+ if (sample.ShouldSynchronizeHalfFloat)
+ {
+ // If we have a NetworkDeltaPosition that has a state applied, then we want to determine
+ // what needs to be synchronized. For owner authoritative mode, the server side
+ // will have no valid state yet.
+ if (halfPositionState.NetworkTick > 0)
+ {
+ // Always synchronize the base position and the ushort values of the
+ // current halfPositionState
+ networkState.CurrentPosition = halfPositionState.CurrentBasePosition;
+ networkState.NetworkDeltaPosition = halfPositionState;
+ // If the server is the owner, in both server and owner authoritative modes,
+ // or we are running in server authoritative mode, then we use the
+ // HalfDeltaConvertedBack value as the delta position
+ if (sample.UseHalfDeltaConvertedBack)
+ {
+ networkState.DeltaPosition = halfPositionState.HalfDeltaConvertedBack;
+ }
+ else
+ {
+ // Otherwise, we are in owner authoritative mode and the server's NetworkDeltaPosition
+ // state is "non-authoritative" relative so we use the DeltaPosition.
+ networkState.DeltaPosition = halfPositionState.DeltaPosition;
+ }
+ }
+ else // Reset everything and just send the current position
+ {
+ networkState.NetworkDeltaPosition = new NetworkDeltaPosition(Vector3.zero, 0, axisToSynchronize);
+ networkState.DeltaPosition = Vector3.zero;
+ networkState.CurrentPosition = position;
+ }
+ }
+ else
+ {
+ networkState.NetworkDeltaPosition = new NetworkDeltaPosition(Vector3.zero, 0, axisToSynchronize);
+ networkState.CurrentPosition = position;
+ }
+ // Report that a log entry should be added for this update relative to the client being
+ // synchronized. The caller invokes the handler once this returns.
+ config.LogSynchronizationEntry = true;
+ }
+ flagStates.HasPositionX = config.SyncPositionX;
+ flagStates.HasPositionY = config.SyncPositionY;
+ flagStates.HasPositionZ = config.SyncPositionZ;
+ flagStates.HasPositionChange = config.SyncPositionX || config.SyncPositionY || config.SyncPositionZ;
+ }
+ }
+
+ if (!config.UseQuaternionSynchronization)
+ {
+ if (config.SyncRotAngleX && (math.abs(NetworkTransformMath.DeltaAngle(networkState.RotAngleX, rotAngles.x)) >= rotationThreshold || flagStates.IsTeleportingNextFrame || isAxisSync || forceState))
+ {
+ networkState.RotAngleX = rotAngles.x;
+ flagStates.SetHasRotation(Axis.X, true);
+ isRotationDirty = true;
+ }
+
+ if (config.SyncRotAngleY && (math.abs(NetworkTransformMath.DeltaAngle(networkState.RotAngleY, rotAngles.y)) >= rotationThreshold || flagStates.IsTeleportingNextFrame || isAxisSync || forceState))
+ {
+ networkState.RotAngleY = rotAngles.y;
+ flagStates.SetHasRotation(Axis.Y, true);
+ isRotationDirty = true;
+ }
+
+ if (config.SyncRotAngleZ && (math.abs(NetworkTransformMath.DeltaAngle(networkState.RotAngleZ, rotAngles.z)) >= rotationThreshold || flagStates.IsTeleportingNextFrame || isAxisSync || forceState))
+ {
+ networkState.RotAngleZ = rotAngles.z;
+ flagStates.SetHasRotation(Axis.Z, true);
+ isRotationDirty = true;
+ }
+ }
+ else if (synchronizeRotation)
+ {
+ // If we are teleporting then we can skip the delta threshold check
+ isRotationDirty = flagStates.IsTeleportingNextFrame || isAxisSync || forceState;
+ // For quaternion synchronization, if one angle is dirty we send a full update
+ if (!isRotationDirty)
+ {
+ // Uses NetworkTransformMath.EulerAngles so this stays free of engine bindings.
+ var previousRotation = NetworkTransformMath.EulerAngles(networkState.Rotation);
+ isRotationDirty = math.abs(NetworkTransformMath.DeltaAngle(previousRotation.x, rotAngles.x)) >= rotationThreshold
+ || math.abs(NetworkTransformMath.DeltaAngle(previousRotation.y, rotAngles.y)) >= rotationThreshold
+ || math.abs(NetworkTransformMath.DeltaAngle(previousRotation.z, rotAngles.z)) >= rotationThreshold;
+ }
+ if (isRotationDirty)
+ {
+ networkState.Rotation = rotation;
+ flagStates.MarkChanged(AxialType.Rotation, true);
+ }
+ }
+
+ // For scale, we need to check for parenting when synchronizing and/or teleporting (synchronization is always teleporting)
+ if (flagStates.IsTeleportingNextFrame)
+ {
+ // 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 (flagStates.IsParented)
+ {
+ networkState.LossyScale = sample.LossyScale;
+ }
+ }
+
+ // Checking scale deltas when not synchronizing
+ if (!isSynchronization)
+ {
+ if (!config.UseHalfFloatPrecision)
+ {
+ if (config.SyncScaleX && (math.abs(networkState.ScaleX - scale.x) >= config.ScaleThreshold || flagStates.IsTeleportingNextFrame || isAxisSync || forceState))
+ {
+ networkState.ScaleX = scale.x;
+ flagStates.SetHasScale(Axis.X, true);
+ isScaleDirty = true;
+ }
+
+ if (config.SyncScaleY && (math.abs(networkState.ScaleY - scale.y) >= config.ScaleThreshold || flagStates.IsTeleportingNextFrame || isAxisSync || forceState))
+ {
+ networkState.ScaleY = scale.y;
+ flagStates.SetHasScale(Axis.Y, true);
+ isScaleDirty = true;
+ }
+
+ if (config.SyncScaleZ && (math.abs(networkState.ScaleZ - scale.z) >= config.ScaleThreshold || flagStates.IsTeleportingNextFrame || isAxisSync || forceState))
+ {
+ networkState.ScaleZ = scale.z;
+ flagStates.SetHasScale(Axis.Z, true);
+ isScaleDirty = true;
+ }
+ }
+ else if (synchronizeScale)
+ {
+ var previousScale = networkState.Scale;
+ // Precompute if it is considered always dirty.
+ var alwaysDirty = flagStates.IsTeleportingNextFrame || isAxisSync || forceState;
+ // Use direct field assignment as opposed to indexing to avoid bounds checking.
+ if (alwaysDirty || math.abs(scale.x - previousScale.x) >= config.ScaleThreshold)
+ {
+ isScaleDirty = true;
+ networkState.Scale.x = scale.x;
+ flagStates.SetHasScale(Axis.X, config.SyncScaleX);
+ }
+
+ if (alwaysDirty || math.abs(scale.y - previousScale.y) >= config.ScaleThreshold)
+ {
+ isScaleDirty = true;
+ networkState.Scale.y = scale.y;
+ flagStates.SetHasScale(Axis.Y, config.SyncScaleY);
+ }
+
+ if (alwaysDirty || math.abs(scale.z - previousScale.z) >= config.ScaleThreshold)
+ {
+ isScaleDirty = true;
+ networkState.Scale.z = scale.z;
+ flagStates.SetHasScale(Axis.Z, config.SyncScaleZ);
+ }
+ }
+ }
+ // Just apply the full local scale when synchronizing
+ else if (synchronizeScale)
+ {
+ if (!config.UseHalfFloatPrecision)
+ {
+ networkState.ScaleX = scale.x;
+ networkState.ScaleY = scale.y;
+ networkState.ScaleZ = scale.z;
+ }
+ else
+ {
+ networkState.Scale = scale;
+ }
+ flagStates.MarkChanged(AxialType.Scale, true);
+ isScaleDirty = true;
+ }
+ isDirty |= isPositionDirty || isRotationDirty || isScaleDirty;
+
+ if (isDirty)
+ {
+ // Some integration/unit tests disable the NetworkTransform and there is no
+ // NetworkManager
+ if (config.Enabled)
+ {
+ // We use the NetworkTickSystem version since ServerTime is set when updating ticks
+ networkState.NetworkTick = config.CurrentTick;
+ }
+ }
+
+ // Mark the state dirty for the next network tick update to clear out the bitset values
+ flagStates.IsDirty |= isDirty;
+
+ // Apply any flag state changes
+ networkState.FlagStates = flagStates;
+ return isDirty;
+ }
+ }
+}
diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransformDeltaCheck.cs.meta b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransformDeltaCheck.cs.meta
new file mode 100644
index 0000000000..7519e150d4
--- /dev/null
+++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransformDeltaCheck.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: bbac16055cc27c546af7143757db776e
\ No newline at end of file