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)); }
}
}
// 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)); }
}
}
// 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 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 Vector3 Position
{
get { return new Vector3(PositionX, PositionY, PositionZ); }
set
{
PositionX = value.x;
PositionY = value.y;
PositionZ = value.z;
}
}
internal Vector3 Rotation
{
get { return new Vector3(RotAngleX, RotAngleY, RotAngleZ); }
set
{
RotAngleX = value.x;
RotAngleY = value.y;
RotAngleZ = value.z;
}
}
internal Vector3 Scale
{
get { return new Vector3(ScaleX, ScaleY, ScaleZ); }
set
{
ScaleX = value.x;
ScaleY = value.y;
ScaleZ = value.z;
}
}
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);
}
}
}
///
/// 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 this transform should sync in local space or in world space.
/// This is important to set since reparenting this transform could have issues,
/// if using world position (depending on who gets synced first: the parent or the child)
/// Having a child always at position 0,0,0 for example will have less possibilities of desync than when using world positions
///
[Tooltip("Sets whether this transform should sync in local space or in world space")]
public bool InLocalSpace = false;
private bool m_LastInterpolateLocal = false; // was the last frame local
///
/// When enabled (default) interpolation is applied and when disabled no interpolation is applied
/// Note: can be changed during runtime.
///
public bool Interpolate = true;
private bool m_LastInterpolate = true; // was the last frame interpolated
///
/// 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
///
// This is public to make sure that users don't depend on this IsClient && IsOwner check in their code. If this logic changes in the future, we can make it invisible here
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;
private readonly NetworkVariable m_ReplicatedNetworkState = new NetworkVariable(new NetworkTransformState());
private NetworkTransformState m_LocalAuthoritativeNetworkState;
private const int k_DebugDrawLineTime = 10;
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 BufferedLinearInterpolator m_PositionXInterpolator; // = new BufferedLinearInterpolatorFloat();
private BufferedLinearInterpolator m_PositionYInterpolator; // = new BufferedLinearInterpolatorFloat();
private BufferedLinearInterpolator m_PositionZInterpolator; // = new BufferedLinearInterpolatorFloat();
private BufferedLinearInterpolator m_RotationInterpolator; // = new BufferedLinearInterpolatorQuaternion(); // rotation is a single Quaternion since each euler axis will affect the quaternion's final value
private BufferedLinearInterpolator m_ScaleXInterpolator; // = new BufferedLinearInterpolatorFloat();
private BufferedLinearInterpolator m_ScaleYInterpolator; // = new BufferedLinearInterpolatorFloat();
private BufferedLinearInterpolator m_ScaleZInterpolator; // = new BufferedLinearInterpolatorFloat();
private readonly List> m_AllFloatInterpolators = new List>(6);
private Transform m_Transform; // cache the transform component to reduce unnecessary bounce between managed and native
private int m_LastSentTick;
private NetworkTransformState m_LastSentState;
///
/// Tries updating the server authoritative transform, only if allowed.
/// If this called server side, this will commit directly.
/// If no update is needed, nothing will be sent. This method should still be called every update, it'll self manage when it should and shouldn't send
///
///
///
protected void TryCommitTransformToServer(Transform transformToCommit, double dirtyTime)
{
var isDirty = ApplyTransformToNetworkState(ref m_LocalAuthoritativeNetworkState, dirtyTime, transformToCommit);
TryCommit(isDirty);
}
private void TryCommitValuesToServer(Vector3 position, Vector3 rotation, Vector3 scale, double dirtyTime)
{
var isDirty = ApplyTransformToNetworkStateWithInfo(ref m_LocalAuthoritativeNetworkState, dirtyTime, position, rotation, scale);
TryCommit(isDirty.isDirty);
}
private void TryCommit(bool isDirty)
{
void Send(NetworkTransformState stateToSend)
{
if (m_CachedIsServer)
{
// server RPC takes a few frames to execute server side, we want this to execute immediately
CommitLocallyAndReplicate(stateToSend);
}
else
{
CommitTransformServerRpc(stateToSend);
}
}
// 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)
{
Send(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
{
m_LastSentState.SentTime = m_CachedNetworkManager.LocalTime.Time; // time 1+ tick later
Send(m_LastSentState);
m_HasSentLastValue = true;
}
}
[ServerRpc(RequireOwnership = false)]
private void CommitTransformServerRpc(NetworkTransformState networkState, ServerRpcParams serverParams = default)
{
if (serverParams.Receive.SenderClientId == OwnerClientId) // RPC call when not authorized to write could happen during the RTT interval during which a server's ownership change hasn't reached the client yet
{
CommitLocallyAndReplicate(networkState);
}
}
private void CommitLocallyAndReplicate(NetworkTransformState networkState)
{
m_ReplicatedNetworkState.Value = networkState;
if (Interpolate)
{
AddInterpolatedState(networkState);
}
}
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.Rotation), serverTime);
m_ScaleXInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.ScaleX, serverTime);
m_ScaleYInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.ScaleY, serverTime);
m_ScaleZInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.ScaleZ, serverTime);
}
///
/// Will apply the transform to the LocalAuthoritativeNetworkState and get detailed isDirty information returned.
///
/// transform to apply
/// bool isDirty, bool isPositionDirty, bool isRotationDirty, bool isScaleDirty
internal (bool isDirty, bool isPositionDirty, bool isRotationDirty, bool isScaleDirty) ApplyLocalNetworkState(Transform transform)
{
return ApplyTransformToNetworkStateWithInfo(ref m_LocalAuthoritativeNetworkState, m_CachedNetworkManager.LocalTime.Time, transform);
}
// updates `NetworkState` properties if they need to and returns a `bool` indicating whether or not there was any changes made
// returned boolean would be useful to change encapsulating `NetworkVariable`'s dirty state, e.g. ReplNetworkState.SetDirty(isDirty);
internal bool ApplyTransformToNetworkState(ref NetworkTransformState networkState, double dirtyTime, Transform transformToUse)
{
return ApplyTransformToNetworkStateWithInfo(ref networkState, dirtyTime, transformToUse).isDirty;
}
private (bool isDirty, bool isPositionDirty, bool isRotationDirty, bool isScaleDirty) ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState networkState, double dirtyTime, Transform transformToUse)
{
var position = InLocalSpace ? transformToUse.localPosition : transformToUse.position;
var rotAngles = InLocalSpace ? transformToUse.localEulerAngles : transformToUse.eulerAngles;
var scale = transformToUse.localScale;
return ApplyTransformToNetworkStateWithInfo(ref networkState, dirtyTime, position, rotAngles, scale);
}
private (bool isDirty, bool isPositionDirty, bool isRotationDirty, bool isScaleDirty) ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState networkState, double dirtyTime, Vector3 position, Vector3 rotAngles, Vector3 scale)
{
var isDirty = false;
var isPositionDirty = false;
var isRotationDirty = false;
var isScaleDirty = false;
// hasPositionZ set to false when it should be true?
if (InLocalSpace != networkState.InLocalSpace)
{
networkState.InLocalSpace = InLocalSpace;
isDirty = true;
}
// we assume that if x, y or z are dirty then we'll have to send all 3 anyway, so for efficiency
// we skip doing the (quite expensive) Math.Approximately() and check against PositionThreshold
// this still is overly costly and could use more improvements.
//
// (ditto for scale components)
if (SyncPositionX &&
Mathf.Abs(networkState.PositionX - position.x) > PositionThreshold)
{
networkState.PositionX = position.x;
networkState.HasPositionX = true;
isPositionDirty = true;
}
if (SyncPositionY &&
Mathf.Abs(networkState.PositionY - position.y) > PositionThreshold)
{
networkState.PositionY = position.y;
networkState.HasPositionY = true;
isPositionDirty = true;
}
if (SyncPositionZ &&
Mathf.Abs(networkState.PositionZ - position.z) > PositionThreshold)
{
networkState.PositionZ = position.z;
networkState.HasPositionZ = true;
isPositionDirty = true;
}
if (SyncRotAngleX &&
Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleX, rotAngles.x)) > RotAngleThreshold)
{
networkState.RotAngleX = rotAngles.x;
networkState.HasRotAngleX = true;
isRotationDirty = true;
}
if (SyncRotAngleY &&
Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleY, rotAngles.y)) > RotAngleThreshold)
{
networkState.RotAngleY = rotAngles.y;
networkState.HasRotAngleY = true;
isRotationDirty = true;
}
if (SyncRotAngleZ &&
Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleZ, rotAngles.z)) > RotAngleThreshold)
{
networkState.RotAngleZ = rotAngles.z;
networkState.HasRotAngleZ = true;
isRotationDirty = true;
}
if (SyncScaleX &&
Mathf.Abs(networkState.ScaleX - scale.x) > ScaleThreshold)
{
networkState.ScaleX = scale.x;
networkState.HasScaleX = true;
isScaleDirty = true;
}
if (SyncScaleY &&
Mathf.Abs(networkState.ScaleY - scale.y) > ScaleThreshold)
{
networkState.ScaleY = scale.y;
networkState.HasScaleY = true;
isScaleDirty = true;
}
if (SyncScaleZ &&
Mathf.Abs(networkState.ScaleZ - scale.z) > ScaleThreshold)
{
networkState.ScaleZ = scale.z;
networkState.HasScaleZ = true;
isScaleDirty = true;
}
isDirty |= isPositionDirty || isRotationDirty || isScaleDirty;
if (isDirty)
{
networkState.SentTime = dirtyTime;
}
return (isDirty, isPositionDirty, isRotationDirty, isScaleDirty);
}
private void ApplyInterpolatedNetworkStateToTransform(NetworkTransformState networkState, Transform transformToUpdate)
{
var interpolatedPosition = InLocalSpace ? transformToUpdate.localPosition : transformToUpdate.position;
// todo: we should store network state w/ quats vs. euler angles
var interpolatedRotAngles = InLocalSpace ? transformToUpdate.localEulerAngles : transformToUpdate.eulerAngles;
var interpolatedScale = transformToUpdate.localScale;
// InLocalSpace Read
InLocalSpace = networkState.InLocalSpace;
// Position Read
if (SyncPositionX)
{
interpolatedPosition.x = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Position.x : m_PositionXInterpolator.GetInterpolatedValue();
}
if (SyncPositionY)
{
interpolatedPosition.y = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Position.y : m_PositionYInterpolator.GetInterpolatedValue();
}
if (SyncPositionZ)
{
interpolatedPosition.z = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Position.z : m_PositionZInterpolator.GetInterpolatedValue();
}
// again, we should be using quats here
if (SyncRotAngleX || SyncRotAngleY || SyncRotAngleZ)
{
var eulerAngles = new Vector3();
if (Interpolate)
{
eulerAngles = m_RotationInterpolator.GetInterpolatedValue().eulerAngles;
}
if (SyncRotAngleX)
{
interpolatedRotAngles.x = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Rotation.x : eulerAngles.x;
}
if (SyncRotAngleY)
{
interpolatedRotAngles.y = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Rotation.y : eulerAngles.y;
}
if (SyncRotAngleZ)
{
interpolatedRotAngles.z = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Rotation.z : eulerAngles.z;
}
}
// Scale Read
if (SyncScaleX)
{
interpolatedScale.x = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Scale.x : m_ScaleXInterpolator.GetInterpolatedValue();
}
if (SyncScaleY)
{
interpolatedScale.y = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Scale.y : m_ScaleYInterpolator.GetInterpolatedValue();
}
if (SyncScaleZ)
{
interpolatedScale.z = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Scale.z : m_ScaleZInterpolator.GetInterpolatedValue();
}
// Position Apply
if (SyncPositionX || SyncPositionY || SyncPositionZ)
{
if (InLocalSpace)
{
transformToUpdate.localPosition = interpolatedPosition;
}
else
{
transformToUpdate.position = interpolatedPosition;
}
}
// RotAngles Apply
if (SyncRotAngleX || SyncRotAngleY || SyncRotAngleZ)
{
if (InLocalSpace)
{
transformToUpdate.localRotation = Quaternion.Euler(interpolatedRotAngles);
}
else
{
transformToUpdate.rotation = Quaternion.Euler(interpolatedRotAngles);
}
}
// Scale Apply
if (SyncScaleX || SyncScaleY || SyncScaleZ)
{
transformToUpdate.localScale = interpolatedScale;
}
}
private void AddInterpolatedState(NetworkTransformState newState, bool reset = false)
{
var sentTime = newState.SentTime;
if (reset)
{
if (newState.HasPositionX)
{
m_PositionXInterpolator.ResetTo(newState.PositionX, sentTime);
}
if (newState.HasPositionY)
{
m_PositionYInterpolator.ResetTo(newState.PositionY, sentTime);
}
if (newState.HasPositionZ)
{
m_PositionZInterpolator.ResetTo(newState.PositionZ, sentTime);
}
m_RotationInterpolator.ResetTo(Quaternion.Euler(newState.Rotation), sentTime);
if (newState.HasScaleX)
{
m_ScaleXInterpolator.ResetTo(newState.ScaleX, sentTime);
}
if (newState.HasScaleY)
{
m_ScaleYInterpolator.ResetTo(newState.ScaleY, sentTime);
}
if (newState.HasScaleZ)
{
m_ScaleZInterpolator.ResetTo(newState.ScaleZ, sentTime);
}
return;
}
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);
}
m_RotationInterpolator.AddMeasurement(Quaternion.Euler(newState.Rotation), 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);
}
}
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, (newState.InLocalSpace != m_LastInterpolateLocal));
}
m_LastInterpolateLocal = newState.InLocalSpace;
if (m_CachedNetworkManager.LogLevel == LogLevel.Developer)
{
var pos = new Vector3(newState.PositionX, newState.PositionY, newState.PositionZ);
}
}
///
/// 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()
{
// must set up m_Transform in OnNetworkSpawn because it's possible an object spawns but is disabled
// and thus awake won't be called.
// TODO: investigate further on not sending data for something that is not enabled
m_Transform = transform;
m_ReplicatedNetworkState.OnValueChanged += OnNetworkStateChanged;
CanCommitToTransform = IsServer;
m_CachedIsServer = IsServer;
m_CachedNetworkManager = NetworkManager;
if (CanCommitToTransform)
{
TryCommitTransformToServer(m_Transform, m_CachedNetworkManager.LocalTime.Time);
}
m_LocalAuthoritativeNetworkState = m_ReplicatedNetworkState.Value;
// crucial we do this to reset the interpolators so that recycled objects when using a pool will
// not have leftover interpolator state from the previous object
Initialize();
}
///
public override void OnNetworkDespawn()
{
m_ReplicatedNetworkState.OnValueChanged -= OnNetworkStateChanged;
}
///
public override void OnGainedOwnership()
{
Initialize();
}
///
public override void OnLostOwnership()
{
Initialize();
}
private void Initialize()
{
ResetInterpolatedStateToCurrentAuthoritativeState(); // useful for late joining
if (CanCommitToTransform)
{
m_ReplicatedNetworkState.SetDirty(true);
}
else if (m_Transform != null)
{
ApplyInterpolatedNetworkStateToTransform(m_ReplicatedNetworkState.Value, m_Transform);
}
}
///
/// Directly sets a state on the 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 (!IsOwner)
{
throw new Exception("Trying to set a state on a not owned transform");
}
if (m_CachedNetworkManager && !(m_CachedNetworkManager.IsConnectedClient || m_CachedNetworkManager.IsListening))
{
return;
}
Vector3 pos = posIn == null ? transform.position : (Vector3)posIn;
Quaternion rot = rotIn == null ? transform.rotation : (Quaternion)rotIn;
Vector3 scale = scaleIn == null ? transform.localScale : (Vector3)scaleIn;
if (!CanCommitToTransform)
{
if (!m_CachedIsServer)
{
SetStateServerRpc(pos, rot, scale, shouldGhostsInterpolate);
}
}
else
{
m_Transform.position = pos;
m_Transform.rotation = rot;
m_Transform.localScale = scale;
m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = shouldGhostsInterpolate;
}
}
[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);
}
m_Transform.position = pos;
m_Transform.rotation = rot;
m_Transform.localScale = scale;
m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = shouldTeleport;
}
// 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.
///
protected virtual void Update()
{
if (!IsSpawned)
{
return;
}
if (!Interpolate && m_LastInterpolate)
{
// if we just stopped interpolating, let's clear the interpolators
foreach (var interpolator in m_AllFloatInterpolators)
{
interpolator.Clear();
}
}
m_LastInterpolate = Interpolate;
if (CanCommitToTransform)
{
if (m_CachedIsServer)
{
TryCommitTransformToServer(m_Transform, m_CachedNetworkManager.LocalTime.Time);
}
}
// apply interpolated value
if (m_CachedNetworkManager.IsConnectedClient || m_CachedNetworkManager.IsListening)
{
// eventually, we could hoist this calculation so that it happens once for all objects, not once per object
var cachedDeltaTime = Time.deltaTime;
var serverTime = NetworkManager.ServerTime;
var cachedServerTime = serverTime.Time;
var cachedRenderTime = serverTime.TimeTicksAgo(1).Time;
if (Interpolate)
{
foreach (var interpolator in m_AllFloatInterpolators)
{
interpolator.Update(cachedDeltaTime, cachedRenderTime, cachedServerTime);
}
m_RotationInterpolator.Update(cachedDeltaTime, cachedRenderTime, cachedServerTime);
}
if (!CanCommitToTransform)
{
// Apply updated interpolated value
ApplyInterpolatedNetworkStateToTransform(m_ReplicatedNetworkState.Value, m_Transform);
}
}
m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = false;
}
///
/// Teleports 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("Teleport not allowed");
}
var newRotationEuler = newRotation.eulerAngles;
var stateToSend = m_LocalAuthoritativeNetworkState;
stateToSend.IsTeleportingNextFrame = true;
stateToSend.Position = newPosition;
stateToSend.Rotation = newRotationEuler;
stateToSend.Scale = newScale;
ApplyInterpolatedNetworkStateToTransform(stateToSend, transform);
// set teleport flag in state to signal to ghosts not to interpolate
m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = true;
// check server side
TryCommitValuesToServer(newPosition, newRotationEuler, newScale, m_CachedNetworkManager.LocalTime.Time);
m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = false;
}
///
/// 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();
}
}
}