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();
}
}
}