using System; using System.Collections.Generic; using UnityEngine; namespace Unity.Netcode.Components { /// /// A component for syncing transforms. /// NetworkTransform will read the underlying transform and replicate it to clients. /// The replicated value will be automatically be interpolated (if active) and applied to the underlying GameObject's transform. /// [DisallowMultipleComponent] [AddComponentMenu("Netcode/Network Transform")] [DefaultExecutionOrder(100000)] // this is needed to catch the update time after the transform was updated by user scripts public class NetworkTransform : NetworkBehaviour { /// /// The default position change threshold value. /// Any changes above this threshold will be replicated. /// public const float PositionThresholdDefault = 0.001f; /// /// The default rotation angle change threshold value. /// Any changes above this threshold will be replicated. /// public const float RotAngleThresholdDefault = 0.01f; /// /// The default scale change threshold value. /// Any changes above this threshold will be replicated. /// public const float ScaleThresholdDefault = 0.01f; /// /// The handler delegate type that takes client requested changes and returns resulting changes handled by the server. /// /// The position requested by the client. /// The rotation requested by the client. /// The scale requested by the client. /// The resulting position, rotation and scale changes after handling. public delegate (Vector3 pos, Quaternion rotOut, Vector3 scale) OnClientRequestChangeDelegate(Vector3 pos, Quaternion rot, Vector3 scale); /// /// The handler that gets invoked when server receives a change from a client. /// This handler would be useful for server to modify pos/rot/scale before applying client's request. /// public OnClientRequestChangeDelegate OnClientRequestChange; internal struct NetworkTransformState : INetworkSerializable { private const int k_InLocalSpaceBit = 0; private const int k_PositionXBit = 1; private const int k_PositionYBit = 2; private const int k_PositionZBit = 3; private const int k_RotAngleXBit = 4; private const int k_RotAngleYBit = 5; private const int k_RotAngleZBit = 6; private const int k_ScaleXBit = 7; private const int k_ScaleYBit = 8; private const int k_ScaleZBit = 9; private const int k_TeleportingBit = 10; // 11-15: private ushort m_Bitset; internal bool InLocalSpace { get => (m_Bitset & (1 << k_InLocalSpaceBit)) != 0; set { if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_InLocalSpaceBit)); } else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_InLocalSpaceBit)); } } } // Position internal bool HasPositionX { get => (m_Bitset & (1 << k_PositionXBit)) != 0; set { if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_PositionXBit)); } else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_PositionXBit)); } } } internal bool HasPositionY { get => (m_Bitset & (1 << k_PositionYBit)) != 0; set { if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_PositionYBit)); } else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_PositionYBit)); } } } internal bool HasPositionZ { get => (m_Bitset & (1 << k_PositionZBit)) != 0; set { if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_PositionZBit)); } else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_PositionZBit)); } } } internal bool HasPositionChange { get { return HasPositionX | HasPositionY | HasPositionZ; } } // RotAngles internal bool HasRotAngleX { get => (m_Bitset & (1 << k_RotAngleXBit)) != 0; set { if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_RotAngleXBit)); } else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_RotAngleXBit)); } } } internal bool HasRotAngleY { get => (m_Bitset & (1 << k_RotAngleYBit)) != 0; set { if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_RotAngleYBit)); } else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_RotAngleYBit)); } } } internal bool HasRotAngleZ { get => (m_Bitset & (1 << k_RotAngleZBit)) != 0; set { if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_RotAngleZBit)); } else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_RotAngleZBit)); } } } internal bool HasRotAngleChange { get { return HasRotAngleX | HasRotAngleY | HasRotAngleZ; } } // Scale internal bool HasScaleX { get => (m_Bitset & (1 << k_ScaleXBit)) != 0; set { if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_ScaleXBit)); } else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_ScaleXBit)); } } } internal bool HasScaleY { get => (m_Bitset & (1 << k_ScaleYBit)) != 0; set { if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_ScaleYBit)); } else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_ScaleYBit)); } } } internal bool HasScaleZ { get => (m_Bitset & (1 << k_ScaleZBit)) != 0; set { if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_ScaleZBit)); } else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_ScaleZBit)); } } } internal bool HasScaleChange { get { return HasScaleX | HasScaleY | HasScaleZ; } } internal bool IsTeleportingNextFrame { get => (m_Bitset & (1 << k_TeleportingBit)) != 0; set { if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_TeleportingBit)); } else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_TeleportingBit)); } } } internal float PositionX, PositionY, PositionZ; internal float RotAngleX, RotAngleY, RotAngleZ; internal float ScaleX, ScaleY, ScaleZ; internal double SentTime; // Authoritative and non-authoritative sides use this to determine if a NetworkTransformState is // dirty or not. internal bool IsDirty; // Non-Authoritative side uses this for ending extrapolation of the last applied state internal int EndExtrapolationTick; /// /// This will reset the NetworkTransform BitSet /// internal void ClearBitSetForNextTick() { // We need to preserve the local space settings for the current state m_Bitset &= (ushort)(m_Bitset & (1 << k_InLocalSpaceBit)); IsDirty = false; } public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter { serializer.SerializeValue(ref SentTime); // InLocalSpace + HasXXX Bits serializer.SerializeValue(ref m_Bitset); // Position Values if (HasPositionX) { serializer.SerializeValue(ref PositionX); } if (HasPositionY) { serializer.SerializeValue(ref PositionY); } if (HasPositionZ) { serializer.SerializeValue(ref PositionZ); } // RotAngle Values if (HasRotAngleX) { serializer.SerializeValue(ref RotAngleX); } if (HasRotAngleY) { serializer.SerializeValue(ref RotAngleY); } if (HasRotAngleZ) { serializer.SerializeValue(ref RotAngleZ); } // Scale Values 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 (serializer.IsReader) { // Go ahead and mark the local state dirty or not dirty as well /// IsDirty = HasPositionChange || HasRotAngleChange || HasScaleChange; } } } /// /// Whether or not x component of position will be replicated /// public bool SyncPositionX = true; /// /// Whether or not y component of position will be replicated /// public bool SyncPositionY = true; /// /// Whether or not z component of position will be replicated /// public bool SyncPositionZ = true; private bool SynchronizePosition { get { return SyncPositionX || SyncPositionY || SyncPositionZ; } } /// /// Whether or not x component of rotation will be replicated /// public bool SyncRotAngleX = true; /// /// Whether or not y component of rotation will be replicated /// public bool SyncRotAngleY = true; /// /// Whether or not z component of rotation will be replicated /// public bool SyncRotAngleZ = true; private bool SynchronizeRotation { get { return SyncRotAngleX || SyncRotAngleY || SyncRotAngleZ; } } /// /// Whether or not x component of scale will be replicated /// public bool SyncScaleX = true; /// /// Whether or not y component of scale will be replicated /// public bool SyncScaleY = true; /// /// Whether or not z component of scale will be replicated /// public bool SyncScaleZ = true; private bool SynchronizeScale { get { return SyncScaleX || SyncScaleY || SyncScaleZ; } } /// /// The current position threshold value /// Any changes to the position that exceeds the current threshold value will be replicated /// public float PositionThreshold = PositionThresholdDefault; /// /// The current rotation threshold value /// Any changes to the rotation that exceeds the current threshold value will be replicated /// Minimum Value: 0.001 /// Maximum Value: 360.0 /// [Range(0.001f, 360.0f)] public float RotAngleThreshold = RotAngleThresholdDefault; /// /// The current scale threshold value /// Any changes to the scale that exceeds the current threshold value will be replicated /// public float ScaleThreshold = ScaleThresholdDefault; /// /// Sets whether the transform should be treated as local (true) or world (false) space. /// /// /// This should only be changed by the authoritative side during runtime. Non-authoritative /// changes will be overridden upon the next state update. /// [Tooltip("Sets whether this transform should sync in local space or in world space")] public bool InLocalSpace = false; /// /// When enabled (default) interpolation is applied and when disabled no interpolation is applied /// public bool Interpolate = true; /// /// Used to determine who can write to this transform. Server only for this transform. /// Changing this value alone in a child implementation will not allow you to create a NetworkTransform which can be written to by clients. See the ClientNetworkTransform Sample /// in the package samples for how to implement a NetworkTransform with client write support. /// If using different values, please use RPCs to write to the server. Netcode doesn't support client side network variable writing /// public bool CanCommitToTransform { get; protected set; } /// /// Internally used by to keep track of whether this derived class instance /// was instantiated on the server side or not. /// protected bool m_CachedIsServer; /// /// Internally used by to keep track of the instance assigned to this /// this derived class instance. /// protected NetworkManager m_CachedNetworkManager; /// /// We have two internal NetworkVariables. /// One for server authoritative and one for "client/owner" authoritative. /// private readonly NetworkVariable m_ReplicatedNetworkStateServer = new NetworkVariable(new NetworkTransformState(), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server); private readonly NetworkVariable m_ReplicatedNetworkStateOwner = new NetworkVariable(new NetworkTransformState(), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); internal NetworkVariable ReplicatedNetworkState { get { if (!IsServerAuthoritative()) { return m_ReplicatedNetworkStateOwner; } return m_ReplicatedNetworkStateServer; } } // Used by both authoritative and non-authoritative instances. // This represents the most recent local authoritative state. private NetworkTransformState m_LocalAuthoritativeNetworkState; private ClientRpcParams m_ClientRpcParams = new ClientRpcParams() { Send = new ClientRpcSendParams() }; private List m_ClientIds = new List() { 0 }; private BufferedLinearInterpolator m_PositionXInterpolator; private BufferedLinearInterpolator m_PositionYInterpolator; private BufferedLinearInterpolator m_PositionZInterpolator; private BufferedLinearInterpolator m_RotationInterpolator; // rotation is a single Quaternion since each Euler axis will affect the quaternion's final value private BufferedLinearInterpolator m_ScaleXInterpolator; private BufferedLinearInterpolator m_ScaleYInterpolator; private BufferedLinearInterpolator m_ScaleZInterpolator; private readonly List> m_AllFloatInterpolators = new List>(6); // Used by integration test private NetworkTransformState m_LastSentState; internal NetworkTransformState GetLastSentState() { return m_LastSentState; } /// /// 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 /// protected override void OnSynchronize(ref BufferSerializer serializer) { // We don't need to synchronize NetworkTransforms that are on the same // GameObject as the NetworkObject. if (NetworkObject.gameObject == gameObject) { return; } var synchronizationState = new NetworkTransformState(); if (serializer.IsWriter) { synchronizationState.IsTeleportingNextFrame = true; ApplyTransformToNetworkStateWithInfo(ref synchronizationState, m_CachedNetworkManager.LocalTime.Time, transform); synchronizationState.NetworkSerialize(serializer); } else { synchronizationState.NetworkSerialize(serializer); AddInterpolatedState(synchronizationState); } } /// /// This will try to send/commit the current transform delta states (if any) /// /// /// Only client owners or the server should invoke this method /// /// the transform to be committed /// time it was marked dirty protected void TryCommitTransformToServer(Transform transformToCommit, double dirtyTime) { // Only client owners or the server should invoke this method if (!IsOwner && !m_CachedIsServer) { NetworkLog.LogError($"Non-owner instance, {name}, is trying to commit a transform!"); return; } // If we are authority, update the authoritative state if (CanCommitToTransform) { UpdateAuthoritativeState(transform); } 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 (!m_CachedIsServer) { SetStateServerRpc(position, rotation, transformToCommit.localScale, false); } else // Server is always authoritative (including owner authoritative) { SetStateClientRpc(position, rotation, transformToCommit.localScale, false); } } } /// /// Authoritative side only /// If there are any transform delta states, this method will synchronize the /// state with all non-authority instances. /// private void TryCommitTransform(Transform transformToCommit, double dirtyTime) { if (!CanCommitToTransform && !IsOwner) { NetworkLog.LogError($"[{name}] is trying to commit the transform without authority!"); return; } // If the transform has deltas (returns dirty) then... if (ApplyTransformToNetworkState(ref m_LocalAuthoritativeNetworkState, dirtyTime, transformToCommit)) { // ...commit the state ReplicatedNetworkState.Value = m_LocalAuthoritativeNetworkState; } } /// /// Initializes the interpolators with the current transform values /// private void ResetInterpolatedStateToCurrentAuthoritativeState() { var serverTime = NetworkManager.ServerTime.Time; var position = InLocalSpace ? transform.localPosition : transform.position; m_PositionXInterpolator.ResetTo(position.x, serverTime); m_PositionYInterpolator.ResetTo(position.y, serverTime); m_PositionZInterpolator.ResetTo(position.z, serverTime); var rotation = InLocalSpace ? transform.localRotation : transform.rotation; m_RotationInterpolator.ResetTo(rotation, serverTime); var scale = transform.localScale; m_ScaleXInterpolator.ResetTo(scale.x, serverTime); m_ScaleYInterpolator.ResetTo(scale.y, serverTime); m_ScaleZInterpolator.ResetTo(scale.z, serverTime); } /// /// Used for integration testing: /// Will apply the transform to the LocalAuthoritativeNetworkState and get detailed dirty information returned /// in the returned. /// /// transform to apply /// NetworkTransformState internal NetworkTransformState ApplyLocalNetworkState(Transform transform) { // Since we never commit these changes, we need to simulate that any changes were committed previously and the bitset // value would already be reset prior to having the state applied m_LocalAuthoritativeNetworkState.ClearBitSetForNextTick(); // Now check the transform for any threshold value changes ApplyTransformToNetworkStateWithInfo(ref m_LocalAuthoritativeNetworkState, m_CachedNetworkManager.LocalTime.Time, transform); // Return the entire state to be used by the integration test return m_LocalAuthoritativeNetworkState; } /// /// Used for integration testing /// internal bool ApplyTransformToNetworkState(ref NetworkTransformState networkState, double dirtyTime, Transform transformToUse) { return ApplyTransformToNetworkStateWithInfo(ref networkState, dirtyTime, transformToUse); } /// /// Applies the transform to the specified. /// private bool ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState networkState, double dirtyTime, Transform transformToUse) { var isDirty = false; var isPositionDirty = false; var isRotationDirty = false; var isScaleDirty = false; var position = InLocalSpace ? transformToUse.localPosition : transformToUse.position; var rotAngles = InLocalSpace ? transformToUse.localEulerAngles : transformToUse.eulerAngles; var scale = transformToUse.localScale; if (InLocalSpace != networkState.InLocalSpace) { networkState.InLocalSpace = InLocalSpace; isDirty = true; } if (SyncPositionX && (Mathf.Abs(networkState.PositionX - position.x) >= PositionThreshold || networkState.IsTeleportingNextFrame)) { networkState.PositionX = position.x; networkState.HasPositionX = true; isPositionDirty = true; } if (SyncPositionY && (Mathf.Abs(networkState.PositionY - position.y) >= PositionThreshold || networkState.IsTeleportingNextFrame)) { networkState.PositionY = position.y; networkState.HasPositionY = true; isPositionDirty = true; } if (SyncPositionZ && (Mathf.Abs(networkState.PositionZ - position.z) >= PositionThreshold || networkState.IsTeleportingNextFrame)) { networkState.PositionZ = position.z; networkState.HasPositionZ = true; isPositionDirty = true; } if (SyncRotAngleX && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleX, rotAngles.x)) >= RotAngleThreshold || networkState.IsTeleportingNextFrame)) { networkState.RotAngleX = rotAngles.x; networkState.HasRotAngleX = true; isRotationDirty = true; } if (SyncRotAngleY && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleY, rotAngles.y)) >= RotAngleThreshold || networkState.IsTeleportingNextFrame)) { networkState.RotAngleY = rotAngles.y; networkState.HasRotAngleY = true; isRotationDirty = true; } if (SyncRotAngleZ && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleZ, rotAngles.z)) >= RotAngleThreshold || networkState.IsTeleportingNextFrame)) { networkState.RotAngleZ = rotAngles.z; networkState.HasRotAngleZ = true; isRotationDirty = true; } if (SyncScaleX && (Mathf.Abs(networkState.ScaleX - scale.x) >= ScaleThreshold || networkState.IsTeleportingNextFrame)) { networkState.ScaleX = scale.x; networkState.HasScaleX = true; isScaleDirty = true; } if (SyncScaleY && (Mathf.Abs(networkState.ScaleY - scale.y) >= ScaleThreshold || networkState.IsTeleportingNextFrame)) { networkState.ScaleY = scale.y; networkState.HasScaleY = true; isScaleDirty = true; } if (SyncScaleZ && (Mathf.Abs(networkState.ScaleZ - scale.z) >= ScaleThreshold || networkState.IsTeleportingNextFrame)) { networkState.ScaleZ = scale.z; networkState.HasScaleZ = true; isScaleDirty = true; } isDirty |= isPositionDirty || isRotationDirty || isScaleDirty; if (isDirty) { networkState.SentTime = dirtyTime; } /// We need to set this in order to know when we can reset our local authority state /// If our state is already dirty or we just found deltas (i.e. isDirty == true) networkState.IsDirty |= isDirty; return isDirty; } /// /// Applies the authoritative state to the transform /// private void ApplyAuthoritativeState() { var networkState = ReplicatedNetworkState.Value; var adjustedPosition = networkState.InLocalSpace ? transform.localPosition : transform.position; // TODO: We should store network state w/ quats vs. euler angles var adjustedRotAngles = networkState.InLocalSpace ? transform.localEulerAngles : transform.eulerAngles; var adjustedScale = transform.localScale; // InLocalSpace Read: InLocalSpace = networkState.InLocalSpace; // 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. var useInterpolatedValue = !networkState.IsTeleportingNextFrame && Interpolate; if (useInterpolatedValue) { if (SyncPositionX) { adjustedPosition.x = m_PositionXInterpolator.GetInterpolatedValue(); } if (SyncPositionY) { adjustedPosition.y = m_PositionYInterpolator.GetInterpolatedValue(); } if (SyncPositionZ) { adjustedPosition.z = m_PositionZInterpolator.GetInterpolatedValue(); } if (SyncScaleX) { adjustedScale.x = m_ScaleXInterpolator.GetInterpolatedValue(); } if (SyncScaleY) { adjustedScale.y = m_ScaleYInterpolator.GetInterpolatedValue(); } if (SyncScaleZ) { adjustedScale.z = m_ScaleZInterpolator.GetInterpolatedValue(); } if (SynchronizeRotation) { var interpolatedEulerAngles = m_RotationInterpolator.GetInterpolatedValue().eulerAngles; if (SyncRotAngleX) { adjustedRotAngles.x = interpolatedEulerAngles.x; } if (SyncRotAngleY) { adjustedRotAngles.y = interpolatedEulerAngles.y; } if (SyncRotAngleZ) { adjustedRotAngles.z = interpolatedEulerAngles.z; } } } 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; } if (networkState.HasRotAngleX) { adjustedRotAngles.x = networkState.RotAngleX; } if (networkState.HasRotAngleY) { adjustedRotAngles.y = networkState.RotAngleY; } if (networkState.HasRotAngleZ) { adjustedRotAngles.z = networkState.RotAngleZ; } } // NOTE: The below conditional checks for applying axial values are required in order to // prevent the non-authoritative side from making adjustments when interpolation is off. // TODO: Determine if we want to enforce, frame by frame, the non-authoritative transform values. // We would want save the position, rotation, and scale (each individually) after applying each // authoritative transform state received. Otherwise, the non-authoritative side could make // changes to an axial value (if interpolation is turned off) until authority sends an update for // that same axial value. When interpolation is on, the state's values being synchronized are // always applied each frame. // Apply the new position if it has changed or we are interpolating and synchronizing position if (networkState.HasPositionChange || (useInterpolatedValue && SynchronizePosition)) { if (InLocalSpace) { transform.localPosition = adjustedPosition; } else { transform.position = adjustedPosition; } } // Apply the new rotation if it has changed or we are interpolating and synchronizing rotation if (networkState.HasRotAngleChange || (useInterpolatedValue && SynchronizeRotation)) { if (InLocalSpace) { transform.localRotation = Quaternion.Euler(adjustedRotAngles); } else { transform.rotation = Quaternion.Euler(adjustedRotAngles); } } // Apply the new scale if it has changed or we are interpolating and synchronizing scale if (networkState.HasScaleChange || (useInterpolatedValue && SynchronizeScale)) { transform.localScale = adjustedScale; } } /// /// Only non-authoritative instances should invoke this /// private void AddInterpolatedState(NetworkTransformState newState) { var sentTime = newState.SentTime; var currentPosition = newState.InLocalSpace ? transform.localPosition : transform.position; var currentRotation = newState.InLocalSpace ? transform.localRotation : transform.rotation; var currentEulerAngles = currentRotation.eulerAngles; // When there is a change in interpolation or if teleporting, we reset if ((newState.InLocalSpace != InLocalSpace) || newState.IsTeleportingNextFrame) { InLocalSpace = newState.InLocalSpace; var currentScale = transform.localScale; // we should clear our float interpolators foreach (var interpolator in m_AllFloatInterpolators) { interpolator.Clear(); } // we should clear our quaternion interpolator m_RotationInterpolator.Clear(); // Adjust based on which axis changed if (newState.HasPositionX) { m_PositionXInterpolator.ResetTo(newState.PositionX, sentTime); currentPosition.x = newState.PositionX; } if (newState.HasPositionY) { m_PositionYInterpolator.ResetTo(newState.PositionY, sentTime); currentPosition.y = newState.PositionY; } if (newState.HasPositionZ) { m_PositionZInterpolator.ResetTo(newState.PositionZ, sentTime); currentPosition.z = newState.PositionZ; } // Apply the position if (newState.InLocalSpace) { transform.localPosition = currentPosition; } else { transform.position = currentPosition; } // Adjust based on which axis changed if (newState.HasScaleX) { m_ScaleXInterpolator.ResetTo(newState.ScaleX, sentTime); currentScale.x = newState.ScaleX; } if (newState.HasScaleY) { m_ScaleYInterpolator.ResetTo(newState.ScaleY, sentTime); currentScale.y = newState.ScaleY; } if (newState.HasScaleZ) { m_ScaleZInterpolator.ResetTo(newState.ScaleZ, sentTime); currentScale.z = newState.ScaleZ; } // Apply the adjusted scale transform.localScale = currentScale; // 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; } // Apply the rotation currentRotation.eulerAngles = currentEulerAngles; transform.rotation = currentRotation; // Reset the rotation interpolator m_RotationInterpolator.ResetTo(currentRotation, sentTime); return; } // Apply axial changes from the new state if (newState.HasPositionX) { m_PositionXInterpolator.AddMeasurement(newState.PositionX, sentTime); } if (newState.HasPositionY) { m_PositionYInterpolator.AddMeasurement(newState.PositionY, sentTime); } if (newState.HasPositionZ) { m_PositionZInterpolator.AddMeasurement(newState.PositionZ, sentTime); } if (newState.HasScaleX) { m_ScaleXInterpolator.AddMeasurement(newState.ScaleX, sentTime); } if (newState.HasScaleY) { m_ScaleYInterpolator.AddMeasurement(newState.ScaleY, sentTime); } if (newState.HasScaleZ) { m_ScaleZInterpolator.AddMeasurement(newState.ScaleZ, 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 (newState.HasRotAngleChange) { 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_RotationInterpolator.AddMeasurement(currentRotation, sentTime); } } /// /// Only non-authoritative instances should invoke this method /// private void OnNetworkStateChanged(NetworkTransformState oldState, NetworkTransformState newState) { if (!NetworkObject.IsSpawned) { return; } if (CanCommitToTransform) { // we're the authority, we ignore incoming changes return; } if (Interpolate) { // Add measurements for the new state's deltas AddInterpolatedState(newState); } } /// /// Will set the maximum interpolation boundary for the interpolators of this instance. /// This value roughly translates to the maximum value of 't' in and /// for all transform elements being monitored by /// (i.e. Position, Rotation, and Scale) /// /// Maximum time boundary that can be used in a frame when interpolating between two values public void SetMaxInterpolationBound(float maxInterpolationBound) { m_PositionXInterpolator.MaxInterpolationBound = maxInterpolationBound; m_PositionYInterpolator.MaxInterpolationBound = maxInterpolationBound; m_PositionZInterpolator.MaxInterpolationBound = maxInterpolationBound; m_RotationInterpolator.MaxInterpolationBound = maxInterpolationBound; m_ScaleXInterpolator.MaxInterpolationBound = maxInterpolationBound; m_ScaleYInterpolator.MaxInterpolationBound = maxInterpolationBound; m_ScaleZInterpolator.MaxInterpolationBound = maxInterpolationBound; } /// /// Create interpolators when first instantiated to avoid memory allocations if the /// associated NetworkObject persists (i.e. despawned but not destroyed or pools) /// private void Awake() { // Rotation is a single Quaternion since each Euler axis will affect the quaternion's final value m_RotationInterpolator = new BufferedLinearInterpolatorQuaternion(); // All other interpolators are BufferedLinearInterpolatorFloats m_PositionXInterpolator = new BufferedLinearInterpolatorFloat(); m_PositionYInterpolator = new BufferedLinearInterpolatorFloat(); m_PositionZInterpolator = new BufferedLinearInterpolatorFloat(); m_ScaleXInterpolator = new BufferedLinearInterpolatorFloat(); m_ScaleYInterpolator = new BufferedLinearInterpolatorFloat(); m_ScaleZInterpolator = new BufferedLinearInterpolatorFloat(); // Used to quickly iteration over the BufferedLinearInterpolatorFloat // instances if (m_AllFloatInterpolators.Count == 0) { m_AllFloatInterpolators.Add(m_PositionXInterpolator); m_AllFloatInterpolators.Add(m_PositionYInterpolator); m_AllFloatInterpolators.Add(m_PositionZInterpolator); m_AllFloatInterpolators.Add(m_ScaleXInterpolator); m_AllFloatInterpolators.Add(m_ScaleYInterpolator); m_AllFloatInterpolators.Add(m_ScaleZInterpolator); } } /// public override void OnNetworkSpawn() { m_CachedIsServer = IsServer; m_CachedNetworkManager = NetworkManager; Initialize(); // This assures the initial spawning of the object synchronizes all connected clients // with the current transform values. This should not be placed within Initialize since // that can be invoked when ownership changes. if (CanCommitToTransform) { var currentPosition = InLocalSpace ? transform.localPosition : transform.position; var currentRotation = InLocalSpace ? transform.localRotation : transform.rotation; // Teleport to current position SetStateInternal(currentPosition, currentRotation, transform.localScale, true); // Force the state update to be sent TryCommitTransform(transform, m_CachedNetworkManager.LocalTime.Time); } } /// public override void OnNetworkDespawn() { ReplicatedNetworkState.OnValueChanged -= OnNetworkStateChanged; } /// public override void OnDestroy() { base.OnDestroy(); m_ReplicatedNetworkStateServer.Dispose(); m_ReplicatedNetworkStateOwner.Dispose(); } /// public override void OnGainedOwnership() { Initialize(); } /// public override void OnLostOwnership() { Initialize(); } /// /// Initializes NetworkTransform when spawned and ownership changes. /// private void Initialize() { if (!IsSpawned) { return; } CanCommitToTransform = IsServerAuthoritative() ? IsServer : IsOwner; var replicatedState = ReplicatedNetworkState; m_LocalAuthoritativeNetworkState = replicatedState.Value; if (CanCommitToTransform) { replicatedState.OnValueChanged -= OnNetworkStateChanged; } else { replicatedState.OnValueChanged += OnNetworkStateChanged; // In case we are late joining ResetInterpolatedStateToCurrentAuthoritativeState(); } } /// /// Directly sets a state on the authoritative transform. /// Owner clients can directly set the state on a server authoritative transform /// This will override any changes made previously to the transform /// This isn't resistant to network jitter. Server side changes due to this method won't be interpolated. /// The parameters are broken up into pos / rot / scale on purpose so that the caller can perturb /// just the desired one(s) /// /// new position to move to. Can be null /// new rotation to rotate to. Can be null /// new scale to scale to. Can be null /// Should other clients interpolate this change or not. True by default /// new scale to scale to. Can be null /// public void SetState(Vector3? posIn = null, Quaternion? rotIn = null, Vector3? scaleIn = null, bool shouldGhostsInterpolate = true) { if (!IsSpawned) { return; } // Only the server or owner can invoke this method if (!IsOwner && !m_CachedIsServer) { throw new Exception("Non-owner client instance cannot set the state of the NetworkTransform!"); } Vector3 pos = posIn == null ? InLocalSpace ? transform.localPosition : transform.position : posIn.Value; Quaternion rot = rotIn == null ? InLocalSpace ? transform.localRotation : transform.rotation : 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 (m_CachedIsServer) { m_ClientIds[0] = OwnerClientId; m_ClientRpcParams.Send.TargetClientIds = m_ClientIds; SetStateClientRpc(pos, rot, scale, !shouldGhostsInterpolate, m_ClientRpcParams); } else // Preserving the ability for server authoritative mode to accept state changes from owner { SetStateServerRpc(pos, rot, scale, !shouldGhostsInterpolate); } return; } SetStateInternal(pos, rot, scale, !shouldGhostsInterpolate); } /// /// Authoritative only method /// Sets the internal state (teleporting or just set state) of the authoritative /// transform directly. /// private void SetStateInternal(Vector3 pos, Quaternion rot, Vector3 scale, bool shouldTeleport) { if (InLocalSpace) { transform.localPosition = pos; transform.localRotation = rot; } else { transform.SetPositionAndRotation(pos, rot); } transform.localScale = scale; m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = shouldTeleport; TryCommitTransform(transform, m_CachedNetworkManager.LocalTime.Time); } /// /// Invoked by , allows a non-owner server to update the transform state /// /// /// Continued support for client-driven server authority model /// [ClientRpc] private void SetStateClientRpc(Vector3 pos, Quaternion rot, Vector3 scale, bool shouldTeleport, ClientRpcParams clientRpcParams = default) { // Server dictated state is always applied SetStateInternal(pos, rot, scale, shouldTeleport); } /// /// Invoked by , allows an owner-client update the transform state /// /// /// Continued support for client-driven server authority model /// [ServerRpc] private void SetStateServerRpc(Vector3 pos, Quaternion rot, Vector3 scale, bool shouldTeleport) { // server has received this RPC request to move change transform. give the server a chance to modify or even reject the move if (OnClientRequestChange != null) { (pos, rot, scale) = OnClientRequestChange(pos, rot, scale); } SetStateInternal(pos, rot, scale, shouldTeleport); } /// /// Will update the authoritative transform state if any deltas are detected. /// This will also reset the m_LocalAuthoritativeNetworkState if it is still dirty /// but the replicated network state is not. /// /// transform to be updated private void UpdateAuthoritativeState(Transform transformSource) { // If our replicated state is not dirty and our local authority state is dirty, clear it. if (!ReplicatedNetworkState.IsDirty() && m_LocalAuthoritativeNetworkState.IsDirty) { m_LastSentState = m_LocalAuthoritativeNetworkState; // Now clear our bitset and prepare for next network tick state update m_LocalAuthoritativeNetworkState.ClearBitSetForNextTick(); } TryCommitTransform(transformSource, m_CachedNetworkManager.LocalTime.Time); } /// /// /// If you override this method, be sure that: /// - Non-owners always invoke this base class method when using interpolation. /// - Authority can opt to use in place of invoking this base class method. /// - Non-authority owners can use but should still invoke the this base class method when using interpolation. /// protected virtual void Update() { if (!IsSpawned) { return; } // If we are authority, update the authoritative state if (CanCommitToTransform) { UpdateAuthoritativeState(transform); } else // Non-Authority { if (Interpolate) { var serverTime = NetworkManager.ServerTime; var cachedDeltaTime = Time.deltaTime; var cachedServerTime = serverTime.Time; var cachedRenderTime = serverTime.TimeTicksAgo(1).Time; foreach (var interpolator in m_AllFloatInterpolators) { interpolator.Update(cachedDeltaTime, cachedRenderTime, cachedServerTime); } m_RotationInterpolator.Update(cachedDeltaTime, cachedRenderTime, cachedServerTime); } // Apply the current authoritative state ApplyAuthoritativeState(); } } /// /// Teleport the transform to the given values without interpolating /// /// new position to move to. /// new rotation to rotate to. /// new scale to scale to. /// public void Teleport(Vector3 newPosition, Quaternion newRotation, Vector3 newScale) { if (!CanCommitToTransform) { throw new Exception("Teleporting on non-authoritative side is not allowed!"); } // Teleporting now is as simple as setting the internal state and passing the teleport flag SetStateInternal(newPosition, newRotation, newScale, true); } /// /// Override this method and return false to switch to owner authoritative mode /// /// ( or ) where when false it runs as owner-client authoritative protected virtual bool OnIsServerAuthoritative() { return true; } /// /// Used by to determines if this is server or owner authoritative. /// internal bool IsServerAuthoritative() { return OnIsServerAuthoritative(); } } }