Skip to content

Commit 92fc3e6

Browse files
feat: transform teleport (#1200)
1 parent c73e232 commit 92fc3e6

File tree

3 files changed

+130
-30
lines changed

3 files changed

+130
-30
lines changed

com.unity.netcode.gameobjects/Components/NetworkTransform.cs

Lines changed: 82 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,13 @@ internal struct NetworkTransformState : INetworkSerializable
2929
private const int k_ScaleXBit = 7;
3030
private const int k_ScaleYBit = 8;
3131
private const int k_ScaleZBit = 9;
32+
private const int k_TeleportingBit = 10;
3233

33-
// 10-15: <unused>
34+
// 11-15: <unused>
3435
private ushort m_Bitset;
3536

37+
38+
3639
public bool InLocalSpace
3740
{
3841
get => (m_Bitset & (1 << k_InLocalSpaceBit)) != 0;
@@ -136,6 +139,16 @@ public bool HasScaleZ
136139
}
137140
}
138141

142+
public bool IsTeleportingNextFrame
143+
{
144+
get => (m_Bitset & (1 << k_TeleportingBit)) != 0;
145+
set
146+
{
147+
if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_TeleportingBit)); }
148+
else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_TeleportingBit)); }
149+
}
150+
}
151+
139152
public float PositionX, PositionY, PositionZ;
140153
public float RotAngleX, RotAngleY, RotAngleZ;
141154
public float ScaleX, ScaleY, ScaleZ;
@@ -277,6 +290,10 @@ public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReade
277290

278291
private Transform m_Transform; // cache the transform component to reduce unnecessary bounce between managed and native
279292
private int m_LastSentTick;
293+
private NetworkTransformState m_LastSentState;
294+
295+
private const string k_NoAuthorityMessage = "A local change to {dirtyField} without authority detected, reverting back to latest interpolated network state!";
296+
280297

281298
/// <summary>
282299
/// Tries updating the server authoritative transform, only if allowed.
@@ -288,17 +305,28 @@ public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReade
288305
protected void TryCommitTransformToServer(Transform transformToCommit, double dirtyTime)
289306
{
290307
var isDirty = ApplyTransformToNetworkState(ref m_LocalAuthoritativeNetworkState, dirtyTime, transformToCommit);
308+
TryCommit(isDirty);
309+
}
291310

292-
void Send()
311+
private void TryCommitValuesToServer(Vector3 position, Vector3 rotation, Vector3 scale, double dirtyTime)
312+
{
313+
var isDirty = ApplyTransformToNetworkStateWithInfo(ref m_LocalAuthoritativeNetworkState, dirtyTime, position, rotation, scale);
314+
315+
TryCommit(isDirty.isDirty);
316+
}
317+
318+
private void TryCommit(bool isDirty)
319+
{
320+
void Send(NetworkTransformState stateToSend)
293321
{
294322
if (IsServer)
295323
{
296324
// server RPC takes a few frames to execute server side, we want this to execute immediately
297-
CommitLocallyAndReplicate(m_LocalAuthoritativeNetworkState);
325+
CommitLocallyAndReplicate(stateToSend);
298326
}
299327
else
300328
{
301-
CommitTransformServerRpc(m_LocalAuthoritativeNetworkState);
329+
CommitTransformServerRpc(stateToSend);
302330
}
303331
}
304332

@@ -311,14 +339,15 @@ void Send()
311339
// making it immobile.
312340
if (isDirty)
313341
{
314-
Send();
342+
Send(m_LocalAuthoritativeNetworkState);
315343
m_HasSentLastValue = false;
316344
m_LastSentTick = NetworkManager.LocalTime.Tick;
345+
m_LastSentState = m_LocalAuthoritativeNetworkState;
317346
}
318347
else if (!m_HasSentLastValue && NetworkManager.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
319348
{
320-
m_LocalAuthoritativeNetworkState.SentTime = NetworkManager.LocalTime.Time; // time 1+ tick later
321-
Send();
349+
m_LastSentState.SentTime = NetworkManager.LocalTime.Time; // time 1+ tick later
350+
Send(m_LastSentState);
322351
m_HasSentLastValue = true;
323352
}
324353
}
@@ -334,7 +363,6 @@ private void CommitTransformServerRpc(NetworkTransformState networkState, Server
334363

335364
private void CommitLocallyAndReplicate(NetworkTransformState networkState)
336365
{
337-
m_LocalAuthoritativeNetworkState = networkState;
338366
m_ReplicatedNetworkState.Value = networkState;
339367
AddInterpolatedState(networkState);
340368
}
@@ -364,7 +392,11 @@ internal bool ApplyTransformToNetworkState(ref NetworkTransformState networkStat
364392
var position = InLocalSpace ? transformToUse.localPosition : transformToUse.position;
365393
var rotAngles = InLocalSpace ? transformToUse.localEulerAngles : transformToUse.eulerAngles;
366394
var scale = InLocalSpace ? transformToUse.localScale : transformToUse.lossyScale;
395+
return ApplyTransformToNetworkStateWithInfo(ref networkState, dirtyTime, position, rotAngles, scale);
396+
}
367397

398+
private (bool isDirty, bool isPositionDirty, bool isRotationDirty, bool isScaleDirty) ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState networkState, double dirtyTime, Vector3 position, Vector3 rotAngles, Vector3 scale)
399+
{
368400
var isDirty = false;
369401
var isPositionDirty = false;
370402
var isRotationDirty = false;
@@ -484,17 +516,17 @@ private void ApplyInterpolatedNetworkStateToTransform(NetworkTransformState netw
484516
// Position Read
485517
if (SyncPositionX)
486518
{
487-
interpolatedPosition.x = Interpolate ? m_PositionXInterpolator.GetInterpolatedValue() : networkState.Position.x;
519+
interpolatedPosition.x = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Position.x : m_PositionXInterpolator.GetInterpolatedValue();
488520
}
489521

490522
if (SyncPositionY)
491523
{
492-
interpolatedPosition.y = Interpolate ? m_PositionYInterpolator.GetInterpolatedValue() : networkState.Position.y;
524+
interpolatedPosition.y = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Position.y : m_PositionYInterpolator.GetInterpolatedValue();
493525
}
494526

495527
if (SyncPositionZ)
496528
{
497-
interpolatedPosition.z = Interpolate ? m_PositionZInterpolator.GetInterpolatedValue() : networkState.Position.z;
529+
interpolatedPosition.z = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Position.z : m_PositionZInterpolator.GetInterpolatedValue();
498530
}
499531

500532
// again, we should be using quats here
@@ -503,34 +535,34 @@ private void ApplyInterpolatedNetworkStateToTransform(NetworkTransformState netw
503535
var eulerAngles = m_RotationInterpolator.GetInterpolatedValue().eulerAngles;
504536
if (SyncRotAngleX)
505537
{
506-
interpolatedRotAngles.x = Interpolate ? eulerAngles.x : networkState.Rotation.x;
538+
interpolatedRotAngles.x = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Rotation.x : eulerAngles.x;
507539
}
508540

509541
if (SyncRotAngleY)
510542
{
511-
interpolatedRotAngles.y = Interpolate ? eulerAngles.y : networkState.Rotation.y;
543+
interpolatedRotAngles.y = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Rotation.y : eulerAngles.y;
512544
}
513545

514546
if (SyncRotAngleZ)
515547
{
516-
interpolatedRotAngles.z = Interpolate ? eulerAngles.z : networkState.Rotation.z;
548+
interpolatedRotAngles.z = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Rotation.z : eulerAngles.z;
517549
}
518550
}
519551

520552
// Scale Read
521553
if (SyncScaleX)
522554
{
523-
interpolatedScale.x = Interpolate ? m_ScaleXInterpolator.GetInterpolatedValue() : networkState.Scale.x;
555+
interpolatedScale.x = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Scale.x : m_ScaleXInterpolator.GetInterpolatedValue();
524556
}
525557

526558
if (SyncScaleY)
527559
{
528-
interpolatedScale.y = Interpolate ? m_ScaleYInterpolator.GetInterpolatedValue() : networkState.Scale.y;
560+
interpolatedScale.y = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Scale.y : m_ScaleYInterpolator.GetInterpolatedValue();
529561
}
530562

531563
if (SyncScaleZ)
532564
{
533-
interpolatedScale.z = Interpolate ? m_ScaleZInterpolator.GetInterpolatedValue() : networkState.Scale.z;
565+
interpolatedScale.z = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Scale.z : m_ScaleZInterpolator.GetInterpolatedValue();
534566
}
535567

536568
// Position Apply
@@ -722,9 +754,11 @@ private void OnDestroy()
722754
/// </summary>
723755
/// <param name="posIn"></param> new position to move to. Can be null
724756
/// <param name="rotIn"></param> new rotation to rotate to. Can be null
725-
/// <param name="scaleIn"></param> new scale to scale to. Can be null
757+
/// <param name="scaleIn">new scale to scale to. Can be null</param>
758+
/// <param name="shouldGhostsInterpolate">Should other clients interpolate this change or not. True by default</param>
759+
/// new scale to scale to. Can be null
726760
/// <exception cref="Exception"></exception>
727-
public void SetState(Vector3? posIn = null, Quaternion? rotIn = null, Vector3? scaleIn = null)
761+
public void SetState(Vector3? posIn = null, Quaternion? rotIn = null, Vector3? scaleIn = null, bool shouldGhostsInterpolate = true)
728762
{
729763
if (!IsOwner)
730764
{
@@ -744,29 +778,31 @@ public void SetState(Vector3? posIn = null, Quaternion? rotIn = null, Vector3? s
744778
{
745779
if (!IsServer)
746780
{
747-
SetStateServerRpc(pos, rot, scale);
781+
SetStateServerRpc(pos, rot, scale, shouldGhostsInterpolate);
748782
}
749783
}
750784
else
751785
{
752-
transform.position = pos;
753-
transform.rotation = rot;
754-
transform.localScale = scale;
786+
m_Transform.position = pos;
787+
m_Transform.rotation = rot;
788+
m_Transform.localScale = scale;
789+
m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = shouldGhostsInterpolate;
755790
}
756791
}
757792

758793
[ServerRpc]
759-
private void SetStateServerRpc(Vector3 pos, Quaternion rot, Vector3 scale)
794+
private void SetStateServerRpc(Vector3 pos, Quaternion rot, Vector3 scale, bool shouldTeleport)
760795
{
761796
// server has received this RPC request to move change transform. Give the server a chance to modify or
762797
// even reject the move
763798
if (OnClientRequestChange != null)
764799
{
765800
(pos, rot, scale) = OnClientRequestChange(pos, rot, scale);
766801
}
767-
transform.position = pos;
768-
transform.rotation = rot;
769-
transform.localScale = scale;
802+
m_Transform.position = pos;
803+
m_Transform.rotation = rot;
804+
m_Transform.localScale = scale;
805+
m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = shouldTeleport;
770806
}
771807
#endregion
772808

@@ -818,23 +854,39 @@ protected virtual void Update()
818854
// ignoring rotation dirty since quaternions will mess with euler angles, making this impossible to determine if the change to a single axis comes
819855
// from an unauthorized transform change or euler to quaternion conversion artifacts.
820856
var dirtyField = oldStateDirtyInfo.isPositionDirty ? "position" : oldStateDirtyInfo.isRotationDirty ? "rotation" : "scale";
821-
Debug.LogWarning($"A local change to {dirtyField} without authority detected, reverting back to latest interpolated network state!", this);
857+
Debug.LogWarning(dirtyField + k_NoAuthorityMessage, this);
822858
}
823859

824860
// Apply updated interpolated value
825861
ApplyInterpolatedNetworkStateToTransform(m_ReplicatedNetworkState.Value, m_Transform);
826862
}
827863
}
864+
865+
m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = false;
828866
}
829867

830868
/// <summary>
831869
/// Teleports the transform to the given values without interpolating
832870
/// </summary>
833871
public void Teleport(Vector3 newPosition, Quaternion newRotation, Vector3 newScale)
834872
{
873+
if (!CanCommitToTransform)
874+
{
875+
throw new Exception("Teleport not allowed, " + k_NoAuthorityMessage);
876+
}
877+
878+
var newRotationEuler = newRotation.eulerAngles;
879+
var stateToSend = m_LocalAuthoritativeNetworkState;
880+
stateToSend.IsTeleportingNextFrame = true;
881+
stateToSend.Position = newPosition;
882+
stateToSend.Rotation = newRotationEuler;
883+
stateToSend.Scale = newScale;
884+
ApplyInterpolatedNetworkStateToTransform(stateToSend, transform);
885+
// set teleport flag in state to signal to ghosts not to interpolate
886+
m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = true;
835887
// check server side
836-
// set teleport flag in state
837-
throw new NotImplementedException(); // TODO MTT-769
888+
TryCommitValuesToServer(newPosition, newRotationEuler, newScale, NetworkManager.LocalTime.Time);
889+
m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = false;
838890
}
839891
}
840892
}

testproject/Assets/SetTeleport.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#if UNITY_EDITOR
2+
using System;
3+
using Unity.Netcode.Components;
4+
using UnityEditor;
5+
using UnityEngine;
6+
7+
public class SetTeleport : MonoBehaviour
8+
{
9+
public void Set(Vector3 pos)
10+
{
11+
// GetComponent<NetworkTransform>().Teleport(pos, transform.rotation, transform.localScale);
12+
GetComponent<NetworkTransform>().SetState(pos, transform.rotation, transform.localScale, false);
13+
}
14+
15+
[CustomEditor(typeof(SetTeleport))]
16+
public class GameEventEditor : Editor
17+
{
18+
private string m_Pos = "position";
19+
20+
public override void OnInspectorGUI()
21+
{
22+
base.OnInspectorGUI();
23+
24+
var setterObject = (SetTeleport)target;
25+
26+
GUILayout.TextArea($"Current pos: {setterObject.transform.position}");
27+
m_Pos = GUILayout.TextField(m_Pos);
28+
29+
if (GUILayout.Button("Set"))
30+
{
31+
var posParsed = m_Pos.Split(',');
32+
setterObject.Set(new Vector3(Convert.ToUInt32(posParsed[0]), Convert.ToUInt32(posParsed[1]), Convert.ToUInt32(posParsed[2])));
33+
}
34+
}
35+
}
36+
}
37+
#endif

testproject/Assets/SetTeleport.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)