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