using System; using System.Collections.Generic; using UnityEngine; using Random = UnityEngine.Random; 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 { public const float PositionThresholdDefault = .001f; public const float RotAngleThresholdDefault = .01f; public const float ScaleThresholdDefault = .01f; public delegate (Vector3 pos, Quaternion rotOut, Vector3 scale) OnClientRequestChangeDelegate(Vector3 pos, Quaternion rot, Vector3 scale); 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; public 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 public 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)); } } } public 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)); } } } public 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 public 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)); } } } public 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)); } } } public 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 public 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)); } } } public 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)); } } } public 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)); } } } public 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)); } } } public float PositionX, PositionY, PositionZ; public float RotAngleX, RotAngleY, RotAngleZ; public float ScaleX, ScaleY, ScaleZ; public double SentTime; public Vector3 Position { get { return new Vector3(PositionX, PositionY, PositionZ); } set { PositionX = value.x; PositionY = value.y; PositionZ = value.z; } } public Vector3 Rotation { get { return new Vector3(RotAngleX, RotAngleY, RotAngleZ); } set { RotAngleX = value.x; RotAngleY = value.y; RotAngleZ = value.z; } } public 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); } } } public bool SyncPositionX = true, SyncPositionY = true, SyncPositionZ = true; public bool SyncRotAngleX = true, SyncRotAngleY = true, SyncRotAngleZ = true; public bool SyncScaleX = true, SyncScaleY = true, SyncScaleZ = true; public float PositionThreshold = PositionThresholdDefault; public float RotAngleThreshold = RotAngleThresholdDefault; 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; 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 /// // 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; protected bool m_CachedIsServer; protected NetworkManager m_CachedNetworkManager; private readonly NetworkVariable m_ReplicatedNetworkState = new NetworkVariable(new NetworkTransformState()); private NetworkTransformState m_LocalAuthoritativeNetworkState; private NetworkTransformState m_PrevNetworkState; 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; 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); } // 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(networkState.RotAngleX - rotAngles.x) > RotAngleThreshold) { networkState.RotAngleX = rotAngles.x; networkState.HasRotAngleX = true; isRotationDirty = true; } if (SyncRotAngleY && Mathf.Abs(networkState.RotAngleY - rotAngles.y) > RotAngleThreshold) { networkState.RotAngleY = rotAngles.y; networkState.HasRotAngleY = true; isRotationDirty = true; } if (SyncRotAngleZ && Mathf.Abs(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) { m_PrevNetworkState = networkState; 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 = 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; } m_PrevNetworkState.Position = interpolatedPosition; } // RotAngles Apply if (SyncRotAngleX || SyncRotAngleY || SyncRotAngleZ) { if (InLocalSpace) { transformToUpdate.localRotation = Quaternion.Euler(interpolatedRotAngles); } else { transformToUpdate.rotation = Quaternion.Euler(interpolatedRotAngles); } m_PrevNetworkState.Rotation = interpolatedRotAngles; } // Scale Apply if (SyncScaleX || SyncScaleY || SyncScaleZ) { transformToUpdate.localScale = interpolatedScale; m_PrevNetworkState.Scale = interpolatedScale; } } private void AddInterpolatedState(NetworkTransformState newState) { var sentTime = newState.SentTime; 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) { // todo MTT-849 should never happen but yet it does! maybe revisit/dig after NetVar updates and snapshot system lands? return; } if (CanCommitToTransform) { // we're the authority, we ignore incoming changes return; } Debug.DrawLine(newState.Position, newState.Position + Vector3.up + Vector3.left, Color.green, 10, false); AddInterpolatedState(newState); if (m_CachedNetworkManager.LogLevel == LogLevel.Developer) { var pos = new Vector3(newState.PositionX, newState.PositionY, newState.PositionZ); Debug.DrawLine(pos, pos + Vector3.up + Vector3.left * Random.Range(0.5f, 2f), Color.green, k_DebugDrawLineTime, false); } } 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 { ApplyInterpolatedNetworkStateToTransform(m_ReplicatedNetworkState.Value, m_Transform); } } #region state set /// /// 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; } #endregion // 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 (!NetworkObject.IsSpawned) { return; } if (CanCommitToTransform) { if (m_CachedIsServer) { TryCommitTransformToServer(m_Transform, m_CachedNetworkManager.LocalTime.Time); } m_PrevNetworkState = m_LocalAuthoritativeNetworkState; } // 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; foreach (var interpolator in m_AllFloatInterpolators) { interpolator.Update(cachedDeltaTime, cachedRenderTime, cachedServerTime); } m_RotationInterpolator.Update(cachedDeltaTime, cachedRenderTime, cachedServerTime); if (!CanCommitToTransform) { #if NGO_TRANSFORM_DEBUG if (m_CachedNetworkManager.LogLevel == LogLevel.Developer) { // TODO: This should be a component gizmo - not some debug draw based on log level var interpolatedPosition = new Vector3(m_PositionXInterpolator.GetInterpolatedValue(), m_PositionYInterpolator.GetInterpolatedValue(), m_PositionZInterpolator.GetInterpolatedValue()); Debug.DrawLine(interpolatedPosition, interpolatedPosition + Vector3.up, Color.magenta, k_DebugDrawLineTime, false); // try to update previously consumed NetworkState // if we have any changes, that means made some updates locally // we apply the latest ReplNetworkState again to revert our changes var oldStateDirtyInfo = ApplyTransformToNetworkStateWithInfo(ref m_PrevNetworkState, 0, m_Transform); // there are several bugs in this code, as we the message is dumped out under odd circumstances // For Matt, it would trigger when an object's rotation was perturbed by colliding with another // object vs. explicitly rotating it if (oldStateDirtyInfo.isPositionDirty || oldStateDirtyInfo.isScaleDirty || (oldStateDirtyInfo.isRotationDirty && SyncRotAngleX && SyncRotAngleY && SyncRotAngleZ)) { // ignoring rotation dirty since quaternions will mess with euler angles, making this impossible to determine if the change to a single axis comes // from an unauthorized transform change or euler to quaternion conversion artifacts. var dirtyField = oldStateDirtyInfo.isPositionDirty ? "position" : oldStateDirtyInfo.isRotationDirty ? "rotation" : "scale"; Debug.LogWarning($"A local change to {dirtyField} without authority detected, reverting back to latest interpolated network state!", this); } } #endif // Apply updated interpolated value ApplyInterpolatedNetworkStateToTransform(m_ReplicatedNetworkState.Value, m_Transform); } } m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = false; } /// /// Teleports the transform to the given values without interpolating /// 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; } } }