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/" + nameof(NetworkTransform))] [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; internal bool IsDirty; /// /// 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 /// if (HasPositionChange || HasRotAngleChange || HasScaleChange) { IsDirty = true; } else { IsDirty = false; } } } } /// /// 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; /// /// 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; /// /// 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; /// /// 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; } } private NetworkTransformState m_LocalAuthoritativeNetworkState; private bool m_HasSentLastValue = false; // used to send one last value, so clients can make the difference between lost replication data (clients extrapolate) and no more data to send. 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); private int m_LastSentTick; private NetworkTransformState m_LastSentState; internal NetworkTransformState GetLastSentState() { return m_LastSentState; } /// /// 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 authority is invoking this, then treat it like we do with if (CanCommitToTransform) { // If our replicated state is not dirty and our local authority state is dirty, clear it. if (!ReplicatedNetworkState.IsDirty() && m_LocalAuthoritativeNetworkState.IsDirty) { // Now clear our bitset and prepare for next network tick state update m_LocalAuthoritativeNetworkState.ClearBitSetForNextTick(); } TryCommitTransform(transformToCommit, m_CachedNetworkManager.LocalTime.Time); } else { // We are an owner requesting to update our state if (!m_CachedIsServer) { SetStateServerRpc(transformToCommit.position, transformToCommit.rotation, transformToCommit.localScale, false); } else // Server is always authoritative (including owner authoritative) { SetStateClientRpc(transformToCommit.position, transformToCommit.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; } var isDirty = ApplyTransformToNetworkState(ref m_LocalAuthoritativeNetworkState, dirtyTime, transformToCommit); // if dirty, send // if not dirty anymore, but hasn't sent last value for limiting extrapolation, still set isDirty // if not dirty and has already sent last value, don't do anything // extrapolation works by using last two values. if it doesn't receive anything anymore, it'll continue to extrapolate. // This is great in case there's message loss, not so great if we just don't have new values to send. // the following will send one last "copied" value so unclamped interpolation tries to extrapolate between two identical values, effectively // making it immobile. if (isDirty) { // Commit the state ReplicatedNetworkState.Value = m_LocalAuthoritativeNetworkState; m_HasSentLastValue = false; m_LastSentTick = m_CachedNetworkManager.LocalTime.Tick; m_LastSentState = m_LocalAuthoritativeNetworkState; } else if (!m_HasSentLastValue && m_CachedNetworkManager.LocalTime.Tick >= m_LastSentTick + 1) // check for state.IsDirty since update can happen more than once per tick. No need for client, RPCs will just queue up { // Since the last m_LocalAuthoritativeNetworkState could have included a IsTeleportingNextFrame // we need to reset this here so only the deltas are applied and interpolation is not reset again. m_LastSentState.IsTeleportingNextFrame = false; m_LastSentState.SentTime = m_CachedNetworkManager.LocalTime.Time; // time 1+ tick later // Commit the state ReplicatedNetworkState.Value = m_LastSentState; m_HasSentLastValue = true; } } private void ResetInterpolatedStateToCurrentAuthoritativeState() { var serverTime = NetworkManager.ServerTime.Time; m_PositionXInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.PositionX, serverTime); m_PositionYInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.PositionY, serverTime); m_PositionZInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.PositionZ, serverTime); m_RotationInterpolator.ResetTo(Quaternion.Euler(m_LocalAuthoritativeNetworkState.RotAngleX, m_LocalAuthoritativeNetworkState.RotAngleY, m_LocalAuthoritativeNetworkState.RotAngleZ), serverTime); m_ScaleXInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.ScaleX, serverTime); m_ScaleYInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.ScaleY, serverTime); m_ScaleZInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.ScaleZ, 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 interpolatedPosition = networkState.InLocalSpace ? transform.localPosition : transform.position; // todo: we should store network state w/ quats vs. euler angles var interpolatedRotAngles = networkState.InLocalSpace ? transform.localEulerAngles : transform.eulerAngles; var interpolatedScale = transform.localScale; var isTeleporting = networkState.IsTeleportingNextFrame; // InLocalSpace Read: InLocalSpace = networkState.InLocalSpace; // Update the position values that were changed in this state update if (networkState.HasPositionX) { interpolatedPosition.x = isTeleporting || !Interpolate ? networkState.PositionX : m_PositionXInterpolator.GetInterpolatedValue(); } if (networkState.HasPositionY) { interpolatedPosition.y = isTeleporting || !Interpolate ? networkState.PositionY : m_PositionYInterpolator.GetInterpolatedValue(); } if (networkState.HasPositionZ) { interpolatedPosition.z = isTeleporting || !Interpolate ? networkState.PositionZ : m_PositionZInterpolator.GetInterpolatedValue(); } // Update the rotation values that were changed in this state update if (networkState.HasRotAngleChange) { var eulerAngles = new Vector3(); if (Interpolate) { eulerAngles = m_RotationInterpolator.GetInterpolatedValue().eulerAngles; } if (networkState.HasRotAngleX) { interpolatedRotAngles.x = isTeleporting || !Interpolate ? networkState.RotAngleX : eulerAngles.x; } if (networkState.HasRotAngleY) { interpolatedRotAngles.y = isTeleporting || !Interpolate ? networkState.RotAngleY : eulerAngles.y; } if (networkState.HasRotAngleZ) { interpolatedRotAngles.z = isTeleporting || !Interpolate ? networkState.RotAngleZ : eulerAngles.z; } } // Update all scale axis that were changed in this state update if (networkState.HasScaleX) { interpolatedScale.x = isTeleporting || !Interpolate ? networkState.ScaleX : m_ScaleXInterpolator.GetInterpolatedValue(); } if (networkState.HasScaleY) { interpolatedScale.y = isTeleporting || !Interpolate ? networkState.ScaleY : m_ScaleYInterpolator.GetInterpolatedValue(); } if (networkState.HasScaleZ) { interpolatedScale.z = isTeleporting || !Interpolate ? networkState.ScaleZ : m_ScaleZInterpolator.GetInterpolatedValue(); } // Apply the new position if (networkState.HasPositionChange) { if (InLocalSpace) { transform.localPosition = interpolatedPosition; } else { transform.position = interpolatedPosition; } } // Apply the new rotation if (networkState.HasRotAngleChange) { if (InLocalSpace) { transform.localRotation = Quaternion.Euler(interpolatedRotAngles); } else { transform.rotation = Quaternion.Euler(interpolatedRotAngles); } } // Apply the new scale if (networkState.HasScaleChange) { transform.localScale = interpolatedScale; } } /// /// 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) { 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; } private void Awake() { // we only want to create our interpolators during Awake so that, when pooled, we do not create tons // of gc thrash each time objects wink out and are re-used m_PositionXInterpolator = new BufferedLinearInterpolatorFloat(); m_PositionYInterpolator = new BufferedLinearInterpolatorFloat(); m_PositionZInterpolator = new BufferedLinearInterpolatorFloat(); m_RotationInterpolator = new BufferedLinearInterpolatorQuaternion(); // rotation is a single Quaternion since each euler axis will affect the quaternion's final value m_ScaleXInterpolator = new BufferedLinearInterpolatorFloat(); m_ScaleYInterpolator = new BufferedLinearInterpolatorFloat(); m_ScaleZInterpolator = new BufferedLinearInterpolatorFloat(); 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) { // Teleport to current position SetStateInternal(transform.position, transform.rotation, 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.position = pos; transform.rotation = 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 transform.position = pos; transform.rotation = rot; transform.localScale = scale; m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = shouldTeleport; TryCommitTransform(transform, m_CachedNetworkManager.LocalTime.Time); } /// /// 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); } transform.position = pos; transform.rotation = rot; transform.localScale = scale; m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = shouldTeleport; TryCommitTransform(transform, m_CachedNetworkManager.LocalTime.Time); } // todo: this is currently in update, to be able to catch any transform changes. A FixedUpdate mode could be added to be less intense, but it'd be // conditional to users only making transform update changes in FixedUpdate. /// /// /// 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 (CanCommitToTransform) { // If our replicated state is not dirty and our local authority state is dirty, clear it. if (!ReplicatedNetworkState.IsDirty() && m_LocalAuthoritativeNetworkState.IsDirty) { // Now clear our bitset and prepare for next network tick state update m_LocalAuthoritativeNetworkState.ClearBitSetForNextTick(); } TryCommitTransform(transform, m_CachedNetworkManager.LocalTime.Time); } else { if (Interpolate) { // eventually, we could hoist this calculation so that it happens once for all objects, not once per object 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); } // Now 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(); } } }