using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using Unity.Mathematics; using UnityEngine; namespace Unity.Netcode.Components { /// /// A component for syncing transforms. /// NetworkTransform will read the underlying transform and replicate it to clients. /// The replicated value will be automatically be interpolated (if active) and applied to the underlying GameObject's transform. /// [DisallowMultipleComponent] [AddComponentMenu("Netcode/Network Transform")] [DefaultExecutionOrder(100000)] // this is needed to catch the update time after the transform was updated by user scripts public class NetworkTransform : NetworkBehaviour { /// /// The default position change threshold value. /// Any changes above this threshold will be replicated. /// public const float PositionThresholdDefault = 0.001f; /// /// The default rotation angle change threshold value. /// Any changes above this threshold will be replicated. /// public const float RotAngleThresholdDefault = 0.01f; /// /// The default scale change threshold value. /// Any changes above this threshold will be replicated. /// public const float ScaleThresholdDefault = 0.01f; /// /// The handler delegate type that takes client requested changes and returns resulting changes handled by the server. /// /// The position requested by the client. /// The rotation requested by the client. /// The scale requested by the client. /// The resulting position, rotation and scale changes after handling. public delegate (Vector3 pos, Quaternion rotOut, Vector3 scale) OnClientRequestChangeDelegate(Vector3 pos, Quaternion rot, Vector3 scale); /// /// The handler that gets invoked when server receives a change from a client. /// This handler would be useful for server to modify pos/rot/scale before applying client's request. /// public OnClientRequestChangeDelegate OnClientRequestChange; /// /// When set each state update will contain a state identifier /// internal static bool TrackByStateId; /// /// Enabled by default. /// When set (enabled by default), NetworkTransform will send common state updates using unreliable network delivery /// to provide a higher tolerance to poor network conditions (especially packet loss). When disabled, all state updates /// are sent using a reliable fragmented sequenced network delivery. /// /// /// The following more critical state updates are still sent as reliable fragmented sequenced: /// - The initial synchronization state update /// - The teleporting state update. /// - When using half float precision and the `NetworkDeltaPosition` delta exceeds the maximum delta forcing the axis in /// question to be collapsed into the core base position, this state update will be sent as reliable fragmented sequenced. /// /// In order to preserve a continual consistency of axial values when unreliable delta messaging is enabled (due to the /// possibility of dropping packets), NetworkTransform instances will send 1 axial frame synchronization update per /// second (only for the axis marked to synchronize are sent as reliable fragmented sequenced) as long as a delta state /// update had been previously sent. When a NetworkObject is at rest, axial frame synchronization updates are not sent. /// [Tooltip("When set, NetworkTransform will send common state updates using unreliable network delivery " + "to provide a higher tolerance to poor network conditions (especially packet loss). When disabled, all state updates are " + "sent using reliable fragmented sequenced network delivery.")] public bool UseUnreliableDeltas = false; /// /// Data structure used to synchronize the /// public struct NetworkTransformState : INetworkSerializable { private const int k_InLocalSpaceBit = 0x00000001; // Persists between state updates (authority dictates if this is set) private const int k_PositionXBit = 0x00000002; private const int k_PositionYBit = 0x00000004; private const int k_PositionZBit = 0x00000008; private const int k_RotAngleXBit = 0x00000010; private const int k_RotAngleYBit = 0x00000020; private const int k_RotAngleZBit = 0x00000040; private const int k_ScaleXBit = 0x00000080; private const int k_ScaleYBit = 0x00000100; private const int k_ScaleZBit = 0x00000200; private const int k_TeleportingBit = 0x00000400; private const int k_Interpolate = 0x00000800; // Persists between state updates (authority dictates if this is set) private const int k_QuaternionSync = 0x00001000; // Persists between state updates (authority dictates if this is set) private const int k_QuaternionCompress = 0x00002000; // Persists between state updates (authority dictates if this is set) private const int k_UseHalfFloats = 0x00004000; // Persists between state updates (authority dictates if this is set) private const int k_Synchronization = 0x00008000; private const int k_PositionSlerp = 0x00010000; // Persists between state updates (authority dictates if this is set) private const int k_IsParented = 0x00020000; // When parented and synchronizing, we need to have both lossy and local scale due to varying spawn order private const int k_SynchBaseHalfFloat = 0x00040000; private const int k_ReliableSequenced = 0x00080000; private const int k_UseUnreliableDeltas = 0x00100000; private const int k_UnreliableFrameSync = 0x00200000; private const int k_TrackStateId = 0x10000000; // (Internal Debugging) When set each state update will contain a state identifier // Stores persistent and state relative flags private uint m_Bitset; internal uint BitSet { get { return m_Bitset; } set { m_Bitset = value; } } // Used to store the tick calculated sent time internal double SentTime; // Used for full precision position updates internal float PositionX, PositionY, PositionZ; // Used for full precision Euler updates internal float RotAngleX, RotAngleY, RotAngleZ; // Used for full precision quaternion updates internal Quaternion Rotation; // Used for full precision scale updates internal float ScaleX, ScaleY, ScaleZ; // Used for half precision delta position updates internal Vector3 CurrentPosition; internal Vector3 DeltaPosition; internal NetworkDeltaPosition NetworkDeltaPosition; // Used for half precision scale internal HalfVector3 HalfVectorScale; internal Vector3 Scale; internal Vector3 LossyScale; // Used for half precision quaternion internal HalfVector4 HalfVectorRotation; // Used to store a compressed quaternion internal uint QuaternionCompressed; // Authoritative and non-authoritative sides use this to determine if a NetworkTransformState is // dirty or not. internal bool IsDirty { get; set; } /// /// The last byte size of the updated. /// public int LastSerializedSize { get; internal set; } // Used for NetworkDeltaPosition delta position synchronization internal int NetworkTick; // Used when tracking by state ID is enabled internal int StateId; // Set when a state has been explicitly set (i.e. SetState) internal bool ExplicitSet; // Used during serialization private FastBufferReader m_Reader; private FastBufferWriter m_Writer; /// /// When set, the is operates in local space /// public bool InLocalSpace { get => GetFlag(k_InLocalSpaceBit); internal set { SetFlag(value, k_InLocalSpaceBit); } } // Position /// /// When set, the X-Axis position value has changed /// public bool HasPositionX { get => GetFlag(k_PositionXBit); internal set { SetFlag(value, k_PositionXBit); } } /// /// When set, the Y-Axis position value has changed /// public bool HasPositionY { get => GetFlag(k_PositionYBit); internal set { SetFlag(value, k_PositionYBit); } } /// /// When set, the Z-Axis position value has changed /// public bool HasPositionZ { get => GetFlag(k_PositionZBit); internal set { SetFlag(value, k_PositionZBit); } } /// /// When set, at least one of the position axis values has changed. /// public bool HasPositionChange { get { return HasPositionX | HasPositionY | HasPositionZ; } } // RotAngles /// /// When set, the Euler rotation X-Axis value has changed. /// /// /// When quaternion synchronization is enabled all axis are always updated. /// public bool HasRotAngleX { get => GetFlag(k_RotAngleXBit); internal set { SetFlag(value, k_RotAngleXBit); } } /// /// When set, the Euler rotation Y-Axis value has changed. /// /// /// When quaternion synchronization is enabled all axis are always updated. /// public bool HasRotAngleY { get => GetFlag(k_RotAngleYBit); internal set { SetFlag(value, k_RotAngleYBit); } } /// /// When set, the Euler rotation Z-Axis value has changed. /// /// /// When quaternion synchronization is enabled all axis are always updated. /// public bool HasRotAngleZ { get => GetFlag(k_RotAngleZBit); internal set { SetFlag(value, k_RotAngleZBit); } } /// /// When set, at least one of the rotation axis values has changed. /// /// /// When quaternion synchronization is enabled all axis are always updated. /// public bool HasRotAngleChange { get { return HasRotAngleX | HasRotAngleY | HasRotAngleZ; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal bool HasScale(int axisIndex) { return GetFlag(k_ScaleXBit << axisIndex); } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void SetHasScale(int axisIndex, bool isSet) { SetFlag(isSet, k_ScaleXBit << axisIndex); } // Scale /// /// When set, the X-Axis scale value has changed. /// public bool HasScaleX { get => GetFlag(k_ScaleXBit); internal set { SetFlag(value, k_ScaleXBit); } } /// /// When set, the Y-Axis scale value has changed. /// public bool HasScaleY { get => GetFlag(k_ScaleYBit); internal set { SetFlag(value, k_ScaleYBit); } } /// /// When set, the Z-Axis scale value has changed. /// public bool HasScaleZ { get => GetFlag(k_ScaleZBit); internal set { SetFlag(value, k_ScaleZBit); } } /// /// When set, at least one of the scale axis values has changed. /// public bool HasScaleChange { get { return HasScaleX | HasScaleY | HasScaleZ; } } /// /// When set, the current state will be treated as a teleport. /// /// /// When teleporting: /// - Interpolation is reset. /// - If using half precision, full precision values are used. /// - All axis marked to be synchronized will be updated. /// public bool IsTeleportingNextFrame { get => GetFlag(k_TeleportingBit); internal set { SetFlag(value, k_TeleportingBit); } } /// /// When set the is uses interpolation. /// /// /// Authority does not apply interpolation via . /// Authority should handle its own motion/rotation/scale smoothing locally. /// public bool UseInterpolation { get => GetFlag(k_Interpolate); internal set { SetFlag(value, k_Interpolate); } } /// /// When enabled, this instance uses synchronization. /// /// /// Use quaternion synchronization if you are nesting s and rotation can occur on both the parent and child. /// When quaternion synchronization is enabled, the entire quaternion is updated when there are any changes to any axial values. /// You can use half float precision or quaternion compression to reduce the bandwidth cost. /// public bool QuaternionSync { get => GetFlag(k_QuaternionSync); internal set { SetFlag(value, k_QuaternionSync); } } /// /// When set s will be compressed down to 4 bytes using a smallest three implementation. /// /// /// This only will be applied when is enabled. /// Half float precision provides a higher precision than quaternion compression but at the cost of 4 additional bytes per update. /// - Quaternion Compression: 4 bytes per delta update /// - Half float precision: 8 bytes per delta update /// public bool QuaternionCompression { get => GetFlag(k_QuaternionCompress); internal set { SetFlag(value, k_QuaternionCompress); } } /// /// When set, the will use half float precision for position, rotation, and scale. /// /// /// Postion is synchronized through delta position updates in order to reduce precision loss/drift and to extend to positions beyond the limitation of half float maximum values. /// Rotation and scale both use half float precision ( and ) /// public bool UseHalfFloatPrecision { get => GetFlag(k_UseHalfFloats); internal set { SetFlag(value, k_UseHalfFloats); } } /// /// When set, this indicates it is the first state being synchronized. /// Typically when the associate is spawned or a client is being synchronized after connecting to a network session in progress. /// public bool IsSynchronizing { get => GetFlag(k_Synchronization); internal set { SetFlag(value, k_Synchronization); } } /// /// Determines if position interpolation will Slerp towards its target position. /// This is only really useful if you are moving around a point in a circular pattern. /// public bool UsePositionSlerp { get => GetFlag(k_PositionSlerp); internal set { SetFlag(value, k_PositionSlerp); } } /// /// Returns whether this state update was a frame synchronization when /// UseUnreliableDeltas is enabled. When set, the entire transform will /// be or has been synchronized. /// public bool IsUnreliableFrameSync() { return UnreliableFrameSync; } /// /// Returns true if this state was sent with reliable delivery. /// If false, then it was sent with unreliable delivery. /// /// /// Unreliable delivery will only be used if is set. /// public bool IsReliableStateUpdate() { return ReliableSequenced; } internal bool IsParented { get => GetFlag(k_IsParented); set { SetFlag(value, k_IsParented); } } internal bool SynchronizeBaseHalfFloat { get => GetFlag(k_SynchBaseHalfFloat); set { SetFlag(value, k_SynchBaseHalfFloat); } } internal bool ReliableSequenced { get => GetFlag(k_ReliableSequenced); set { SetFlag(value, k_ReliableSequenced); } } internal bool UseUnreliableDeltas { get => GetFlag(k_UseUnreliableDeltas); set { SetFlag(value, k_UseUnreliableDeltas); } } internal bool UnreliableFrameSync { get => GetFlag(k_UnreliableFrameSync); set { SetFlag(value, k_UnreliableFrameSync); } } internal bool TrackByStateId { get => GetFlag(k_TrackStateId); set { SetFlag(value, k_TrackStateId); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool GetFlag(int flag) { return (m_Bitset & flag) != 0; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void SetFlag(bool set, int flag) { if (set) { m_Bitset = m_Bitset | (uint)flag; } else { m_Bitset = m_Bitset & (uint)~flag; } } internal void ClearBitSetForNextTick() { // Clear everything but flags that should persist between state updates until changed by authority m_Bitset &= k_InLocalSpaceBit | k_Interpolate | k_UseHalfFloats | k_QuaternionSync | k_QuaternionCompress | k_PositionSlerp | k_UseUnreliableDeltas; IsDirty = false; } /// /// Returns the current state's rotation. If there is no change in the rotation, /// then it will return . /// /// /// When there is no change in an updated state's rotation then there are no values to return. /// Checking for is one way to detect this. /// /// public Quaternion GetRotation() { if (HasRotAngleChange) { if (QuaternionSync) { return Rotation; } else { return Quaternion.Euler(RotAngleX, RotAngleY, RotAngleZ); } } return Quaternion.identity; } /// /// Returns the current state's position. If there is no change in position, /// then it returns . /// /// /// When there is no change in an updated state's position then there are no values to return. /// Checking for is one way to detect this. /// When used with half precision it returns the half precision delta position state update /// which will not be the full position. /// To get a NettworkTransform's full position, use and /// pass true as the parameter. /// /// public Vector3 GetPosition() { if (HasPositionChange) { if (UseHalfFloatPrecision) { if (IsTeleportingNextFrame) { return CurrentPosition; } else { return NetworkDeltaPosition.GetFullPosition(); } } else { return new Vector3(PositionX, PositionY, PositionZ); } } return Vector3.zero; } /// /// Returns the current state's scale. If there is no change in scale, /// then it returns . /// /// /// When there is no change in an updated state's scale then there are no values to return. /// Checking for is one way to detect this. /// /// public Vector3 GetScale() { if (HasScaleChange) { if (UseHalfFloatPrecision) { if (IsTeleportingNextFrame) { return Scale; } else { return HalfVectorScale.ToVector3(); } } else { return new Vector3(ScaleX, ScaleY, ScaleZ); } } return Vector3.zero; } /// /// The network tick that this state was sent by the authoritative instance. /// /// public int GetNetworkTick() { return NetworkTick; } internal HalfVector3 HalfEulerRotation; /// /// Serializes this /// public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter { // Used to calculate the LastSerializedSize value var positionStart = 0; var isWriting = serializer.IsWriter; if (isWriting) { m_Writer = serializer.GetFastBufferWriter(); positionStart = m_Writer.Position; } else { m_Reader = serializer.GetFastBufferReader(); positionStart = m_Reader.Position; } // Synchronize State Flags and Network Tick { if (isWriting) { if (UseUnreliableDeltas) { // If teleporting, synchronizing, doing an axial frame sync, or using half float precision and we collapsed a delta into the base position if (IsTeleportingNextFrame || IsSynchronizing || UnreliableFrameSync || (UseHalfFloatPrecision && NetworkDeltaPosition.CollapsedDeltaIntoBase)) { // Send the message reliably ReliableSequenced = true; } else { ReliableSequenced = false; } } else // If not using UseUnreliableDeltas, then always use reliable fragmented sequenced { ReliableSequenced = true; } BytePacker.WriteValueBitPacked(m_Writer, m_Bitset); // We use network ticks as opposed to absolute time as the authoritative // side updates on every new tick. BytePacker.WriteValueBitPacked(m_Writer, NetworkTick); } else { ByteUnpacker.ReadValueBitPacked(m_Reader, out m_Bitset); // We use network ticks as opposed to absolute time as the authoritative // side updates on every new tick. ByteUnpacker.ReadValueBitPacked(m_Reader, out NetworkTick); } } // If debugging states and track by state identifier is enabled, serialize the current state identifier if (TrackByStateId) { serializer.SerializeValue(ref StateId); } // Synchronize Position if (HasPositionChange) { if (UseHalfFloatPrecision) { NetworkDeltaPosition.SynchronizeBase = SynchronizeBaseHalfFloat; // Apply which axis should be updated for both write/read (teleporting, synchronizing, or just updating) NetworkDeltaPosition.HalfVector3.AxisToSynchronize[0] = HasPositionX; NetworkDeltaPosition.HalfVector3.AxisToSynchronize[1] = HasPositionY; NetworkDeltaPosition.HalfVector3.AxisToSynchronize[2] = HasPositionZ; if (IsTeleportingNextFrame) { // **Always use full precision when teleporting and UseHalfFloatPrecision is enabled** serializer.SerializeValue(ref CurrentPosition); // If we are synchronizing, then include the half vector position's delta offset if (IsSynchronizing) { serializer.SerializeValue(ref DeltaPosition); if (!isWriting) { NetworkDeltaPosition.NetworkTick = NetworkTick; NetworkDeltaPosition.NetworkSerialize(serializer); } else { serializer.SerializeNetworkSerializable(ref NetworkDeltaPosition); } } } else { if (!isWriting) { NetworkDeltaPosition.NetworkTick = NetworkTick; NetworkDeltaPosition.NetworkSerialize(serializer); } else { serializer.SerializeNetworkSerializable(ref NetworkDeltaPosition); } } } else // Full precision axis specific position synchronization { if (HasPositionX) { serializer.SerializeValue(ref PositionX); } if (HasPositionY) { serializer.SerializeValue(ref PositionY); } if (HasPositionZ) { serializer.SerializeValue(ref PositionZ); } } } // Synchronize Rotation if (HasRotAngleChange) { if (QuaternionSync) { // Always use the full quaternion if teleporting if (IsTeleportingNextFrame) { serializer.SerializeValue(ref Rotation); } else { // Use the quaternion compressor if enabled if (QuaternionCompression) { if (isWriting) { QuaternionCompressed = QuaternionCompressor.CompressQuaternion(ref Rotation); } serializer.SerializeValue(ref QuaternionCompressed); if (!isWriting) { QuaternionCompressor.DecompressQuaternion(ref Rotation, QuaternionCompressed); } } else { if (UseHalfFloatPrecision) { if (isWriting) { HalfVectorRotation.UpdateFrom(ref Rotation); } serializer.SerializeNetworkSerializable(ref HalfVectorRotation); if (!isWriting) { Rotation = HalfVectorRotation.ToQuaternion(); } } else { serializer.SerializeValue(ref Rotation); } } } } else // Euler Rotation Synchronization { // Half float precision (full precision when teleporting) if (UseHalfFloatPrecision && !IsTeleportingNextFrame) { if (HasRotAngleChange) { // Apply which axis should be updated for both write/read HalfEulerRotation.AxisToSynchronize[0] = HasRotAngleX; HalfEulerRotation.AxisToSynchronize[1] = HasRotAngleY; HalfEulerRotation.AxisToSynchronize[2] = HasRotAngleZ; if (isWriting) { HalfEulerRotation.Set(RotAngleX, RotAngleY, RotAngleZ); } serializer.SerializeValue(ref HalfEulerRotation); if (!isWriting) { var eulerRotation = HalfEulerRotation.ToVector3(); if (HasRotAngleX) { RotAngleX = eulerRotation.x; } if (HasRotAngleY) { RotAngleY = eulerRotation.y; } if (HasRotAngleZ) { RotAngleZ = eulerRotation.z; } } } } else // Full precision Euler { // RotAngle Values if (HasRotAngleX) { serializer.SerializeValue(ref RotAngleX); } if (HasRotAngleY) { serializer.SerializeValue(ref RotAngleY); } if (HasRotAngleZ) { serializer.SerializeValue(ref RotAngleZ); } } } } // Synchronize Scale if (HasScaleChange) { // If we are teleporting (which includes synchronizing) and the associated NetworkObject has a parent // then we want to serialize the LossyScale since NetworkObject spawn order is not guaranteed if (IsTeleportingNextFrame && IsParented) { serializer.SerializeValue(ref LossyScale); } // Half precision scale synchronization if (UseHalfFloatPrecision) { if (IsTeleportingNextFrame) { serializer.SerializeValue(ref Scale); } else { // Apply which axis should be updated for both write/read HalfVectorScale.AxisToSynchronize[0] = HasScaleX; HalfVectorScale.AxisToSynchronize[1] = HasScaleY; HalfVectorScale.AxisToSynchronize[2] = HasScaleZ; // For scale, when half precision is enabled we can still only send the axis with deltas if (isWriting) { HalfVectorScale.Set(Scale[0], Scale[1], Scale[2]); } serializer.SerializeValue(ref HalfVectorScale); if (!isWriting) { Scale = HalfVectorScale.ToVector3(); if (HasScaleX) { ScaleX = Scale.x; } if (HasScaleY) { ScaleY = Scale.y; } if (HasScaleZ) { ScaleZ = Scale.x; } } } } else // Full precision scale synchronization { if (HasScaleX) { serializer.SerializeValue(ref ScaleX); } if (HasScaleY) { serializer.SerializeValue(ref ScaleY); } if (HasScaleZ) { serializer.SerializeValue(ref ScaleZ); } } } // Only if we are receiving state if (!isWriting) { // Go ahead and mark the local state dirty IsDirty = HasPositionChange || HasRotAngleChange || HasScaleChange; LastSerializedSize = m_Reader.Position - positionStart; } else { LastSerializedSize = m_Writer.Position - positionStart; } } } /// /// When enabled (default), the x component of position will be synchronized by authority. /// /// /// Changes to this on non-authoritative instances has no effect. /// public bool SyncPositionX = true; /// /// When enabled (default), the y component of position will be synchronized by authority. /// /// /// Changes to this on non-authoritative instances has no effect. /// public bool SyncPositionY = true; /// /// When enabled (default), the z component of position will be synchronized by authority. /// /// /// Changes to this on non-authoritative instances has no effect. /// public bool SyncPositionZ = true; private bool SynchronizePosition { get { return SyncPositionX || SyncPositionY || SyncPositionZ; } } /// /// When enabled (default), the x component of rotation will be synchronized by authority. /// /// /// When is enabled this does not apply. /// Changes to this on non-authoritative instances has no effect. /// public bool SyncRotAngleX = true; /// /// When enabled (default), the y component of rotation will be synchronized by authority. /// /// /// When is enabled this does not apply. /// Changes to this on non-authoritative instances has no effect. /// public bool SyncRotAngleY = true; /// /// When enabled (default), the z component of rotation will be synchronized by authority. /// /// /// When is enabled this does not apply. /// Changes to this on non-authoritative instances has no effect. /// public bool SyncRotAngleZ = true; private bool SynchronizeRotation { get { return SyncRotAngleX || SyncRotAngleY || SyncRotAngleZ; } } /// /// When enabled (default), the x component of scale will be synchronized by authority. /// /// /// Changes to this on non-authoritative instances has no effect. /// public bool SyncScaleX = true; /// /// When enabled (default), the y component of scale will be synchronized by authority. /// /// /// Changes to this on non-authoritative instances has no effect. /// public bool SyncScaleY = true; /// /// When enabled (default), the z component of scale will be synchronized by authority. /// /// /// Changes to this on non-authoritative instances has no effect. /// public bool SyncScaleZ = true; private bool SynchronizeScale { get { return SyncScaleX || SyncScaleY || SyncScaleZ; } } /// /// The position threshold value that triggers a delta state update by the authoritative instance. /// /// /// Note: setting this to zero will update position every network tick whether it changed or not. /// public float PositionThreshold = PositionThresholdDefault; /// /// The rotation threshold value that triggers a delta state update by the authoritative instance. /// /// /// Minimum Value: 0.00001 /// Maximum Value: 360.0 /// [Range(0.00001f, 360.0f)] public float RotAngleThreshold = RotAngleThresholdDefault; /// /// The scale threshold value that triggers a delta state update by the authoritative instance. /// /// /// Note: setting this to zero will update position every network tick whether it changed or not. /// public float ScaleThreshold = ScaleThresholdDefault; /// /// Enable this on the authority side for quaternion synchronization /// /// /// This is synchronized by authority. During runtime, this should only be changed by the /// authoritative side. Non-authoritative instances will be overridden by the next /// authoritative state update. /// [Tooltip("When enabled, this will synchronize the full Quaternion (i.e. all Euler rotation axis are updated if one axis has a delta)")] public bool UseQuaternionSynchronization = false; /// /// Enabled this on the authority side for quaternion compression /// /// /// This has a lower precision than half float precision. Recommended only for low precision /// scenarios. provides better precision at roughly half /// the cost of a full quaternion update. /// This is synchronized by authority. During runtime, this should only be changed by the /// authoritative side. Non-authoritative instances will be overridden by the next /// authoritative state update. /// [Tooltip("When enabled, this uses a smallest three implementation that reduces full Quaternion updates down to the size of an unsigned integer (ignores half float precision settings).")] public bool UseQuaternionCompression = false; /// /// Enable this to use half float precision for position, rotation, and scale. /// When enabled, delta position synchronization is used. /// /// /// This is synchronized by authority. During runtime, this should only be changed by the /// authoritative side. Non-authoritative instances will be overridden by the next /// authoritative state update. /// [Tooltip("When enabled, this will use half float precision values for position (uses delta position updating), rotation (except when Quaternion compression is enabled), and scale.")] public bool UseHalfFloatPrecision = false; /// /// Sets whether the transform should be treated as local (true) or world (false) space. /// /// /// This is synchronized by authority. During runtime, this should only be changed by the /// authoritative side. Non-authoritative instances will be overridden by the next /// authoritative state update. /// [Tooltip("Sets whether this transform should sync in local space or in world space")] public bool InLocalSpace = false; /// /// When enabled (default) interpolation is applied. /// When disabled interpolation is disabled. /// /// /// This is synchronized by authority and changes to interpolation during runtime forces a /// teleport/full update. During runtime, this should only be changed by the authoritative /// side. Non-authoritative instances will be overridden by the next authoritative state update. /// public bool Interpolate = true; /// /// When true and interpolation is enabled, this will Slerp to the target position. /// /// /// This is synchronized by authority and only applies to position interpolation. /// During runtime, this should only be changed by the authoritative side. Non-authoritative /// instances will be overridden by the next authoritative state update. /// [Tooltip("When enabled the position interpolator will Slerp towards its current target position.")] public bool SlerpPosition = false; /// /// Used to determine who can write to this transform. Server only for this transform. /// Changing this value alone in a child implementation will not allow you to create a NetworkTransform which can be written to by clients. See the ClientNetworkTransform Sample /// in the package samples for how to implement a NetworkTransform with client write support. /// If using different values, please use RPCs to write to the server. Netcode doesn't support client side network variable writing /// public bool CanCommitToTransform { get; protected set; } /// /// Internally used by to keep track of whether this derived class instance /// was instantiated on the server side or not. /// protected bool m_CachedIsServer; // Note: we no longer use this and are only keeping it until we decide to deprecate it /// /// Internally used by to keep track of the instance assigned to this /// this derived class instance. /// protected NetworkManager m_CachedNetworkManager; /// /// Helper method that returns the space relative position of the transform. /// /// /// If InLocalSpace is then it returns the transform.localPosition /// If InLocalSpace is then it returns the transform.position /// When invoked on the non-authority side: /// If is true then it will return the most /// current authority position from the most recent state update. This can be useful /// if interpolation is enabled and you need to determine the final target position. /// When invoked on the authority side: /// It will always return the space relative position. /// /// /// Authority always returns the space relative transform position (whether true or false). /// Non-authority: /// When false (default): returns the space relative transform position /// When true: returns the authority position from the most recent state update. /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public Vector3 GetSpaceRelativePosition(bool getCurrentState = false) { if (!getCurrentState || CanCommitToTransform) { return InLocalSpace ? transform.localPosition : transform.position; } else { // When half float precision is enabled, get the NetworkDeltaPosition's full position if (UseHalfFloatPrecision) { return m_HalfPositionState.GetFullPosition(); } else { // Otherwise, just get the current position return m_CurrentPosition; } } } /// /// Helper method that returns the space relative rotation of the transform. /// /// /// If InLocalSpace is then it returns the transform.localRotation /// If InLocalSpace is then it returns the transform.rotation /// When invoked on the non-authority side: /// If is true then it will return the most /// current authority rotation from the most recent state update. This can be useful /// if interpolation is enabled and you need to determine the final target rotation. /// When invoked on the authority side: /// It will always return the space relative rotation. /// /// /// Authority always returns the space relative transform rotation (whether true or false). /// Non-authority: /// When false (default): returns the space relative transform rotation /// When true: returns the authority rotation from the most recent state update. /// /// public Quaternion GetSpaceRelativeRotation(bool getCurrentState = false) { if (!getCurrentState || CanCommitToTransform) { return InLocalSpace ? transform.localRotation : transform.rotation; } else { return m_CurrentRotation; } } /// /// Helper method that returns the scale of the transform. /// /// /// When invoked on the non-authority side: /// If is true then it will return the most /// current authority scale from the most recent state update. This can be useful /// if interpolation is enabled and you need to determine the final target scale. /// When invoked on the authority side: /// It will always return the space relative scale. /// /// /// Authority always returns the space relative transform scale (whether true or false). /// Non-authority: /// When false (default): returns the space relative transform scale /// When true: returns the authority scale from the most recent state update. /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public Vector3 GetScale(bool getCurrentState = false) { if (!getCurrentState || CanCommitToTransform) { return transform.localScale; } else { return m_CurrentScale; } } // Used by both authoritative and non-authoritative instances. // This represents the most recent local authoritative state. private NetworkTransformState m_LocalAuthoritativeNetworkState; internal NetworkTransformState LocalAuthoritativeNetworkState { get { return m_LocalAuthoritativeNetworkState; } set { m_LocalAuthoritativeNetworkState = value; } } private ClientRpcParams m_ClientRpcParams = new ClientRpcParams() { Send = new ClientRpcSendParams() }; private List m_ClientIds = new List() { 0 }; private BufferedLinearInterpolatorVector3 m_PositionInterpolator; private BufferedLinearInterpolatorVector3 m_ScaleInterpolator; private BufferedLinearInterpolatorQuaternion m_RotationInterpolator; // rotation is a single Quaternion since each Euler axis will affect the quaternion's final value // Non-Authoritative's current position, scale, and rotation that is used to assure the non-authoritative side cannot make adjustments to // the portions of the transform being synchronized. private Vector3 m_CurrentPosition; private Vector3 m_TargetPosition; private Vector3 m_CurrentScale; private Vector3 m_TargetScale; private Quaternion m_CurrentRotation; private Vector3 m_TargetRotation; [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void UpdatePositionInterpolator(Vector3 position, double time, bool resetInterpolator = false) { if (!CanCommitToTransform) { if (resetInterpolator) { m_PositionInterpolator.ResetTo(position, time); } else { m_PositionInterpolator.AddMeasurement(position, time); } } } #if DEBUG_NETWORKTRANSFORM || UNITY_INCLUDE_TESTS /// /// For debugging delta position and half vector3 /// protected delegate void AddLogEntryHandler(ref NetworkTransformState networkTransformState, ulong targetClient, bool preUpdate = false); protected AddLogEntryHandler m_AddLogEntry; [MethodImpl(MethodImplOptions.AggressiveInlining)] private void AddLogEntry(ref NetworkTransformState networkTransformState, ulong targetClient, bool preUpdate = false) { m_AddLogEntry?.Invoke(ref networkTransformState, targetClient, preUpdate); } [MethodImpl(MethodImplOptions.AggressiveInlining)] protected int GetStateId(ref NetworkTransformState state) { return state.StateId; } [MethodImpl(MethodImplOptions.AggressiveInlining)] protected NetworkDeltaPosition GetHalfPositionState() { return m_HalfPositionState; } #else [MethodImpl(MethodImplOptions.AggressiveInlining)] private void AddLogEntry(ref NetworkTransformState networkTransformState, ulong targetClient, bool preUpdate = false) { } #endif /// /// Only used when UseHalfFloatPrecision is enabled /// private NetworkDeltaPosition m_HalfPositionState = new NetworkDeltaPosition(Vector3.zero, 0); internal void UpdatePositionSlerp() { if (m_PositionInterpolator != null) { m_PositionInterpolator.IsSlerp = SlerpPosition; } } /// /// Determines if synchronization is needed. /// Basically only if we are running in owner authoritative mode and it /// is the owner being synchronized we don't want to synchronize with /// the exception of the NetworkObject being owned by the server. /// private bool ShouldSynchronizeHalfFloat(ulong targetClientId) { if (!IsServerAuthoritative() && NetworkObject.OwnerClientId == targetClientId) { // Return false for all client owners but return true for the server return NetworkObject.IsOwnedByServer; } return true; } // For test logging purposes internal NetworkTransformState SynchronizeState; /// /// This is invoked when a new client joins (server and client sides) /// Server Side: Serializes as if we were teleporting (everything is sent via NetworkTransformState) /// Client Side: Adds the interpolated state which applies the NetworkTransformState as well /// /// /// If a derived class overrides this, then make sure to invoke this base method! /// /// /// /// the clientId being synchronized (both reading and writing) protected override void OnSynchronize(ref BufferSerializer serializer) { m_CachedNetworkManager = NetworkManager; var targetClientId = m_TargetIdBeingSynchronized; var synchronizationState = new NetworkTransformState() { HalfEulerRotation = new HalfVector3(), HalfVectorRotation = new HalfVector4(), HalfVectorScale = new HalfVector3(), NetworkDeltaPosition = new NetworkDeltaPosition(), }; if (serializer.IsWriter) { synchronizationState.IsTeleportingNextFrame = true; var transformToCommit = transform; // If we are using Half Float Precision, then we want to only synchronize the authority's m_HalfPositionState.FullPosition in order for // for the non-authority side to be able to properly synchronize delta position updates. ApplyTransformToNetworkStateWithInfo(ref synchronizationState, ref transformToCommit, true, targetClientId); synchronizationState.NetworkSerialize(serializer); SynchronizeState = synchronizationState; } else { synchronizationState.NetworkSerialize(serializer); // Set the transform's synchronization modes InLocalSpace = synchronizationState.InLocalSpace; Interpolate = synchronizationState.UseInterpolation; UseQuaternionSynchronization = synchronizationState.QuaternionSync; UseHalfFloatPrecision = synchronizationState.UseHalfFloatPrecision; UseQuaternionCompression = synchronizationState.QuaternionCompression; SlerpPosition = synchronizationState.UsePositionSlerp; UpdatePositionSlerp(); // Teleport/Fully Initialize based on the state ApplyTeleportingState(synchronizationState); SynchronizeState = synchronizationState; m_LocalAuthoritativeNetworkState = synchronizationState; m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = false; m_LocalAuthoritativeNetworkState.IsSynchronizing = false; } } /// /// This will try to send/commit the current transform delta states (if any) /// /// /// Only client owners or the server should invoke this method /// /// the transform to be committed /// time it was marked dirty protected void TryCommitTransformToServer(Transform transformToCommit, double dirtyTime) { if (!IsSpawned) { NetworkLog.LogError($"Cannot commit transform when not spawned!"); return; } // Only the server or the owner is allowed to commit a transform if (!IsServer && !IsOwner) { var errorMessage = gameObject != NetworkObject.gameObject ? $"Non-authority instance of {NetworkObject.gameObject.name} is trying to commit a transform on {gameObject.name}!" : $"Non-authority instance of {NetworkObject.gameObject.name} is trying to commit a transform!"; NetworkLog.LogError(errorMessage); return; } // If we are authority, update the authoritative state if (CanCommitToTransform) { OnUpdateAuthoritativeState(ref transformToCommit); } else // Non-Authority { var position = InLocalSpace ? transformToCommit.localPosition : transformToCommit.position; var rotation = InLocalSpace ? transformToCommit.localRotation : transformToCommit.rotation; // We are an owner requesting to update our state if (!IsServer) { SetStateServerRpc(position, rotation, transformToCommit.localScale, false); } else // Server is always authoritative (including owner authoritative) { SetStateClientRpc(position, rotation, transformToCommit.localScale, false); } } } /// /// Invoked just prior to being pushed to non-authority instances. /// /// /// This is useful to know the exact position, rotation, or scale values sent /// to non-authoritative instances. This is only invoked on the authoritative /// instance. /// /// the state being pushed protected virtual void OnAuthorityPushTransformState(ref NetworkTransformState networkTransformState) { } // Only set if a delta has been sent, this is reset after an axial synch has been sent // to assure the instance doesn't continue to send axial synchs when an object is at rest. private bool m_DeltaSynch; /// /// Authoritative side only /// If there are any transform delta states, this method will synchronize the /// state with all non-authority instances. /// private void TryCommitTransform(ref Transform transformToCommit, bool synchronize = false, bool settingState = false) { // Only the server or the owner is allowed to commit a transform if (!IsServer && !IsOwner) { NetworkLog.LogError($"[{name}] is trying to commit the transform without authority!"); return; } // If the transform has deltas (returns dirty) or if an explicitly set state is pending if (m_LocalAuthoritativeNetworkState.ExplicitSet || ApplyTransformToNetworkStateWithInfo(ref m_LocalAuthoritativeNetworkState, ref transformToCommit, synchronize)) { m_LocalAuthoritativeNetworkState.LastSerializedSize = m_OldState.LastSerializedSize; // If the state was explicitly set, then update the network tick to match the locally calculate tick if (m_LocalAuthoritativeNetworkState.ExplicitSet) { m_LocalAuthoritativeNetworkState.NetworkTick = m_CachedNetworkManager.NetworkTickSystem.ServerTime.Tick; } // Send the state update UpdateTransformState(); // Mark the last tick and the old state (for next ticks) m_OldState = m_LocalAuthoritativeNetworkState; // Reset the teleport and explicit state flags after we have sent the state update. // These could be set again in the below OnAuthorityPushTransformState virtual method m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = false; m_LocalAuthoritativeNetworkState.ExplicitSet = false; try { // Notify of the pushed state update OnAuthorityPushTransformState(ref m_LocalAuthoritativeNetworkState); } catch (Exception ex) { Debug.LogException(ex); } // The below is part of assuring we only send a frame synch, when sending unreliable deltas, if // we have already sent at least one unreliable delta state update. At this point in the callstack, // a delta state update has just been sent in the above UpdateTransformState() call and as long as // we didn't send a frame synch and we are not synchronizing then we know at least one unreliable // delta has been sent. Under this scenario, we should start checking for this instance's alloted // frame synch "tick slot". Once we send a frame synch, if no other deltas occur after that // (i.e. the object is at rest) then we will stop sending frame synch's until the object begins // moving, rotating, or scaling again. if (UseUnreliableDeltas && !m_LocalAuthoritativeNetworkState.UnreliableFrameSync && !synchronize) { m_DeltaSynch = true; } } } /// /// Initializes the interpolators with the current transform values /// private void ResetInterpolatedStateToCurrentAuthoritativeState() { var serverTime = NetworkManager.ServerTime.Time; UpdatePositionInterpolator(GetSpaceRelativePosition(), serverTime, true); UpdatePositionSlerp(); m_ScaleInterpolator.ResetTo(transform.localScale, serverTime); m_RotationInterpolator.ResetTo(GetSpaceRelativeRotation(), serverTime); } /// /// Used for integration testing: /// Will apply the transform to the LocalAuthoritativeNetworkState and get detailed dirty information returned /// in the returned. /// /// transform to apply /// NetworkTransformState internal NetworkTransformState ApplyLocalNetworkState(Transform transform) { // Since we never commit these changes, we need to simulate that any changes were committed previously and the bitset // value would already be reset prior to having the state applied m_LocalAuthoritativeNetworkState.ClearBitSetForNextTick(); // Now check the transform for any threshold value changes ApplyTransformToNetworkStateWithInfo(ref m_LocalAuthoritativeNetworkState, ref transform); // Return the entire state to be used by the integration test return m_LocalAuthoritativeNetworkState; } /// /// Used for integration testing /// internal bool ApplyTransformToNetworkState(ref NetworkTransformState networkState, double dirtyTime, Transform transformToUse) { m_CachedNetworkManager = NetworkManager; // Apply the interpolate and PostionDeltaCompression flags, otherwise we get false positives whether something changed or not. networkState.UseInterpolation = Interpolate; networkState.QuaternionSync = UseQuaternionSynchronization; networkState.UseHalfFloatPrecision = UseHalfFloatPrecision; networkState.QuaternionCompression = UseQuaternionCompression; networkState.UseUnreliableDeltas = UseUnreliableDeltas; m_HalfPositionState = new NetworkDeltaPosition(Vector3.zero, 0, math.bool3(SyncPositionX, SyncPositionY, SyncPositionZ)); return ApplyTransformToNetworkStateWithInfo(ref networkState, ref transformToUse); } /// /// Applies the transform to the specified. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState networkState, ref Transform transformToUse, bool isSynchronization = false, ulong targetClientId = 0) { // 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 <= m_CachedNetworkManager.NetworkTickSystem.ServerTime.Tick) { // Increment to the next frame synch tick position for this instance m_NextTickSync += (int)m_CachedNetworkManager.NetworkConfig.TickRate; // 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 = !networkState.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; } // This is used to determine if we need to send the state update reliably (if we are doing an axial sync) networkState.UnreliableFrameSync = isAxisSync; var isTeleportingAndNotSynchronizing = networkState.IsTeleportingNextFrame && !isSynchronization; var isDirty = false; var isPositionDirty = isTeleportingAndNotSynchronizing ? networkState.HasPositionChange : false; var isRotationDirty = isTeleportingAndNotSynchronizing ? networkState.HasRotAngleChange : false; var isScaleDirty = isTeleportingAndNotSynchronizing ? networkState.HasScaleChange : false; var position = InLocalSpace ? transformToUse.localPosition : transformToUse.position; var rotAngles = InLocalSpace ? transformToUse.localEulerAngles : transformToUse.eulerAngles; var scale = transformToUse.localScale; networkState.IsSynchronizing = isSynchronization; // 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 (InLocalSpace != networkState.InLocalSpace) { networkState.InLocalSpace = InLocalSpace; isDirty = true; networkState.IsTeleportingNextFrame = true; } // Check for parenting when synchronizing and/or teleporting if (isSynchronization || networkState.IsTeleportingNextFrame) { // This all has to do with complex nested hierarchies and how it impacts scale // when set for the first time 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; var parentNetworkObject = (NetworkObject)null; // 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) { parentNetworkObject = NetworkObject.transform.parent.GetComponent(); // In-scene placed NetworkObjects parented under a GameObject with no // NetworkObject preserve their lossyScale when synchronizing. if (parentNetworkObject == null && NetworkObject.IsSceneObject != false) { hasParentNetworkObject = true; } else { // Or if the relative NetworkObject has a parent NetworkObject hasParentNetworkObject = parentNetworkObject != null; } } networkState.IsParented = hasParentNetworkObject; // When synchronizing with a parent, world position stays impacts position whether // the NetworkTransform is using world or local space synchronization. // WorldPositionStays: (always use world space) // !WorldPositionStays: (always use local space) // Exception: If it is an in-scene placed NetworkObject and it is parented under a GameObject // then always use local space unless AutoObjectParentSync is disabled and the NetworkTransform // is synchronizing in world space. if (isSynchronization && networkState.IsParented) { var parentedUnderGameObject = NetworkObject.transform.parent != null && !parentNetworkObject && NetworkObject.IsSceneObject.Value; if (NetworkObject.WorldPositionStays() && (!parentedUnderGameObject || (parentedUnderGameObject && !NetworkObject.AutoObjectParentSync && !InLocalSpace))) { position = transformToUse.position; networkState.InLocalSpace = false; } else { position = transformToUse.localPosition; networkState.InLocalSpace = true; } } } if (Interpolate != networkState.UseInterpolation) { networkState.UseInterpolation = Interpolate; isDirty = true; // When we change from interpolating to not interpolating (or vice versa) we need to synchronize/reset everything networkState.IsTeleportingNextFrame = true; } if (UseQuaternionSynchronization != networkState.QuaternionSync) { networkState.QuaternionSync = UseQuaternionSynchronization; isDirty = true; networkState.IsTeleportingNextFrame = true; } if (UseQuaternionCompression != networkState.QuaternionCompression) { networkState.QuaternionCompression = UseQuaternionCompression; isDirty = true; networkState.IsTeleportingNextFrame = true; } if (UseHalfFloatPrecision != networkState.UseHalfFloatPrecision) { networkState.UseHalfFloatPrecision = UseHalfFloatPrecision; isDirty = true; networkState.IsTeleportingNextFrame = true; } if (SlerpPosition != networkState.UsePositionSlerp) { networkState.UsePositionSlerp = SlerpPosition; isDirty = true; networkState.IsTeleportingNextFrame = true; } if (UseUnreliableDeltas != networkState.UseUnreliableDeltas) { networkState.UseUnreliableDeltas = UseUnreliableDeltas; isDirty = true; networkState.IsTeleportingNextFrame = true; } // Begin delta checks against last sent state update if (!UseHalfFloatPrecision) { if (SyncPositionX && (Mathf.Abs(networkState.PositionX - position.x) >= PositionThreshold || networkState.IsTeleportingNextFrame || isAxisSync)) { networkState.PositionX = position.x; networkState.HasPositionX = true; isPositionDirty = true; } if (SyncPositionY && (Mathf.Abs(networkState.PositionY - position.y) >= PositionThreshold || networkState.IsTeleportingNextFrame || isAxisSync)) { networkState.PositionY = position.y; networkState.HasPositionY = true; isPositionDirty = true; } if (SyncPositionZ && (Mathf.Abs(networkState.PositionZ - position.z) >= PositionThreshold || networkState.IsTeleportingNextFrame || isAxisSync)) { networkState.PositionZ = position.z; networkState.HasPositionZ = true; isPositionDirty = true; } } else if (SynchronizePosition) { // If we are teleporting then we can skip the delta threshold check isPositionDirty = networkState.IsTeleportingNextFrame || isAxisSync; if (m_HalfFloatTargetTickOwnership > m_CachedNetworkManager.ServerTime.Tick) { 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) { 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 (networkState.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 > m_CachedNetworkManager.ServerTime.Tick || isAxisSync) && !networkState.IsTeleportingNextFrame) { networkState.SynchronizeBaseHalfFloat = true; } else { networkState.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); } networkState.HasPositionX = SyncPositionX; networkState.HasPositionY = SyncPositionY; networkState.HasPositionZ = SyncPositionZ; } } if (!UseQuaternionSynchronization) { if (SyncRotAngleX && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleX, rotAngles.x)) >= RotAngleThreshold || networkState.IsTeleportingNextFrame || isAxisSync)) { networkState.RotAngleX = rotAngles.x; networkState.HasRotAngleX = true; isRotationDirty = true; } if (SyncRotAngleY && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleY, rotAngles.y)) >= RotAngleThreshold || networkState.IsTeleportingNextFrame || isAxisSync)) { networkState.RotAngleY = rotAngles.y; networkState.HasRotAngleY = true; isRotationDirty = true; } if (SyncRotAngleZ && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleZ, rotAngles.z)) >= RotAngleThreshold || networkState.IsTeleportingNextFrame || isAxisSync)) { networkState.RotAngleZ = rotAngles.z; networkState.HasRotAngleZ = true; isRotationDirty = true; } } else if (SynchronizeRotation) { // If we are teleporting then we can skip the delta threshold check isRotationDirty = networkState.IsTeleportingNextFrame || isAxisSync; // 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])) >= RotAngleThreshold) { isRotationDirty = true; break; } } } if (isRotationDirty) { networkState.Rotation = InLocalSpace ? transformToUse.localRotation : transformToUse.rotation; networkState.HasRotAngleX = true; networkState.HasRotAngleY = true; networkState.HasRotAngleZ = true; } } // For scale, we need to check for parenting when synchronizing and/or teleporting if (isSynchronization || networkState.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 (networkState.IsParented) { networkState.LossyScale = transform.lossyScale; } } // Checking scale deltas when not synchronizing if (!isSynchronization) { if (!UseHalfFloatPrecision) { if (SyncScaleX && (Mathf.Abs(networkState.ScaleX - scale.x) >= ScaleThreshold || networkState.IsTeleportingNextFrame || isAxisSync)) { networkState.ScaleX = scale.x; networkState.HasScaleX = true; isScaleDirty = true; } if (SyncScaleY && (Mathf.Abs(networkState.ScaleY - scale.y) >= ScaleThreshold || networkState.IsTeleportingNextFrame || isAxisSync)) { networkState.ScaleY = scale.y; networkState.HasScaleY = true; isScaleDirty = true; } if (SyncScaleZ && (Mathf.Abs(networkState.ScaleZ - scale.z) >= ScaleThreshold || networkState.IsTeleportingNextFrame || isAxisSync)) { networkState.ScaleZ = scale.z; networkState.HasScaleZ = 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 || networkState.IsTeleportingNextFrame || isAxisSync) { isScaleDirty = true; networkState.Scale[i] = scale[i]; networkState.SetHasScale(i, i == 0 ? SyncScaleX : i == 1 ? SyncScaleY : SyncScaleZ); } } } } else // Just apply the full local scale when synchronizing if (SynchronizeScale) { if (!UseHalfFloatPrecision) { networkState.ScaleX = transform.localScale.x; networkState.ScaleY = transform.localScale.y; networkState.ScaleZ = transform.localScale.z; } else { networkState.Scale = transform.localScale; } networkState.HasScaleX = true; networkState.HasScaleY = true; networkState.HasScaleZ = true; isScaleDirty = true; } isDirty |= isPositionDirty || isRotationDirty || isScaleDirty; if (isDirty) { // 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 = m_CachedNetworkManager.NetworkTickSystem.ServerTime.Tick; } } // Mark the state dirty for the next network tick update to clear out the bitset values networkState.IsDirty |= isDirty; return isDirty; } protected virtual void OnTransformUpdated() { } /// /// Applies the authoritative state to the transform /// protected internal void ApplyAuthoritativeState() { var networkState = m_LocalAuthoritativeNetworkState; // The m_CurrentPosition, m_CurrentRotation, and m_CurrentScale values are continually updated // at the end of this method and assure that when not interpolating the non-authoritative side // cannot make adjustments to any portions the transform not being synchronized. var adjustedPosition = m_CurrentPosition; var adjustedRotation = m_CurrentRotation; var adjustedRotAngles = adjustedRotation.eulerAngles; var adjustedScale = m_CurrentScale; // Non-Authority Preservers the authority's transform state update modes InLocalSpace = networkState.InLocalSpace; Interpolate = networkState.UseInterpolation; UseHalfFloatPrecision = networkState.UseHalfFloatPrecision; UseQuaternionSynchronization = networkState.QuaternionSync; UseQuaternionCompression = networkState.QuaternionCompression; UseUnreliableDeltas = networkState.UseUnreliableDeltas; if (SlerpPosition != networkState.UsePositionSlerp) { SlerpPosition = networkState.UsePositionSlerp; UpdatePositionSlerp(); } // NOTE ABOUT INTERPOLATING AND THE CODE BELOW: // We always apply the interpolated state for any axis we are synchronizing even when the state has no deltas // to assure we fully interpolate to our target even after we stop extrapolating 1 tick later. if (Interpolate) { if (SynchronizePosition) { var interpolatedPosition = m_PositionInterpolator.GetInterpolatedValue(); if (UseHalfFloatPrecision) { adjustedPosition = interpolatedPosition; } else { if (SyncPositionX) { adjustedPosition.x = interpolatedPosition.x; } if (SyncPositionY) { adjustedPosition.y = interpolatedPosition.y; } if (SyncPositionZ) { adjustedPosition.z = interpolatedPosition.z; } } } if (SynchronizeScale) { if (UseHalfFloatPrecision) { adjustedScale = m_ScaleInterpolator.GetInterpolatedValue(); } else { var interpolatedScale = m_ScaleInterpolator.GetInterpolatedValue(); if (SyncScaleX) { adjustedScale.x = interpolatedScale.x; } if (SyncScaleY) { adjustedScale.y = interpolatedScale.y; } if (SyncScaleZ) { adjustedScale.z = interpolatedScale.z; } } } if (SynchronizeRotation) { var interpolatedRotation = m_RotationInterpolator.GetInterpolatedValue(); if (UseQuaternionSynchronization) { adjustedRotation = interpolatedRotation; } else { var interpolatedEulerAngles = interpolatedRotation.eulerAngles; if (SyncRotAngleX) { adjustedRotAngles.x = interpolatedEulerAngles.x; } if (SyncRotAngleY) { adjustedRotAngles.y = interpolatedEulerAngles.y; } if (SyncRotAngleZ) { adjustedRotAngles.z = interpolatedEulerAngles.z; } adjustedRotation.eulerAngles = adjustedRotAngles; } } } else { // Non-Interpolated Position and Scale if (UseHalfFloatPrecision) { if (networkState.HasPositionChange && SynchronizePosition) { adjustedPosition = m_TargetPosition; } if (networkState.HasScaleChange && SynchronizeScale) { for (int i = 0; i < 3; i++) { if (m_LocalAuthoritativeNetworkState.HasScale(i)) { adjustedScale[i] = m_LocalAuthoritativeNetworkState.Scale[i]; } } } } else { if (networkState.HasPositionX) { adjustedPosition.x = networkState.PositionX; } if (networkState.HasPositionY) { adjustedPosition.y = networkState.PositionY; } if (networkState.HasPositionZ) { adjustedPosition.z = networkState.PositionZ; } if (networkState.HasScaleX) { adjustedScale.x = networkState.ScaleX; } if (networkState.HasScaleY) { adjustedScale.y = networkState.ScaleY; } if (networkState.HasScaleZ) { adjustedScale.z = networkState.ScaleZ; } } // Non-interpolated rotation if (SynchronizeRotation) { if (networkState.QuaternionSync && networkState.HasRotAngleChange) { adjustedRotation = networkState.Rotation; } else { if (networkState.HasRotAngleX) { adjustedRotAngles.x = networkState.RotAngleX; } if (networkState.HasRotAngleY) { adjustedRotAngles.y = networkState.RotAngleY; } if (networkState.HasRotAngleZ) { adjustedRotAngles.z = networkState.RotAngleZ; } adjustedRotation.eulerAngles = adjustedRotAngles; } } } // Apply the position if we are synchronizing position if (SynchronizePosition) { // Update our current position if it changed or we are interpolating if (networkState.HasPositionChange || Interpolate) { m_CurrentPosition = adjustedPosition; } if (InLocalSpace) { transform.localPosition = m_CurrentPosition; } else { transform.position = m_CurrentPosition; } } // Apply the rotation if we are synchronizing rotation if (SynchronizeRotation) { // Update our current rotation if it changed or we are interpolating if (networkState.HasRotAngleChange || Interpolate) { m_CurrentRotation = adjustedRotation; } if (InLocalSpace) { transform.localRotation = m_CurrentRotation; } else { transform.rotation = m_CurrentRotation; } } // Apply the scale if we are synchronizing scale if (SynchronizeScale) { // Update our current scale if it changed or we are interpolating if (networkState.HasScaleChange || Interpolate) { m_CurrentScale = adjustedScale; } transform.localScale = m_CurrentScale; } OnTransformUpdated(); } /// /// Handles applying the full authoritative state (i.e. teleporting) /// /// /// Only non-authoritative instances should invoke this /// private void ApplyTeleportingState(NetworkTransformState newState) { if (!newState.IsTeleportingNextFrame) { return; } var sentTime = newState.SentTime; var currentPosition = GetSpaceRelativePosition(); var currentRotation = GetSpaceRelativeRotation(); var currentEulerAngles = currentRotation.eulerAngles; var currentScale = transform.localScale; var isSynchronization = newState.IsSynchronizing; // Clear all interpolators m_ScaleInterpolator.Clear(); m_PositionInterpolator.Clear(); m_RotationInterpolator.Clear(); if (newState.HasPositionChange) { if (!UseHalfFloatPrecision) { // Adjust based on which axis changed if (newState.HasPositionX) { currentPosition.x = newState.PositionX; } if (newState.HasPositionY) { currentPosition.y = newState.PositionY; } if (newState.HasPositionZ) { currentPosition.z = newState.PositionZ; } } else { // With delta position teleport updates or synchronization, we create a new instance and provide the current network tick. m_HalfPositionState = new NetworkDeltaPosition(newState.CurrentPosition, newState.NetworkTick, math.bool3(SyncPositionX, SyncPositionY, SyncPositionZ)); // When first synchronizing we determine if we need to apply the current delta position // offset or not. This is specific to owner authoritative mode on the owner side only if (isSynchronization) { // Need to use NetworkManager vs m_CachedNetworkManager here since we are yet to be spawned if (ShouldSynchronizeHalfFloat(NetworkManager.LocalClientId)) { m_HalfPositionState.HalfVector3.Axis = newState.NetworkDeltaPosition.HalfVector3.Axis; m_HalfPositionState.DeltaPosition = newState.DeltaPosition; currentPosition = m_HalfPositionState.ToVector3(newState.NetworkTick); } else { currentPosition = newState.CurrentPosition; } // Before the state is applied add a log entry if AddLogEntry is assigned AddLogEntry(ref newState, NetworkObject.OwnerClientId, true); } else { // If we are just teleporting, then we already created a new NetworkDeltaPosition value. // set the current position to the state's current position currentPosition = newState.CurrentPosition; } } m_CurrentPosition = currentPosition; m_TargetPosition = currentPosition; // Apply the position if (newState.InLocalSpace) { transform.localPosition = currentPosition; } else { transform.position = currentPosition; } if (Interpolate) { UpdatePositionInterpolator(currentPosition, sentTime, true); } } if (newState.HasScaleChange) { bool shouldUseLossy = false; if (newState.IsParented) { if (transform.parent == null) { shouldUseLossy = NetworkObject.WorldPositionStays(); } else { shouldUseLossy = !NetworkObject.WorldPositionStays(); } } if (UseHalfFloatPrecision) { currentScale = shouldUseLossy ? newState.LossyScale : newState.Scale; } else { // Adjust based on which axis changed if (newState.HasScaleX) { currentScale.x = shouldUseLossy ? newState.LossyScale.x : newState.ScaleX; } if (newState.HasScaleY) { currentScale.y = shouldUseLossy ? newState.LossyScale.y : newState.ScaleY; } if (newState.HasScaleZ) { currentScale.z = shouldUseLossy ? newState.LossyScale.z : newState.ScaleZ; } } m_CurrentScale = currentScale; m_TargetScale = currentScale; // Apply the adjusted scale transform.localScale = currentScale; if (Interpolate) { m_ScaleInterpolator.ResetTo(currentScale, sentTime); } } if (newState.HasRotAngleChange) { if (newState.QuaternionSync) { currentRotation = newState.Rotation; } else { // Adjust based on which axis changed if (newState.HasRotAngleX) { currentEulerAngles.x = newState.RotAngleX; } if (newState.HasRotAngleY) { currentEulerAngles.y = newState.RotAngleY; } if (newState.HasRotAngleZ) { currentEulerAngles.z = newState.RotAngleZ; } currentRotation.eulerAngles = currentEulerAngles; } m_CurrentRotation = currentRotation; m_TargetRotation = currentRotation.eulerAngles; if (InLocalSpace) { transform.localRotation = currentRotation; } else { transform.rotation = currentRotation; } if (Interpolate) { m_RotationInterpolator.ResetTo(currentRotation, sentTime); } } // Add log after to applying the update if AddLogEntry is defined if (isSynchronization) { AddLogEntry(ref newState, NetworkObject.OwnerClientId); } OnTransformUpdated(); } /// /// Adds the new state's values to their respective interpolator /// /// /// Only non-authoritative instances should invoke this /// private void ApplyUpdatedState(NetworkTransformState newState) { // Set the transforms's synchronization modes InLocalSpace = newState.InLocalSpace; Interpolate = newState.UseInterpolation; UseQuaternionSynchronization = newState.QuaternionSync; UseQuaternionCompression = newState.QuaternionCompression; UseHalfFloatPrecision = newState.UseHalfFloatPrecision; UseUnreliableDeltas = newState.UseUnreliableDeltas; if (SlerpPosition != newState.UsePositionSlerp) { SlerpPosition = newState.UsePositionSlerp; UpdatePositionSlerp(); } m_LocalAuthoritativeNetworkState = newState; if (m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame) { ApplyTeleportingState(m_LocalAuthoritativeNetworkState); return; } var sentTime = newState.SentTime; var currentRotation = GetSpaceRelativeRotation(); var currentEulerAngles = currentRotation.eulerAngles; // Only if using half float precision and our position had changed last update then if (UseHalfFloatPrecision && m_LocalAuthoritativeNetworkState.HasPositionChange) { if (m_LocalAuthoritativeNetworkState.SynchronizeBaseHalfFloat) { m_HalfPositionState = m_LocalAuthoritativeNetworkState.NetworkDeltaPosition; } else { // assure our local NetworkDeltaPosition state is updated m_HalfPositionState.HalfVector3.Axis = m_LocalAuthoritativeNetworkState.NetworkDeltaPosition.HalfVector3.Axis; m_LocalAuthoritativeNetworkState.NetworkDeltaPosition.CurrentBasePosition = m_HalfPositionState.CurrentBasePosition; // This is to assure when you get the position of the state it is the correct position m_LocalAuthoritativeNetworkState.NetworkDeltaPosition.ToVector3(0); } // Update our target position m_TargetPosition = m_HalfPositionState.ToVector3(newState.NetworkTick); m_LocalAuthoritativeNetworkState.CurrentPosition = m_TargetPosition; } if (!Interpolate) { return; } // Apply axial changes from the new state // Either apply the delta position target position or the current state's delta position // depending upon whether UsePositionDeltaCompression is enabled if (m_LocalAuthoritativeNetworkState.HasPositionChange) { if (!m_LocalAuthoritativeNetworkState.UseHalfFloatPrecision) { var newTargetPosition = m_TargetPosition; if (m_LocalAuthoritativeNetworkState.HasPositionX) { newTargetPosition.x = m_LocalAuthoritativeNetworkState.PositionX; } if (m_LocalAuthoritativeNetworkState.HasPositionY) { newTargetPosition.y = m_LocalAuthoritativeNetworkState.PositionY; } if (m_LocalAuthoritativeNetworkState.HasPositionZ) { newTargetPosition.z = m_LocalAuthoritativeNetworkState.PositionZ; } m_TargetPosition = newTargetPosition; } UpdatePositionInterpolator(m_TargetPosition, sentTime); } if (m_LocalAuthoritativeNetworkState.HasScaleChange) { var currentScale = m_TargetScale; if (UseHalfFloatPrecision) { for (int i = 0; i < 3; i++) { if (m_LocalAuthoritativeNetworkState.HasScale(i)) { currentScale[i] = m_LocalAuthoritativeNetworkState.Scale[i]; } } } else { if (m_LocalAuthoritativeNetworkState.HasScaleX) { currentScale.x = m_LocalAuthoritativeNetworkState.ScaleX; } if (m_LocalAuthoritativeNetworkState.HasScaleY) { currentScale.y = m_LocalAuthoritativeNetworkState.ScaleY; } if (m_LocalAuthoritativeNetworkState.HasScaleZ) { currentScale.z = m_LocalAuthoritativeNetworkState.ScaleZ; } } m_TargetScale = currentScale; m_ScaleInterpolator.AddMeasurement(currentScale, sentTime); } // With rotation, we check if there are any changes first and // if so then apply the changes to the current Euler rotation // values. if (m_LocalAuthoritativeNetworkState.HasRotAngleChange) { if (m_LocalAuthoritativeNetworkState.QuaternionSync) { currentRotation = m_LocalAuthoritativeNetworkState.Rotation; } else { currentEulerAngles = m_TargetRotation; // Adjust based on which axis changed // (both half precision and full precision apply Eulers to the RotAngle properties when reading the update) if (m_LocalAuthoritativeNetworkState.HasRotAngleX) { currentEulerAngles.x = m_LocalAuthoritativeNetworkState.RotAngleX; } if (m_LocalAuthoritativeNetworkState.HasRotAngleY) { currentEulerAngles.y = m_LocalAuthoritativeNetworkState.RotAngleY; } if (m_LocalAuthoritativeNetworkState.HasRotAngleZ) { currentEulerAngles.z = m_LocalAuthoritativeNetworkState.RotAngleZ; } m_TargetRotation = currentEulerAngles; currentRotation.eulerAngles = currentEulerAngles; } m_RotationInterpolator.AddMeasurement(currentRotation, sentTime); } } /// /// Invoked on the non-authoritative side when the NetworkTransformState has been updated /// /// the previous /// the new protected virtual void OnNetworkTransformStateUpdated(ref NetworkTransformState oldState, ref NetworkTransformState newState) { } protected virtual void OnBeforeUpdateTransformState() { } private NetworkTransformState m_OldState = new NetworkTransformState(); /// /// Only non-authoritative instances should invoke this method /// private void OnNetworkStateChanged(NetworkTransformState oldState, NetworkTransformState newState) { if (!NetworkObject.IsSpawned || CanCommitToTransform) { return; } // If we are using UseUnreliableDeltas and our old state tick is greater than the new state tick, // then just ignore the newstate. This avoids any scenario where the new state is out of order // from the old state (with unreliable traffic and/or mixed unreliable and reliable) if (UseUnreliableDeltas && oldState.NetworkTick > newState.NetworkTick && !newState.IsTeleportingNextFrame && !newState.UnreliableFrameSync) { return; } // Get the time when this new state was sent newState.SentTime = new NetworkTime(m_CachedNetworkManager.NetworkConfig.TickRate, newState.NetworkTick).Time; OnBeforeUpdateTransformState(); // Apply the new state ApplyUpdatedState(newState); // Provide notifications when the state has been updated // We use the m_LocalAuthoritativeNetworkState because newState has been applied and adjustments could have // been made (i.e. half float precision position values will have been updated) OnNetworkTransformStateUpdated(ref oldState, ref m_LocalAuthoritativeNetworkState); } /// /// Will set the maximum interpolation boundary for the interpolators of this instance. /// This value roughly translates to the maximum value of 't' in and /// for all transform elements being monitored by /// (i.e. Position, Scale, and Rotation) /// /// Maximum time boundary that can be used in a frame when interpolating between two values public void SetMaxInterpolationBound(float maxInterpolationBound) { m_RotationInterpolator.MaxInterpolationBound = maxInterpolationBound; m_PositionInterpolator.MaxInterpolationBound = maxInterpolationBound; m_ScaleInterpolator.MaxInterpolationBound = maxInterpolationBound; } /// /// Create interpolators when first instantiated to avoid memory allocations if the /// associated NetworkObject persists (i.e. despawned but not destroyed or pools) /// protected virtual void Awake() { // Rotation is a single Quaternion since each Euler axis will affect the quaternion's final value m_RotationInterpolator = new BufferedLinearInterpolatorQuaternion(); m_PositionInterpolator = new BufferedLinearInterpolatorVector3(); m_ScaleInterpolator = new BufferedLinearInterpolatorVector3(); } /// /// Checks for changes in the axis to synchronize. If one or more did change it /// then determines if the axis were enabled and if the delta between the last known /// delta position and the current position for the axis exceeds the adjustment range /// before it is collapsed into the base position. /// If it does exceed the adjustment range, then we have to teleport the object so /// a full position synchronization takes place and the NetworkDeltaPosition is /// reset with the updated base position that it then will generating a new delta position from. /// /// /// This only happens if a user disables an axis, continues to update the disabled axis, /// and then later enables the axis. (which will not be a recommended best practice) /// private void AxisChangedDeltaPositionCheck() { if (UseHalfFloatPrecision && SynchronizePosition) { var synAxis = m_HalfPositionState.HalfVector3.AxisToSynchronize; if (SyncPositionX != synAxis.x || SyncPositionY != synAxis.y || SyncPositionZ != synAxis.z) { var positionState = m_HalfPositionState.GetFullPosition(); var relativePosition = GetSpaceRelativePosition(); bool needsToTeleport = false; // Only if the synchronization of an axis is turned on do we need to // check if a teleport is required due to the delta from the last known // to the currently known axis value exceeds MaxDeltaBeforeAdjustment. if (SyncPositionX && SyncPositionX != synAxis.x) { needsToTeleport = Mathf.Abs(relativePosition.x - positionState.x) >= NetworkDeltaPosition.MaxDeltaBeforeAdjustment; } if (SyncPositionY && SyncPositionY != synAxis.y) { needsToTeleport = Mathf.Abs(relativePosition.y - positionState.y) >= NetworkDeltaPosition.MaxDeltaBeforeAdjustment; } if (SyncPositionZ && SyncPositionZ != synAxis.z) { needsToTeleport = Mathf.Abs(relativePosition.z - positionState.z) >= NetworkDeltaPosition.MaxDeltaBeforeAdjustment; } // If needed, force a teleport as the delta is outside of the valid delta boundary m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = needsToTeleport; } } } /// /// Called by authority to check for deltas and update non-authoritative instances /// if any are found. /// internal void OnUpdateAuthoritativeState(ref Transform transformSource) { // If our replicated state is not dirty and our local authority state is dirty, clear it. if (!m_LocalAuthoritativeNetworkState.ExplicitSet && m_LocalAuthoritativeNetworkState.IsDirty && !m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame) { // Now clear our bitset and prepare for next network tick state update m_LocalAuthoritativeNetworkState.ClearBitSetForNextTick(); if (TrackByStateId) { m_LocalAuthoritativeNetworkState.TrackByStateId = true; m_LocalAuthoritativeNetworkState.StateId++; } else { m_LocalAuthoritativeNetworkState.TrackByStateId = false; } } AxisChangedDeltaPositionCheck(); TryCommitTransform(ref transformSource); } /// /// Authority subscribes to network tick events and will invoke /// each network tick. /// private void NetworkTickSystem_Tick() { // As long as we are still authority if (CanCommitToTransform) { // Update any changes to the transform var transformSource = transform; OnUpdateAuthoritativeState(ref transformSource); m_CurrentPosition = GetSpaceRelativePosition(); m_TargetPosition = GetSpaceRelativePosition(); } else // If we are no longer authority, unsubscribe to the tick event if (NetworkManager != null && NetworkManager.NetworkTickSystem != null) { NetworkManager.NetworkTickSystem.Tick -= NetworkTickSystem_Tick; } } /// public override void OnNetworkSpawn() { /////////////////////////////////////////////////////////////// // NOTE: Legacy and no longer used (candidates for deprecation) m_CachedIsServer = IsServer; /////////////////////////////////////////////////////////////// // Started using this again to avoid the getter processing cost of NetworkBehaviour.NetworkManager m_CachedNetworkManager = NetworkManager; Initialize(); if (CanCommitToTransform && UseHalfFloatPrecision) { SetState(GetSpaceRelativePosition(), GetSpaceRelativeRotation(), GetScale(), false); } } /// public override void OnNetworkDespawn() { DeregisterForTickUpdate(this); CanCommitToTransform = false; if (NetworkManager != null && NetworkManager.NetworkTickSystem != null) { NetworkManager.NetworkTickSystem.Tick -= NetworkTickSystem_Tick; } } /// public override void OnDestroy() { // During destroy, use NetworkBehaviour.NetworkManager as opposed to m_CachedNetworkManager if (NetworkManager != null && NetworkManager.NetworkTickSystem != null) { NetworkManager.NetworkTickSystem.Tick -= NetworkTickSystem_Tick; } CanCommitToTransform = false; base.OnDestroy(); } /// public override void OnLostOwnership() { base.OnLostOwnership(); } /// public override void OnGainedOwnership() { base.OnGainedOwnership(); } protected override void OnOwnershipChanged(ulong previous, ulong current) { // If we were the previous owner or the newly assigned owner then reinitialize if (current == m_CachedNetworkManager.LocalClientId || previous == m_CachedNetworkManager.LocalClientId) { InternalInitialization(true); } base.OnOwnershipChanged(previous, current); } /// /// Invoked when first spawned and when ownership changes. /// /// the current after initializing protected virtual void OnInitialize(ref NetworkTransformState replicatedState) { } /// /// An owner read and owner write NetworkVariable so it doesn't generate any messages /// private NetworkVariable m_InternalStatNetVar = new NetworkVariable(default, NetworkVariableReadPermission.Owner, NetworkVariableWritePermission.Owner); /// /// This method is only invoked by the owner /// Use: OnInitialize(ref NetworkTransformState replicatedState) to be notified on all instances /// /// protected virtual void OnInitialize(ref NetworkVariable replicatedState) { } private int m_HalfFloatTargetTickOwnership; /// /// The internal initialzation method to allow for internal API adjustments /// /// private void InternalInitialization(bool isOwnershipChange = false) { if (!IsSpawned) { return; } CanCommitToTransform = IsServerAuthoritative() ? IsServer : IsOwner; var currentPosition = GetSpaceRelativePosition(); var currentRotation = GetSpaceRelativeRotation(); if (CanCommitToTransform) { if (UseHalfFloatPrecision) { m_HalfPositionState = new NetworkDeltaPosition(currentPosition, m_CachedNetworkManager.ServerTime.Tick, math.bool3(SyncPositionX, SyncPositionY, SyncPositionZ)); } m_CurrentPosition = currentPosition; m_TargetPosition = currentPosition; RegisterForTickUpdate(this); m_LocalAuthoritativeNetworkState.SynchronizeBaseHalfFloat = false; if (UseHalfFloatPrecision && isOwnershipChange && !IsServerAuthoritative() && Interpolate) { m_HalfFloatTargetTickOwnership = m_CachedNetworkManager.ServerTime.Tick; } } else { // Remove this instance from the tick update DeregisterForTickUpdate(this); ResetInterpolatedStateToCurrentAuthoritativeState(); m_LocalAuthoritativeNetworkState.SynchronizeBaseHalfFloat = false; m_CurrentPosition = currentPosition; m_TargetPosition = currentPosition; m_CurrentScale = transform.localScale; m_TargetScale = transform.localScale; m_CurrentRotation = currentRotation; m_TargetRotation = currentRotation.eulerAngles; } OnInitialize(ref m_LocalAuthoritativeNetworkState); if (IsOwner) { m_InternalStatNetVar.Value = m_LocalAuthoritativeNetworkState; OnInitialize(ref m_InternalStatNetVar); } } /// /// Initializes NetworkTransform when spawned and ownership changes. /// protected void Initialize() { InternalInitialization(); } /// /// /// When a parent changes, non-authoritative instances should: /// - Apply the resultant position, rotation, and scale from the parenting action. /// - Clear interpolators (even if not enabled on this frame) /// - Reset the interpolators to the position, rotation, and scale resultant values. /// This prevents interpolation visual anomalies and issues during initial synchronization /// public override void OnNetworkObjectParentChanged(NetworkObject parentNetworkObject) { // Only if we are not authority if (!CanCommitToTransform) { m_TargetPosition = m_CurrentPosition = GetSpaceRelativePosition(); m_CurrentRotation = GetSpaceRelativeRotation(); m_TargetRotation = m_CurrentRotation.eulerAngles; m_TargetScale = m_CurrentScale = GetScale(); if (Interpolate) { m_ScaleInterpolator.Clear(); m_PositionInterpolator.Clear(); m_RotationInterpolator.Clear(); // Always use NetworkManager here as this can be invoked prior to spawning var tempTime = new NetworkTime(NetworkManager.NetworkConfig.TickRate, NetworkManager.ServerTime.Tick).Time; UpdatePositionInterpolator(m_CurrentPosition, tempTime, true); m_ScaleInterpolator.ResetTo(m_CurrentScale, tempTime); m_RotationInterpolator.ResetTo(m_CurrentRotation, tempTime); } } base.OnNetworkObjectParentChanged(parentNetworkObject); } /// /// Directly sets a state on the authoritative transform. /// Owner clients can directly set the state on a server authoritative transform /// This will override any changes made previously to the transform /// This isn't resistant to network jitter. Server side changes due to this method won't be interpolated. /// The parameters are broken up into pos / rot / scale on purpose so that the caller can perturb /// just the desired one(s) /// /// new position to move to. Can be null /// new rotation to rotate to. Can be null /// new scale to scale to. Can be null /// When true (the default) the will not be teleported and, if enabled, will interpolate. When false the will teleport/apply the parameters provided immediately. /// public void SetState(Vector3? posIn = null, Quaternion? rotIn = null, Vector3? scaleIn = null, bool teleportDisabled = true) { if (!IsSpawned) { NetworkLog.LogError($"Cannot commit transform when not spawned!"); return; } // Only the server or the owner is allowed to commit a transform if (!IsServer && !IsOwner) { var errorMessage = gameObject != NetworkObject.gameObject ? $"Non-authority instance of {NetworkObject.gameObject.name} is trying to commit a transform on {gameObject.name}!" : $"Non-authority instance of {NetworkObject.gameObject.name} is trying to commit a transform!"; NetworkLog.LogError(errorMessage); return; } Vector3 pos = posIn == null ? GetSpaceRelativePosition() : posIn.Value; Quaternion rot = rotIn == null ? GetSpaceRelativeRotation() : rotIn.Value; Vector3 scale = scaleIn == null ? transform.localScale : scaleIn.Value; if (!CanCommitToTransform) { // Preserving the ability for owner authoritative mode to accept state changes from server if (IsServer) { m_ClientIds[0] = OwnerClientId; m_ClientRpcParams.Send.TargetClientIds = m_ClientIds; SetStateClientRpc(pos, rot, scale, !teleportDisabled, m_ClientRpcParams); } else // Preserving the ability for server authoritative mode to accept state changes from owner { SetStateServerRpc(pos, rot, scale, !teleportDisabled); } return; } SetStateInternal(pos, rot, scale, !teleportDisabled); } /// /// Authoritative only method /// Sets the internal state (teleporting or just set state) of the authoritative /// transform directly. /// private void SetStateInternal(Vector3 pos, Quaternion rot, Vector3 scale, bool shouldTeleport) { if (InLocalSpace) { transform.localPosition = pos; transform.localRotation = rot; } else { transform.SetPositionAndRotation(pos, rot); } transform.localScale = scale; m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = shouldTeleport; var transformToCommit = transform; // Explicit set states are cumulative during a fractional tick period of time (i.e. each SetState invocation will // update the axial deltas to whatever changes are applied). As such, we need to preserve the dirty and explicit // state flags. var stateWasDirty = m_LocalAuthoritativeNetworkState.IsDirty; var explicitState = m_LocalAuthoritativeNetworkState.ExplicitSet; // Apply any delta states to the m_LocalAuthoritativeNetworkState var isDirty = ApplyTransformToNetworkStateWithInfo(ref m_LocalAuthoritativeNetworkState, ref transformToCommit); // If we were dirty and the explicit state was set (prior to checking for deltas) or the current explicit state is dirty, // then we set the explicit state flag. m_LocalAuthoritativeNetworkState.ExplicitSet = (stateWasDirty && explicitState) || isDirty; // If the current explicit set flag is set, then we are dirty. This assures if more than one explicit set state is invoked // in between a fractional tick period and the current explicit set state did not find any deltas that we preserve any // previous dirty state. m_LocalAuthoritativeNetworkState.IsDirty = m_LocalAuthoritativeNetworkState.ExplicitSet; } /// /// Invoked by , allows a non-owner server to update the transform state /// /// /// Continued support for client-driven server authority model /// [ClientRpc] private void SetStateClientRpc(Vector3 pos, Quaternion rot, Vector3 scale, bool shouldTeleport, ClientRpcParams clientRpcParams = default) { // Server dictated state is always applied SetStateInternal(pos, rot, scale, shouldTeleport); } /// /// Invoked by , allows an owner-client update the transform state /// /// /// Continued support for client-driven server authority model /// [ServerRpc] private void SetStateServerRpc(Vector3 pos, Quaternion rot, Vector3 scale, bool shouldTeleport) { // server has received this RPC request to move change transform. give the server a chance to modify or even reject the move if (OnClientRequestChange != null) { (pos, rot, scale) = OnClientRequestChange(pos, rot, scale); } SetStateInternal(pos, rot, scale, shouldTeleport); } private void UpdateInterpolation() { // Non-Authority if (Interpolate) { var serverTime = m_CachedNetworkManager.ServerTime; var cachedDeltaTime = m_CachedNetworkManager.RealTimeProvider.DeltaTime; var cachedServerTime = serverTime.Time; // With owner authoritative mode, non-authority clients can lag behind // by more than 1 tick period of time. The current "solution" for now // is to make their cachedRenderTime run 2 ticks behind. var ticksAgo = !IsServerAuthoritative() && !IsServer ? 2 : 1; var cachedRenderTime = serverTime.TimeTicksAgo(ticksAgo).Time; // Now only update the interpolators for the portions of the transform being synchronized if (SynchronizePosition) { m_PositionInterpolator.Update(cachedDeltaTime, cachedRenderTime, cachedServerTime); } if (SynchronizeRotation) { // When using half precision Lerp towards the target rotation. // When using full precision Slerp towards the target rotation. /// m_RotationInterpolator.IsSlerp = !UseHalfFloatPrecision; m_RotationInterpolator.Update(cachedDeltaTime, cachedRenderTime, cachedServerTime); } if (SynchronizeScale) { m_ScaleInterpolator.Update(cachedDeltaTime, cachedRenderTime, cachedServerTime); } } } /// /// /// If you override this method, be sure that: /// - Non-authority always invokes this base class method. /// protected virtual void Update() { // If not spawned or this instance has authority, exit early if (!IsSpawned || CanCommitToTransform) { return; } // Non-Authority UpdateInterpolation(); // Apply the current authoritative state ApplyAuthoritativeState(); } /// /// Teleport the transform to the given values without interpolating /// /// new position to move to. /// new rotation to rotate to. /// new scale to scale to. /// public void Teleport(Vector3 newPosition, Quaternion newRotation, Vector3 newScale) { if (!CanCommitToTransform) { throw new Exception("Teleporting on non-authoritative side is not allowed!"); } // Teleporting now is as simple as setting the internal state and passing the teleport flag SetStateInternal(newPosition, newRotation, newScale, true); } /// /// Override this method and return false to switch to owner authoritative mode /// /// ( or ) where when false it runs as owner-client authoritative protected virtual bool OnIsServerAuthoritative() { return true; } /// /// Method to determine if this instance is owner or server authoritative. /// /// /// Used by to determines if this is server or owner authoritative. /// /// or public bool IsServerAuthoritative() { return OnIsServerAuthoritative(); } /// /// Invoked by to update the transform state /// /// internal void TransformStateUpdate(ref NetworkTransformState networkTransformState) { // Store the previous/old state m_OldState = m_LocalAuthoritativeNetworkState; // Assign the new incoming state m_LocalAuthoritativeNetworkState = networkTransformState; // Apply the state update OnNetworkStateChanged(m_OldState, m_LocalAuthoritativeNetworkState); } /// /// Invoked by the authoritative instance to sends a containing the /// private void UpdateTransformState() { if (m_CachedNetworkManager.ShutdownInProgress) { return; } bool isServerAuthoritative = OnIsServerAuthoritative(); if (isServerAuthoritative && !IsServer) { Debug.LogError($"Server authoritative {nameof(NetworkTransform)} can only be updated by the server!"); } else if (!isServerAuthoritative && !IsServer && !IsOwner) { Debug.LogError($"Owner authoritative {nameof(NetworkTransform)} can only be updated by the owner!"); } var customMessageManager = m_CachedNetworkManager.CustomMessagingManager; var networkTransformMessage = new NetworkTransformMessage() { NetworkObjectId = NetworkObjectId, NetworkBehaviourId = NetworkBehaviourId, State = m_LocalAuthoritativeNetworkState }; // Determine what network delivery method to use: // When to send reliable packets: // - If UsUnrealiable is not enabled // - If teleporting or synchronizing // - If sending an UnrealiableFrameSync or synchronizing the base position of the NetworkDeltaPosition var networkDelivery = !UseUnreliableDeltas | m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame | m_LocalAuthoritativeNetworkState.IsSynchronizing | m_LocalAuthoritativeNetworkState.UnreliableFrameSync | m_LocalAuthoritativeNetworkState.SynchronizeBaseHalfFloat ? NetworkDelivery.ReliableSequenced : NetworkDelivery.UnreliableSequenced; // Server-host always sends updates to all clients (but itself) if (IsServer) { var clientCount = m_CachedNetworkManager.ConnectionManager.ConnectedClientsList.Count; for (int i = 0; i < clientCount; i++) { var clientId = m_CachedNetworkManager.ConnectionManager.ConnectedClientsList[i].ClientId; if (NetworkManager.ServerClientId == clientId) { continue; } if (!NetworkObject.Observers.Contains(clientId)) { continue; } NetworkManager.MessageManager.SendMessage(ref networkTransformMessage, networkDelivery, clientId); } } else { // Clients (owner authoritative) send messages to the server-host NetworkManager.MessageManager.SendMessage(ref networkTransformMessage, networkDelivery, NetworkManager.ServerClientId); } } #region Network Tick Registration and Handling private static Dictionary s_NetworkTickRegistration = new Dictionary(); private static void RemoveTickUpdate(NetworkManager networkManager) { s_NetworkTickRegistration.Remove(networkManager); } /// /// Having the tick update once and cycling through registered instances to update is evidently less processor /// intensive than having each instance subscribe and update individually. /// private class NetworkTransformTickRegistration { private Action m_NetworkTickUpdate; private NetworkManager m_NetworkManager; public HashSet NetworkTransforms = new HashSet(); private int m_LastTick; private void OnNetworkManagerStopped(bool value) { Remove(); } public void Remove() { m_NetworkManager.NetworkTickSystem.Tick -= m_NetworkTickUpdate; m_NetworkTickUpdate = null; NetworkTransforms.Clear(); RemoveTickUpdate(m_NetworkManager); } /// /// Invoked once per network tick, this will update any registered /// authority instances. /// private void TickUpdate() { // TODO FIX: The local NetworkTickSystem can invoke with the same network tick as before if (m_NetworkManager.ServerTime.Tick <= m_LastTick) { return; } foreach (var networkTransform in NetworkTransforms) { if (networkTransform.IsSpawned) { networkTransform.NetworkTickSystem_Tick(); } } m_LastTick = m_NetworkManager.ServerTime.Tick; } public NetworkTransformTickRegistration(NetworkManager networkManager) { m_NetworkManager = networkManager; m_NetworkTickUpdate = new Action(TickUpdate); networkManager.NetworkTickSystem.Tick += m_NetworkTickUpdate; if (networkManager.IsServer) { networkManager.OnServerStopped += OnNetworkManagerStopped; } else { networkManager.OnClientStopped += OnNetworkManagerStopped; } } } private static int s_TickSynchPosition; private int m_NextTickSync; internal void RegisterForTickSynchronization() { s_TickSynchPosition++; m_NextTickSync = NetworkManager.ServerTime.Tick + (s_TickSynchPosition % (int)NetworkManager.NetworkConfig.TickRate); } /// /// Will register the NetworkTransform instance for the single tick update entry point. /// If a NetworkTransformTickRegistration has not yet been registered for the NetworkManager /// instance, then create an entry. /// /// private static void RegisterForTickUpdate(NetworkTransform networkTransform) { if (!s_NetworkTickRegistration.ContainsKey(networkTransform.NetworkManager)) { s_NetworkTickRegistration.Add(networkTransform.NetworkManager, new NetworkTransformTickRegistration(networkTransform.NetworkManager)); } networkTransform.RegisterForTickSynchronization(); s_NetworkTickRegistration[networkTransform.NetworkManager].NetworkTransforms.Add(networkTransform); } /// /// If a NetworkTransformTickRegistration exists for the NetworkManager instance, then this will /// remove the NetworkTransform instance from the single tick update entry point. /// /// private static void DeregisterForTickUpdate(NetworkTransform networkTransform) { if (s_NetworkTickRegistration.ContainsKey(networkTransform.NetworkManager)) { s_NetworkTickRegistration[networkTransform.NetworkManager].NetworkTransforms.Remove(networkTransform); if (s_NetworkTickRegistration[networkTransform.NetworkManager].NetworkTransforms.Count == 0) { var registrationEntry = s_NetworkTickRegistration[networkTransform.NetworkManager]; registrationEntry.Remove(); } } } #endregion } internal interface INetworkTransformLogStateEntry { void AddLogEntry(NetworkTransform.NetworkTransformState networkTransformState, ulong targetClient, bool preUpdate = false); } }