diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 8eebfffb3d..47d74d92fd 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -11,14 +11,26 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Added - Added "Check for NetworkObject Component" property to the Multiplayer->Netcode for GameObjects project settings. When disabled, this will bypass the in-editor `NetworkObject` check on `NetworkBehaviour` components. (#3031) +- Added `NetworkTransform.SwitchTransformSpaceWhenParented` property that, when enabled, will handle the world to local, local to world, and local to local transform space transitions when interpolation is enabled. (#3013) +- Added `NetworkTransform.TickSyncChildren` that, when enabled, will tick synchronize nested and/or child `NetworkTransform` components to eliminate any potential visual jittering that could occur if the `NetworkTransform` instances get into a state where their state updates are landing on different network ticks. (#3013) +- Added `NetworkObject.AllowOwnerToParent` property to provide the ability to allow clients to parent owned objects when running in a client-server network topology. (#3013) +- Added `NetworkObject.SyncOwnerTransformWhenParented` property to provide a way to disable applying the server's transform information in the parenting message on the client owner instance which can be useful for owner authoritative motion models. (#3013) +- Added `NetcodeEditorBase` editor helper class to provide easier modification and extension of the SDK's components. (#3013) ### Fixed --Fixed issue where the `NetworkSpawnManager.HandleNetworkObjectShow` could throw an exception if one of the `NetworkObject` components to show was destroyed during the same frame. (#3030) +- Fixed issue where the `NetworkSpawnManager.HandleNetworkObjectShow` could throw an exception if one of the `NetworkObject` components to show was destroyed during the same frame. (#3030) - Fixed issue where the `NetworkManagerHelper` was continuing to check for hierarchy changes when in play mode. (#3026) +- Fixed issue with newly/late joined clients and `NetworkTransform` synchronization of parented `NetworkObject` instances. (#3013) +- Fixed issue with smooth transitions between transform spaces when interpolation is enabled (requires `NetworkTransform.SwitchTransformSpaceWhenParented` to be enabled). (#3013) ### Changed +- Changed `NetworkTransformEditor` so it now derives from `NetcodeEditorBase`. (#3013) +- Changed `NetworkRigidbodyBaseEditor` so it now derives from `NetcodeEditorBase`. (#3013) +- Changed `NetworkManagerEditor` so it now derives from `NetcodeEditorBase`. (#3013) + + ## [2.0.0-pre.4] - 2024-08-21 ### Added diff --git a/com.unity.netcode.gameobjects/Editor/NetcodeEditorBase.cs b/com.unity.netcode.gameobjects/Editor/NetcodeEditorBase.cs new file mode 100644 index 0000000000..5112065e80 --- /dev/null +++ b/com.unity.netcode.gameobjects/Editor/NetcodeEditorBase.cs @@ -0,0 +1,62 @@ +using System; +using UnityEditor; +using UnityEngine; + +namespace Unity.Netcode.Editor +{ + /// + /// The base Netcode Editor helper class to display derived based components
+ /// where each child generation's properties will be displayed within a FoldoutHeaderGroup. + ///
+ [CanEditMultipleObjects] + public partial class NetcodeEditorBase : UnityEditor.Editor where TT : MonoBehaviour + { + /// + public virtual void OnEnable() + { + } + + /// + /// Helper method to draw the properties of the specified child type component within a FoldoutHeaderGroup. + /// + /// The specific child type that should have its properties drawn. + /// The component type of the . + /// The to invoke that will draw the type properties. + /// The current expanded property value + /// The invoked to apply the updated value. + protected void DrawFoldOutGroup(Type type, Action displayProperties, bool expanded, Action setExpandedProperty) + { + var baseClass = target as TT; + EditorGUI.BeginChangeCheck(); + serializedObject.Update(); + var currentClass = typeof(T); + if (type.IsSubclassOf(currentClass) || (!type.IsSubclassOf(currentClass) && currentClass.IsSubclassOf(typeof(TT)))) + { + var expandedValue = EditorGUILayout.BeginFoldoutHeaderGroup(expanded, $"{currentClass.Name} Properties"); + if (expandedValue) + { + EditorGUILayout.EndFoldoutHeaderGroup(); + displayProperties.Invoke(); + } + else + { + EditorGUILayout.EndFoldoutHeaderGroup(); + } + EditorGUILayout.Space(); + setExpandedProperty.Invoke(expandedValue); + } + else + { + displayProperties.Invoke(); + } + serializedObject.ApplyModifiedProperties(); + EditorGUI.EndChangeCheck(); + } + + /// + public override void OnInspectorGUI() + { + serializedObject.ApplyModifiedProperties(); + } + } +} diff --git a/com.unity.netcode.gameobjects/Editor/NetcodeEditorBase.cs.meta b/com.unity.netcode.gameobjects/Editor/NetcodeEditorBase.cs.meta new file mode 100644 index 0000000000..25f0b07e38 --- /dev/null +++ b/com.unity.netcode.gameobjects/Editor/NetcodeEditorBase.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4ce97256a2d80f94bb340e13c71a24b8 \ No newline at end of file diff --git a/com.unity.netcode.gameobjects/Editor/NetworkBehaviourEditor.cs b/com.unity.netcode.gameobjects/Editor/NetworkBehaviourEditor.cs index c98870c591..1312ff69e6 100644 --- a/com.unity.netcode.gameobjects/Editor/NetworkBehaviourEditor.cs +++ b/com.unity.netcode.gameobjects/Editor/NetworkBehaviourEditor.cs @@ -301,9 +301,8 @@ public override void OnInspectorGUI() expanded = false; } - - serializedObject.ApplyModifiedProperties(); EditorGUI.EndChangeCheck(); + serializedObject.ApplyModifiedProperties(); } /// diff --git a/com.unity.netcode.gameobjects/Editor/NetworkManagerEditor.cs b/com.unity.netcode.gameobjects/Editor/NetworkManagerEditor.cs index 0db3a653f7..5445b0d8e8 100644 --- a/com.unity.netcode.gameobjects/Editor/NetworkManagerEditor.cs +++ b/com.unity.netcode.gameobjects/Editor/NetworkManagerEditor.cs @@ -13,7 +13,7 @@ namespace Unity.Netcode.Editor /// [CustomEditor(typeof(NetworkManager), true)] [CanEditMultipleObjects] - public class NetworkManagerEditor : UnityEditor.Editor + public class NetworkManagerEditor : NetcodeEditorBase { private static GUIStyle s_CenteredWordWrappedLabelStyle; private static GUIStyle s_HelpBoxStyle; @@ -168,16 +168,8 @@ private void CheckNullProperties() .FindPropertyRelative(nameof(NetworkPrefabs.NetworkPrefabsLists)); } - /// - public override void OnInspectorGUI() + private void DisplayNetworkManagerProperties() { - Initialize(); - CheckNullProperties(); - -#if !MULTIPLAYER_TOOLS - DrawInstallMultiplayerToolsTip(); -#endif - if (!m_NetworkManager.IsServer && !m_NetworkManager.IsClient) { serializedObject.Update(); @@ -298,48 +290,50 @@ public override void OnInspectorGUI() } serializedObject.ApplyModifiedProperties(); + } + } + private void DisplayCallToActionButtons() + { + if (!m_NetworkManager.IsServer && !m_NetworkManager.IsClient) + { + string buttonDisabledReasonSuffix = ""; - // Start buttons below + if (!EditorApplication.isPlaying) { - string buttonDisabledReasonSuffix = ""; + buttonDisabledReasonSuffix = ". This can only be done in play mode"; + GUI.enabled = false; + } - if (!EditorApplication.isPlaying) + if (m_NetworkManager.NetworkConfig.NetworkTopology == NetworkTopologyTypes.ClientServer) + { + if (GUILayout.Button(new GUIContent("Start Host", "Starts a host instance" + buttonDisabledReasonSuffix))) { - buttonDisabledReasonSuffix = ". This can only be done in play mode"; - GUI.enabled = false; + m_NetworkManager.StartHost(); } - if (m_NetworkManager.NetworkConfig.NetworkTopology == NetworkTopologyTypes.ClientServer) + if (GUILayout.Button(new GUIContent("Start Server", "Starts a server instance" + buttonDisabledReasonSuffix))) { - if (GUILayout.Button(new GUIContent("Start Host", "Starts a host instance" + buttonDisabledReasonSuffix))) - { - m_NetworkManager.StartHost(); - } - - if (GUILayout.Button(new GUIContent("Start Server", "Starts a server instance" + buttonDisabledReasonSuffix))) - { - m_NetworkManager.StartServer(); - } + m_NetworkManager.StartServer(); + } - if (GUILayout.Button(new GUIContent("Start Client", "Starts a client instance" + buttonDisabledReasonSuffix))) - { - m_NetworkManager.StartClient(); - } + if (GUILayout.Button(new GUIContent("Start Client", "Starts a client instance" + buttonDisabledReasonSuffix))) + { + m_NetworkManager.StartClient(); } - else + } + else + { + if (GUILayout.Button(new GUIContent("Start Client", "Starts a distributed authority client instance" + buttonDisabledReasonSuffix))) { - if (GUILayout.Button(new GUIContent("Start Client", "Starts a distributed authority client instance" + buttonDisabledReasonSuffix))) - { - m_NetworkManager.StartClient(); - } + m_NetworkManager.StartClient(); } + } - if (!EditorApplication.isPlaying) - { - GUI.enabled = true; - } + if (!EditorApplication.isPlaying) + { + GUI.enabled = true; } } else @@ -368,6 +362,21 @@ public override void OnInspectorGUI() } } + /// + public override void OnInspectorGUI() + { + var networkManager = target as NetworkManager; + Initialize(); + CheckNullProperties(); +#if !MULTIPLAYER_TOOLS + DrawInstallMultiplayerToolsTip(); +#endif + void SetExpanded(bool expanded) { networkManager.NetworkManagerExpanded = expanded; }; + DrawFoldOutGroup(networkManager.GetType(), DisplayNetworkManagerProperties, networkManager.NetworkManagerExpanded, SetExpanded); + DisplayCallToActionButtons(); + base.OnInspectorGUI(); + } + private static void DrawInstallMultiplayerToolsTip() { const string getToolsText = "Access additional tools for multiplayer development by installing the Multiplayer Tools package in the Package Manager."; diff --git a/com.unity.netcode.gameobjects/Editor/NetworkRigidbodyBaseEditor.cs b/com.unity.netcode.gameobjects/Editor/NetworkRigidbodyBaseEditor.cs new file mode 100644 index 0000000000..8ab99436d8 --- /dev/null +++ b/com.unity.netcode.gameobjects/Editor/NetworkRigidbodyBaseEditor.cs @@ -0,0 +1,42 @@ +#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D +using Unity.Netcode.Components; +using UnityEditor; + +namespace Unity.Netcode.Editor +{ + [CustomEditor(typeof(NetworkRigidbodyBase), true)] + [CanEditMultipleObjects] + public class NetworkRigidbodyBaseEditor : NetcodeEditorBase + { + private SerializedProperty m_UseRigidBodyForMotion; + private SerializedProperty m_AutoUpdateKinematicState; + private SerializedProperty m_AutoSetKinematicOnDespawn; + + + public override void OnEnable() + { + m_UseRigidBodyForMotion = serializedObject.FindProperty(nameof(NetworkRigidbodyBase.UseRigidBodyForMotion)); + m_AutoUpdateKinematicState = serializedObject.FindProperty(nameof(NetworkRigidbodyBase.AutoUpdateKinematicState)); + m_AutoSetKinematicOnDespawn = serializedObject.FindProperty(nameof(NetworkRigidbodyBase.AutoSetKinematicOnDespawn)); + + base.OnEnable(); + } + + private void DisplayNetworkRigidbodyProperties() + { + EditorGUILayout.PropertyField(m_UseRigidBodyForMotion); + EditorGUILayout.PropertyField(m_AutoUpdateKinematicState); + EditorGUILayout.PropertyField(m_AutoSetKinematicOnDespawn); + } + + /// + public override void OnInspectorGUI() + { + var networkRigidbodyBase = target as NetworkRigidbodyBase; + void SetExpanded(bool expanded) { networkRigidbodyBase.NetworkRigidbodyBaseExpanded = expanded; }; + DrawFoldOutGroup(networkRigidbodyBase.GetType(), DisplayNetworkRigidbodyProperties, networkRigidbodyBase.NetworkRigidbodyBaseExpanded, SetExpanded); + base.OnInspectorGUI(); + } + } +} +#endif diff --git a/com.unity.netcode.gameobjects/Editor/NetworkRigidbodyBaseEditor.cs.meta b/com.unity.netcode.gameobjects/Editor/NetworkRigidbodyBaseEditor.cs.meta new file mode 100644 index 0000000000..c7fef8a318 --- /dev/null +++ b/com.unity.netcode.gameobjects/Editor/NetworkRigidbodyBaseEditor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 06561c57f81a6354f8bb16076f1de3a9 \ No newline at end of file diff --git a/com.unity.netcode.gameobjects/Editor/NetworkTransformEditor.cs b/com.unity.netcode.gameobjects/Editor/NetworkTransformEditor.cs index 4affff1ffb..7d8b26522c 100644 --- a/com.unity.netcode.gameobjects/Editor/NetworkTransformEditor.cs +++ b/com.unity.netcode.gameobjects/Editor/NetworkTransformEditor.cs @@ -8,8 +8,11 @@ namespace Unity.Netcode.Editor /// The for /// [CustomEditor(typeof(NetworkTransform), true)] - public class NetworkTransformEditor : UnityEditor.Editor + [CanEditMultipleObjects] + public class NetworkTransformEditor : NetcodeEditorBase { + private SerializedProperty m_SwitchTransformSpaceWhenParented; + private SerializedProperty m_TickSyncChildren; private SerializedProperty m_UseUnreliableDeltas; private SerializedProperty m_SyncPositionXProperty; private SerializedProperty m_SyncPositionYProperty; @@ -39,8 +42,10 @@ public class NetworkTransformEditor : UnityEditor.Editor private static GUIContent s_ScaleLabel = EditorGUIUtility.TrTextContent("Scale"); /// - public virtual void OnEnable() + public override void OnEnable() { + m_SwitchTransformSpaceWhenParented = serializedObject.FindProperty(nameof(NetworkTransform.SwitchTransformSpaceWhenParented)); + m_TickSyncChildren = serializedObject.FindProperty(nameof(NetworkTransform.TickSyncChildren)); m_UseUnreliableDeltas = serializedObject.FindProperty(nameof(NetworkTransform.UseUnreliableDeltas)); m_SyncPositionXProperty = serializedObject.FindProperty(nameof(NetworkTransform.SyncPositionX)); m_SyncPositionYProperty = serializedObject.FindProperty(nameof(NetworkTransform.SyncPositionY)); @@ -61,10 +66,10 @@ public virtual void OnEnable() m_UseHalfFloatPrecision = serializedObject.FindProperty(nameof(NetworkTransform.UseHalfFloatPrecision)); m_SlerpPosition = serializedObject.FindProperty(nameof(NetworkTransform.SlerpPosition)); m_AuthorityMode = serializedObject.FindProperty(nameof(NetworkTransform.AuthorityMode)); + base.OnEnable(); } - /// - public override void OnInspectorGUI() + private void DisplayNetworkTransformProperties() { var networkTransform = target as NetworkTransform; EditorGUILayout.LabelField("Axis to Synchronize", EditorStyles.boldLabel); @@ -141,9 +146,15 @@ public override void OnInspectorGUI() EditorGUILayout.PropertyField(m_ScaleThresholdProperty); EditorGUILayout.Space(); EditorGUILayout.LabelField("Delivery", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(m_TickSyncChildren); EditorGUILayout.PropertyField(m_UseUnreliableDeltas); EditorGUILayout.Space(); EditorGUILayout.LabelField("Configurations", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(m_SwitchTransformSpaceWhenParented); + if (m_SwitchTransformSpaceWhenParented.boolValue) + { + m_TickSyncChildren.boolValue = true; + } EditorGUILayout.PropertyField(m_InLocalSpaceProperty); if (!networkTransform.HideInterpolateValue) { @@ -163,8 +174,7 @@ public override void OnInspectorGUI() #if COM_UNITY_MODULES_PHYSICS // if rigidbody is present but network rigidbody is not present - var go = ((NetworkTransform)target).gameObject; - if (go.TryGetComponent(out _) && go.TryGetComponent(out _) == false) + if (networkTransform.TryGetComponent(out _) && networkTransform.TryGetComponent(out _) == false) { EditorGUILayout.HelpBox("This GameObject contains a Rigidbody but no NetworkRigidbody.\n" + "Add a NetworkRigidbody component to improve Rigidbody synchronization.", MessageType.Warning); @@ -172,14 +182,23 @@ public override void OnInspectorGUI() #endif // COM_UNITY_MODULES_PHYSICS #if COM_UNITY_MODULES_PHYSICS2D - if (go.TryGetComponent(out _) && go.TryGetComponent(out _) == false) + if (networkTransform.TryGetComponent(out _) && networkTransform.TryGetComponent(out _) == false) { EditorGUILayout.HelpBox("This GameObject contains a Rigidbody2D but no NetworkRigidbody2D.\n" + "Add a NetworkRigidbody2D component to improve Rigidbody2D synchronization.", MessageType.Warning); } #endif // COM_UNITY_MODULES_PHYSICS2D + } + - serializedObject.ApplyModifiedProperties(); + + /// + public override void OnInspectorGUI() + { + var networkTransform = target as NetworkTransform; + void SetExpanded(bool expanded) { networkTransform.NetworkTransformExpanded = expanded; }; + DrawFoldOutGroup(networkTransform.GetType(), DisplayNetworkTransformProperties, networkTransform.NetworkTransformExpanded, SetExpanded); + base.OnInspectorGUI(); } } } diff --git a/com.unity.netcode.gameobjects/Runtime/Components/AnticipatedNetworkTransform.cs b/com.unity.netcode.gameobjects/Runtime/Components/AnticipatedNetworkTransform.cs index 10c1d18979..21d3c054bf 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/AnticipatedNetworkTransform.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/AnticipatedNetworkTransform.cs @@ -239,19 +239,13 @@ public void AnticipateState(TransformState newState) m_CurrentSmoothTime = 0; } - public override void OnUpdate() + private void ProcessSmoothing() { // If not spawned or this instance has authority, exit early if (!IsSpawned) { return; } - // Do not call the base class implementation... - // AnticipatedNetworkTransform applies its authoritative state immediately rather than waiting for update - // This is because AnticipatedNetworkTransforms may need to reference each other in reanticipating - // and we will want all reanticipation done before anything else wants to reference the transform in - // OnUpdate() - //base.Update(); if (m_CurrentSmoothTime < m_SmoothDuration) { @@ -262,7 +256,7 @@ public override void OnUpdate() m_AnticipatedTransform = new TransformState { Position = Vector3.Lerp(m_SmoothFrom.Position, m_SmoothTo.Position, pct), - Rotation = Quaternion.Slerp(m_SmoothFrom.Rotation, m_SmoothTo.Rotation, pct), + Rotation = Quaternion.Lerp(m_SmoothFrom.Rotation, m_SmoothTo.Rotation, pct), Scale = Vector3.Lerp(m_SmoothFrom.Scale, m_SmoothTo.Scale, pct) }; m_PreviousAnticipatedTransform = m_AnticipatedTransform; @@ -275,6 +269,32 @@ public override void OnUpdate() } } + // TODO: This does not handle OnFixedUpdate + // This requires a complete overhaul in this class to switch between using + // NetworkRigidbody's position and rotation values. + public override void OnUpdate() + { + ProcessSmoothing(); + // Do not call the base class implementation... + // AnticipatedNetworkTransform applies its authoritative state immediately rather than waiting for update + // This is because AnticipatedNetworkTransforms may need to reference each other in reanticipating + // and we will want all reanticipation done before anything else wants to reference the transform in + // OnUpdate() + //base.OnUpdate(); + } + + /// + /// Since authority does not subscribe to updates (OnUpdate or OnFixedUpdate), + /// we have to update every frame to assure authority processes soothing. + /// + private void Update() + { + if (CanCommitToTransform && IsSpawned) + { + ProcessSmoothing(); + } + } + internal class AnticipatedObject : IAnticipationEventReceiver, IAnticipatedObject { public AnticipatedNetworkTransform Transform; @@ -347,14 +367,44 @@ private void ResetAnticipatedState() m_CurrentSmoothTime = 0; } - protected override void OnSynchronize(ref BufferSerializer serializer) + /// + /// (This replaces the first OnSynchronize for NetworkTransforms) + /// This is needed to initialize when fully synchronized since non-authority instances + /// don't apply the initial synchronization (new client synchronization) until after + /// everything has been spawned and synchronized. + /// + protected internal override void InternalOnNetworkSessionSynchronized() { - base.OnSynchronize(ref serializer); - if (!CanCommitToTransform) + var wasSynchronizing = SynchronizeState.IsSynchronizing; + base.InternalOnNetworkSessionSynchronized(); + if (!CanCommitToTransform && wasSynchronizing && !SynchronizeState.IsSynchronizing) + { + m_OutstandingAuthorityChange = true; + ApplyAuthoritativeState(); + ResetAnticipatedState(); + + m_AnticipatedObject = new AnticipatedObject { Transform = this }; + NetworkManager.AnticipationSystem.RegisterForAnticipationEvents(m_AnticipatedObject); + NetworkManager.AnticipationSystem.AllAnticipatedObjects.Add(m_AnticipatedObject); + } + } + + /// + /// (This replaces the any subsequent OnSynchronize for NetworkTransforms post client synchronization) + /// This occurs on already connected clients when dynamically spawning a NetworkObject for + /// non-authoritative instances. + /// + protected internal override void InternalOnNetworkPostSpawn() + { + base.InternalOnNetworkPostSpawn(); + if (!CanCommitToTransform && NetworkManager.IsConnectedClient && !SynchronizeState.IsSynchronizing) { m_OutstandingAuthorityChange = true; ApplyAuthoritativeState(); ResetAnticipatedState(); + m_AnticipatedObject = new AnticipatedObject { Transform = this }; + NetworkManager.AnticipationSystem.RegisterForAnticipationEvents(m_AnticipatedObject); + NetworkManager.AnticipationSystem.AllAnticipatedObjects.Add(m_AnticipatedObject); } } @@ -365,6 +415,13 @@ public override void OnNetworkSpawn() Debug.LogWarning($"This component is not currently supported in distributed authority."); } base.OnNetworkSpawn(); + + // Non-authoritative instances exit early if the synchronization has yet to + // be applied at this point + if (SynchronizeState.IsSynchronizing && !CanCommitToTransform) + { + return; + } m_OutstandingAuthorityChange = true; ApplyAuthoritativeState(); ResetAnticipatedState(); diff --git a/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs b/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs index ef5ec09d86..e628c7cab2 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs @@ -12,7 +12,7 @@ namespace Unity.Netcode public abstract class BufferedLinearInterpolator where T : struct { internal float MaxInterpolationBound = 3.0f; - private struct BufferedItem + protected internal struct BufferedItem { public T Item; public double TimeSent; @@ -31,14 +31,16 @@ public BufferedItem(T item, double timeSent) private const double k_SmallValue = 9.999999439624929E-11; // copied from Vector3's equal operator - private T m_InterpStartValue; - private T m_CurrentInterpValue; - private T m_InterpEndValue; + protected internal T m_InterpStartValue; + protected internal T m_CurrentInterpValue; + protected internal T m_InterpEndValue; private double m_EndTimeConsumed; private double m_StartTimeConsumed; - private readonly List m_Buffer = new List(k_BufferCountLimit); + protected internal readonly List m_Buffer = new List(k_BufferCountLimit); + + // Buffer consumption scenarios // Perfect case consumption @@ -73,6 +75,21 @@ public BufferedItem(T item, double timeSent) private bool InvalidState => m_Buffer.Count == 0 && m_LifetimeConsumedCount == 0; + internal bool EndOfBuffer => m_Buffer.Count == 0; + + internal bool InLocalSpace; + + protected internal virtual void OnConvertTransformSpace(Transform transform, bool inLocalSpace) + { + + } + + internal void ConvertTransformSpace(Transform transform, bool inLocalSpace) + { + OnConvertTransformSpace(transform, inLocalSpace); + InLocalSpace = inLocalSpace; + } + /// /// Resets interpolator to initial state /// @@ -351,6 +368,35 @@ protected override Quaternion Interpolate(Quaternion start, Quaternion end, floa return Quaternion.Lerp(start, end, time); } } + + private Quaternion ConvertToNewTransformSpace(Transform transform, Quaternion rotation, bool inLocalSpace) + { + if (inLocalSpace) + { + return Quaternion.Inverse(transform.rotation) * rotation; + + } + else + { + return transform.rotation * rotation; + } + } + + protected internal override void OnConvertTransformSpace(Transform transform, bool inLocalSpace) + { + for (int i = 0; i < m_Buffer.Count; i++) + { + var entry = m_Buffer[i]; + entry.Item = ConvertToNewTransformSpace(transform, entry.Item, inLocalSpace); + m_Buffer[i] = entry; + } + + m_InterpStartValue = ConvertToNewTransformSpace(transform, m_InterpStartValue, inLocalSpace); + m_CurrentInterpValue = ConvertToNewTransformSpace(transform, m_CurrentInterpValue, inLocalSpace); + m_InterpEndValue = ConvertToNewTransformSpace(transform, m_InterpEndValue, inLocalSpace); + + base.OnConvertTransformSpace(transform, inLocalSpace); + } } /// @@ -388,5 +434,34 @@ protected override Vector3 Interpolate(Vector3 start, Vector3 end, float time) return Vector3.Lerp(start, end, time); } } + + private Vector3 ConvertToNewTransformSpace(Transform transform, Vector3 position, bool inLocalSpace) + { + if (inLocalSpace) + { + return transform.InverseTransformPoint(position); + + } + else + { + return transform.TransformPoint(position); + } + } + + protected internal override void OnConvertTransformSpace(Transform transform, bool inLocalSpace) + { + for (int i = 0; i < m_Buffer.Count; i++) + { + var entry = m_Buffer[i]; + entry.Item = ConvertToNewTransformSpace(transform, entry.Item, inLocalSpace); + m_Buffer[i] = entry; + } + + m_InterpStartValue = ConvertToNewTransformSpace(transform, m_InterpStartValue, inLocalSpace); + m_CurrentInterpValue = ConvertToNewTransformSpace(transform, m_CurrentInterpValue, inLocalSpace); + m_InterpEndValue = ConvertToNewTransformSpace(transform, m_InterpEndValue, inLocalSpace); + + base.OnConvertTransformSpace(transform, inLocalSpace); + } } } diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidBodyBase.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidBodyBase.cs index a85f7c09bd..7e8808171a 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidBodyBase.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidBodyBase.cs @@ -1,4 +1,4 @@ -#if COM_UNITY_MODULES_PHYSICS +#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D using System.Runtime.CompilerServices; using UnityEngine; @@ -14,6 +14,12 @@ namespace Unity.Netcode.Components /// public abstract class NetworkRigidbodyBase : NetworkBehaviour { +#if UNITY_EDITOR + [HideInInspector] + [SerializeField] + internal bool NetworkRigidbodyBaseExpanded; +#endif + /// /// When enabled, the associated will use the Rigidbody/Rigidbody2D to apply and synchronize changes in position, rotation, and /// allows for the use of Rigidbody interpolation/extrapolation. @@ -42,8 +48,10 @@ public abstract class NetworkRigidbodyBase : NetworkBehaviour private bool m_IsRigidbody2D => RigidbodyType == RigidbodyTypes.Rigidbody2D; // Used to cache the authority state of this Rigidbody during the last frame private bool m_IsAuthority; - private Rigidbody m_Rigidbody; - private Rigidbody2D m_Rigidbody2D; + + protected internal Rigidbody m_InternalRigidbody { get; private set; } + protected internal Rigidbody2D m_InternalRigidbody2D { get; private set; } + internal NetworkTransform NetworkTransform; private float m_TickFrequency; private float m_TickRate; @@ -87,18 +95,18 @@ protected void Initialize(RigidbodyTypes rigidbodyType, NetworkTransform network return; } RigidbodyType = rigidbodyType; - m_Rigidbody2D = rigidbody2D; - m_Rigidbody = rigidbody; + m_InternalRigidbody2D = rigidbody2D; + m_InternalRigidbody = rigidbody; NetworkTransform = networkTransform; - if (m_IsRigidbody2D && m_Rigidbody2D == null) + if (m_IsRigidbody2D && m_InternalRigidbody2D == null) { - m_Rigidbody2D = GetComponent(); + m_InternalRigidbody2D = GetComponent(); } - else if (m_Rigidbody == null) + else if (m_InternalRigidbody == null) { - m_Rigidbody = GetComponent(); + m_InternalRigidbody = GetComponent(); } SetOriginalInterpolation(); @@ -178,14 +186,14 @@ public void SetLinearVelocity(Vector3 linearVelocity) if (m_IsRigidbody2D) { #if COM_UNITY_MODULES_PHYSICS2D_LINEAR - m_Rigidbody2D.linearVelocity = linearVelocity; + m_InternalRigidbody2D.linearVelocity = linearVelocity; #else - m_Rigidbody2D.velocity = linearVelocity; + m_InternalRigidbody2D.velocity = linearVelocity; #endif } else { - m_Rigidbody.linearVelocity = linearVelocity; + m_InternalRigidbody.linearVelocity = linearVelocity; } } @@ -202,14 +210,14 @@ public Vector3 GetLinearVelocity() if (m_IsRigidbody2D) { #if COM_UNITY_MODULES_PHYSICS2D_LINEAR - return m_Rigidbody2D.linearVelocity; + return m_InternalRigidbody2D.linearVelocity; #else - return m_Rigidbody2D.velocity; + return m_InternalRigidbody2D.velocity; #endif } else { - return m_Rigidbody.linearVelocity; + return m_InternalRigidbody.linearVelocity; } } @@ -226,11 +234,11 @@ public void SetAngularVelocity(Vector3 angularVelocity) { if (m_IsRigidbody2D) { - m_Rigidbody2D.angularVelocity = angularVelocity.z; + m_InternalRigidbody2D.angularVelocity = angularVelocity.z; } else { - m_Rigidbody.angularVelocity = angularVelocity; + m_InternalRigidbody.angularVelocity = angularVelocity; } } @@ -246,11 +254,11 @@ public Vector3 GetAngularVelocity() { if (m_IsRigidbody2D) { - return Vector3.forward * m_Rigidbody2D.angularVelocity; + return Vector3.forward * m_InternalRigidbody2D.angularVelocity; } else { - return m_Rigidbody.angularVelocity; + return m_InternalRigidbody.angularVelocity; } } @@ -263,11 +271,11 @@ public Vector3 GetPosition() { if (m_IsRigidbody2D) { - return m_Rigidbody2D.position; + return m_InternalRigidbody2D.position; } else { - return m_Rigidbody.position; + return m_InternalRigidbody.position; } } @@ -282,13 +290,13 @@ public Quaternion GetRotation() { var quaternion = Quaternion.identity; var angles = quaternion.eulerAngles; - angles.z = m_Rigidbody2D.rotation; + angles.z = m_InternalRigidbody2D.rotation; quaternion.eulerAngles = angles; return quaternion; } else { - return m_Rigidbody.rotation; + return m_InternalRigidbody.rotation; } } @@ -301,11 +309,11 @@ public void MovePosition(Vector3 position) { if (m_IsRigidbody2D) { - m_Rigidbody2D.MovePosition(position); + m_InternalRigidbody2D.MovePosition(position); } else { - m_Rigidbody.MovePosition(position); + m_InternalRigidbody.MovePosition(position); } } @@ -318,11 +326,11 @@ public void SetPosition(Vector3 position) { if (m_IsRigidbody2D) { - m_Rigidbody2D.position = position; + m_InternalRigidbody2D.position = position; } else { - m_Rigidbody.position = position; + m_InternalRigidbody.position = position; } } @@ -334,13 +342,13 @@ public void ApplyCurrentTransform() { if (m_IsRigidbody2D) { - m_Rigidbody2D.position = transform.position; - m_Rigidbody2D.rotation = transform.eulerAngles.z; + m_InternalRigidbody2D.position = transform.position; + m_InternalRigidbody2D.rotation = transform.eulerAngles.z; } else { - m_Rigidbody.position = transform.position; - m_Rigidbody.rotation = transform.rotation; + m_InternalRigidbody.position = transform.position; + m_InternalRigidbody.rotation = transform.rotation; } } @@ -358,9 +366,9 @@ public void MoveRotation(Quaternion rotation) { var quaternion = Quaternion.identity; var angles = quaternion.eulerAngles; - angles.z = m_Rigidbody2D.rotation; + angles.z = m_InternalRigidbody2D.rotation; quaternion.eulerAngles = angles; - m_Rigidbody2D.MoveRotation(quaternion); + m_InternalRigidbody2D.MoveRotation(quaternion); } else { @@ -375,7 +383,7 @@ public void MoveRotation(Quaternion rotation) { rotation.Normalize(); } - m_Rigidbody.MoveRotation(rotation); + m_InternalRigidbody.MoveRotation(rotation); } } @@ -388,11 +396,11 @@ public void SetRotation(Quaternion rotation) { if (m_IsRigidbody2D) { - m_Rigidbody2D.rotation = rotation.eulerAngles.z; + m_InternalRigidbody2D.rotation = rotation.eulerAngles.z; } else { - m_Rigidbody.rotation = rotation; + m_InternalRigidbody.rotation = rotation; } } @@ -404,7 +412,7 @@ private void SetOriginalInterpolation() { if (m_IsRigidbody2D) { - switch (m_Rigidbody2D.interpolation) + switch (m_InternalRigidbody2D.interpolation) { case RigidbodyInterpolation2D.None: { @@ -425,7 +433,7 @@ private void SetOriginalInterpolation() } else { - switch (m_Rigidbody.interpolation) + switch (m_InternalRigidbody.interpolation) { case RigidbodyInterpolation.None: { @@ -454,16 +462,16 @@ public void WakeIfSleeping() { if (m_IsRigidbody2D) { - if (m_Rigidbody2D.IsSleeping()) + if (m_InternalRigidbody2D.IsSleeping()) { - m_Rigidbody2D.WakeUp(); + m_InternalRigidbody2D.WakeUp(); } } else { - if (m_Rigidbody.IsSleeping()) + if (m_InternalRigidbody.IsSleeping()) { - m_Rigidbody.WakeUp(); + m_InternalRigidbody.WakeUp(); } } } @@ -476,11 +484,11 @@ public void SleepRigidbody() { if (m_IsRigidbody2D) { - m_Rigidbody2D.Sleep(); + m_InternalRigidbody2D.Sleep(); } else { - m_Rigidbody.Sleep(); + m_InternalRigidbody.Sleep(); } } @@ -489,11 +497,11 @@ public bool IsKinematic() { if (m_IsRigidbody2D) { - return m_Rigidbody2D.bodyType == RigidbodyType2D.Kinematic; + return m_InternalRigidbody2D.bodyType == RigidbodyType2D.Kinematic; } else { - return m_Rigidbody.isKinematic; + return m_InternalRigidbody.isKinematic; } } @@ -518,11 +526,11 @@ public void SetIsKinematic(bool isKinematic) { if (m_IsRigidbody2D) { - m_Rigidbody2D.bodyType = isKinematic ? RigidbodyType2D.Kinematic : RigidbodyType2D.Dynamic; + m_InternalRigidbody2D.bodyType = isKinematic ? RigidbodyType2D.Kinematic : RigidbodyType2D.Dynamic; } else { - m_Rigidbody.isKinematic = isKinematic; + m_InternalRigidbody.isKinematic = isKinematic; } // If we are not spawned, then exit early @@ -539,7 +547,7 @@ public void SetIsKinematic(bool isKinematic) if (IsKinematic()) { // If not already set to interpolate then set the Rigidbody to interpolate - if (m_Rigidbody.interpolation == RigidbodyInterpolation.Extrapolate) + if (m_InternalRigidbody.interpolation == RigidbodyInterpolation.Extrapolate) { // Sleep until the next fixed update when switching from extrapolation to interpolation SleepRigidbody(); @@ -568,11 +576,11 @@ private void SetInterpolation(InterpolationTypes interpolationType) { if (m_IsRigidbody2D) { - m_Rigidbody2D.interpolation = RigidbodyInterpolation2D.None; + m_InternalRigidbody2D.interpolation = RigidbodyInterpolation2D.None; } else { - m_Rigidbody.interpolation = RigidbodyInterpolation.None; + m_InternalRigidbody.interpolation = RigidbodyInterpolation.None; } break; } @@ -580,11 +588,11 @@ private void SetInterpolation(InterpolationTypes interpolationType) { if (m_IsRigidbody2D) { - m_Rigidbody2D.interpolation = RigidbodyInterpolation2D.Interpolate; + m_InternalRigidbody2D.interpolation = RigidbodyInterpolation2D.Interpolate; } else { - m_Rigidbody.interpolation = RigidbodyInterpolation.Interpolate; + m_InternalRigidbody.interpolation = RigidbodyInterpolation.Interpolate; } break; } @@ -592,11 +600,11 @@ private void SetInterpolation(InterpolationTypes interpolationType) { if (m_IsRigidbody2D) { - m_Rigidbody2D.interpolation = RigidbodyInterpolation2D.Extrapolate; + m_InternalRigidbody2D.interpolation = RigidbodyInterpolation2D.Extrapolate; } else { - m_Rigidbody.interpolation = RigidbodyInterpolation.Extrapolate; + m_InternalRigidbody.interpolation = RigidbodyInterpolation.Extrapolate; } break; } @@ -711,28 +719,28 @@ protected virtual void OnFixedJoint2DCreated() private void ApplyFixedJoint2D(NetworkRigidbodyBase bodyToConnect, Vector3 position, float connectedMassScale = 0.0f, float massScale = 1.0f, bool useGravity = false, bool zeroVelocity = true) { transform.position = position; - m_Rigidbody2D.position = position; - m_OriginalGravitySetting = bodyToConnect.m_Rigidbody.useGravity; + m_InternalRigidbody2D.position = position; + m_OriginalGravitySetting = bodyToConnect.m_InternalRigidbody.useGravity; m_FixedJoint2DUsingGravity = useGravity; if (!useGravity) { - m_OriginalGravityScale = m_Rigidbody2D.gravityScale; - m_Rigidbody2D.gravityScale = 0.0f; + m_OriginalGravityScale = m_InternalRigidbody2D.gravityScale; + m_InternalRigidbody2D.gravityScale = 0.0f; } if (zeroVelocity) { #if COM_UNITY_MODULES_PHYSICS2D_LINEAR - m_Rigidbody2D.linearVelocity = Vector2.zero; + m_InternalRigidbody2D.linearVelocity = Vector2.zero; #else - m_Rigidbody2D.velocity = Vector2.zero; + m_InternalRigidbody2D.velocity = Vector2.zero; #endif - m_Rigidbody2D.angularVelocity = 0.0f; + m_InternalRigidbody2D.angularVelocity = 0.0f; } FixedJoint2D = gameObject.AddComponent(); - FixedJoint2D.connectedBody = bodyToConnect.m_Rigidbody2D; + FixedJoint2D.connectedBody = bodyToConnect.m_InternalRigidbody2D; OnFixedJoint2DCreated(); } @@ -740,16 +748,16 @@ private void ApplyFixedJoint2D(NetworkRigidbodyBase bodyToConnect, Vector3 posit private void ApplyFixedJoint(NetworkRigidbodyBase bodyToConnectTo, Vector3 position, float connectedMassScale = 0.0f, float massScale = 1.0f, bool useGravity = false, bool zeroVelocity = true) { transform.position = position; - m_Rigidbody.position = position; + m_InternalRigidbody.position = position; if (zeroVelocity) { - m_Rigidbody.linearVelocity = Vector3.zero; - m_Rigidbody.angularVelocity = Vector3.zero; + m_InternalRigidbody.linearVelocity = Vector3.zero; + m_InternalRigidbody.angularVelocity = Vector3.zero; } - m_OriginalGravitySetting = m_Rigidbody.useGravity; - m_Rigidbody.useGravity = useGravity; + m_OriginalGravitySetting = m_InternalRigidbody.useGravity; + m_InternalRigidbody.useGravity = useGravity; FixedJoint = gameObject.AddComponent(); - FixedJoint.connectedBody = bodyToConnectTo.m_Rigidbody; + FixedJoint.connectedBody = bodyToConnectTo.m_InternalRigidbody; FixedJoint.connectedMassScale = connectedMassScale; FixedJoint.massScale = massScale; OnFixedJointCreated(); @@ -861,7 +869,7 @@ public void DetachFromFixedJoint() if (FixedJoint != null) { FixedJoint.connectedBody = null; - m_Rigidbody.useGravity = m_OriginalGravitySetting; + m_InternalRigidbody.useGravity = m_OriginalGravitySetting; Destroy(FixedJoint); FixedJoint = null; ResetInterpolation(); diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidbody.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidbody.cs index 7c93a5deac..a157d26c63 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidbody.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidbody.cs @@ -12,6 +12,9 @@ namespace Unity.Netcode.Components [AddComponentMenu("Netcode/Network Rigidbody")] public class NetworkRigidbody : NetworkRigidbodyBase { + + public Rigidbody Rigidbody => m_InternalRigidbody; + protected virtual void Awake() { Initialize(RigidbodyTypes.Rigidbody); diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidbody2D.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidbody2D.cs index a178660df5..f7c9e14d1e 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidbody2D.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidbody2D.cs @@ -12,6 +12,7 @@ namespace Unity.Netcode.Components [AddComponentMenu("Netcode/Network Rigidbody 2D")] public class NetworkRigidbody2D : NetworkRigidbodyBase { + public Rigidbody2D Rigidbody2D => m_InternalRigidbody2D; protected virtual void Awake() { Initialize(RigidbodyTypes.Rigidbody2D); diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs index 0cf2b41631..13fb21a5ce 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs @@ -20,6 +20,10 @@ public class NetworkTransform : NetworkBehaviour #if UNITY_EDITOR internal virtual bool HideInterpolateValue => false; + + [HideInInspector] + [SerializeField] + internal bool NetworkTransformExpanded; #endif #region NETWORK TRANSFORM STATE @@ -941,6 +945,19 @@ public enum AuthorityModes #endif public AuthorityModes AuthorityMode; + + /// + /// When enabled, any parented s (children) of this will be forced to synchronize their transform when this instance sends a state update.
+ /// This can help to reduce out of sync updates that can lead to slight jitter between a parent and its child/children. + ///
+ /// + /// - If this is set on a child and the parent does not have this set then the child will not be tick synchronized with its parent.
+ /// - If the parent instance does not send any state updates, the children will still send state updates when exceeding axis delta threshold.
+ /// - This does not need to be set on children to be applied. + ///
+ [Tooltip("When enabled, any parented children of this instance will send a state update when this instance sends a state update. If this instance doesn't send a state update, the children will still send state updates when reaching their axis specified threshold delta. Children do not have to have this setting enabled.")] + public bool TickSyncChildren = false; + /// /// The default position change threshold value. /// Any changes above this threshold will be replicated. @@ -1175,6 +1192,22 @@ private bool SynchronizeScale [Tooltip("Sets whether this transform should sync in local space or in world space")] public bool InLocalSpace = false; + /// + /// When enabled, the NetworkTransform will automatically handle transitioning into the respective transform space when its parent changes.
+ /// When parented: Automatically transitions into local space and coverts any existing pending interpolated states to local space on non-authority instances.
+ /// When deparented: Automatically transitions into world space and converts any existing pending interpolated states to world space on non-authority instances.
+ /// Set on the root instance (nested components should be pre-set in-editor to local space.
+ ///
+ /// + /// Only works with components that are not paired with a or component that is configured to use the rigid body for motion.
+ /// will automatically be set when this is enabled. + /// Does not auto-synchronize clients if changed on the authority instance during runtime (i.e. apply this setting in-editor). + ///
+ public bool SwitchTransformSpaceWhenParented = false; + + protected bool PositionInLocalSpace => (!SwitchTransformSpaceWhenParented && InLocalSpace) || (m_PositionInterpolator != null && m_PositionInterpolator.InLocalSpace && SwitchTransformSpaceWhenParented); + protected bool RotationInLocalSpace => (!SwitchTransformSpaceWhenParented && InLocalSpace) || (m_RotationInterpolator != null && m_RotationInterpolator.InLocalSpace && SwitchTransformSpaceWhenParented); + /// /// When enabled (default) interpolation is applied. /// When disabled interpolation is disabled. @@ -1248,7 +1281,7 @@ public Vector3 GetSpaceRelativePosition(bool getCurrentState = false) else { // Otherwise, just get the current position - return m_CurrentPosition; + return m_InternalCurrentPosition; } } } @@ -1281,7 +1314,7 @@ public Quaternion GetSpaceRelativeRotation(bool getCurrentState = false) } else { - return m_CurrentRotation; + return m_InternalCurrentRotation; } } @@ -1312,7 +1345,7 @@ public Vector3 GetScale(bool getCurrentState = false) } else { - return m_CurrentScale; + return m_InternalCurrentScale; } } @@ -1344,15 +1377,14 @@ internal NetworkTransformState LocalAuthoritativeNetworkState // Non-Authoritative's current position, scale, and rotation that is used to assure the non-authoritative side cannot make adjustments to // the portions of the transform being synchronized. - private Vector3 m_CurrentPosition; + private Vector3 m_InternalCurrentPosition; private Vector3 m_TargetPosition; - private Vector3 m_CurrentScale; + private Vector3 m_InternalCurrentScale; private Vector3 m_TargetScale; - private Quaternion m_CurrentRotation; + private Quaternion m_InternalCurrentRotation; private Vector3 m_TargetRotation; - // DANGO-EXP TODO: ADD Rigidbody2D -#if COM_UNITY_MODULES_PHYSICS +#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D private bool m_UseRigidbodyForMotion; private NetworkRigidbodyBase m_NetworkRigidbodyInternal; @@ -1436,6 +1468,7 @@ private bool ShouldSynchronizeHalfFloat(ulong targetClientId) #endregion #region ONSYNCHRONIZE + /// /// 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) @@ -1450,7 +1483,7 @@ private bool ShouldSynchronizeHalfFloat(ulong targetClientId) protected override void OnSynchronize(ref BufferSerializer serializer) { var targetClientId = m_TargetIdBeingSynchronized; - var synchronizationState = new NetworkTransformState() + SynchronizeState = new NetworkTransformState() { HalfEulerRotation = new HalfVector3(), HalfVectorRotation = new HalfVector4(), @@ -1469,34 +1502,39 @@ protected override void OnSynchronize(ref BufferSerializer serializer) writer.WriteValueSafe(k_NetworkTransformStateMagic); } - synchronizationState.IsTeleportingNextFrame = true; + SynchronizeState.IsTeleportingNextFrame = true; var transformToCommit = transform; // If we are using Half Float Precision, then we want to only synchronize the authority's m_HalfPositionState.FullPosition in order for // for the non-authority side to be able to properly synchronize delta position updates. - CheckForStateChange(ref synchronizationState, ref transformToCommit, true, targetClientId); - synchronizationState.NetworkSerialize(serializer); - SynchronizeState = synchronizationState; + CheckForStateChange(ref SynchronizeState, ref transformToCommit, true, targetClientId); + SynchronizeState.NetworkSerialize(serializer); } else { - synchronizationState.NetworkSerialize(serializer); - // Set the transform's synchronization modes - InLocalSpace = synchronizationState.InLocalSpace; - Interpolate = synchronizationState.UseInterpolation; - UseQuaternionSynchronization = synchronizationState.QuaternionSync; - UseHalfFloatPrecision = synchronizationState.UseHalfFloatPrecision; - UseQuaternionCompression = synchronizationState.QuaternionCompression; - SlerpPosition = synchronizationState.UsePositionSlerp; - UpdatePositionSlerp(); - - // Teleport/Fully Initialize based on the state - ApplyTeleportingState(synchronizationState); - SynchronizeState = synchronizationState; - m_LocalAuthoritativeNetworkState = synchronizationState; - m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = false; - m_LocalAuthoritativeNetworkState.IsSynchronizing = false; + SynchronizeState.NetworkSerialize(serializer); } } + + /// + /// We now apply synchronization after everything has spawned + /// + private void ApplySynchronization() + { + // Set the transform's synchronization modes + InLocalSpace = SynchronizeState.InLocalSpace; + Interpolate = SynchronizeState.UseInterpolation; + UseQuaternionSynchronization = SynchronizeState.QuaternionSync; + UseHalfFloatPrecision = SynchronizeState.UseHalfFloatPrecision; + UseQuaternionCompression = SynchronizeState.QuaternionCompression; + SlerpPosition = SynchronizeState.UsePositionSlerp; + UpdatePositionSlerp(); + // Teleport/Fully Initialize based on the state + ApplyTeleportingState(SynchronizeState); + m_LocalAuthoritativeNetworkState = SynchronizeState; + m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = false; + m_LocalAuthoritativeNetworkState.IsSynchronizing = false; + SynchronizeState.IsSynchronizing = false; + } #endregion #region AUTHORITY STATE UPDATE @@ -1577,7 +1615,7 @@ private void TryCommitTransform(ref Transform transformToCommit, bool synchroniz NetworkLog.LogError($"[{name}] is trying to commit the transform without authority!"); return; } -#if COM_UNITY_MODULES_PHYSICS +#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D // TODO: Make this an authority flag // For now, just synchronize with the NetworkRigidbodyBase UseRigidBodyForMotion if (m_NetworkRigidbodyInternal != null) @@ -1587,7 +1625,7 @@ private void TryCommitTransform(ref Transform transformToCommit, bool synchroniz #endif // If the transform has deltas (returns dirty) or if an explicitly set state is pending - if (m_LocalAuthoritativeNetworkState.ExplicitSet || CheckForStateChange(ref m_LocalAuthoritativeNetworkState, ref transformToCommit, synchronize)) + if (m_LocalAuthoritativeNetworkState.ExplicitSet || CheckForStateChange(ref m_LocalAuthoritativeNetworkState, ref transformToCommit, synchronize, forceState: settingState)) { // If the state was explicitly set, then update the network tick to match the locally calculate tick if (m_LocalAuthoritativeNetworkState.ExplicitSet) @@ -1629,7 +1667,7 @@ private void TryCommitTransform(ref Transform transformToCommit, bool synchroniz m_DeltaSynch = true; } -#if COM_UNITY_MODULES_PHYSICS +#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D // We handle updating attached bodies when the "parent" body has a state update in order to keep their delta state updates tick synchronized. if (m_UseRigidbodyForMotion && m_NetworkRigidbodyInternal.NetworkRigidbodyConnections.Count > 0) { @@ -1639,6 +1677,36 @@ private void TryCommitTransform(ref Transform transformToCommit, bool synchroniz } } #endif + // When enabled, any children will get tick synchronized with state updates + if (TickSyncChildren) + { + // Synchronize any nested NetworkTransforms with the parent's + foreach (var childNetworkTransform in NetworkObject.NetworkTransforms) + { + // Don't update the same instance + if (childNetworkTransform == this) + { + continue; + } + if (childNetworkTransform.CanCommitToTransform) + { + childNetworkTransform.OnNetworkTick(true); + } + } + + // Synchronize any parented children with the parent's motion + foreach (var child in m_ParentedChildren) + { + // Synchronize any nested NetworkTransforms of the child with the parent's + foreach (var childNetworkTransform in child.NetworkTransforms) + { + if (childNetworkTransform.CanCommitToTransform) + { + childNetworkTransform.OnNetworkTick(true); + } + } + } + } } } @@ -1683,7 +1751,7 @@ internal bool ApplyTransformToNetworkState(ref NetworkTransformState networkStat /// Applies the transform to the specified. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool CheckForStateChange(ref NetworkTransformState networkState, ref Transform transformToUse, bool isSynchronization = false, ulong targetClientId = 0) + private bool CheckForStateChange(ref NetworkTransformState networkState, ref Transform transformToUse, bool isSynchronization = false, ulong targetClientId = 0, bool forceState = false) { // As long as we are not doing our first synchronization and we are sending unreliable deltas, each // NetworkTransform will stagger its full transfom synchronization over a 1 second period based on the @@ -1715,7 +1783,7 @@ private bool CheckForStateChange(ref NetworkTransformState networkState, ref Tra var isRotationDirty = isTeleportingAndNotSynchronizing ? networkState.HasRotAngleChange : false; var isScaleDirty = isTeleportingAndNotSynchronizing ? networkState.HasScaleChange : false; -#if COM_UNITY_MODULES_PHYSICS +#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D var position = m_UseRigidbodyForMotion ? m_NetworkRigidbodyInternal.GetPosition() : InLocalSpace ? transformToUse.localPosition : transformToUse.position; var rotation = m_UseRigidbodyForMotion ? m_NetworkRigidbodyInternal.GetRotation() : InLocalSpace ? transformToUse.localRotation : transformToUse.rotation; @@ -1739,17 +1807,18 @@ private bool CheckForStateChange(ref NetworkTransformState networkState, ref Tra // All of the checks below, up to the delta position checking portion, are to determine if the // authority changed a property during runtime that requires a full synchronizing. -#if COM_UNITY_MODULES_PHYSICS - if (InLocalSpace != networkState.InLocalSpace && !m_UseRigidbodyForMotion) +#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D + if ((InLocalSpace != networkState.InLocalSpace || isSynchronization) && !m_UseRigidbodyForMotion) #else if (InLocalSpace != networkState.InLocalSpace) #endif { - networkState.InLocalSpace = InLocalSpace; + networkState.InLocalSpace = SwitchTransformSpaceWhenParented ? transform.parent != null : InLocalSpace; isDirty = true; - networkState.IsTeleportingNextFrame = true; + networkState.IsTeleportingNextFrame = !SwitchTransformSpaceWhenParented; + forceState = SwitchTransformSpaceWhenParented; } -#if COM_UNITY_MODULES_PHYSICS +#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D else if (InLocalSpace && m_UseRigidbodyForMotion) { // TODO: Provide more options than just FixedJoint @@ -1788,28 +1857,6 @@ private bool CheckForStateChange(ref NetworkTransformState networkState, ref Tra } networkState.IsParented = hasParentNetworkObject; - - // When synchronizing with a parent, world position stays impacts position whether - // the NetworkTransform is using world or local space synchronization. - // WorldPositionStays: (always use world space) - // !WorldPositionStays: (always use local space) - // Exception: If it is an in-scene placed NetworkObject and it is parented under a GameObject - // then always use local space unless AutoObjectParentSync is disabled and the NetworkTransform - // is synchronizing in world space. - if (isSynchronization && networkState.IsParented) - { - var parentedUnderGameObject = NetworkObject.transform.parent != null && !parentNetworkObject && NetworkObject.IsSceneObject.Value; - if (NetworkObject.WorldPositionStays() && (!parentedUnderGameObject || (parentedUnderGameObject && !NetworkObject.AutoObjectParentSync && !InLocalSpace))) - { - position = transformToUse.position; - networkState.InLocalSpace = false; - } - else - { - position = transformToUse.localPosition; - networkState.InLocalSpace = true; - } - } } if (Interpolate != networkState.UseInterpolation) @@ -1858,21 +1905,21 @@ private bool CheckForStateChange(ref NetworkTransformState networkState, ref Tra // Begin delta checks against last sent state update if (!UseHalfFloatPrecision) { - if (SyncPositionX && (Mathf.Abs(networkState.PositionX - position.x) >= positionThreshold.x || networkState.IsTeleportingNextFrame || isAxisSync)) + if (SyncPositionX && (Mathf.Abs(networkState.PositionX - position.x) >= positionThreshold.x || networkState.IsTeleportingNextFrame || isAxisSync || forceState)) { networkState.PositionX = position.x; networkState.HasPositionX = true; isPositionDirty = true; } - if (SyncPositionY && (Mathf.Abs(networkState.PositionY - position.y) >= positionThreshold.y || networkState.IsTeleportingNextFrame || isAxisSync)) + if (SyncPositionY && (Mathf.Abs(networkState.PositionY - position.y) >= positionThreshold.y || networkState.IsTeleportingNextFrame || isAxisSync || forceState)) { networkState.PositionY = position.y; networkState.HasPositionY = true; isPositionDirty = true; } - if (SyncPositionZ && (Mathf.Abs(networkState.PositionZ - position.z) >= positionThreshold.z || networkState.IsTeleportingNextFrame || isAxisSync)) + if (SyncPositionZ && (Mathf.Abs(networkState.PositionZ - position.z) >= positionThreshold.z || networkState.IsTeleportingNextFrame || isAxisSync || forceState)) { networkState.PositionZ = position.z; networkState.HasPositionZ = true; @@ -1882,7 +1929,7 @@ private bool CheckForStateChange(ref NetworkTransformState networkState, ref Tra else if (SynchronizePosition) { // If we are teleporting then we can skip the delta threshold check - isPositionDirty = networkState.IsTeleportingNextFrame || isAxisSync; + isPositionDirty = networkState.IsTeleportingNextFrame || isAxisSync || forceState; if (m_HalfFloatTargetTickOwnership > m_CachedNetworkManager.ServerTime.Tick) { isPositionDirty = true; @@ -1988,21 +2035,21 @@ private bool CheckForStateChange(ref NetworkTransformState networkState, ref Tra if (!UseQuaternionSynchronization) { - if (SyncRotAngleX && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleX, rotAngles.x)) >= rotationThreshold.x || networkState.IsTeleportingNextFrame || isAxisSync)) + if (SyncRotAngleX && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleX, rotAngles.x)) >= rotationThreshold.x || networkState.IsTeleportingNextFrame || isAxisSync || forceState)) { networkState.RotAngleX = rotAngles.x; networkState.HasRotAngleX = true; isRotationDirty = true; } - if (SyncRotAngleY && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleY, rotAngles.y)) >= rotationThreshold.y || networkState.IsTeleportingNextFrame || isAxisSync)) + if (SyncRotAngleY && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleY, rotAngles.y)) >= rotationThreshold.y || networkState.IsTeleportingNextFrame || isAxisSync || forceState)) { networkState.RotAngleY = rotAngles.y; networkState.HasRotAngleY = true; isRotationDirty = true; } - if (SyncRotAngleZ && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleZ, rotAngles.z)) >= rotationThreshold.z || networkState.IsTeleportingNextFrame || isAxisSync)) + if (SyncRotAngleZ && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleZ, rotAngles.z)) >= rotationThreshold.z || networkState.IsTeleportingNextFrame || isAxisSync || forceState)) { networkState.RotAngleZ = rotAngles.z; networkState.HasRotAngleZ = true; @@ -2012,7 +2059,7 @@ private bool CheckForStateChange(ref NetworkTransformState networkState, ref Tra else if (SynchronizeRotation) { // If we are teleporting then we can skip the delta threshold check - isRotationDirty = networkState.IsTeleportingNextFrame || isAxisSync; + isRotationDirty = networkState.IsTeleportingNextFrame || isAxisSync || forceState; // For quaternion synchronization, if one angle is dirty we send a full update if (!isRotationDirty) { @@ -2051,21 +2098,21 @@ private bool CheckForStateChange(ref NetworkTransformState networkState, ref Tra { if (!UseHalfFloatPrecision) { - if (SyncScaleX && (Mathf.Abs(networkState.ScaleX - scale.x) >= ScaleThreshold || networkState.IsTeleportingNextFrame || isAxisSync)) + if (SyncScaleX && (Mathf.Abs(networkState.ScaleX - scale.x) >= ScaleThreshold || networkState.IsTeleportingNextFrame || isAxisSync || forceState)) { networkState.ScaleX = scale.x; networkState.HasScaleX = true; isScaleDirty = true; } - if (SyncScaleY && (Mathf.Abs(networkState.ScaleY - scale.y) >= ScaleThreshold || networkState.IsTeleportingNextFrame || isAxisSync)) + if (SyncScaleY && (Mathf.Abs(networkState.ScaleY - scale.y) >= ScaleThreshold || networkState.IsTeleportingNextFrame || isAxisSync || forceState)) { networkState.ScaleY = scale.y; networkState.HasScaleY = true; isScaleDirty = true; } - if (SyncScaleZ && (Mathf.Abs(networkState.ScaleZ - scale.z) >= ScaleThreshold || networkState.IsTeleportingNextFrame || isAxisSync)) + if (SyncScaleZ && (Mathf.Abs(networkState.ScaleZ - scale.z) >= ScaleThreshold || networkState.IsTeleportingNextFrame || isAxisSync || forceState)) { networkState.ScaleZ = scale.z; networkState.HasScaleZ = true; @@ -2077,7 +2124,7 @@ private bool CheckForStateChange(ref NetworkTransformState networkState, ref Tra var previousScale = networkState.Scale; for (int i = 0; i < 3; i++) { - if (Mathf.Abs(scale[i] - previousScale[i]) >= ScaleThreshold || networkState.IsTeleportingNextFrame || isAxisSync) + if (Mathf.Abs(scale[i] - previousScale[i]) >= ScaleThreshold || networkState.IsTeleportingNextFrame || isAxisSync || forceState) { isScaleDirty = true; networkState.Scale[i] = scale[i]; @@ -2122,7 +2169,6 @@ private bool CheckForStateChange(ref NetworkTransformState networkState, ref Tra return isDirty; } - /// /// Authority subscribes to network tick events and will invoke /// each network tick. @@ -2144,7 +2190,13 @@ private void OnNetworkTick(bool isCalledFromParent = false) return; } -#if COM_UNITY_MODULES_PHYSICS + // If we are nested and have already sent a state update this tick, then exit early (otherwise check for any changes in state) + if (IsNested && m_LocalAuthoritativeNetworkState.NetworkTick == m_CachedNetworkManager.ServerTime.Tick) + { + return; + } + +#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D // Let the parent handle the updating of this to keep the two synchronized if (!isCalledFromParent && m_UseRigidbodyForMotion && m_NetworkRigidbodyInternal.ParentBody != null && !m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame) { @@ -2154,15 +2206,15 @@ private void OnNetworkTick(bool isCalledFromParent = false) // Update any changes to the transform var transformSource = transform; - OnUpdateAuthoritativeState(ref transformSource); -#if COM_UNITY_MODULES_PHYSICS - m_CurrentPosition = m_TargetPosition = m_UseRigidbodyForMotion ? m_NetworkRigidbodyInternal.GetPosition() : GetSpaceRelativePosition(); + OnUpdateAuthoritativeState(ref transformSource, isCalledFromParent); +#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D + m_InternalCurrentPosition = m_TargetPosition = m_UseRigidbodyForMotion ? m_NetworkRigidbodyInternal.GetPosition() : GetSpaceRelativePosition(); #else - m_CurrentPosition = GetSpaceRelativePosition(); + m_InternalCurrentPosition = GetSpaceRelativePosition(); m_TargetPosition = GetSpaceRelativePosition(); #endif } - else // If we are no longer authority, unsubscribe to the tick event + else // If we are no longer authority, unsubscribe to the tick event { DeregisterForTickUpdate(this); } @@ -2199,7 +2251,7 @@ protected virtual void OnTransformUpdated() /// protected internal void ApplyAuthoritativeState() { -#if COM_UNITY_MODULES_PHYSICS +#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D // TODO: Make this an authority flag // For now, just synchronize with the NetworkRigidbodyBase UseRigidBodyForMotion if (m_NetworkRigidbodyInternal != null) @@ -2208,14 +2260,14 @@ protected internal void ApplyAuthoritativeState() } #endif var networkState = m_LocalAuthoritativeNetworkState; - // The m_CurrentPosition, m_CurrentRotation, and m_CurrentScale values are continually updated + // The m_InternalCurrentPosition, m_InternalCurrentRotation, and m_InternalCurrentScale values are continually updated // at the end of this method and assure that when not interpolating the non-authoritative side // cannot make adjustments to any portions the transform not being synchronized. - var adjustedPosition = m_CurrentPosition; - var adjustedRotation = m_CurrentRotation; + var adjustedPosition = m_InternalCurrentPosition; + var adjustedRotation = m_InternalCurrentRotation; var adjustedRotAngles = adjustedRotation.eulerAngles; - var adjustedScale = m_CurrentScale; + var adjustedScale = m_InternalCurrentScale; // Non-Authority Preservers the authority's transform state update modes InLocalSpace = networkState.InLocalSpace; @@ -2234,6 +2286,7 @@ protected internal void ApplyAuthoritativeState() // 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. + if (Interpolate) { if (SynchronizePosition) @@ -2337,28 +2390,42 @@ protected internal void ApplyAuthoritativeState() // Update our current position if it changed or we are interpolating if (networkState.HasPositionChange || Interpolate) { - m_CurrentPosition = adjustedPosition; + m_InternalCurrentPosition = adjustedPosition; } -#if COM_UNITY_MODULES_PHYSICS +#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D if (m_UseRigidbodyForMotion) { - m_NetworkRigidbodyInternal.MovePosition(m_CurrentPosition); + m_NetworkRigidbodyInternal.MovePosition(m_InternalCurrentPosition); if (LogMotion) { - Debug.Log($"[Client-{m_CachedNetworkManager.LocalClientId}][Interpolate: {networkState.UseInterpolation}][TransPos: {transform.position}][RBPos: {m_NetworkRigidbodyInternal.GetPosition()}][CurrentPos: {m_CurrentPosition}"); + Debug.Log($"[Client-{m_CachedNetworkManager.LocalClientId}][Interpolate: {networkState.UseInterpolation}][TransPos: {transform.position}][RBPos: {m_NetworkRigidbodyInternal.GetPosition()}][CurrentPos: {m_InternalCurrentPosition}"); } } else #endif { - if (InLocalSpace) + if (PositionInLocalSpace) { - transform.localPosition = m_CurrentPosition; + // This handles the edge case of transitioning from local to world space where applying a local + // space value to a non-parented transform will be applied in world space. Since parenting is not + // tick synchronized, there can be one or two ticks between a state update with the InLocalSpace + // state update which can cause the body to seemingly "teleport" when it is just applying a local + // space value relative to world space 0,0,0. + if (SwitchTransformSpaceWhenParented && m_IsFirstNetworkTransform && Interpolate && m_PreviousNetworkObjectParent != null + && transform.parent == null) + { + m_InternalCurrentPosition = m_PreviousNetworkObjectParent.transform.TransformPoint(m_InternalCurrentPosition); + transform.position = m_InternalCurrentPosition; + } + else + { + transform.localPosition = m_InternalCurrentPosition; + } } else { - transform.position = m_CurrentPosition; + transform.position = m_InternalCurrentPosition; } } } @@ -2369,24 +2436,37 @@ protected internal void ApplyAuthoritativeState() // Update our current rotation if it changed or we are interpolating if (networkState.HasRotAngleChange || Interpolate) { - m_CurrentRotation = adjustedRotation; + m_InternalCurrentRotation = adjustedRotation; } -#if COM_UNITY_MODULES_PHYSICS +#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D if (m_UseRigidbodyForMotion) { - m_NetworkRigidbodyInternal.MoveRotation(m_CurrentRotation); + m_NetworkRigidbodyInternal.MoveRotation(m_InternalCurrentRotation); } else #endif { - if (InLocalSpace) + if (RotationInLocalSpace) { - transform.localRotation = m_CurrentRotation; + // This handles the edge case of transitioning from local to world space where applying a local + // space value to a non-parented transform will be applied in world space. Since parenting is not + // tick synchronized, there can be one or two ticks between a state update with the InLocalSpace + // state update which can cause the body to rotate world space relative and cause a slight rotation + // of the body in-between this transition period. + if (SwitchTransformSpaceWhenParented && m_IsFirstNetworkTransform && Interpolate && m_PreviousNetworkObjectParent != null && transform.parent == null) + { + m_InternalCurrentRotation = m_PreviousNetworkObjectParent.transform.rotation * m_InternalCurrentRotation; + transform.rotation = m_InternalCurrentRotation; + } + else + { + transform.localRotation = m_InternalCurrentRotation; + } } else { - transform.rotation = m_CurrentRotation; + transform.rotation = m_InternalCurrentRotation; } } } @@ -2397,9 +2477,9 @@ protected internal void ApplyAuthoritativeState() // Update our current scale if it changed or we are interpolating if (networkState.HasScaleChange || Interpolate) { - m_CurrentScale = adjustedScale; + m_InternalCurrentScale = adjustedScale; } - transform.localScale = m_CurrentScale; + transform.localScale = m_InternalCurrentScale; } OnTransformUpdated(); } @@ -2481,7 +2561,7 @@ private void ApplyTeleportingState(NetworkTransformState newState) } } - m_CurrentPosition = currentPosition; + m_InternalCurrentPosition = currentPosition; m_TargetPosition = currentPosition; // Apply the position @@ -2494,7 +2574,7 @@ private void ApplyTeleportingState(NetworkTransformState newState) transform.position = currentPosition; } -#if COM_UNITY_MODULES_PHYSICS +#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D if (m_UseRigidbodyForMotion) { m_NetworkRigidbodyInternal.SetPosition(transform.position); @@ -2510,17 +2590,6 @@ private void ApplyTeleportingState(NetworkTransformState newState) if (newState.HasScaleChange) { bool shouldUseLossy = false; - if (newState.IsParented) - { - if (transform.parent == null) - { - shouldUseLossy = NetworkObject.WorldPositionStays(); - } - else - { - shouldUseLossy = !NetworkObject.WorldPositionStays(); - } - } if (UseHalfFloatPrecision) { @@ -2545,7 +2614,7 @@ private void ApplyTeleportingState(NetworkTransformState newState) } } - m_CurrentScale = currentScale; + m_InternalCurrentScale = currentScale; m_TargetScale = currentScale; // Apply the adjusted scale @@ -2583,7 +2652,7 @@ private void ApplyTeleportingState(NetworkTransformState newState) currentRotation.eulerAngles = currentEulerAngles; } - m_CurrentRotation = currentRotation; + m_InternalCurrentRotation = currentRotation; m_TargetRotation = currentRotation.eulerAngles; if (InLocalSpace) @@ -2595,7 +2664,7 @@ private void ApplyTeleportingState(NetworkTransformState newState) transform.rotation = currentRotation; } -#if COM_UNITY_MODULES_PHYSICS +#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D if (m_UseRigidbodyForMotion) { m_NetworkRigidbodyInternal.SetRotation(transform.rotation); @@ -2675,6 +2744,8 @@ private void ApplyUpdatedState(NetworkTransformState newState) return; } + AdjustForChangeInTransformSpace(); + // Apply axial changes from the new state // Either apply the delta position target position or the current state's delta position // depending upon whether UsePositionDeltaCompression is enabled @@ -2682,20 +2753,21 @@ private void ApplyUpdatedState(NetworkTransformState newState) { if (!m_LocalAuthoritativeNetworkState.UseHalfFloatPrecision) { + var position = m_LocalAuthoritativeNetworkState.GetPosition(); var newTargetPosition = m_TargetPosition; if (m_LocalAuthoritativeNetworkState.HasPositionX) { - newTargetPosition.x = m_LocalAuthoritativeNetworkState.PositionX; + newTargetPosition.x = position.x; } if (m_LocalAuthoritativeNetworkState.HasPositionY) { - newTargetPosition.y = m_LocalAuthoritativeNetworkState.PositionY; + newTargetPosition.y = position.y; } if (m_LocalAuthoritativeNetworkState.HasPositionZ) { - newTargetPosition.z = m_LocalAuthoritativeNetworkState.PositionZ; + newTargetPosition.z = position.z; } m_TargetPosition = newTargetPosition; } @@ -2834,6 +2906,37 @@ private void OnNetworkStateChanged(NetworkTransformState oldState, NetworkTransf // Apply the new state ApplyUpdatedState(newState); + // Tick synchronize any parented child NetworkObject(s) NetworkTransform(s) + if (TickSyncChildren && m_IsFirstNetworkTransform) + { + // Synchronize any nested NetworkTransforms with the parent's + foreach (var childNetworkTransform in NetworkObject.NetworkTransforms) + { + // Don't update the same instance + if (childNetworkTransform == this) + { + continue; + } + if (childNetworkTransform.CanCommitToTransform) + { + childNetworkTransform.OnNetworkTick(true); + } + } + + // Synchronize any parented children with the parent's motion + foreach (var child in m_ParentedChildren) + { + // Synchronize any nested NetworkTransforms of the child with the parent's + foreach (var childNetworkTransform in child.NetworkTransforms) + { + if (childNetworkTransform.CanCommitToTransform) + { + childNetworkTransform.OnNetworkTick(true); + } + } + } + } + // Provide notifications when the state has been updated // We use the m_LocalAuthoritativeNetworkState because newState has been applied and adjustments could have // been made (i.e. half float precision position values will have been updated) @@ -2902,7 +3005,7 @@ private void AxisChangedDeltaPositionCheck() /// Called by authority to check for deltas and update non-authoritative instances /// if any are found. /// - internal void OnUpdateAuthoritativeState(ref Transform transformSource) + internal void OnUpdateAuthoritativeState(ref Transform transformSource, bool settingState = false) { // If our replicated state is not dirty and our local authority state is dirty, clear it. if (!m_LocalAuthoritativeNetworkState.ExplicitSet && m_LocalAuthoritativeNetworkState.IsDirty && !m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame) @@ -2922,11 +3025,55 @@ internal void OnUpdateAuthoritativeState(ref Transform transformSource) AxisChangedDeltaPositionCheck(); - TryCommitTransform(ref transformSource); + TryCommitTransform(ref transformSource, settingState: settingState); } #endregion #region SPAWN, DESPAWN, AND INITIALIZATION + + private void NonAuthorityFinalizeSynchronization() + { + // For all child NetworkTransforms nested under the same NetworkObject, + // we apply the initial synchronization based on their parented/ordered + // heirarchy. + if (SynchronizeState.IsSynchronizing && m_IsFirstNetworkTransform) + { + foreach (var child in NetworkObject.NetworkTransforms) + { + child.ApplySynchronization(); + + // For all nested (under the root/same NetworkObject) child NetworkTransforms, we need to run through + // initialization once more to assure any values applied or stored are relative to the Root's transform. + child.InternalInitialization(); + } + } + } + + /// + /// Handle applying the synchronization state once everything has spawned. + /// The first NetowrkTransform handles invoking this on any other nested NetworkTransform. + /// + protected internal override void InternalOnNetworkSessionSynchronized() + { + NonAuthorityFinalizeSynchronization(); + + base.InternalOnNetworkSessionSynchronized(); + } + + /// + /// For dynamically spawned NetworkObjects, when the non-authority instance's client is already connected and + /// the SynchronizeState is still pending synchronization then we want to finalize the synchornization at this time. + /// + protected internal override void InternalOnNetworkPostSpawn() + { + if (!CanCommitToTransform && NetworkManager.IsConnectedClient && SynchronizeState.IsSynchronizing) + { + NonAuthorityFinalizeSynchronization(); + } + + base.InternalOnNetworkPostSpawn(); + } + /// /// Create interpolators when first instantiated to avoid memory allocations if the /// associated NetworkObject persists (i.e. despawned but not destroyed or pools) @@ -2942,6 +3089,7 @@ protected virtual void Awake() /// public override void OnNetworkSpawn() { + m_ParentedChildren.Clear(); m_CachedNetworkManager = NetworkManager; Initialize(); @@ -2954,8 +3102,8 @@ public override void OnNetworkSpawn() private void CleanUpOnDestroyOrDespawn() { - -#if COM_UNITY_MODULES_PHYSICS + m_ParentedChildren.Clear(); +#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D var forUpdate = !m_UseRigidbodyForMotion; #else var forUpdate = true; @@ -2964,6 +3112,7 @@ private void CleanUpOnDestroyOrDespawn() { NetworkManager?.NetworkTransformRegistration(m_CachedNetworkObject, forUpdate, false); } + DeregisterForTickUpdate(this); CanCommitToTransform = false; } @@ -3008,13 +3157,15 @@ protected virtual void OnInitialize(ref NetworkVariable r private void ResetInterpolatedStateToCurrentAuthoritativeState() { var serverTime = NetworkManager.ServerTime.Time; -#if COM_UNITY_MODULES_PHYSICS +#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D var position = m_UseRigidbodyForMotion ? m_NetworkRigidbodyInternal.GetPosition() : GetSpaceRelativePosition(); var rotation = m_UseRigidbodyForMotion ? m_NetworkRigidbodyInternal.GetRotation() : GetSpaceRelativeRotation(); #else var position = GetSpaceRelativePosition(); var rotation = GetSpaceRelativeRotation(); #endif + m_PositionInterpolator.InLocalSpace = InLocalSpace; + m_RotationInterpolator.InLocalSpace = InLocalSpace; UpdatePositionInterpolator(position, serverTime, true); UpdatePositionSlerp(); @@ -3034,12 +3185,26 @@ private void InternalInitialization(bool isOwnershipChange = false) return; } m_CachedNetworkObject = NetworkObject; + + // Determine if this is the first NetworkTransform in the associated NetworkObject's list + m_IsFirstNetworkTransform = NetworkObject.NetworkTransforms[0] == this; + + if (m_CachedNetworkManager && m_CachedNetworkManager.DistributedAuthorityMode) { AuthorityMode = AuthorityModes.Owner; } CanCommitToTransform = IsServerAuthoritative() ? IsServer : IsOwner; + if (SwitchTransformSpaceWhenParented) + { + if (CanCommitToTransform) + { + InLocalSpace = transform.parent != null; + } + // Always apply this if SwitchTransformSpaceWhenParented is set. + TickSyncChildren = true; + } var currentPosition = GetSpaceRelativePosition(); var currentRotation = GetSpaceRelativeRotation(); @@ -3050,7 +3215,7 @@ private void InternalInitialization(bool isOwnershipChange = false) m_NetworkTransformTickRegistration = s_NetworkTickRegistration[m_CachedNetworkManager]; } -#if COM_UNITY_MODULES_PHYSICS +#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D // Depending upon order of operations, we invoke this in order to assure that proper settings are applied. if (m_NetworkRigidbodyInternal) { @@ -3080,7 +3245,7 @@ private void InternalInitialization(bool isOwnershipChange = false) SetState(teleportDisabled: false); } - m_CurrentPosition = currentPosition; + m_InternalCurrentPosition = currentPosition; m_TargetPosition = currentPosition; RegisterForTickUpdate(this); @@ -3097,11 +3262,11 @@ private void InternalInitialization(bool isOwnershipChange = false) // Remove this instance from the tick update DeregisterForTickUpdate(this); ResetInterpolatedStateToCurrentAuthoritativeState(); - m_CurrentPosition = currentPosition; + m_InternalCurrentPosition = currentPosition; m_TargetPosition = currentPosition; - m_CurrentScale = transform.localScale; + m_InternalCurrentScale = transform.localScale; m_TargetScale = transform.localScale; - m_CurrentRotation = currentRotation; + m_InternalCurrentRotation = currentRotation; m_TargetRotation = currentRotation.eulerAngles; } OnInitialize(ref m_LocalAuthoritativeNetworkState); @@ -3117,6 +3282,51 @@ protected void Initialize() #endregion #region PARENTING AND OWNERSHIP + // This might seem aweful, but when transitioning between two parents in local space we need to + // catch the moment the transition happens and only apply the special case parenting from one parent + // to another parent once. Keeping track of the "previous previous" allows us to detect the + // back and fourth scenario: + // - No parent (world space) + // - Parent under NetworkObjectA (world to local) + // - Parent under NetworkObjectB (local to local) (catch with "previous previous") + // - Parent under NetworkObjectA (local to local) (catch with "previous previous") + // - Parent under NetworkObjectB (local to local) (catch with "previous previous") + private NetworkObject m_PreviousCurrentParent; + private NetworkObject m_PreviousPreviousParent; + private void AdjustForChangeInTransformSpace() + { + if (SwitchTransformSpaceWhenParented && m_IsFirstNetworkTransform && (m_PositionInterpolator.InLocalSpace != InLocalSpace || + m_RotationInterpolator.InLocalSpace != InLocalSpace || + (InLocalSpace && m_CurrentNetworkObjectParent && m_PreviousNetworkObjectParent && m_PreviousCurrentParent != m_CurrentNetworkObjectParent && m_PreviousPreviousParent != m_PreviousNetworkObjectParent))) + { + var parent = m_CurrentNetworkObjectParent ? m_CurrentNetworkObjectParent : m_PreviousNetworkObjectParent; + if (parent) + { + // In the event it is a NetworkObject to NetworkObject parenting transfer, we will need to migrate our interpolators + // and our current position and rotation to world space relative to the previous parent before converting them to local + // space relative to the new parent + if (InLocalSpace && m_CurrentNetworkObjectParent && m_PreviousNetworkObjectParent) + { + m_PreviousCurrentParent = m_CurrentNetworkObjectParent; + m_PreviousPreviousParent = m_PreviousNetworkObjectParent; + // Convert our current postion and rotation to world space based on the previous parent's transform + m_InternalCurrentPosition = m_PreviousNetworkObjectParent.transform.TransformPoint(m_InternalCurrentPosition); + m_InternalCurrentRotation = m_PreviousNetworkObjectParent.transform.rotation * m_InternalCurrentRotation; + // Convert our current postion and rotation to local space based on the current parent's transform + m_InternalCurrentPosition = m_CurrentNetworkObjectParent.transform.InverseTransformPoint(m_InternalCurrentPosition); + m_InternalCurrentRotation = Quaternion.Inverse(m_CurrentNetworkObjectParent.transform.rotation) * m_InternalCurrentRotation; + // Convert both interpolators to world space based on the previous parent's transform + m_PositionInterpolator.ConvertTransformSpace(m_PreviousNetworkObjectParent.transform, false); + m_RotationInterpolator.ConvertTransformSpace(m_PreviousNetworkObjectParent.transform, false); + // Next, fall into normal transform space conversion of both interpolators to local space based on the current parent's transform + } + + m_PositionInterpolator.ConvertTransformSpace(parent.transform, InLocalSpace); + m_RotationInterpolator.ConvertTransformSpace(parent.transform, InLocalSpace); + } + } + } + /// public override void OnLostOwnership() { @@ -3139,45 +3349,115 @@ protected override void OnOwnershipChanged(ulong previous, ulong current) base.OnOwnershipChanged(previous, current); } + internal bool IsNested; + private List m_ParentedChildren = new List(); + + private bool m_IsFirstNetworkTransform; + private NetworkObject m_CurrentNetworkObjectParent = null; + private NetworkObject m_PreviousNetworkObjectParent = null; + + internal void ChildRegistration(NetworkObject child, bool isAdding) + { + if (isAdding) + { + m_ParentedChildren.Add(child); + } + else + { + m_ParentedChildren.Remove(child); + } + } + /// /// - /// When a parent changes, non-authoritative instances should: - /// - Apply the resultant position, rotation, and scale from the parenting action. - /// - Clear interpolators (even if not enabled on this frame) - /// - Reset the interpolators to the position, rotation, and scale resultant values. - /// This prevents interpolation visual anomalies and issues during initial synchronization + /// When not using a NetworkRigidbody and using an owner authoritative motion model, you can
+ /// improve parenting transitions into and out of world and local space by:
+ /// - Disabling
+ /// - Enabling
+ /// - Enabling
+ /// -- Note: This handles changing from world space to local space for you.
+ /// When these settings are applied, transitioning from:
+ /// - World space to local space (root-null parent/null to parent) + /// - Local space back to world space ( parent to root-null parent) + /// - Local space to local space ( parent to parent) + /// Will all smoothly transition while interpolation is enabled. + /// (Does not work if using a or for motion) + /// + /// When a parent changes, non-authoritative instances should:
+ /// - Apply the resultant position, rotation, and scale from the parenting action.
+ /// - Clear interpolators (even if not enabled on this frame)
+ /// - Reset the interpolators to the position, rotation, and scale resultant values.
+ /// This prevents interpolation visual anomalies and issues during initial synchronization
///
public override void OnNetworkObjectParentChanged(NetworkObject parentNetworkObject) { - // Only if we are not authority - if (!CanCommitToTransform) + base.OnNetworkObjectParentChanged(parentNetworkObject); + } + + + internal override void InternalOnNetworkObjectParentChanged(NetworkObject parentNetworkObject) + { + // The root NetworkTransform handles tracking any NetworkObject parenting since nested NetworkTransforms (of the same NetworkObject) + // will never (or rather should never) change their world space once spawned. +#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D + // Handling automatic transform space switching can only be applied to NetworkTransforms that don't use the Rigidbody for motion + if (!m_UseRigidbodyForMotion && SwitchTransformSpaceWhenParented) +#else + if (SwitchTransformSpaceWhenParented) +#endif { -#if COM_UNITY_MODULES_PHYSICS - var position = m_UseRigidbodyForMotion ? m_NetworkRigidbodyInternal.GetPosition() : GetSpaceRelativePosition(); - var rotation = m_UseRigidbodyForMotion ? m_NetworkRigidbodyInternal.GetRotation() : GetSpaceRelativeRotation(); + m_PreviousNetworkObjectParent = m_CurrentNetworkObjectParent; + m_CurrentNetworkObjectParent = parentNetworkObject; + if (m_IsFirstNetworkTransform) + { + if (CanCommitToTransform) + { + InLocalSpace = m_CurrentNetworkObjectParent != null; + } + if (m_PreviousNetworkObjectParent && m_PreviousNetworkObjectParent.NetworkTransforms != null && m_PreviousNetworkObjectParent.NetworkTransforms.Count > 0) + { + // Always deregister with the first NetworkTransform in the list + m_PreviousNetworkObjectParent.NetworkTransforms[0].ChildRegistration(NetworkObject, false); + } + if (m_CurrentNetworkObjectParent && m_CurrentNetworkObjectParent.NetworkTransforms != null && m_CurrentNetworkObjectParent.NetworkTransforms.Count > 0) + { + // Always register with the first NetworkTransform in the list + m_CurrentNetworkObjectParent.NetworkTransforms[0].ChildRegistration(NetworkObject, true); + } + } + } + else + { + // Keep the same legacy behaviour for compatibility purposes + if (!CanCommitToTransform) + { +#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D + var position = m_UseRigidbodyForMotion ? m_NetworkRigidbodyInternal.GetPosition() : GetSpaceRelativePosition(); + var rotation = m_UseRigidbodyForMotion ? m_NetworkRigidbodyInternal.GetRotation() : GetSpaceRelativeRotation(); #else - var position = GetSpaceRelativePosition(); - var rotation = GetSpaceRelativeRotation(); + var position = GetSpaceRelativePosition(); + var rotation = GetSpaceRelativeRotation(); #endif - m_TargetPosition = m_CurrentPosition = position; - m_CurrentRotation = rotation; - m_TargetRotation = m_CurrentRotation.eulerAngles; - m_TargetScale = m_CurrentScale = GetScale(); + m_TargetPosition = m_InternalCurrentPosition = position; + m_InternalCurrentRotation = rotation; + m_TargetRotation = m_InternalCurrentRotation.eulerAngles; + m_TargetScale = m_InternalCurrentScale = GetScale(); - if (Interpolate) - { - m_ScaleInterpolator.Clear(); - m_PositionInterpolator.Clear(); - m_RotationInterpolator.Clear(); + if (Interpolate) + { + m_ScaleInterpolator.Clear(); + m_PositionInterpolator.Clear(); + m_RotationInterpolator.Clear(); - // Always use NetworkManager here as this can be invoked prior to spawning - var tempTime = new NetworkTime(NetworkManager.NetworkConfig.TickRate, NetworkManager.ServerTime.Tick).Time; - UpdatePositionInterpolator(m_CurrentPosition, tempTime, true); - m_ScaleInterpolator.ResetTo(m_CurrentScale, tempTime); - m_RotationInterpolator.ResetTo(m_CurrentRotation, tempTime); + // Always use NetworkManager here as this can be invoked prior to spawning + var tempTime = new NetworkTime(NetworkManager.NetworkConfig.TickRate, NetworkManager.ServerTime.Tick).Time; + UpdatePositionInterpolator(m_InternalCurrentPosition, tempTime, true); + m_ScaleInterpolator.ResetTo(m_InternalCurrentScale, tempTime); + m_RotationInterpolator.ResetTo(m_InternalCurrentRotation, tempTime); + } } } - base.OnNetworkObjectParentChanged(parentNetworkObject); + base.InternalOnNetworkObjectParentChanged(parentNetworkObject); } #endregion @@ -3212,7 +3492,7 @@ public void SetState(Vector3? posIn = null, Quaternion? rotIn = null, Vector3? s NetworkLog.LogError(errorMessage); return; } -#if COM_UNITY_MODULES_PHYSICS +#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D var position = m_UseRigidbodyForMotion ? m_NetworkRigidbodyInternal.GetPosition() : GetSpaceRelativePosition(); var rotation = m_UseRigidbodyForMotion ? m_NetworkRigidbodyInternal.GetRotation() : GetSpaceRelativeRotation(); #else @@ -3249,7 +3529,7 @@ public void SetState(Vector3? posIn = null, Quaternion? rotIn = null, Vector3? s ///
private void SetStateInternal(Vector3 pos, Quaternion rot, Vector3 scale, bool shouldTeleport) { -#if COM_UNITY_MODULES_PHYSICS +#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D if (m_UseRigidbodyForMotion) { m_NetworkRigidbodyInternal.SetPosition(pos); @@ -3349,10 +3629,12 @@ private void UpdateInterpolation() // Non-Authority if (Interpolate) { + AdjustForChangeInTransformSpace(); + var serverTime = m_CachedNetworkManager.ServerTime; var cachedServerTime = serverTime.Time; //var offset = (float)serverTime.TickOffset; -#if COM_UNITY_MODULES_PHYSICS +#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D var cachedDeltaTime = m_UseRigidbodyForMotion ? m_CachedNetworkManager.RealTimeProvider.FixedDeltaTime : m_CachedNetworkManager.RealTimeProvider.DeltaTime; #else var cachedDeltaTime = m_CachedNetworkManager.RealTimeProvider.DeltaTime; @@ -3360,14 +3642,10 @@ private void UpdateInterpolation() // With owner authoritative mode, non-authority clients can lag behind // by more than 1 tick period of time. The current "solution" for now // is to make their cachedRenderTime run 2 ticks behind. - var ticksAgo = (!IsServerAuthoritative() && !IsServer) || m_CachedNetworkManager.DistributedAuthorityMode ? 2 : 1; - // TODO: We need an RTT that updates regularly and not only when the client sends packets - //if (m_CachedNetworkManager.DistributedAuthorityMode) - //{ - //ticksAgo = m_CachedNetworkManager.CMBServiceConnection ? 2 : 3; - //ticksAgo = Mathf.Max(ticksAgo, (int)m_NetworkTransformTickRegistration.TicksAgo); - //offset = m_NetworkTransformTickRegistration.Offset; - //} + + // TODO: This could most likely just always be 2 + //var ticksAgo = ((!IsServerAuthoritative() && !IsServer) || m_CachedNetworkManager.DistributedAuthorityMode) && !m_CachedNetworkManager.DAHost ? 2 : 1; + var ticksAgo = 2; var cachedRenderTime = serverTime.TimeTicksAgo(ticksAgo).Time; @@ -3401,7 +3679,7 @@ private void UpdateInterpolation() public virtual void OnUpdate() { // If not spawned or this instance has authority, exit early -#if COM_UNITY_MODULES_PHYSICS +#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D if (!IsSpawned || CanCommitToTransform || m_UseRigidbodyForMotion) #else if (!IsSpawned || CanCommitToTransform) @@ -3417,8 +3695,7 @@ public virtual void OnUpdate() ApplyAuthoritativeState(); } - -#if COM_UNITY_MODULES_PHYSICS +#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D /// /// When paired with a NetworkRigidbody and NetworkRigidbody.UseRigidBodyForMotion is enabled, diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs index 5e088c7f06..b5d549501a 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs @@ -19,6 +19,12 @@ public RpcException(string message) : base(message) /// public abstract class NetworkBehaviour : MonoBehaviour { +#if UNITY_EDITOR + [HideInInspector] + [SerializeField] + internal bool ShowTopMostFoldoutHeaderGroup = true; +#endif + #pragma warning disable IDE1006 // disable naming rule violation check // RuntimeAccessModifiersILPP will make this `public` @@ -688,6 +694,8 @@ public virtual void OnNetworkSpawn() { } /// protected virtual void OnNetworkPostSpawn() { } + protected internal virtual void InternalOnNetworkPostSpawn() { } + /// /// This method is only available client-side. /// When a new client joins it's synchronized with all spawned NetworkObjects and scenes loaded for the session joined. At the end of the synchronization process, when all @@ -700,6 +708,8 @@ protected virtual void OnNetworkPostSpawn() { } /// protected virtual void OnNetworkSessionSynchronized() { } + protected internal virtual void InternalOnNetworkSessionSynchronized() { } + /// /// When a scene is loaded and in-scene placed NetworkObjects are finished spawning, this method is invoked on all of the newly spawned in-scene placed NetworkObjects. /// This method runs both client and server side. @@ -759,6 +769,7 @@ internal void NetworkPostSpawn() { try { + InternalOnNetworkPostSpawn(); OnNetworkPostSpawn(); } catch (Exception e) @@ -771,6 +782,7 @@ internal void NetworkSessionSynchronized() { try { + InternalOnNetworkSessionSynchronized(); OnNetworkSessionSynchronized(); } catch (Exception e) @@ -853,6 +865,8 @@ internal void InternalOnLostOwnership() /// the new parent public virtual void OnNetworkObjectParentChanged(NetworkObject parentNetworkObject) { } + internal virtual void InternalOnNetworkObjectParentChanged(NetworkObject parentNetworkObject) { } + private bool m_VarInit = false; private readonly List> m_DeliveryMappedNetworkVariableIndices = new List>(); diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs index b5a042c843..1239c93c7e 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs @@ -17,6 +17,12 @@ namespace Unity.Netcode [AddComponentMenu("Netcode/Network Manager", -100)] public class NetworkManager : MonoBehaviour, INetworkUpdateSystem { +#if UNITY_EDITOR + // Inspector view expand/collapse settings for this derived child class + [HideInInspector] + public bool NetworkManagerExpanded; +#endif + // TODO: Deprecate... // The following internal values are not used, but because ILPP makes them public in the assembly, they cannot // be removed thanks to our semver validation. @@ -890,6 +896,11 @@ private void Reset() OnNetworkManagerReset?.Invoke(this); } + protected virtual void OnValidateComponent() + { + + } + internal void OnValidate() { if (NetworkConfig == null) @@ -950,6 +961,15 @@ internal void OnValidate() } } } + + try + { + OnValidateComponent(); + } + catch (Exception ex) + { + Debug.LogException(ex); + } } private void ModeChanged(PlayModeStateChange change) diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 884ea748db..591fd417e7 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -67,6 +67,7 @@ public uint PrefabIdHash /// public List NetworkTransforms { get; private set; } + #if COM_UNITY_MODULES_PHYSICS /// /// All component instances associated with a component instance. @@ -1080,6 +1081,24 @@ public void SetSceneObjectStatus(bool isSceneObject = false) /// public bool AutoObjectParentSync = true; + /// + /// Determines if the owner will apply transform values sent by the parenting message. + /// + /// + /// When enabled, the resultant parenting transform changes sent by the authority will be applied on all instances.
+ /// When disabled, the resultant parenting transform changes sent by the authority will not be applied on the owner's instance.
+ /// When disabled, all non-owner instances will still be synchronized by the authority's transform values when parented. + /// When using a network topology and an owner authoritative motion model, disabling this can help smooth parenting transitions. + /// When using a network topology this will have no impact on the owner's instance since only the authority/owner can parent. + ///
+ public bool SyncOwnerTransformWhenParented = true; + + /// + /// Client-Server specific, when enabled an owner of a NetworkObject can parent locally as opposed to requiring the owner to notify the server it would like to be parented. + /// This behavior is always true when using a distributed authority network topology and does not require it to be set. + /// + public bool AllowOwnerToParent; + internal readonly HashSet Observers = new HashSet(); #if MULTIPLAYER_TOOLS @@ -1787,6 +1806,9 @@ internal void InvokeBehaviourOnNetworkObjectParentChanged(NetworkObject parentNe { for (int i = 0; i < ChildNetworkBehaviours.Count; i++) { + // Invoke internal notification + ChildNetworkBehaviours[i].InternalOnNetworkObjectParentChanged(parentNetworkObject); + // Invoke public notification ChildNetworkBehaviours[i].OnNetworkObjectParentChanged(parentNetworkObject); } } @@ -1918,7 +1940,7 @@ public bool TrySetParent(NetworkObject parent, bool worldPositionStays = true) // DANGO-TODO: Do we want to worry about ownership permissions here? // It wouldn't make sense to not allow parenting, but keeping this note here as a reminder. - var isAuthority = HasAuthority; + var isAuthority = HasAuthority || (AllowOwnerToParent && IsOwner); // If we don't have authority and we are not shutting down, then don't allow any parenting. // If we are shutting down and don't have authority then allow it. @@ -1984,7 +2006,7 @@ private void OnTransformParentChanged() var isAuthority = false; // With distributed authority, we need to track "valid authoritative" parenting changes. // So, either the authority or AuthorityAppliedParenting is considered a "valid parenting change". - isAuthority = HasAuthority || AuthorityAppliedParenting; + isAuthority = HasAuthority || AuthorityAppliedParenting || (AllowOwnerToParent && IsOwner); var distributedAuthority = NetworkManager.DistributedAuthorityMode; // If we do not have authority and we are spawned @@ -2076,7 +2098,7 @@ private void OnTransformParentChanged() } // If we are connected to a CMB service or we are running a mock CMB service then send to the "server" identifier - if (distributedAuthority) + if (distributedAuthority || (!distributedAuthority && AllowOwnerToParent && IsOwner && !NetworkManager.IsServer)) { if (!NetworkManager.DAHost) { @@ -2365,7 +2387,9 @@ internal List ChildNetworkBehaviours { NetworkTransforms = new List(); } - NetworkTransforms.Add(networkBehaviours[i] as NetworkTransform); + var networkTransform = networkBehaviours[i] as NetworkTransform; + networkTransform.IsNested = i != 0 && networkTransform.gameObject != gameObject; + NetworkTransforms.Add(networkTransform); } #if COM_UNITY_MODULES_PHYSICS else if (type.IsSubclassOf(typeof(NetworkRigidbodyBase))) diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ParentSyncMessage.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ParentSyncMessage.cs index a55767d1d6..bef5fe8123 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ParentSyncMessage.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ParentSyncMessage.cs @@ -117,22 +117,28 @@ public void Handle(ref NetworkContext context) networkObject.SetNetworkParenting(LatestParent, WorldPositionStays); networkObject.ApplyNetworkParenting(RemoveParent); - // We set all of the transform values after parenting as they are - // the values of the server-side post-parenting transform values - if (!WorldPositionStays) + // This check is primarily for client-server network topologies when the motion model is owner authoritative: + // When SyncOwnerTransformWhenParented is enabled, then always apply the transform values. + // When SyncOwnerTransformWhenParented is disabled, then only synchronize the transform on non-owner instances. + if (networkObject.SyncOwnerTransformWhenParented || (!networkObject.SyncOwnerTransformWhenParented && !networkObject.IsOwner)) { - networkObject.transform.localPosition = Position; - networkObject.transform.localRotation = Rotation; - } - else - { - networkObject.transform.position = Position; - networkObject.transform.rotation = Rotation; + // We set all of the transform values after parenting as they are + // the values of the server-side post-parenting transform values + if (!WorldPositionStays) + { + networkObject.transform.localPosition = Position; + networkObject.transform.localRotation = Rotation; + } + else + { + networkObject.transform.position = Position; + networkObject.transform.rotation = Rotation; + } + networkObject.transform.localScale = Scale; } - networkObject.transform.localScale = Scale; // If in distributed authority mode and we are running a DAHost and this is the DAHost, then forward the parent changed message to any remaining clients - if (networkManager.DistributedAuthorityMode && !networkManager.CMBServiceConnection && networkManager.DAHost) + if ((networkManager.DistributedAuthorityMode && !networkManager.CMBServiceConnection && networkManager.DAHost) || (networkObject.AllowOwnerToParent && context.SenderId == networkObject.OwnerClientId && networkManager.IsServer)) { var size = 0; var message = this; diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformBase.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformBase.cs index da35c76fec..4413b73fee 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformBase.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformBase.cs @@ -200,9 +200,8 @@ protected override IEnumerator OnTearDown() ///
/// Determines if we are running as a server or host /// Determines if we are using server or owner authority - public NetworkTransformBase(HostOrServer testWithHost, Authority authority, RotationCompression rotationCompression, Rotation rotation, Precision precision) + public NetworkTransformBase(HostOrServer testWithHost, Authority authority, RotationCompression rotationCompression, Rotation rotation, Precision precision) : base(testWithHost) { - m_UseHost = testWithHost == HostOrServer.Host; m_Authority = authority; m_Precision = precision; m_RotationCompression = rotationCompression; @@ -376,6 +375,18 @@ protected bool AllChildObjectInstancesAreSpawned() return true; } + protected bool AllFirstLevelChildObjectInstancesHaveChild() + { + foreach (var instance in ChildObjectComponent.ClientInstances.Values) + { + if (instance.transform.parent == null) + { + return false; + } + } + return true; + } + protected bool AllChildObjectInstancesHaveChild() { foreach (var instance in ChildObjectComponent.ClientInstances.Values) @@ -398,6 +409,33 @@ protected bool AllChildObjectInstancesHaveChild() return true; } + protected bool AllFirstLevelChildObjectInstancesHaveNoParent() + { + foreach (var instance in ChildObjectComponent.ClientInstances.Values) + { + if (instance.transform.parent != null) + { + return false; + } + } + return true; + } + + protected bool AllSubChildObjectInstancesHaveNoParent() + { + if (ChildObjectComponent.HasSubChild) + { + foreach (var instance in ChildObjectComponent.ClientSubChildInstances.Values) + { + if (instance.transform.parent != null) + { + return false; + } + } + } + return true; + } + /// /// A wait condition specific method that assures the local space coordinates /// are not impacted by NetworkTransform when parented. diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs index 7b9ac9935d..95ca7e0f20 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs @@ -101,6 +101,225 @@ private void AllChildrenLocalTransformValuesMatch(bool useSubChild, ChildrenTran } #if !MULTIPLAYER_TOOLS + + private void UpdateTransformLocal(Components.NetworkTransform networkTransformTestComponent) + { + networkTransformTestComponent.transform.localPosition += GetRandomVector3(0.5f, 2.0f); + var rotation = networkTransformTestComponent.transform.localRotation; + var eulerRotation = rotation.eulerAngles; + eulerRotation += GetRandomVector3(0.5f, 5.0f); + rotation.eulerAngles = eulerRotation; + networkTransformTestComponent.transform.localRotation = rotation; + } + + private void UpdateTransformWorld(Components.NetworkTransform networkTransformTestComponent) + { + networkTransformTestComponent.transform.position += GetRandomVector3(0.5f, 2.0f); + var rotation = networkTransformTestComponent.transform.rotation; + var eulerRotation = rotation.eulerAngles; + eulerRotation += GetRandomVector3(0.5f, 5.0f); + rotation.eulerAngles = eulerRotation; + networkTransformTestComponent.transform.rotation = rotation; + } + + /// + /// This test validates the SwitchTransformSpaceWhenParented setting under all network topologies + /// + [Test] + public void SwitchTransformSpaceWhenParentedTest([Values(0.5f, 1.0f, 5.0f)] float scale) + { + m_UseParentingThreshold = true; + // Get the NetworkManager that will have authority in order to spawn with the correct authority + var isServerAuthority = m_Authority == Authority.ServerAuthority; + var authorityNetworkManager = m_ServerNetworkManager; + if (!isServerAuthority) + { + authorityNetworkManager = m_ClientNetworkManagers[0]; + } + + var childAuthorityNetworkManager = m_ClientNetworkManagers[0]; + if (!isServerAuthority) + { + childAuthorityNetworkManager = m_ServerNetworkManager; + } + + // Spawn a parent and children + ChildObjectComponent.HasSubChild = true; + // Modify our prefabs for this specific test + m_ParentObject.GetComponent().TickSyncChildren = true; + m_ChildObject.GetComponent().SwitchTransformSpaceWhenParented = true; + m_ChildObject.GetComponent().TickSyncChildren = true; + m_SubChildObject.GetComponent().SwitchTransformSpaceWhenParented = true; + m_SubChildObject.GetComponent().TickSyncChildren = true; + m_ChildObject.AllowOwnerToParent = true; + m_SubChildObject.AllowOwnerToParent = true; + + + var authoritySideParent = SpawnObject(m_ParentObject.gameObject, authorityNetworkManager).GetComponent(); + var authoritySideChild = SpawnObject(m_ChildObject.gameObject, childAuthorityNetworkManager).GetComponent(); + var authoritySideSubChild = SpawnObject(m_SubChildObject.gameObject, childAuthorityNetworkManager).GetComponent(); + + // Assure all of the child object instances are spawned before proceeding to parenting + var success = WaitForConditionOrTimeOutWithTimeTravel(AllChildObjectInstancesAreSpawned); + Assert.True(success, "Timed out waiting for all child instances to be spawned!"); + + // Get the owner instance if in client-server mode with owner authority + if (m_Authority == Authority.OwnerAuthority && !m_DistributedAuthority) + { + authoritySideParent = s_GlobalNetworkObjects[authoritySideParent.OwnerClientId][authoritySideParent.NetworkObjectId]; + authoritySideChild = s_GlobalNetworkObjects[authoritySideChild.OwnerClientId][authoritySideChild.NetworkObjectId]; + authoritySideSubChild = s_GlobalNetworkObjects[authoritySideSubChild.OwnerClientId][authoritySideSubChild.NetworkObjectId]; + } + + // Get the authority parent and child instances + m_AuthorityParentObject = NetworkTransformTestComponent.AuthorityInstance.NetworkObject; + m_AuthorityChildObject = ChildObjectComponent.AuthorityInstance.NetworkObject; + m_AuthoritySubChildObject = ChildObjectComponent.AuthoritySubInstance.NetworkObject; + + // The child NetworkTransform will use world space when world position stays and + // local space when world position does not stay when parenting. + ChildObjectComponent.AuthorityInstance.UseHalfFloatPrecision = m_Precision == Precision.Half; + ChildObjectComponent.AuthorityInstance.UseQuaternionSynchronization = m_Rotation == Rotation.Quaternion; + ChildObjectComponent.AuthorityInstance.UseQuaternionCompression = m_RotationCompression == RotationCompression.QuaternionCompress; + + ChildObjectComponent.AuthoritySubInstance.UseHalfFloatPrecision = m_Precision == Precision.Half; + ChildObjectComponent.AuthoritySubInstance.UseQuaternionSynchronization = m_Rotation == Rotation.Quaternion; + ChildObjectComponent.AuthoritySubInstance.UseQuaternionCompression = m_RotationCompression == RotationCompression.QuaternionCompress; + + // Set whether we are interpolating or not + m_AuthorityParentNetworkTransform = m_AuthorityParentObject.GetComponent(); + m_AuthorityParentNetworkTransform.Interpolate = true; + m_AuthorityChildNetworkTransform = m_AuthorityChildObject.GetComponent(); + m_AuthorityChildNetworkTransform.Interpolate = true; + m_AuthoritySubChildNetworkTransform = m_AuthoritySubChildObject.GetComponent(); + m_AuthoritySubChildNetworkTransform.Interpolate = true; + + // Apply a scale to the parent object to make sure the scale on the child is properly updated on + // non-authority instances. + var halfScale = scale * 0.5f; + m_AuthorityParentObject.transform.localScale = GetRandomVector3(scale - halfScale, scale + halfScale); + m_AuthorityChildObject.transform.localScale = GetRandomVector3(scale - halfScale, scale + halfScale); + m_AuthoritySubChildObject.transform.localScale = GetRandomVector3(scale - halfScale, scale + halfScale); + + // Allow one tick for authority to update these changes + TimeTravelAdvanceTick(); + success = WaitForConditionOrTimeOutWithTimeTravel(PositionRotationScaleMatches); + + Assert.True(success, "All transform values did not match prior to parenting!"); + + success = WaitForConditionOrTimeOutWithTimeTravel(PositionRotationScaleMatches); + + Assert.True(success, "All transform values did not match prior to parenting!"); + + // Move things around while parenting and removing the parent + // Not the absolute "perfect" test, but it validates the clients all synchronize + // parenting and transform values. + for (int i = 0; i < 30; i++) + { + // Provide two network ticks for interpolation to finalize + TimeTravelAdvanceTick(); + TimeTravelAdvanceTick(); + + // This validates each child instance has preserved their local space values + AllChildrenLocalTransformValuesMatch(false, ChildrenTransformCheckType.Connected_Clients); + + // This validates each sub-child instance has preserved their local space values + AllChildrenLocalTransformValuesMatch(true, ChildrenTransformCheckType.Connected_Clients); + // Parent while in motion + if (i == 5) + { + // Parent the child under the parent with the current world position stays setting + Assert.True(authoritySideChild.TrySetParent(authoritySideParent.transform), $"[Child][Client-{authoritySideChild.NetworkManagerOwner.LocalClientId}] Failed to set child's parent!"); + + // This waits for all child instances to be parented + success = WaitForConditionOrTimeOutWithTimeTravel(AllFirstLevelChildObjectInstancesHaveChild, 300); + Assert.True(success, "Timed out waiting for all instances to have parented a child!"); + } + + if (i == 10) + { + // Parent the sub-child under the child with the current world position stays setting + Assert.True(authoritySideSubChild.TrySetParent(authoritySideChild.transform), $"[Sub-Child][Client-{authoritySideSubChild.NetworkManagerOwner.LocalClientId}] Failed to set sub-child's parent!"); + + // This waits for all child instances to be parented + success = WaitForConditionOrTimeOutWithTimeTravel(AllChildObjectInstancesHaveChild, 300); + Assert.True(success, "Timed out waiting for all instances to have parented a child!"); + } + + if (i == 15) + { + // Verify that a late joining client will synchronize to the parented NetworkObjects properly + CreateAndStartNewClientWithTimeTravel(); + + // Assure all of the child object instances are spawned (basically for the newly connected client) + success = WaitForConditionOrTimeOutWithTimeTravel(AllChildObjectInstancesAreSpawned, 300); + Assert.True(success, "Timed out waiting for all child instances to be spawned!"); + + // This waits for all child instances to be parented + success = WaitForConditionOrTimeOutWithTimeTravel(AllChildObjectInstancesHaveChild, 300); + Assert.True(success, "Timed out waiting for all instances to have parented a child!"); + + // This validates each child instance has preserved their local space values + AllChildrenLocalTransformValuesMatch(false, ChildrenTransformCheckType.Late_Join_Client); + + // This validates each sub-child instance has preserved their local space values + AllChildrenLocalTransformValuesMatch(true, ChildrenTransformCheckType.Late_Join_Client); + } + + if (i == 20) + { + // Remove the parent + Assert.True(authoritySideSubChild.TryRemoveParent(), $"[Sub-Child][Client-{authoritySideSubChild.NetworkManagerOwner.LocalClientId}] Failed to set sub-child's parent!"); + + // This waits for all child instances to have the parent removed + success = WaitForConditionOrTimeOutWithTimeTravel(AllSubChildObjectInstancesHaveNoParent, 300); + Assert.True(success, "Timed out waiting for all instances remove the parent!"); + } + + if (i == 25) + { + // Parent the child under the parent with the current world position stays setting + Assert.True(authoritySideChild.TryRemoveParent(), $"[Child][Client-{authoritySideChild.NetworkManagerOwner.LocalClientId}] Failed to remove parent!"); + + // This waits for all child instances to be parented + success = WaitForConditionOrTimeOutWithTimeTravel(AllFirstLevelChildObjectInstancesHaveNoParent, 300); + Assert.True(success, "Timed out waiting for all instances remove the parent!"); + } + UpdateTransformWorld(m_AuthorityParentNetworkTransform); + if (m_AuthorityChildNetworkTransform.InLocalSpace) + { + UpdateTransformLocal(m_AuthorityChildNetworkTransform); + } + else + { + UpdateTransformWorld(m_AuthorityChildNetworkTransform); + } + + if (m_AuthoritySubChildNetworkTransform.InLocalSpace) + { + UpdateTransformLocal(m_AuthoritySubChildNetworkTransform); + } + else + { + UpdateTransformWorld(m_AuthoritySubChildNetworkTransform); + } + } + + success = WaitForConditionOrTimeOutWithTimeTravel(PositionRotationScaleMatches, 300); + + Assert.True(success, "All transform values did not match prior to parenting!"); + + // Revert the modifications made for this specific test + m_ParentObject.GetComponent().TickSyncChildren = false; + m_ChildObject.GetComponent().SwitchTransformSpaceWhenParented = false; + m_ChildObject.GetComponent().TickSyncChildren = false; + m_ChildObject.AllowOwnerToParent = false; + m_SubChildObject.AllowOwnerToParent = false; + m_SubChildObject.GetComponent().SwitchTransformSpaceWhenParented = false; + m_SubChildObject.GetComponent().TickSyncChildren = false; + } + + /// /// Validates that transform values remain the same when a NetworkTransform is /// parented under another NetworkTransform under all of the possible axial conditions @@ -410,6 +629,7 @@ public void TestAuthoritativeTransformChangeOneAtATime([Values] TransformSpace t Assert.AreEqual(Vector3.zero, m_NonAuthoritativeTransform.transform.position, "server side pos should be zero at first"); // sanity check TimeTravelAdvanceTick(); + TimeTravelToNextTick(); m_AuthoritativeTransform.StatePushed = false; var nextPosition = GetRandomVector3(2f, 30f); diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransformAnticipationTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransformAnticipationTests.cs index 44530a782c..c4921d1fed 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransformAnticipationTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransformAnticipationTests.cs @@ -299,16 +299,15 @@ public void WhenServerChangesSmoothValue_ValuesAreLerped() }, new List { m_ServerNetworkManager }); WaitForMessageReceivedWithTimeTravel(m_ClientNetworkManagers.ToList()); - var percentChanged = 1f / 60f; AssertVectorsAreEquivalent(Vector3.Lerp(anticipePosition, serverSetPosition, percentChanged), testComponent.transform.position); AssertVectorsAreEquivalent(Vector3.Lerp(anticipeScale, serverSetScale, percentChanged), testComponent.transform.localScale); - AssertQuaternionsAreEquivalent(Quaternion.Slerp(anticipeRotation, serverSetRotation, percentChanged), testComponent.transform.rotation); + AssertQuaternionsAreEquivalent(Quaternion.Lerp(anticipeRotation, serverSetRotation, percentChanged), testComponent.transform.rotation); AssertVectorsAreEquivalent(Vector3.Lerp(anticipePosition, serverSetPosition, percentChanged), testComponent.AnticipatedState.Position); AssertVectorsAreEquivalent(Vector3.Lerp(anticipeScale, serverSetScale, percentChanged), testComponent.AnticipatedState.Scale); - AssertQuaternionsAreEquivalent(Quaternion.Slerp(anticipeRotation, serverSetRotation, percentChanged), testComponent.AnticipatedState.Rotation); + AssertQuaternionsAreEquivalent(Quaternion.Lerp(anticipeRotation, serverSetRotation, percentChanged), testComponent.AnticipatedState.Rotation); AssertVectorsAreEquivalent(serverSetPosition, testComponent.AuthoritativeState.Position); AssertVectorsAreEquivalent(serverSetScale, testComponent.AuthoritativeState.Scale); @@ -316,11 +315,11 @@ public void WhenServerChangesSmoothValue_ValuesAreLerped() AssertVectorsAreEquivalent(Vector3.Lerp(startPosition, serverSetPosition, percentChanged), otherClientComponent.transform.position); AssertVectorsAreEquivalent(Vector3.Lerp(startScale, serverSetScale, percentChanged), otherClientComponent.transform.localScale); - AssertQuaternionsAreEquivalent(Quaternion.Slerp(startRotation, serverSetRotation, percentChanged), otherClientComponent.transform.rotation); + AssertQuaternionsAreEquivalent(Quaternion.Lerp(startRotation, serverSetRotation, percentChanged), otherClientComponent.transform.rotation); AssertVectorsAreEquivalent(Vector3.Lerp(startPosition, serverSetPosition, percentChanged), otherClientComponent.AnticipatedState.Position); AssertVectorsAreEquivalent(Vector3.Lerp(startScale, serverSetScale, percentChanged), otherClientComponent.AnticipatedState.Scale); - AssertQuaternionsAreEquivalent(Quaternion.Slerp(startRotation, serverSetRotation, percentChanged), otherClientComponent.AnticipatedState.Rotation); + AssertQuaternionsAreEquivalent(Quaternion.Lerp(startRotation, serverSetRotation, percentChanged), otherClientComponent.AnticipatedState.Rotation); AssertVectorsAreEquivalent(serverSetPosition, otherClientComponent.AuthoritativeState.Position); AssertVectorsAreEquivalent(serverSetScale, otherClientComponent.AuthoritativeState.Scale); @@ -333,11 +332,11 @@ public void WhenServerChangesSmoothValue_ValuesAreLerped() AssertVectorsAreEquivalent(Vector3.Lerp(anticipePosition, serverSetPosition, percentChanged), testComponent.transform.position); AssertVectorsAreEquivalent(Vector3.Lerp(anticipeScale, serverSetScale, percentChanged), testComponent.transform.localScale); - AssertQuaternionsAreEquivalent(Quaternion.Slerp(anticipeRotation, serverSetRotation, percentChanged), testComponent.transform.rotation); + AssertQuaternionsAreEquivalent(Quaternion.Lerp(anticipeRotation, serverSetRotation, percentChanged), testComponent.transform.rotation); AssertVectorsAreEquivalent(Vector3.Lerp(anticipePosition, serverSetPosition, percentChanged), testComponent.AnticipatedState.Position); AssertVectorsAreEquivalent(Vector3.Lerp(anticipeScale, serverSetScale, percentChanged), testComponent.AnticipatedState.Scale); - AssertQuaternionsAreEquivalent(Quaternion.Slerp(anticipeRotation, serverSetRotation, percentChanged), testComponent.AnticipatedState.Rotation); + AssertQuaternionsAreEquivalent(Quaternion.Lerp(anticipeRotation, serverSetRotation, percentChanged), testComponent.AnticipatedState.Rotation); AssertVectorsAreEquivalent(serverSetPosition, testComponent.AuthoritativeState.Position); AssertVectorsAreEquivalent(serverSetScale, testComponent.AuthoritativeState.Scale); @@ -345,11 +344,11 @@ public void WhenServerChangesSmoothValue_ValuesAreLerped() AssertVectorsAreEquivalent(Vector3.Lerp(startPosition, serverSetPosition, percentChanged), otherClientComponent.transform.position); AssertVectorsAreEquivalent(Vector3.Lerp(startScale, serverSetScale, percentChanged), otherClientComponent.transform.localScale); - AssertQuaternionsAreEquivalent(Quaternion.Slerp(startRotation, serverSetRotation, percentChanged), otherClientComponent.transform.rotation); + AssertQuaternionsAreEquivalent(Quaternion.Lerp(startRotation, serverSetRotation, percentChanged), otherClientComponent.transform.rotation); AssertVectorsAreEquivalent(Vector3.Lerp(startPosition, serverSetPosition, percentChanged), otherClientComponent.AnticipatedState.Position); AssertVectorsAreEquivalent(Vector3.Lerp(startScale, serverSetScale, percentChanged), otherClientComponent.AnticipatedState.Scale); - AssertQuaternionsAreEquivalent(Quaternion.Slerp(startRotation, serverSetRotation, percentChanged), otherClientComponent.AnticipatedState.Rotation); + AssertQuaternionsAreEquivalent(Quaternion.Lerp(startRotation, serverSetRotation, percentChanged), otherClientComponent.AnticipatedState.Rotation); AssertVectorsAreEquivalent(serverSetPosition, otherClientComponent.AuthoritativeState.Position); AssertVectorsAreEquivalent(serverSetScale, otherClientComponent.AuthoritativeState.Scale); diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/TransformInterpolationTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/TransformInterpolationTests.cs index 9c402ef60e..4e0cea8e53 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/TransformInterpolationTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/TransformInterpolationTests.cs @@ -63,14 +63,10 @@ public void StopMoving() private const int k_MaxThresholdFailures = 4; private int m_ExceededThresholdCount; - private void Update() + public override void OnUpdate() { base.OnUpdate(); - if (!IsSpawned || TestComplete) - { - return; - } // Check the position of the nested object on the client if (CheckPosition) @@ -92,6 +88,17 @@ private void Update() m_ExceededThresholdCount = 0; } } + } + + private void Update() + { + base.OnUpdate(); + + if (!IsSpawned || !CanCommitToTransform || TestComplete) + { + return; + } + // Move the nested object on the server if (IsMoving) @@ -136,7 +143,6 @@ private void Update() Assert.True(CanCommitToTransform, $"Using non-authority instance to update transform!"); transform.position = new Vector3(1000.0f, 1000.0f, 1000.0f); } - } } diff --git a/testproject/Assets/Tests/Manual/NestedNetworkTransforms/AutomatedPlayer-SA.prefab b/testproject/Assets/Tests/Manual/NestedNetworkTransforms/AutomatedPlayer-SA.prefab index 3361d528e0..eb8da63629 100644 --- a/testproject/Assets/Tests/Manual/NestedNetworkTransforms/AutomatedPlayer-SA.prefab +++ b/testproject/Assets/Tests/Manual/NestedNetworkTransforms/AutomatedPlayer-SA.prefab @@ -26,13 +26,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 772585991204072682} + serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 4, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 296612175404815447} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!23 &6327137497379236391 MeshRenderer: @@ -51,6 +51,9 @@ MeshRenderer: m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -96,6 +99,9 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 0d8ad30fca3f9a240bdce16f0166033b, type: 3} m_Name: m_EditorClassIdentifier: + AuthorityMode: 0 + TickSyncChildren: 0 + UseUnreliableDeltas: 0 SyncPositionX: 1 SyncPositionY: 1 SyncPositionZ: 1 @@ -105,23 +111,27 @@ MonoBehaviour: SyncScaleX: 1 SyncScaleY: 1 SyncScaleZ: 1 - SlerpPosition: 1 - UseQuaternionSynchronization: 1 - UseQuaternionCompression: 1 - UseHalfFloatPrecision: 1 PositionThreshold: 0.001 RotAngleThreshold: 0.01 ScaleThreshold: 0.01 + UseQuaternionSynchronization: 1 + UseQuaternionCompression: 1 + UseHalfFloatPrecision: 1 InLocalSpace: 1 Interpolate: 1 + SlerpPosition: 1 + IsServerAuthority: 1 DebugTransform: 0 - IsServerAuthoritative: 1 LastUpdatedPosition: {x: 0, y: 0, z: 0} LastUpdatedScale: {x: 0, y: 0, z: 0} LastUpdatedRotation: {x: 0, y: 0, z: 0, w: 0} + PushedPosition: {x: 0, y: 0, z: 0} + PushedScale: {x: 0, y: 0, z: 0} + PushedRotation: {x: 0, y: 0, z: 0, w: 0} PreviousUpdatedPosition: {x: 0, y: 0, z: 0} PreviousUpdatedScale: {x: 0, y: 0, z: 0} PreviousUpdatedRotation: {x: 0, y: 0, z: 0, w: 0} + m_StatesToLog: 80 RotationSpeed: 7.4 RotateBasedOnDirection: 0 --- !u!1 &2771624607751045562 @@ -147,6 +157,7 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 2771624607751045562} + serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} @@ -154,7 +165,6 @@ Transform: m_Children: - {fileID: 4974009855568796650} m_Father: {fileID: 296612175404815447} - m_RootOrder: 1 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &4147667212972069939 GameObject: @@ -180,13 +190,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 4147667212972069939} + serializedVersion: 2 m_LocalRotation: {x: 0.17364816, y: -0, z: -0, w: 0.9848078} m_LocalPosition: {x: 0, y: 8, z: -10} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 296612175404815447} - m_RootOrder: 2 m_LocalEulerAnglesHint: {x: 20, y: 0, z: 0} --- !u!20 &8372809022110481992 Camera: @@ -202,9 +212,17 @@ Camera: m_projectionMatrixMode: 1 m_GateFitMode: 2 m_FOVAxisMode: 0 + m_Iso: 200 + m_ShutterSpeed: 0.005 + m_Aperture: 16 + m_FocusDistance: 10 + m_FocalLength: 50 + m_BladeCount: 5 + m_Curvature: {x: 2, y: 11} + m_BarrelClipping: 0.25 + m_Anamorphism: 0 m_SensorSize: {x: 36, y: 24} m_LensShift: {x: 0, y: 0} - m_FocalLength: 50 m_NormalizedViewPortRect: serializedVersion: 2 x: 0 @@ -257,13 +275,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 6292214655028195304} + serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: -4, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 5485086383386216104} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!23 &3062926429781172158 MeshRenderer: @@ -282,6 +300,9 @@ MeshRenderer: m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -327,6 +348,9 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 0d8ad30fca3f9a240bdce16f0166033b, type: 3} m_Name: m_EditorClassIdentifier: + AuthorityMode: 0 + TickSyncChildren: 0 + UseUnreliableDeltas: 0 SyncPositionX: 1 SyncPositionY: 1 SyncPositionZ: 1 @@ -336,23 +360,27 @@ MonoBehaviour: SyncScaleX: 1 SyncScaleY: 1 SyncScaleZ: 1 - SlerpPosition: 1 - UseQuaternionSynchronization: 1 - UseQuaternionCompression: 1 - UseHalfFloatPrecision: 1 PositionThreshold: 0.001 RotAngleThreshold: 0.01 ScaleThreshold: 0.01 + UseQuaternionSynchronization: 1 + UseQuaternionCompression: 1 + UseHalfFloatPrecision: 1 InLocalSpace: 1 Interpolate: 1 + SlerpPosition: 1 + IsServerAuthority: 1 DebugTransform: 0 - IsServerAuthoritative: 1 LastUpdatedPosition: {x: 0, y: 0, z: 0} LastUpdatedScale: {x: 0, y: 0, z: 0} LastUpdatedRotation: {x: 0, y: 0, z: 0, w: 0} + PushedPosition: {x: 0, y: 0, z: 0} + PushedScale: {x: 0, y: 0, z: 0} + PushedRotation: {x: 0, y: 0, z: 0, w: 0} PreviousUpdatedPosition: {x: 0, y: 0, z: 0} PreviousUpdatedScale: {x: 0, y: 0, z: 0} PreviousUpdatedRotation: {x: 0, y: 0, z: 0, w: 0} + m_StatesToLog: 80 RotationSpeed: 7.4 RotateBasedOnDirection: 0 --- !u!1001 &8977898853425847701 @@ -360,6 +388,7 @@ PrefabInstance: m_ObjectHideFlags: 0 serializedVersion: 2 m_Modification: + serializedVersion: 3 m_TransformParent: {fileID: 0} m_Modifications: - target: {fileID: -745482209883575862, guid: 96e0a72e30d0c46c8a5c9a750e8f5807, @@ -369,7 +398,7 @@ PrefabInstance: objectReference: {fileID: 0} - target: {fileID: 947981134, guid: 96e0a72e30d0c46c8a5c9a750e8f5807, type: 3} propertyPath: GlobalObjectIdHash - value: 951099334 + value: 2547197533 objectReference: {fileID: 0} - target: {fileID: 3809075828520557319, guid: 96e0a72e30d0c46c8a5c9a750e8f5807, type: 3} @@ -469,6 +498,33 @@ PrefabInstance: - {fileID: 8685790303553767877, guid: 96e0a72e30d0c46c8a5c9a750e8f5807, type: 3} - {fileID: -745482209883575862, guid: 96e0a72e30d0c46c8a5c9a750e8f5807, type: 3} - {fileID: 3809075828520557319, guid: 96e0a72e30d0c46c8a5c9a750e8f5807, type: 3} + m_RemovedGameObjects: [] + m_AddedGameObjects: + - targetCorrespondingSourceObject: {fileID: 8685790303553767874, guid: 96e0a72e30d0c46c8a5c9a750e8f5807, + type: 3} + insertIndex: -1 + addedObject: {fileID: 1522619104359096714} + - targetCorrespondingSourceObject: {fileID: 8685790303553767874, guid: 96e0a72e30d0c46c8a5c9a750e8f5807, + type: 3} + insertIndex: -1 + addedObject: {fileID: 5485086383386216104} + - targetCorrespondingSourceObject: {fileID: 8685790303553767874, guid: 96e0a72e30d0c46c8a5c9a750e8f5807, + type: 3} + insertIndex: -1 + addedObject: {fileID: 7199215624742357828} + m_AddedComponents: + - targetCorrespondingSourceObject: {fileID: 8685790303553767886, guid: 96e0a72e30d0c46c8a5c9a750e8f5807, + type: 3} + insertIndex: -1 + addedObject: {fileID: 4389916208190318681} + - targetCorrespondingSourceObject: {fileID: 8685790303553767886, guid: 96e0a72e30d0c46c8a5c9a750e8f5807, + type: 3} + insertIndex: -1 + addedObject: {fileID: 5376990732947334198} + - targetCorrespondingSourceObject: {fileID: 8685790303553767886, guid: 96e0a72e30d0c46c8a5c9a750e8f5807, + type: 3} + insertIndex: -1 + addedObject: {fileID: 7431853297519116304} m_SourcePrefab: {fileID: 100100000, guid: 96e0a72e30d0c46c8a5c9a750e8f5807, type: 3} --- !u!4 &296612175404815447 stripped Transform: @@ -511,6 +567,9 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: dfb1af1a9249278438d2daa2877ee2ad, type: 3} m_Name: m_EditorClassIdentifier: + AuthorityMode: 0 + TickSyncChildren: 1 + UseUnreliableDeltas: 0 SyncPositionX: 1 SyncPositionY: 1 SyncPositionZ: 1 @@ -520,23 +579,27 @@ MonoBehaviour: SyncScaleX: 1 SyncScaleY: 1 SyncScaleZ: 1 - SlerpPosition: 0 - UseQuaternionSynchronization: 1 - UseQuaternionCompression: 0 - UseHalfFloatPrecision: 1 PositionThreshold: 0.001 RotAngleThreshold: 0.001 ScaleThreshold: 0.001 + UseQuaternionSynchronization: 1 + UseQuaternionCompression: 0 + UseHalfFloatPrecision: 1 InLocalSpace: 0 Interpolate: 1 + SlerpPosition: 0 + IsServerAuthority: 1 DebugTransform: 0 - IsServerAuthoritative: 1 LastUpdatedPosition: {x: 0, y: 0, z: 0} LastUpdatedScale: {x: 0, y: 0, z: 0} LastUpdatedRotation: {x: 0, y: 0, z: 0, w: 0} + PushedPosition: {x: 0, y: 0, z: 0} + PushedScale: {x: 0, y: 0, z: 0} + PushedRotation: {x: 0, y: 0, z: 0, w: 0} PreviousUpdatedPosition: {x: 0, y: 0, z: 0} PreviousUpdatedScale: {x: 0, y: 0, z: 0} PreviousUpdatedRotation: {x: 0, y: 0, z: 0, w: 0} + m_StatesToLog: 80 --- !u!114 &7431853297519116304 MonoBehaviour: m_ObjectHideFlags: 0 diff --git a/testproject/Assets/Tests/Manual/NestedNetworkTransforms/ChildMover.cs b/testproject/Assets/Tests/Manual/NestedNetworkTransforms/ChildMover.cs index 86ce689e3d..f731d6fe95 100644 --- a/testproject/Assets/Tests/Manual/NestedNetworkTransforms/ChildMover.cs +++ b/testproject/Assets/Tests/Manual/NestedNetworkTransforms/ChildMover.cs @@ -37,8 +37,11 @@ public void PlayerIsMoving(float movementDirection) if (IsSpawned && CanCommitToTransform) { var rotateDirection = RotateBasedOnDirection ? movementDirection * RotationSpeed : RotationSpeed; - - transform.RotateAround(m_RootParentTransform.position, transform.TransformDirection(Vector3.up), RotationSpeed); + // Just make sure we are set to local space for this test + if (InLocalSpace) + { + transform.RotateAround(m_RootParentTransform.position, transform.TransformDirection(Vector3.up), RotationSpeed); + } } } diff --git a/testproject/Assets/Tests/Runtime/NetworkTransform/NestedNetworkTransformTests.cs b/testproject/Assets/Tests/Runtime/NetworkTransform/NestedNetworkTransformTests.cs index 8a19fc22f4..0cae4ecb12 100644 --- a/testproject/Assets/Tests/Runtime/NetworkTransform/NestedNetworkTransformTests.cs +++ b/testproject/Assets/Tests/Runtime/NetworkTransform/NestedNetworkTransformTests.cs @@ -9,18 +9,31 @@ namespace TestProject.RuntimeTests { - [TestFixture(Interpolation.Interpolation, Precision.Full, NetworkTransform.AuthorityModes.Server)] - [TestFixture(Interpolation.Interpolation, Precision.Full, NetworkTransform.AuthorityModes.Owner)] - [TestFixture(Interpolation.Interpolation, Precision.Half, NetworkTransform.AuthorityModes.Server)] - [TestFixture(Interpolation.Interpolation, Precision.Half, NetworkTransform.AuthorityModes.Owner)] - [TestFixture(Interpolation.Interpolation, Precision.Compressed, NetworkTransform.AuthorityModes.Server)] - [TestFixture(Interpolation.Interpolation, Precision.Compressed, NetworkTransform.AuthorityModes.Owner)] - [TestFixture(Interpolation.NoInterpolation, Precision.Full, NetworkTransform.AuthorityModes.Server)] - [TestFixture(Interpolation.NoInterpolation, Precision.Full, NetworkTransform.AuthorityModes.Owner)] - [TestFixture(Interpolation.NoInterpolation, Precision.Half, NetworkTransform.AuthorityModes.Server)] - [TestFixture(Interpolation.NoInterpolation, Precision.Half, NetworkTransform.AuthorityModes.Owner)] - [TestFixture(Interpolation.NoInterpolation, Precision.Compressed, NetworkTransform.AuthorityModes.Server)] - [TestFixture(Interpolation.NoInterpolation, Precision.Compressed, NetworkTransform.AuthorityModes.Owner)] + [TestFixture(Interpolation.Interpolation, Precision.Full, NetworkTransform.AuthorityModes.Server, NestedTickSynchronization.TickSynchronized)] + [TestFixture(Interpolation.Interpolation, Precision.Full, NetworkTransform.AuthorityModes.Owner, NestedTickSynchronization.TickSynchronized)] + [TestFixture(Interpolation.Interpolation, Precision.Half, NetworkTransform.AuthorityModes.Server, NestedTickSynchronization.TickSynchronized)] + [TestFixture(Interpolation.Interpolation, Precision.Half, NetworkTransform.AuthorityModes.Owner, NestedTickSynchronization.TickSynchronized)] + [TestFixture(Interpolation.Interpolation, Precision.Compressed, NetworkTransform.AuthorityModes.Server, NestedTickSynchronization.TickSynchronized)] + [TestFixture(Interpolation.Interpolation, Precision.Compressed, NetworkTransform.AuthorityModes.Owner, NestedTickSynchronization.TickSynchronized)] + [TestFixture(Interpolation.NoInterpolation, Precision.Full, NetworkTransform.AuthorityModes.Server, NestedTickSynchronization.TickSynchronized)] + [TestFixture(Interpolation.NoInterpolation, Precision.Full, NetworkTransform.AuthorityModes.Owner, NestedTickSynchronization.TickSynchronized)] + [TestFixture(Interpolation.NoInterpolation, Precision.Half, NetworkTransform.AuthorityModes.Server, NestedTickSynchronization.TickSynchronized)] + [TestFixture(Interpolation.NoInterpolation, Precision.Half, NetworkTransform.AuthorityModes.Owner, NestedTickSynchronization.TickSynchronized)] + [TestFixture(Interpolation.NoInterpolation, Precision.Compressed, NetworkTransform.AuthorityModes.Server, NestedTickSynchronization.TickSynchronized)] + [TestFixture(Interpolation.NoInterpolation, Precision.Compressed, NetworkTransform.AuthorityModes.Owner, NestedTickSynchronization.TickSynchronized)] + + [TestFixture(Interpolation.Interpolation, Precision.Full, NetworkTransform.AuthorityModes.Server, NestedTickSynchronization.NormalSynchronize)] + [TestFixture(Interpolation.Interpolation, Precision.Full, NetworkTransform.AuthorityModes.Owner, NestedTickSynchronization.NormalSynchronize)] + [TestFixture(Interpolation.Interpolation, Precision.Half, NetworkTransform.AuthorityModes.Server, NestedTickSynchronization.NormalSynchronize)] + [TestFixture(Interpolation.Interpolation, Precision.Half, NetworkTransform.AuthorityModes.Owner, NestedTickSynchronization.NormalSynchronize)] + [TestFixture(Interpolation.Interpolation, Precision.Compressed, NetworkTransform.AuthorityModes.Server, NestedTickSynchronization.NormalSynchronize)] + [TestFixture(Interpolation.Interpolation, Precision.Compressed, NetworkTransform.AuthorityModes.Owner, NestedTickSynchronization.NormalSynchronize)] + [TestFixture(Interpolation.NoInterpolation, Precision.Full, NetworkTransform.AuthorityModes.Server, NestedTickSynchronization.NormalSynchronize)] + [TestFixture(Interpolation.NoInterpolation, Precision.Full, NetworkTransform.AuthorityModes.Owner, NestedTickSynchronization.NormalSynchronize)] + [TestFixture(Interpolation.NoInterpolation, Precision.Half, NetworkTransform.AuthorityModes.Server, NestedTickSynchronization.NormalSynchronize)] + [TestFixture(Interpolation.NoInterpolation, Precision.Half, NetworkTransform.AuthorityModes.Owner, NestedTickSynchronization.NormalSynchronize)] + [TestFixture(Interpolation.NoInterpolation, Precision.Compressed, NetworkTransform.AuthorityModes.Server, NestedTickSynchronization.NormalSynchronize)] + [TestFixture(Interpolation.NoInterpolation, Precision.Compressed, NetworkTransform.AuthorityModes.Owner, NestedTickSynchronization.NormalSynchronize)] public class NestedNetworkTransformTests : IntegrationTestWithApproximation { private const string k_TestScene = "NestedNetworkTransformTestScene"; @@ -41,6 +54,7 @@ public class NestedNetworkTransformTests : IntegrationTestWithApproximation private Interpolation m_Interpolation; private Precision m_Precision; private NetworkTransform.AuthorityModes m_Authority; + private NestedTickSynchronization m_NestedTickSynchronization; public enum Interpolation { @@ -61,12 +75,19 @@ public enum AuthoritativeModel Owner } + public enum NestedTickSynchronization + { + NormalSynchronize, + TickSynchronized, + } + - public NestedNetworkTransformTests(Interpolation interpolation, Precision precision, NetworkTransform.AuthorityModes authoritativeModel) + public NestedNetworkTransformTests(Interpolation interpolation, Precision precision, NetworkTransform.AuthorityModes authoritativeModel, NestedTickSynchronization nestedTickSynchronization) { m_Interpolation = interpolation; m_Precision = precision; m_Authority = authoritativeModel; + m_NestedTickSynchronization = nestedTickSynchronization; } public NestedNetworkTransformTests() @@ -136,6 +157,7 @@ private void ConfigureNetworkTransform(IntegrationNetworkTransform networkTransf networkTransform.UseHalfFloatPrecision = m_Precision == Precision.Half || m_Precision == Precision.Compressed; networkTransform.UseQuaternionCompression = m_Precision == Precision.Compressed; networkTransform.AuthorityMode = m_Authority; + networkTransform.TickSyncChildren = m_NestedTickSynchronization == NestedTickSynchronization.TickSynchronized; } @@ -152,6 +174,12 @@ protected override void OnCreatePlayerPrefab() foreach (var networkTransform in networkTransforms) { ConfigureNetworkTransform(networkTransform); + if (networkTransform.TickSyncChildren && networkTransform.gameObject != m_PlayerPrefab) + { + // Skew the thresholds of the children + networkTransform.PositionThreshold *= Random.Range(0.75f, 0.90f); + networkTransform.RotAngleThreshold *= Random.Range(0.75f, 0.90f); + } } base.OnCreatePlayerPrefab();