com.unity.multiplayer.mlapi/com.unity.netcode.gameobjects/Components/NetworkTransform.cs

3370 строки
148 KiB
C#

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Unity.Mathematics;
using UnityEngine;
namespace Unity.Netcode.Components
{
/// <summary>
/// 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.
/// </summary>
[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
{
/// <summary>
/// The default position change threshold value.
/// Any changes above this threshold will be replicated.
/// </summary>
public const float PositionThresholdDefault = 0.001f;
/// <summary>
/// The default rotation angle change threshold value.
/// Any changes above this threshold will be replicated.
/// </summary>
public const float RotAngleThresholdDefault = 0.01f;
/// <summary>
/// The default scale change threshold value.
/// Any changes above this threshold will be replicated.
/// </summary>
public const float ScaleThresholdDefault = 0.01f;
/// <summary>
/// The handler delegate type that takes client requested changes and returns resulting changes handled by the server.
/// </summary>
/// <param name="pos">The position requested by the client.</param>
/// <param name="rot">The rotation requested by the client.</param>
/// <param name="scale">The scale requested by the client.</param>
/// <returns>The resulting position, rotation and scale changes after handling.</returns>
public delegate (Vector3 pos, Quaternion rotOut, Vector3 scale) OnClientRequestChangeDelegate(Vector3 pos, Quaternion rot, Vector3 scale);
/// <summary>
/// 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.
/// </summary>
public OnClientRequestChangeDelegate OnClientRequestChange;
/// <summary>
/// When set each state update will contain a state identifier
/// </summary>
internal static bool TrackByStateId;
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[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;
/// <summary>
/// Data structure used to synchronize the <see cref="NetworkTransform"/>
/// </summary>
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; }
/// <summary>
/// The last byte size of the <see cref="NetworkTransformState"/> updated.
/// </summary>
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;
/// <summary>
/// When set, the <see cref="NetworkTransform"/> is operates in local space
/// </summary>
public bool InLocalSpace
{
get => GetFlag(k_InLocalSpaceBit);
internal set
{
SetFlag(value, k_InLocalSpaceBit);
}
}
// Position
/// <summary>
/// When set, the X-Axis position value has changed
/// </summary>
public bool HasPositionX
{
get => GetFlag(k_PositionXBit);
internal set
{
SetFlag(value, k_PositionXBit);
}
}
/// <summary>
/// When set, the Y-Axis position value has changed
/// </summary>
public bool HasPositionY
{
get => GetFlag(k_PositionYBit);
internal set
{
SetFlag(value, k_PositionYBit);
}
}
/// <summary>
/// When set, the Z-Axis position value has changed
/// </summary>
public bool HasPositionZ
{
get => GetFlag(k_PositionZBit);
internal set
{
SetFlag(value, k_PositionZBit);
}
}
/// <summary>
/// When set, at least one of the position axis values has changed.
/// </summary>
public bool HasPositionChange
{
get
{
return HasPositionX | HasPositionY | HasPositionZ;
}
}
// RotAngles
/// <summary>
/// When set, the Euler rotation X-Axis value has changed.
/// </summary>
/// <remarks>
/// When quaternion synchronization is enabled all axis are always updated.
/// </remarks>
public bool HasRotAngleX
{
get => GetFlag(k_RotAngleXBit);
internal set
{
SetFlag(value, k_RotAngleXBit);
}
}
/// <summary>
/// When set, the Euler rotation Y-Axis value has changed.
/// </summary>
/// <remarks>
/// When quaternion synchronization is enabled all axis are always updated.
/// </remarks>
public bool HasRotAngleY
{
get => GetFlag(k_RotAngleYBit);
internal set
{
SetFlag(value, k_RotAngleYBit);
}
}
/// <summary>
/// When set, the Euler rotation Z-Axis value has changed.
/// </summary>
/// <remarks>
/// When quaternion synchronization is enabled all axis are always updated.
/// </remarks>
public bool HasRotAngleZ
{
get => GetFlag(k_RotAngleZBit);
internal set
{
SetFlag(value, k_RotAngleZBit);
}
}
/// <summary>
/// When set, at least one of the rotation axis values has changed.
/// </summary>
/// <remarks>
/// When quaternion synchronization is enabled all axis are always updated.
/// </remarks>
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
/// <summary>
/// When set, the X-Axis scale value has changed.
/// </summary>
public bool HasScaleX
{
get => GetFlag(k_ScaleXBit);
internal set
{
SetFlag(value, k_ScaleXBit);
}
}
/// <summary>
/// When set, the Y-Axis scale value has changed.
/// </summary>
public bool HasScaleY
{
get => GetFlag(k_ScaleYBit);
internal set
{
SetFlag(value, k_ScaleYBit);
}
}
/// <summary>
/// When set, the Z-Axis scale value has changed.
/// </summary>
public bool HasScaleZ
{
get => GetFlag(k_ScaleZBit);
internal set
{
SetFlag(value, k_ScaleZBit);
}
}
/// <summary>
/// When set, at least one of the scale axis values has changed.
/// </summary>
public bool HasScaleChange
{
get
{
return HasScaleX | HasScaleY | HasScaleZ;
}
}
/// <summary>
/// When set, the current state will be treated as a teleport.
/// </summary>
/// <remarks>
/// When teleporting:
/// - Interpolation is reset.
/// - If using half precision, full precision values are used.
/// - All axis marked to be synchronized will be updated.
/// </remarks>
public bool IsTeleportingNextFrame
{
get => GetFlag(k_TeleportingBit);
internal set
{
SetFlag(value, k_TeleportingBit);
}
}
/// <summary>
/// When set the <see cref="NetworkTransform"/> is uses interpolation.
/// </summary>
/// <remarks>
/// Authority does not apply interpolation via <see cref="NetworkTransform"/>.
/// Authority should handle its own motion/rotation/scale smoothing locally.
/// </remarks>
public bool UseInterpolation
{
get => GetFlag(k_Interpolate);
internal set
{
SetFlag(value, k_Interpolate);
}
}
/// <summary>
/// When enabled, this <see cref="NetworkTransform"/> instance uses <see cref="Quaternion"/> synchronization.
/// </summary>
/// <remarks>
/// Use quaternion synchronization if you are nesting <see cref="NetworkTransform"/>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.
/// </remarks>
public bool QuaternionSync
{
get => GetFlag(k_QuaternionSync);
internal set
{
SetFlag(value, k_QuaternionSync);
}
}
/// <summary>
/// When set <see cref="Quaternion"/>s will be compressed down to 4 bytes using a smallest three implementation.
/// </summary>
/// <remarks>
/// This only will be applied when <see cref="QuaternionSync"/> 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
/// </remarks>
public bool QuaternionCompression
{
get => GetFlag(k_QuaternionCompress);
internal set
{
SetFlag(value, k_QuaternionCompress);
}
}
/// <summary>
/// When set, the <see cref="NetworkTransform"/> will use half float precision for position, rotation, and scale.
/// </summary>
/// <remarks>
/// 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 (<see cref="HalfVector4"/> and <see cref="HalfVector3"/>)
/// </remarks>
public bool UseHalfFloatPrecision
{
get => GetFlag(k_UseHalfFloats);
internal set
{
SetFlag(value, k_UseHalfFloats);
}
}
/// <summary>
/// When set, this indicates it is the first state being synchronized.
/// Typically when the associate <see cref="NetworkObject"/> is spawned or a client is being synchronized after connecting to a network session in progress.
/// </summary>
public bool IsSynchronizing
{
get => GetFlag(k_Synchronization);
internal set
{
SetFlag(value, k_Synchronization);
}
}
/// <summary>
/// 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.
/// </summary>
public bool UsePositionSlerp
{
get => GetFlag(k_PositionSlerp);
internal set
{
SetFlag(value, k_PositionSlerp);
}
}
/// <summary>
/// Returns whether this state update was a frame synchronization when
/// UseUnreliableDeltas is enabled. When set, the entire transform will
/// be or has been synchronized.
/// </summary>
public bool IsUnreliableFrameSync()
{
return UnreliableFrameSync;
}
/// <summary>
/// Returns true if this state was sent with reliable delivery.
/// If false, then it was sent with unreliable delivery.
/// </summary>
/// <remarks>
/// Unreliable delivery will only be used if <see cref="UseUnreliableDeltas"/> is set.
/// </remarks>
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;
}
/// <summary>
/// Returns the current state's rotation. If there is no change in the rotation,
/// then it will return <see cref="Quaternion.identity"/>.
/// </summary>
/// <remarks>
/// When there is no change in an updated state's rotation then there are no values to return.
/// Checking for <see cref="HasRotAngleChange"/> is one way to detect this.
/// </remarks>
/// <returns><see cref="Quaternion"/></returns>
public Quaternion GetRotation()
{
if (HasRotAngleChange)
{
if (QuaternionSync)
{
return Rotation;
}
else
{
return Quaternion.Euler(RotAngleX, RotAngleY, RotAngleZ);
}
}
return Quaternion.identity;
}
/// <summary>
/// Returns the current state's position. If there is no change in position,
/// then it returns <see cref="Vector3.zero"/>.
/// </summary>
/// <remarks>
/// When there is no change in an updated state's position then there are no values to return.
/// Checking for <see cref="HasPositionChange"/> is one way to detect this.
/// When used with half precision it returns the half precision delta position state update
/// which will not be the full position.
/// To get a NettworkTransform's full position, use <see cref="GetSpaceRelativePosition(bool)"/> and
/// pass true as the parameter.
/// </remarks>
/// <returns><see cref="Vector3"/></returns>
public Vector3 GetPosition()
{
if (HasPositionChange)
{
if (UseHalfFloatPrecision)
{
if (IsTeleportingNextFrame)
{
return CurrentPosition;
}
else
{
return NetworkDeltaPosition.GetFullPosition();
}
}
else
{
return new Vector3(PositionX, PositionY, PositionZ);
}
}
return Vector3.zero;
}
/// <summary>
/// Returns the current state's scale. If there is no change in scale,
/// then it returns <see cref="Vector3.zero"/>.
/// </summary>
/// <remarks>
/// When there is no change in an updated state's scale then there are no values to return.
/// Checking for <see cref="HasScaleChange"/> is one way to detect this.
/// </remarks>
/// <returns><see cref="Vector3"/></returns>
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;
}
/// <summary>
/// The network tick that this state was sent by the authoritative instance.
/// </summary>
/// <returns><see cref="int"/></returns>
public int GetNetworkTick()
{
return NetworkTick;
}
internal HalfVector3 HalfEulerRotation;
/// <summary>
/// Serializes this <see cref="NetworkTransformState"/>
/// </summary>
public void NetworkSerialize<T>(BufferSerializer<T> 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;
}
}
}
/// <summary>
/// When enabled (default), the x component of position will be synchronized by authority.
/// </summary>
/// <remarks>
/// Changes to this on non-authoritative instances has no effect.
/// </remarks>
public bool SyncPositionX = true;
/// <summary>
/// When enabled (default), the y component of position will be synchronized by authority.
/// </summary>
/// <remarks>
/// Changes to this on non-authoritative instances has no effect.
/// </remarks>
public bool SyncPositionY = true;
/// <summary>
/// When enabled (default), the z component of position will be synchronized by authority.
/// </summary>
/// <remarks>
/// Changes to this on non-authoritative instances has no effect.
/// </remarks>
public bool SyncPositionZ = true;
private bool SynchronizePosition
{
get
{
return SyncPositionX || SyncPositionY || SyncPositionZ;
}
}
/// <summary>
/// When enabled (default), the x component of rotation will be synchronized by authority.
/// </summary>
/// <remarks>
/// When <see cref="UseQuaternionSynchronization"/> is enabled this does not apply.
/// Changes to this on non-authoritative instances has no effect.
/// </remarks>
public bool SyncRotAngleX = true;
/// <summary>
/// When enabled (default), the y component of rotation will be synchronized by authority.
/// </summary>
/// <remarks>
/// When <see cref="UseQuaternionSynchronization"/> is enabled this does not apply.
/// Changes to this on non-authoritative instances has no effect.
/// </remarks>
public bool SyncRotAngleY = true;
/// <summary>
/// When enabled (default), the z component of rotation will be synchronized by authority.
/// </summary>
/// <remarks>
/// When <see cref="UseQuaternionSynchronization"/> is enabled this does not apply.
/// Changes to this on non-authoritative instances has no effect.
/// </remarks>
public bool SyncRotAngleZ = true;
private bool SynchronizeRotation
{
get
{
return SyncRotAngleX || SyncRotAngleY || SyncRotAngleZ;
}
}
/// <summary>
/// When enabled (default), the x component of scale will be synchronized by authority.
/// </summary>
/// <remarks>
/// Changes to this on non-authoritative instances has no effect.
/// </remarks>
public bool SyncScaleX = true;
/// <summary>
/// When enabled (default), the y component of scale will be synchronized by authority.
/// </summary>
/// <remarks>
/// Changes to this on non-authoritative instances has no effect.
/// </remarks>
public bool SyncScaleY = true;
/// <summary>
/// When enabled (default), the z component of scale will be synchronized by authority.
/// </summary>
/// <remarks>
/// Changes to this on non-authoritative instances has no effect.
/// </remarks>
public bool SyncScaleZ = true;
private bool SynchronizeScale
{
get
{
return SyncScaleX || SyncScaleY || SyncScaleZ;
}
}
/// <summary>
/// The position threshold value that triggers a delta state update by the authoritative instance.
/// </summary>
/// <remarks>
/// Note: setting this to zero will update position every network tick whether it changed or not.
/// </remarks>
public float PositionThreshold = PositionThresholdDefault;
/// <summary>
/// The rotation threshold value that triggers a delta state update by the authoritative instance.
/// </summary>
/// <remarks>
/// Minimum Value: 0.00001
/// Maximum Value: 360.0
/// </remarks>
[Range(0.00001f, 360.0f)]
public float RotAngleThreshold = RotAngleThresholdDefault;
/// <summary>
/// The scale threshold value that triggers a delta state update by the authoritative instance.
/// </summary>
/// <remarks>
/// Note: setting this to zero will update position every network tick whether it changed or not.
/// </remarks>
public float ScaleThreshold = ScaleThresholdDefault;
/// <summary>
/// Enable this on the authority side for quaternion synchronization
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[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;
/// <summary>
/// Enabled this on the authority side for quaternion compression
/// </summary>
/// <remarks>
/// This has a lower precision than half float precision. Recommended only for low precision
/// scenarios. <see cref="UseHalfFloatPrecision"/> 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.
/// </remarks>
[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;
/// <summary>
/// Enable this to use half float precision for position, rotation, and scale.
/// When enabled, delta position synchronization is used.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[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;
/// <summary>
/// Sets whether the transform should be treated as local (true) or world (false) space.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[Tooltip("Sets whether this transform should sync in local space or in world space")]
public bool InLocalSpace = false;
/// <summary>
/// When enabled (default) interpolation is applied.
/// When disabled interpolation is disabled.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public bool Interpolate = true;
/// <summary>
/// When true and interpolation is enabled, this will Slerp to the target position.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[Tooltip("When enabled the position interpolator will Slerp towards its current target position.")]
public bool SlerpPosition = false;
/// <summary>
/// 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
/// </summary>
public bool CanCommitToTransform { get; protected set; }
/// <summary>
/// Internally used by <see cref="NetworkTransform"/> to keep track of whether this <see cref="NetworkBehaviour"/> derived class instance
/// was instantiated on the server side or not.
/// </summary>
protected bool m_CachedIsServer; // Note: we no longer use this and are only keeping it until we decide to deprecate it
/// <summary>
/// Internally used by <see cref="NetworkTransform"/> to keep track of the <see cref="NetworkManager"/> instance assigned to this
/// this <see cref="NetworkBehaviour"/> derived class instance.
/// </summary>
protected NetworkManager m_CachedNetworkManager;
/// <summary>
/// Helper method that returns the space relative position of the transform.
/// </summary>
/// <remarks>
/// If InLocalSpace is <see cref="true"/> then it returns the transform.localPosition
/// If InLocalSpace is <see cref="false"/> then it returns the transform.position
/// When invoked on the non-authority side:
/// If <see cref="getCurrentState"/> 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.
/// </remarks>
/// <param name="getCurrentState">
/// 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.
/// </param>
/// <returns><see cref="Vector3"/></returns>
[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;
}
}
}
/// <summary>
/// Helper method that returns the space relative rotation of the transform.
/// </summary>
/// <remarks>
/// If InLocalSpace is <see cref="true"/> then it returns the transform.localRotation
/// If InLocalSpace is <see cref="false"/> then it returns the transform.rotation
/// When invoked on the non-authority side:
/// If <see cref="getCurrentState"/> 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.
/// </remarks>
/// <param name="getCurrentState">
/// 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.
/// </param>
/// <returns><see cref="Quaternion"/></returns>
public Quaternion GetSpaceRelativeRotation(bool getCurrentState = false)
{
if (!getCurrentState || CanCommitToTransform)
{
return InLocalSpace ? transform.localRotation : transform.rotation;
}
else
{
return m_CurrentRotation;
}
}
/// <summary>
/// Helper method that returns the scale of the transform.
/// </summary>
/// <remarks>
/// When invoked on the non-authority side:
/// If <see cref="getCurrentState"/> 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.
/// </remarks>
/// <param name="getCurrentState">
/// 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.
/// </param>
/// <returns><see cref="Vector3"/></returns>
[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<ulong> m_ClientIds = new List<ulong>() { 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
/// <summary>
/// For debugging delta position and half vector3
/// </summary>
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
/// <summary>
/// Only used when UseHalfFloatPrecision is enabled
/// </summary>
private NetworkDeltaPosition m_HalfPositionState = new NetworkDeltaPosition(Vector3.zero, 0);
internal void UpdatePositionSlerp()
{
if (m_PositionInterpolator != null)
{
m_PositionInterpolator.IsSlerp = SlerpPosition;
}
}
/// <summary>
/// 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.
/// </summary>
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;
/// <summary>
/// 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
/// </summary>
/// <remarks>
/// If a derived class overrides this, then make sure to invoke this base method!
/// </remarks>
/// <typeparam name="T"></typeparam>
/// <param name="serializer"></param>
/// <param name="targetClientId">the clientId being synchronized (both reading and writing)</param>
protected override void OnSynchronize<T>(ref BufferSerializer<T> 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;
}
}
/// <summary>
/// This will try to send/commit the current transform delta states (if any)
/// </summary>
/// <remarks>
/// Only client owners or the server should invoke this method
/// </remarks>
/// <param name="transformToCommit">the transform to be committed</param>
/// <param name="dirtyTime">time it was marked dirty</param>
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);
}
}
}
/// <summary>
/// Invoked just prior to being pushed to non-authority instances.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="networkTransformState">the state being pushed</param>
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;
/// <summary>
/// Authoritative side only
/// If there are any transform delta states, this method will synchronize the
/// state with all non-authority instances.
/// </summary>
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;
}
}
}
/// <summary>
/// Initializes the interpolators with the current transform values
/// </summary>
private void ResetInterpolatedStateToCurrentAuthoritativeState()
{
var serverTime = NetworkManager.ServerTime.Time;
UpdatePositionInterpolator(GetSpaceRelativePosition(), serverTime, true);
UpdatePositionSlerp();
m_ScaleInterpolator.ResetTo(transform.localScale, serverTime);
m_RotationInterpolator.ResetTo(GetSpaceRelativeRotation(), serverTime);
}
/// <summary>
/// Used for integration testing:
/// Will apply the transform to the LocalAuthoritativeNetworkState and get detailed dirty information returned
/// in the <see cref="NetworkTransformState"/> returned.
/// </summary>
/// <param name="transform">transform to apply</param>
/// <returns>NetworkTransformState</returns>
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;
}
/// <summary>
/// Used for integration testing
/// </summary>
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);
}
/// <summary>
/// Applies the transform to the <see cref="NetworkTransformState"/> specified.
/// </summary>
[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<NetworkObject>();
// In-scene placed NetworkObjects parented under a GameObject with no
// NetworkObject preserve their lossyScale when synchronizing.
if (parentNetworkObject == null && NetworkObject.IsSceneObject != false)
{
hasParentNetworkObject = true;
}
else
{
// Or if the relative NetworkObject has a parent NetworkObject
hasParentNetworkObject = parentNetworkObject != null;
}
}
networkState.IsParented = hasParentNetworkObject;
// 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()
{
}
/// <summary>
/// Applies the authoritative state to the transform
/// </summary>
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();
}
/// <summary>
/// Handles applying the full authoritative state (i.e. teleporting)
/// </summary>
/// <remarks>
/// Only non-authoritative instances should invoke this
/// </remarks>
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();
}
/// <summary>
/// Adds the new state's values to their respective interpolator
/// </summary>
/// <remarks>
/// Only non-authoritative instances should invoke this
/// </remarks>
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);
}
}
/// <summary>
/// Invoked on the non-authoritative side when the NetworkTransformState has been updated
/// </summary>
/// <param name="oldState">the previous <see cref="NetworkTransformState"/></param>
/// <param name="newState">the new <see cref="NetworkTransformState"/></param>
protected virtual void OnNetworkTransformStateUpdated(ref NetworkTransformState oldState, ref NetworkTransformState newState)
{
}
protected virtual void OnBeforeUpdateTransformState()
{
}
private NetworkTransformState m_OldState = new NetworkTransformState();
/// <summary>
/// Only non-authoritative instances should invoke this method
/// </summary>
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);
}
/// <summary>
/// Will set the maximum interpolation boundary for the interpolators of this <see cref="NetworkTransform"/> instance.
/// This value roughly translates to the maximum value of 't' in <see cref="Vector3.Lerp(Vector3, Vector3, float)"/> and
/// <see cref="Quaternion.Lerp(Quaternion, Quaternion, float)"/> for all transform elements being monitored by
/// <see cref="NetworkTransform"/> (i.e. Position, Scale, and Rotation)
/// </summary>
/// <param name="maxInterpolationBound">Maximum time boundary that can be used in a frame when interpolating between two values</param>
public void SetMaxInterpolationBound(float maxInterpolationBound)
{
m_RotationInterpolator.MaxInterpolationBound = maxInterpolationBound;
m_PositionInterpolator.MaxInterpolationBound = maxInterpolationBound;
m_ScaleInterpolator.MaxInterpolationBound = maxInterpolationBound;
}
/// <summary>
/// Create interpolators when first instantiated to avoid memory allocations if the
/// associated NetworkObject persists (i.e. despawned but not destroyed or pools)
/// </summary>
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();
}
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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)
/// </remarks>
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;
}
}
}
/// <summary>
/// Called by authority to check for deltas and update non-authoritative instances
/// if any are found.
/// </summary>
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);
}
/// <summary>
/// Authority subscribes to network tick events and will invoke
/// <see cref="OnUpdateAuthoritativeState(ref Transform)"/> each network tick.
/// </summary>
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;
}
}
/// <inheritdoc/>
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);
}
}
/// <inheritdoc/>
public override void OnNetworkDespawn()
{
DeregisterForTickUpdate(this);
CanCommitToTransform = false;
if (NetworkManager != null && NetworkManager.NetworkTickSystem != null)
{
NetworkManager.NetworkTickSystem.Tick -= NetworkTickSystem_Tick;
}
}
/// <inheritdoc/>
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();
}
/// <inheritdoc/>
public override void OnLostOwnership()
{
base.OnLostOwnership();
}
/// <inheritdoc/>
public override void OnGainedOwnership()
{
base.OnGainedOwnership();
}
protected override void OnOwnershipChanged(ulong previous, ulong current)
{
// If we were the previous owner or the newly assigned owner then reinitialize
if (current == m_CachedNetworkManager.LocalClientId || previous == m_CachedNetworkManager.LocalClientId)
{
InternalInitialization(true);
}
base.OnOwnershipChanged(previous, current);
}
/// <summary>
/// Invoked when first spawned and when ownership changes.
/// </summary>
/// <param name="replicatedState">the current <see cref="NetworkTransformState"/> after initializing</param>
protected virtual void OnInitialize(ref NetworkTransformState replicatedState)
{
}
/// <summary>
/// An owner read and owner write NetworkVariable so it doesn't generate any messages
/// </summary>
private NetworkVariable<NetworkTransformState> m_InternalStatNetVar = new NetworkVariable<NetworkTransformState>(default, NetworkVariableReadPermission.Owner, NetworkVariableWritePermission.Owner);
/// <summary>
/// This method is only invoked by the owner
/// Use: OnInitialize(ref NetworkTransformState replicatedState) to be notified on all instances
/// </summary>
/// <param name="replicatedState"></param>
protected virtual void OnInitialize(ref NetworkVariable<NetworkTransformState> replicatedState)
{
}
private int m_HalfFloatTargetTickOwnership;
/// <summary>
/// The internal initialzation method to allow for internal API adjustments
/// </summary>
/// <param name="isOwnershipChange"></param>
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);
}
}
/// <summary>
/// Initializes NetworkTransform when spawned and ownership changes.
/// </summary>
protected void Initialize()
{
InternalInitialization();
}
/// <inheritdoc/>
/// <remarks>
/// 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
/// </remarks>
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);
}
/// <summary>
/// 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)
/// </summary>
/// <param name="posIn"></param> new position to move to. Can be null
/// <param name="rotIn"></param> new rotation to rotate to. Can be null
/// <param name="scaleIn">new scale to scale to. Can be null</param>
/// <param name="teleportDisabled">When true (the default) the <see cref="NetworkObject"/> will not be teleported and, if enabled, will interpolate. When false the <see cref="NetworkObject"/> will teleport/apply the parameters provided immediately.</param>
/// <exception cref="Exception"></exception>
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);
}
/// <summary>
/// Authoritative only method
/// Sets the internal state (teleporting or just set state) of the authoritative
/// transform directly.
/// </summary>
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;
}
/// <summary>
/// Invoked by <see cref="SetState"/>, allows a non-owner server to update the transform state
/// </summary>
/// <remarks>
/// Continued support for client-driven server authority model
/// </remarks>
[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);
}
/// <summary>
/// Invoked by <see cref="SetState"/>, allows an owner-client update the transform state
/// </summary>
/// <remarks>
/// Continued support for client-driven server authority model
/// </remarks>
[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.
/// <see cref="BufferedLinearInterpolatorQuaternion.IsSlerp"/>
m_RotationInterpolator.IsSlerp = !UseHalfFloatPrecision;
m_RotationInterpolator.Update(cachedDeltaTime, cachedRenderTime, cachedServerTime);
}
if (SynchronizeScale)
{
m_ScaleInterpolator.Update(cachedDeltaTime, cachedRenderTime, cachedServerTime);
}
}
}
/// <inheritdoc/>
/// <remarks>
/// If you override this method, be sure that:
/// - Non-authority always invokes this base class method.
/// </remarks>
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();
}
/// <summary>
/// Teleport the transform to the given values without interpolating
/// </summary>
/// <param name="newPosition"></param> new position to move to.
/// <param name="newRotation"></param> new rotation to rotate to.
/// <param name="newScale">new scale to scale to.</param>
/// <exception cref="Exception"></exception>
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);
}
/// <summary>
/// Override this method and return false to switch to owner authoritative mode
/// </summary>
/// <returns>(<see cref="true"/> or <see cref="false"/>) where when false it runs as owner-client authoritative</returns>
protected virtual bool OnIsServerAuthoritative()
{
return true;
}
/// <summary>
/// Method to determine if this <see cref="NetworkTransform"/> instance is owner or server authoritative.
/// </summary>
/// <remarks>
/// Used by <see cref="NetworkRigidbody"/> to determines if this is server or owner authoritative.
/// </remarks>
/// <returns><see cref="true"/> or <see cref="false"/></returns>
public bool IsServerAuthoritative()
{
return OnIsServerAuthoritative();
}
/// <summary>
/// Invoked by <see cref="NetworkTransformMessage"/> to update the transform state
/// </summary>
/// <param name="networkTransformState"></param>
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);
}
/// <summary>
/// Invoked by the authoritative instance to sends a <see cref="NetworkTransformMessage"/> containing the <see cref="NetworkTransformState"/>
/// </summary>
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<NetworkManager, NetworkTransformTickRegistration> s_NetworkTickRegistration = new Dictionary<NetworkManager, NetworkTransformTickRegistration>();
private static void RemoveTickUpdate(NetworkManager networkManager)
{
s_NetworkTickRegistration.Remove(networkManager);
}
/// <summary>
/// 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.
/// </summary>
private class NetworkTransformTickRegistration
{
private Action m_NetworkTickUpdate;
private NetworkManager m_NetworkManager;
public HashSet<NetworkTransform> NetworkTransforms = new HashSet<NetworkTransform>();
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);
}
/// <summary>
/// Invoked once per network tick, this will update any registered
/// authority instances.
/// </summary>
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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="networkTransform"></param>
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);
}
/// <summary>
/// If a NetworkTransformTickRegistration exists for the NetworkManager instance, then this will
/// remove the NetworkTransform instance from the single tick update entry point.
/// </summary>
/// <param name="networkTransform"></param>
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);
}
}