diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bf4b11..7213ea9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,41 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) Additional documentation and release notes are available at [Multiplayer Documentation](https://docs-multiplayer.unity3d.com). +## [2.0.0] - 2024-09-12 + +### Added + +- Added tooltips for all of the `NetworkObject` component's properties. (#3052) +- Added message size validation to named and unnamed message sending functions for better error messages. (#3049) +- 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 `NetworkAnimator` would send updates to non-observer clients. (#3057) +- Fixed issue where an exception could occur when receiving a universal RPC for a `NetworkObject` that has been despawned. (#3052) +- Fixed issue where a NetworkObject hidden from a client that is then promoted to be session owner was not being synchronized with newly joining clients.(#3051) +- Fixed issue where clients could have a wrong time delta on `NetworkVariableBase` which could prevent from sending delta state updates. (#3045) +- Fixed issue where setting a prefab hash value during connection approval but not having a player prefab assigned could cause an exception when spawning a player. (#3042) +- 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` now uses `NetworkTransform` as the base type class to assure it doesn't display a foldout group when using the base `NetworkTransform` component class. (#3052) +- Changed `NetworkAnimator.Awake` is now a protected virtual method. (#3052) +- Changed when invoking `NetworkManager.ConnectionManager.DisconnectClient` during a distributed authority session a more appropriate message is logged. (#3052) +- 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/Editor/Configuration/NetcodeForGameObjectsSettings.cs b/Editor/Configuration/NetcodeForGameObjectsSettings.cs index 4fc4b0c..70dfc02 100644 --- a/Editor/Configuration/NetcodeForGameObjectsSettings.cs +++ b/Editor/Configuration/NetcodeForGameObjectsSettings.cs @@ -5,6 +5,7 @@ namespace Unity.Netcode.Editor.Configuration internal class NetcodeForGameObjectsEditorSettings { internal const string AutoAddNetworkObjectIfNoneExists = "AutoAdd-NetworkObject-When-None-Exist"; + internal const string CheckForNetworkObject = "NetworkBehaviour-Check-For-NetworkObject"; internal const string InstallMultiplayerToolsTipDismissedPlayerPrefKey = "Netcode_Tip_InstallMPTools_Dismissed"; internal static int GetNetcodeInstallMultiplayerToolTips() @@ -28,7 +29,7 @@ internal static bool GetAutoAddNetworkObjectSetting() { return EditorPrefs.GetBool(AutoAddNetworkObjectIfNoneExists); } - + // Default for this is false return false; } @@ -36,5 +37,20 @@ internal static void SetAutoAddNetworkObjectSetting(bool autoAddSetting) { EditorPrefs.SetBool(AutoAddNetworkObjectIfNoneExists, autoAddSetting); } + + internal static bool GetCheckForNetworkObjectSetting() + { + if (EditorPrefs.HasKey(CheckForNetworkObject)) + { + return EditorPrefs.GetBool(CheckForNetworkObject); + } + // Default for this is true + return true; + } + + internal static void SetCheckForNetworkObjectSetting(bool checkForNetworkObject) + { + EditorPrefs.SetBool(CheckForNetworkObject, checkForNetworkObject); + } } } diff --git a/Editor/Configuration/NetcodeSettingsProvider.cs b/Editor/Configuration/NetcodeSettingsProvider.cs index 8a814ac..26b1217 100644 --- a/Editor/Configuration/NetcodeSettingsProvider.cs +++ b/Editor/Configuration/NetcodeSettingsProvider.cs @@ -81,6 +81,7 @@ private static void OnDeactivate() internal static NetcodeSettingsLabel NetworkObjectsSectionLabel; internal static NetcodeSettingsToggle AutoAddNetworkObjectToggle; + internal static NetcodeSettingsToggle CheckForNetworkObjectToggle; internal static NetcodeSettingsLabel MultiplayerToolsLabel; internal static NetcodeSettingsToggle MultiplayerToolTipStatusToggle; @@ -103,6 +104,11 @@ private static void CheckForInitialize() AutoAddNetworkObjectToggle = new NetcodeSettingsToggle("Auto-Add NetworkObject Component", "When enabled, NetworkObject components are automatically added to GameObjects when NetworkBehaviour components are added first.", 20); } + if (CheckForNetworkObjectToggle == null) + { + CheckForNetworkObjectToggle = new NetcodeSettingsToggle("Check for NetworkObject Component", "When disabled, the automatic check on NetworkBehaviours for an associated NetworkObject component will not be performed and Auto-Add NetworkObject Component will be disabled.", 20); + } + if (MultiplayerToolsLabel == null) { MultiplayerToolsLabel = new NetcodeSettingsLabel("Multiplayer Tools", 20); @@ -120,7 +126,9 @@ private static void OnGuiHandler(string obj) CheckForInitialize(); var autoAddNetworkObjectSetting = NetcodeForGameObjectsEditorSettings.GetAutoAddNetworkObjectSetting(); + var checkForNetworkObjectSetting = NetcodeForGameObjectsEditorSettings.GetCheckForNetworkObjectSetting(); var multiplayerToolsTipStatus = NetcodeForGameObjectsEditorSettings.GetNetcodeInstallMultiplayerToolTips() == 0; + var settings = NetcodeForGameObjectsProjectSettings.instance; var generateDefaultPrefabs = settings.GenerateDefaultNetworkPrefabs; var networkPrefabsPath = settings.TempNetworkPrefabsPath; @@ -134,7 +142,13 @@ private static void OnGuiHandler(string obj) { GUILayout.BeginVertical("Box"); NetworkObjectsSectionLabel.DrawLabel(); - autoAddNetworkObjectSetting = AutoAddNetworkObjectToggle.DrawToggle(autoAddNetworkObjectSetting); + + autoAddNetworkObjectSetting = AutoAddNetworkObjectToggle.DrawToggle(autoAddNetworkObjectSetting, checkForNetworkObjectSetting); + checkForNetworkObjectSetting = CheckForNetworkObjectToggle.DrawToggle(checkForNetworkObjectSetting); + if (autoAddNetworkObjectSetting && !checkForNetworkObjectSetting) + { + autoAddNetworkObjectSetting = false; + } GUILayout.EndVertical(); GUILayout.BeginVertical("Box"); @@ -184,6 +198,7 @@ private static void OnGuiHandler(string obj) if (EditorGUI.EndChangeCheck()) { NetcodeForGameObjectsEditorSettings.SetAutoAddNetworkObjectSetting(autoAddNetworkObjectSetting); + NetcodeForGameObjectsEditorSettings.SetCheckForNetworkObjectSetting(checkForNetworkObjectSetting); NetcodeForGameObjectsEditorSettings.SetNetcodeInstallMultiplayerToolTips(multiplayerToolsTipStatus ? 0 : 1); settings.GenerateDefaultNetworkPrefabs = generateDefaultPrefabs; settings.TempNetworkPrefabsPath = networkPrefabsPath; @@ -213,10 +228,13 @@ internal class NetcodeSettingsToggle : NetcodeGUISettings { private GUIContent m_ToggleContent; - public bool DrawToggle(bool currentSetting) + public bool DrawToggle(bool currentSetting, bool enabled = true) { EditorGUIUtility.labelWidth = m_LabelSize; - return EditorGUILayout.Toggle(m_ToggleContent, currentSetting, m_LayoutWidth); + GUI.enabled = enabled; + var returnValue = EditorGUILayout.Toggle(m_ToggleContent, currentSetting, m_LayoutWidth); + GUI.enabled = true; + return returnValue; } public NetcodeSettingsToggle(string labelText, string toolTip, float layoutOffset) diff --git a/Editor/NetcodeEditorBase.cs b/Editor/NetcodeEditorBase.cs new file mode 100644 index 0000000..5112065 --- /dev/null +++ b/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/Editor/NetcodeEditorBase.cs.meta b/Editor/NetcodeEditorBase.cs.meta new file mode 100644 index 0000000..25f0b07 --- /dev/null +++ b/Editor/NetcodeEditorBase.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4ce97256a2d80f94bb340e13c71a24b8 \ No newline at end of file diff --git a/Editor/NetworkBehaviourEditor.cs b/Editor/NetworkBehaviourEditor.cs index 7d57afb..1312ff6 100644 --- a/Editor/NetworkBehaviourEditor.cs +++ b/Editor/NetworkBehaviourEditor.cs @@ -301,9 +301,8 @@ public override void OnInspectorGUI() expanded = false; } - - serializedObject.ApplyModifiedProperties(); EditorGUI.EndChangeCheck(); + serializedObject.ApplyModifiedProperties(); } /// @@ -352,6 +351,12 @@ public static void CheckForNetworkObject(GameObject gameObject, bool networkObje return; } + // If this automatic check is disabled, then do not perform this check. + if (!NetcodeForGameObjectsEditorSettings.GetCheckForNetworkObjectSetting()) + { + return; + } + // Now get the root parent transform to the current GameObject (or itself) var rootTransform = GetRootParentTransform(gameObject.transform); if (!rootTransform.TryGetComponent(out var networkManager)) diff --git a/Editor/NetworkManagerEditor.cs b/Editor/NetworkManagerEditor.cs index 0db3a65..5445b0d 100644 --- a/Editor/NetworkManagerEditor.cs +++ b/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/Editor/NetworkManagerHelper.cs b/Editor/NetworkManagerHelper.cs index 19643d4..3138369 100644 --- a/Editor/NetworkManagerHelper.cs +++ b/Editor/NetworkManagerHelper.cs @@ -61,6 +61,12 @@ private static void EditorApplication_playModeStateChanged(PlayModeStateChange p { s_LastKnownNetworkManagerParents.Clear(); ScenesInBuildActiveSceneCheck(); + EditorApplication.hierarchyChanged -= EditorApplication_hierarchyChanged; + break; + } + case PlayModeStateChange.EnteredEditMode: + { + EditorApplication.hierarchyChanged += EditorApplication_hierarchyChanged; break; } } @@ -110,6 +116,12 @@ private static void ScenesInBuildActiveSceneCheck() /// private static void EditorApplication_hierarchyChanged() { + if (Application.isPlaying) + { + EditorApplication.hierarchyChanged -= EditorApplication_hierarchyChanged; + return; + } + var allNetworkManagers = Resources.FindObjectsOfTypeAll(); foreach (var networkManager in allNetworkManagers) { diff --git a/Editor/NetworkRigidbodyBaseEditor.cs b/Editor/NetworkRigidbodyBaseEditor.cs new file mode 100644 index 0000000..8ab9943 --- /dev/null +++ b/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/Editor/NetworkRigidbodyBaseEditor.cs.meta b/Editor/NetworkRigidbodyBaseEditor.cs.meta new file mode 100644 index 0000000..c7fef8a --- /dev/null +++ b/Editor/NetworkRigidbodyBaseEditor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 06561c57f81a6354f8bb16076f1de3a9 \ No newline at end of file diff --git a/Editor/NetworkTransformEditor.cs b/Editor/NetworkTransformEditor.cs index 4affff1..e93226e 100644 --- a/Editor/NetworkTransformEditor.cs +++ b/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/Runtime/Components/AnticipatedNetworkTransform.cs b/Runtime/Components/AnticipatedNetworkTransform.cs index 10c1d18..21d3c05 100644 --- a/Runtime/Components/AnticipatedNetworkTransform.cs +++ b/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/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs b/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs index ef5ec09..e628c7c 100644 --- a/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs +++ b/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/Runtime/Components/NetworkAnimator.cs b/Runtime/Components/NetworkAnimator.cs index e01f4e7..16b77ca 100644 --- a/Runtime/Components/NetworkAnimator.cs +++ b/Runtime/Components/NetworkAnimator.cs @@ -584,7 +584,7 @@ public override void OnDestroy() base.OnDestroy(); } - private void Awake() + protected virtual void Awake() { int layers = m_Animator.layerCount; // Initializing the below arrays for everyone handles an issue @@ -952,8 +952,14 @@ internal void CheckForAnimatorChanges() { // Just notify all remote clients and not the local server m_ClientSendList.Clear(); - m_ClientSendList.AddRange(NetworkManager.ConnectedClientsIds); - m_ClientSendList.Remove(NetworkManager.LocalClientId); + foreach (var clientId in NetworkManager.ConnectedClientsIds) + { + if (clientId == NetworkManager.LocalClientId || !NetworkObject.Observers.Contains(clientId)) + { + continue; + } + m_ClientSendList.Add(clientId); + } m_ClientRpcParams.Send.TargetClientIds = m_ClientSendList; SendAnimStateClientRpc(m_AnimationMessage, m_ClientRpcParams); } @@ -1264,9 +1270,15 @@ private unsafe void SendParametersUpdateServerRpc(ParametersUpdateMessage parame if (NetworkManager.ConnectedClientsIds.Count > (IsHost ? 2 : 1)) { m_ClientSendList.Clear(); - m_ClientSendList.AddRange(NetworkManager.ConnectedClientsIds); - m_ClientSendList.Remove(serverRpcParams.Receive.SenderClientId); - m_ClientSendList.Remove(NetworkManager.ServerClientId); + foreach (var clientId in NetworkManager.ConnectedClientsIds) + { + if (clientId == serverRpcParams.Receive.SenderClientId || clientId == NetworkManager.ServerClientId || !NetworkObject.Observers.Contains(clientId)) + { + continue; + } + m_ClientSendList.Add(clientId); + } + m_ClientRpcParams.Send.TargetClientIds = m_ClientSendList; m_NetworkAnimatorStateChangeHandler.SendParameterUpdate(parametersUpdate, m_ClientRpcParams); } @@ -1321,9 +1333,14 @@ private void SendAnimStateServerRpc(AnimationMessage animationMessage, ServerRpc if (NetworkManager.ConnectedClientsIds.Count > (IsHost ? 2 : 1)) { m_ClientSendList.Clear(); - m_ClientSendList.AddRange(NetworkManager.ConnectedClientsIds); - m_ClientSendList.Remove(serverRpcParams.Receive.SenderClientId); - m_ClientSendList.Remove(NetworkManager.ServerClientId); + foreach (var clientId in NetworkManager.ConnectedClientsIds) + { + if (clientId == serverRpcParams.Receive.SenderClientId || clientId == NetworkManager.ServerClientId || !NetworkObject.Observers.Contains(clientId)) + { + continue; + } + m_ClientSendList.Add(clientId); + } m_ClientRpcParams.Send.TargetClientIds = m_ClientSendList; m_NetworkAnimatorStateChangeHandler.SendAnimationUpdate(animationMessage, m_ClientRpcParams); } @@ -1390,9 +1407,14 @@ internal void SendAnimTriggerServerRpc(AnimationTriggerMessage animationTriggerM InternalSetTrigger(animationTriggerMessage.Hash, animationTriggerMessage.IsTriggerSet); m_ClientSendList.Clear(); - m_ClientSendList.AddRange(NetworkManager.ConnectedClientsIds); - m_ClientSendList.Remove(NetworkManager.ServerClientId); - + foreach (var clientId in NetworkManager.ConnectedClientsIds) + { + if (clientId == NetworkManager.ServerClientId || !NetworkObject.Observers.Contains(clientId)) + { + continue; + } + m_ClientSendList.Add(clientId); + } if (IsServerAuthoritative()) { m_NetworkAnimatorStateChangeHandler.QueueTriggerUpdateToClient(animationTriggerMessage, m_ClientRpcParams); diff --git a/Runtime/Components/NetworkRigidBodyBase.cs b/Runtime/Components/NetworkRigidBodyBase.cs index a85f7c0..7e88081 100644 --- a/Runtime/Components/NetworkRigidBodyBase.cs +++ b/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/Runtime/Components/NetworkRigidbody.cs b/Runtime/Components/NetworkRigidbody.cs index 7c93a5d..a157d26 100644 --- a/Runtime/Components/NetworkRigidbody.cs +++ b/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/Runtime/Components/NetworkRigidbody2D.cs b/Runtime/Components/NetworkRigidbody2D.cs index a178660..f7c9e14 100644 --- a/Runtime/Components/NetworkRigidbody2D.cs +++ b/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/Runtime/Components/NetworkTransform.cs b/Runtime/Components/NetworkTransform.cs index 0cf2b41..b9d8eab 100644 --- a/Runtime/Components/NetworkTransform.cs +++ b/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) - { -#if COM_UNITY_MODULES_PHYSICS - var position = m_UseRigidbodyForMotion ? m_NetworkRigidbodyInternal.GetPosition() : GetSpaceRelativePosition(); - var rotation = m_UseRigidbodyForMotion ? m_NetworkRigidbodyInternal.GetRotation() : GetSpaceRelativeRotation(); + 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 - var position = GetSpaceRelativePosition(); - var rotation = GetSpaceRelativeRotation(); + if (SwitchTransformSpaceWhenParented) #endif - m_TargetPosition = m_CurrentPosition = position; - m_CurrentRotation = rotation; - m_TargetRotation = m_CurrentRotation.eulerAngles; - m_TargetScale = m_CurrentScale = GetScale(); - - if (Interpolate) + { + m_PreviousNetworkObjectParent = m_CurrentNetworkObjectParent; + m_CurrentNetworkObjectParent = parentNetworkObject; + if (m_IsFirstNetworkTransform) { - m_ScaleInterpolator.Clear(); - m_PositionInterpolator.Clear(); - m_RotationInterpolator.Clear(); + 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(); +#endif + m_TargetPosition = m_InternalCurrentPosition = position; + m_InternalCurrentRotation = rotation; + m_TargetRotation = m_InternalCurrentRotation.eulerAngles; + m_TargetScale = m_InternalCurrentScale = GetScale(); - // 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); + 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_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); @@ -3324,8 +3604,12 @@ private void SetStateServerRpc(Vector3 pos, Quaternion rot, Vector3 scale, bool } /// - /// Teleport the transform to the given values without interpolating + /// Teleport an already spawned object to the given values without interpolating. /// + /// + /// This is intended to be used on already spawned objects, for setting the position of a dynamically spawned object just apply the transform values prior to spawning.
+ /// With player objects, override the method and have the authority make adjustments to the transform prior to invoking base.OnNetworkSpawn. + ///
/// new position to move to. /// new rotation to rotate to. /// new scale to scale to. @@ -3349,10 +3633,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 +3646,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 +3683,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 +3699,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/Runtime/Connection/NetworkConnectionManager.cs b/Runtime/Connection/NetworkConnectionManager.cs index 722fbb2..b5fff54 100644 --- a/Runtime/Connection/NetworkConnectionManager.cs +++ b/Runtime/Connection/NetworkConnectionManager.cs @@ -735,42 +735,23 @@ internal void HandleConnectionApproval(ulong ownerClientId, NetworkManager.Conne RemovePendingClient(ownerClientId); var client = AddClient(ownerClientId); - if (!NetworkManager.DistributedAuthorityMode && response.CreatePlayerObject && NetworkManager.NetworkConfig.PlayerPrefab != null) - { - var prefabNetworkObject = NetworkManager.NetworkConfig.PlayerPrefab.GetComponent(); - var playerPrefabHash = response.PlayerPrefabHash ?? prefabNetworkObject.GlobalObjectIdHash; - // Generate a SceneObject for the player object to spawn - // Note: This is only to create the local NetworkObject, many of the serialized properties of the player prefab will be set when instantiated. - var sceneObject = new NetworkObject.SceneObject - { - OwnerClientId = ownerClientId, - IsPlayerObject = true, - IsSceneObject = false, - HasTransform = prefabNetworkObject.SynchronizeTransform, - Hash = playerPrefabHash, - TargetClientId = ownerClientId, - DontDestroyWithOwner = prefabNetworkObject.DontDestroyWithOwner, - Transform = new NetworkObject.SceneObject.TransformData - { - Position = response.Position.GetValueOrDefault(), - Rotation = response.Rotation.GetValueOrDefault() - } - }; - - // Create the player NetworkObject locally - var networkObject = NetworkManager.SpawnManager.CreateLocalNetworkObject(sceneObject); + // Server-side spawning (only if there is a prefab hash or player prefab provided) + if (!NetworkManager.DistributedAuthorityMode && response.CreatePlayerObject && (response.PlayerPrefabHash.HasValue || NetworkManager.NetworkConfig.PlayerPrefab != null)) + { + var playerObject = response.PlayerPrefabHash.HasValue ? NetworkManager.SpawnManager.GetNetworkObjectToSpawn(response.PlayerPrefabHash.Value, ownerClientId, response.Position.GetValueOrDefault(), response.Rotation.GetValueOrDefault()) + : NetworkManager.SpawnManager.GetNetworkObjectToSpawn(NetworkManager.NetworkConfig.PlayerPrefab.GetComponent().GlobalObjectIdHash, ownerClientId, response.Position.GetValueOrDefault(), response.Rotation.GetValueOrDefault()); // Spawn the player NetworkObject locally NetworkManager.SpawnManager.SpawnNetworkObjectLocally( - networkObject, + playerObject, NetworkManager.SpawnManager.GetNetworkObjectId(), sceneObject: false, playerObject: true, ownerClientId, destroyWithScene: false); - client.AssignPlayerObject(ref networkObject); + client.AssignPlayerObject(ref playerObject); } // Server doesn't send itself the connection approved message @@ -871,6 +852,7 @@ internal void HandleConnectionApproval(ulong ownerClientId, NetworkManager.Conne } } + // Exit early if no player object was spawned if (!response.CreatePlayerObject || (response.PlayerPrefabHash == null && NetworkManager.NetworkConfig.PlayerPrefab == null)) { return; @@ -1003,10 +985,18 @@ internal NetworkClient AddClient(ulong clientId) ConnectedClientIds.Add(clientId); } + var distributedAuthority = NetworkManager.DistributedAuthorityMode; + var sessionOwnerId = NetworkManager.CurrentSessionOwner; + var isSessionOwner = NetworkManager.LocalClient.IsSessionOwner; foreach (var networkObject in NetworkManager.SpawnManager.SpawnedObjectsList) { if (networkObject.SpawnWithObservers) { + // Don't add the client to the observers if hidden from the session owner + if (networkObject.IsOwner && distributedAuthority && !isSessionOwner && !networkObject.Observers.Contains(sessionOwnerId)) + { + continue; + } networkObject.Observers.Add(clientId); } } @@ -1309,7 +1299,15 @@ internal void DisconnectClient(ulong clientId, string reason = null) { if (!LocalClient.IsServer) { - throw new NotServerException($"Only server can disconnect remote clients. Please use `{nameof(Shutdown)}()` instead."); + if (NetworkManager.NetworkConfig.NetworkTopology == NetworkTopologyTypes.ClientServer) + { + throw new NotServerException($"Only server can disconnect remote clients. Please use `{nameof(Shutdown)}()` instead."); + } + else + { + Debug.LogWarning($"Currently, clients cannot disconnect other clients from a distributed authority session. Please use `{nameof(Shutdown)}()` instead."); + return; + } } if (clientId == NetworkManager.ServerClientId) diff --git a/Runtime/Core/NetworkBehaviour.cs b/Runtime/Core/NetworkBehaviour.cs index 5e088c7..b5d5495 100644 --- a/Runtime/Core/NetworkBehaviour.cs +++ b/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/Runtime/Core/NetworkManager.cs b/Runtime/Core/NetworkManager.cs index b5a042c..0cdb995 100644 --- a/Runtime/Core/NetworkManager.cs +++ b/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. @@ -189,7 +195,6 @@ internal void SetSessionOwner(ulong sessionOwner) OnSessionOwnerPromoted?.Invoke(sessionOwner); } - // TODO: Make this internal after testing internal void PromoteSessionOwner(ulong clientId) { if (!DistributedAuthorityMode) @@ -890,6 +895,11 @@ private void Reset() OnNetworkManagerReset?.Invoke(this); } + protected virtual void OnValidateComponent() + { + + } + internal void OnValidate() { if (NetworkConfig == null) @@ -950,6 +960,15 @@ internal void OnValidate() } } } + + try + { + OnValidateComponent(); + } + catch (Exception ex) + { + Debug.LogException(ex); + } } private void ModeChanged(PlayModeStateChange change) diff --git a/Runtime/Core/NetworkObject.cs b/Runtime/Core/NetworkObject.cs index 884ea74..f89bc1d 100644 --- a/Runtime/Core/NetworkObject.cs +++ b/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. @@ -937,6 +938,7 @@ private bool InternalHasAuthority() /// /// If true, the object will always be replicated as root on clients and the parent will be ignored. /// + [Tooltip("If enabled (default disabled), instances of this NetworkObject will ignore any parent(s) it might have and replicate on clients as the root being its parent.")] public bool AlwaysReplicateAsRoot; /// @@ -954,6 +956,8 @@ private bool InternalHasAuthority() /// bandwidth cost. This can also be useful for UI elements that have /// a predetermined fixed position. /// + [Tooltip("If enabled (default enabled), newly joining clients will be synchronized with the transform of the associated GameObject this component is attached to. Typical use case" + + " scenario would be for managment objects or in-scene placed objects that don't move and already have their transform settings applied within the scene information.")] public bool SynchronizeTransform = true; /// @@ -1011,6 +1015,7 @@ public void SetSceneObjectStatus(bool isSceneObject = false) /// To synchronize clients of a 's scene being changed via , /// make sure is enabled (it is by default). /// + [Tooltip("When enabled (default disabled), spawned instances of this NetworkObject will automatically migrate to any newly assigned active scene.")] public bool ActiveSceneSynchronization; /// @@ -1029,6 +1034,7 @@ public void SetSceneObjectStatus(bool isSceneObject = false) /// is and is and the scene is not the currently /// active scene, then the will be destroyed. /// + [Tooltip("When enabled (default enabled), dynamically spawned instances of this NetworkObject's migration to a different scene will automatically be synchonize amongst clients.")] public bool SceneMigrationSynchronization = true; /// @@ -1044,7 +1050,7 @@ public void SetSceneObjectStatus(bool isSceneObject = false) /// /// When set to false, the NetworkObject will be spawned with no observers initially (other than the server) /// - [Tooltip("When false, the NetworkObject will spawn with no observers initially. (default is true)")] + [Tooltip("When disabled (default enabled), the NetworkObject will spawn with no observers. You control object visibility using NetworkShow. This applies to newly joining clients as well.")] public bool SpawnWithObservers = true; /// @@ -1073,13 +1079,35 @@ public void SetSceneObjectStatus(bool isSceneObject = false) /// Whether or not to destroy this object if it's owner is destroyed. /// If true, the objects ownership will be given to the server. /// + [Tooltip("When enabled (default disabled), instances of this NetworkObject will not be destroyed if the owning client disconnects.")] public bool DontDestroyWithOwner; /// /// Whether or not to enable automatic NetworkObject parent synchronization. /// + [Tooltip("When disabled (default enabled), NetworkObject parenting will not be automatically synchronized. This is typically used when you want to implement your own custom parenting solution.")] 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. + ///
+ [Tooltip("When disabled (default enabled), the owner will not apply a server or host's transform properties when parenting changes. Primarily useful for client-server network topology configurations.")] + 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. + /// + [Tooltip("When enabled (default disabled), owner's can parent a NetworkObject locally without having to send an RPC to the server or host. Only pertinent when using client-server network topology configurations.")] + public bool AllowOwnerToParent; + internal readonly HashSet Observers = new HashSet(); #if MULTIPLAYER_TOOLS @@ -1787,6 +1815,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 +1949,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 +2015,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 +2107,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 +2396,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/Runtime/Messaging/CustomMessageManager.cs b/Runtime/Messaging/CustomMessageManager.cs index dfea03e..9a83c6d 100644 --- a/Runtime/Messaging/CustomMessageManager.cs +++ b/Runtime/Messaging/CustomMessageManager.cs @@ -95,6 +95,7 @@ public void SendUnnamedMessage(IReadOnlyList clientIds, FastBufferWriter return; } + ValidateMessageSize(messageBuffer, networkDelivery, isNamed: false); if (m_NetworkManager.IsHost) { @@ -131,6 +132,8 @@ public void SendUnnamedMessage(IReadOnlyList clientIds, FastBufferWriter /// The delivery type (QoS) to send data with public void SendUnnamedMessage(ulong clientId, FastBufferWriter messageBuffer, NetworkDelivery networkDelivery = NetworkDelivery.ReliableSequenced) { + ValidateMessageSize(messageBuffer, networkDelivery, isNamed: false); + if (m_NetworkManager.IsHost) { if (clientId == m_NetworkManager.LocalClientId) @@ -286,6 +289,8 @@ public void SendNamedMessageToAll(string messageName, FastBufferWriter messageSt /// The delivery type (QoS) to send data with public void SendNamedMessage(string messageName, ulong clientId, FastBufferWriter messageStream, NetworkDelivery networkDelivery = NetworkDelivery.ReliableSequenced) { + ValidateMessageSize(messageStream, networkDelivery, isNamed: true); + ulong hash = 0; switch (m_NetworkManager.NetworkConfig.RpcHashSize) { @@ -367,6 +372,8 @@ public void SendNamedMessage(string messageName, IReadOnlyList clientIds, return; } + ValidateMessageSize(messageStream, networkDelivery, isNamed: true); + ulong hash = 0; switch (m_NetworkManager.NetworkConfig.RpcHashSize) { @@ -405,5 +412,32 @@ public void SendNamedMessage(string messageName, IReadOnlyList clientIds, m_NetworkManager.NetworkMetrics.TrackNamedMessageSent(clientIds, messageName, size); } } + + /// + /// Validate the size of the message. If it's a non-fragmented delivery type the message must fit within the + /// max allowed size with headers also subtracted. Named messages also include the hash + /// of the name string. Only validates in editor and development builds. + /// + /// The named message payload + /// Delivery method + /// Is the message named (or unnamed) + /// Exception thrown in case validation fails + private unsafe void ValidateMessageSize(FastBufferWriter messageStream, NetworkDelivery networkDelivery, bool isNamed) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + var maxNonFragmentedSize = m_NetworkManager.MessageManager.NonFragmentedMessageMaxSize - FastBufferWriter.GetWriteSize() - sizeof(NetworkBatchHeader); + if (isNamed) + { + maxNonFragmentedSize -= sizeof(ulong); // MessageName hash + } + if (networkDelivery != NetworkDelivery.ReliableFragmentedSequenced + && messageStream.Length > maxNonFragmentedSize) + { + throw new OverflowException($"Given message size ({messageStream.Length} bytes) is greater than " + + $"the maximum allowed for the selected delivery method ({maxNonFragmentedSize} bytes). Try using " + + $"ReliableFragmentedSequenced delivery method instead."); + } +#endif + } } } diff --git a/Runtime/Messaging/Messages/ClientConnectedMessage.cs b/Runtime/Messaging/Messages/ClientConnectedMessage.cs index 0a1b1ae..ef7ef5f 100644 --- a/Runtime/Messaging/Messages/ClientConnectedMessage.cs +++ b/Runtime/Messaging/Messages/ClientConnectedMessage.cs @@ -55,6 +55,15 @@ public void Handle(ref NetworkContext context) // Don't redistribute for the local instance if (ClientId != networkManager.LocalClientId) { + // Show any NetworkObjects that are: + // - Hidden from the session owner + // - Owned by this client + // - Has NetworkObject.SpawnWithObservers set to true (the default) + if (!networkManager.LocalClient.IsSessionOwner) + { + networkManager.SpawnManager.ShowHiddenObjectsToNewlyJoinedClient(ClientId); + } + // We defer redistribution to the end of the NetworkUpdateStage.PostLateUpdate networkManager.RedistributeToClient = true; networkManager.ClientToRedistribute = ClientId; diff --git a/Runtime/Messaging/Messages/ParentSyncMessage.cs b/Runtime/Messaging/Messages/ParentSyncMessage.cs index a55767d..bef5fe8 100644 --- a/Runtime/Messaging/Messages/ParentSyncMessage.cs +++ b/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/Runtime/Messaging/Messages/ProxyMessage.cs b/Runtime/Messaging/Messages/ProxyMessage.cs index c7322c4..57c8345 100644 --- a/Runtime/Messaging/Messages/ProxyMessage.cs +++ b/Runtime/Messaging/Messages/ProxyMessage.cs @@ -1,4 +1,3 @@ -using System; using Unity.Collections; namespace Unity.Netcode @@ -34,21 +33,13 @@ public unsafe void Handle(ref NetworkContext context) var networkManager = (NetworkManager)context.SystemOwner; if (!networkManager.SpawnManager.SpawnedObjects.TryGetValue(WrappedMessage.Metadata.NetworkObjectId, out var networkObject)) { - // With distributed authority mode, we can send Rpcs before we have been notified the NetworkObject is despawned. - // DANGO-TODO: Should the CMB Service cull out any Rpcs targeting recently despawned NetworkObjects? - // DANGO-TODO: This would require the service to keep track of despawned NetworkObjects since we re-use NetworkObject identifiers. - if (networkManager.DistributedAuthorityMode) + // If the NetworkObject no longer exists then just log a warning when developer mode logging is enabled and exit. + // This can happen if NetworkObject is despawned and a client sends an RPC before receiving the despawn message. + if (networkManager.LogLevel == LogLevel.Developer) { - if (networkManager.LogLevel == LogLevel.Developer) - { - NetworkLog.LogWarning($"[{WrappedMessage.Metadata.NetworkObjectId}, {WrappedMessage.Metadata.NetworkBehaviourId}, {WrappedMessage.Metadata.NetworkRpcMethodId}]An RPC called on a {nameof(NetworkObject)} that is not in the spawned objects list. Please make sure the {nameof(NetworkObject)} is spawned before calling RPCs."); - } - return; - } - else - { - throw new InvalidOperationException($"[{WrappedMessage.Metadata.NetworkObjectId}, {WrappedMessage.Metadata.NetworkBehaviourId}, {WrappedMessage.Metadata.NetworkRpcMethodId}]An RPC called on a {nameof(NetworkObject)} that is not in the spawned objects list. Please make sure the {nameof(NetworkObject)} is spawned before calling RPCs."); + NetworkLog.LogWarning($"[{WrappedMessage.Metadata.NetworkObjectId}, {WrappedMessage.Metadata.NetworkBehaviourId}, {WrappedMessage.Metadata.NetworkRpcMethodId}] An RPC called on a {nameof(NetworkObject)} that is not in the spawned objects list. Please make sure the {nameof(NetworkObject)} is spawned before calling RPCs."); } + return; } var observers = networkObject.Observers; diff --git a/Runtime/Messaging/Messages/RpcMessages.cs b/Runtime/Messaging/Messages/RpcMessages.cs index ba207e1..70e4c2a 100644 --- a/Runtime/Messaging/Messages/RpcMessages.cs +++ b/Runtime/Messaging/Messages/RpcMessages.cs @@ -60,7 +60,13 @@ public static void Handle(ref NetworkContext context, ref RpcMetadata metadata, var networkManager = (NetworkManager)context.SystemOwner; if (!networkManager.SpawnManager.SpawnedObjects.TryGetValue(metadata.NetworkObjectId, out var networkObject)) { - throw new InvalidOperationException($"An RPC called on a {nameof(NetworkObject)} that is not in the spawned objects list. Please make sure the {nameof(NetworkObject)} is spawned before calling RPCs."); + // If the NetworkObject no longer exists then just log a warning when developer mode logging is enabled and exit. + // This can happen if NetworkObject is despawned and a client sends an RPC before receiving the despawn message. + if (networkManager.LogLevel == LogLevel.Developer) + { + NetworkLog.LogWarning($"[{metadata.NetworkObjectId}, {metadata.NetworkBehaviourId}, {metadata.NetworkRpcMethodId}] An RPC called on a {nameof(NetworkObject)} that is not in the spawned objects list. Please make sure the {nameof(NetworkObject)} is spawned before calling RPCs."); + } + return; } var networkBehaviour = networkObject.GetNetworkBehaviourAtOrderIndex(metadata.NetworkBehaviourId); diff --git a/Runtime/Messaging/Messages/UnnamedMessage.cs b/Runtime/Messaging/Messages/UnnamedMessage.cs index ccce67c..c5c0b03 100644 --- a/Runtime/Messaging/Messages/UnnamedMessage.cs +++ b/Runtime/Messaging/Messages/UnnamedMessage.cs @@ -20,7 +20,7 @@ public bool Deserialize(FastBufferReader reader, ref NetworkContext context, int public void Handle(ref NetworkContext context) { - ((NetworkManager)context.SystemOwner).CustomMessagingManager.InvokeUnnamedMessage(context.SenderId, m_ReceivedData, context.SerializedHeaderSize); + ((NetworkManager)context.SystemOwner).CustomMessagingManager?.InvokeUnnamedMessage(context.SenderId, m_ReceivedData, context.SerializedHeaderSize); } } } diff --git a/Runtime/Messaging/NetworkMessageManager.cs b/Runtime/Messaging/NetworkMessageManager.cs index d42251e..1850725 100644 --- a/Runtime/Messaging/NetworkMessageManager.cs +++ b/Runtime/Messaging/NetworkMessageManager.cs @@ -733,7 +733,11 @@ internal unsafe int SendPreSerializedMessage(in FastBufferWriter t } ref var writeQueueItem = ref sendQueueItem.ElementAt(sendQueueItem.Length - 1); - writeQueueItem.Writer.TryBeginWrite(tmpSerializer.Length + headerSerializer.Length); + if (!writeQueueItem.Writer.TryBeginWrite(tmpSerializer.Length + headerSerializer.Length)) + { + Debug.LogError($"Not enough space to write message, size={tmpSerializer.Length + headerSerializer.Length} space used={writeQueueItem.Writer.Position} total size={writeQueueItem.Writer.Capacity}"); + continue; + } writeQueueItem.Writer.WriteBytes(headerSerializer.GetUnsafePtr(), headerSerializer.Length); writeQueueItem.Writer.WriteBytes(tmpSerializer.GetUnsafePtr(), tmpSerializer.Length); diff --git a/Runtime/NetworkVariable/NetworkVariableBase.cs b/Runtime/NetworkVariable/NetworkVariableBase.cs index a048cea..75ce48a 100644 --- a/Runtime/NetworkVariable/NetworkVariableBase.cs +++ b/Runtime/NetworkVariable/NetworkVariableBase.cs @@ -187,7 +187,9 @@ public virtual void SetDirty(bool isDirty) internal bool CanSend() { - var timeSinceLastUpdate = m_NetworkBehaviour.NetworkManager.NetworkTimeSystem.LocalTime - LastUpdateSent; + // When connected to a service or not the server, always use the synchronized server time as opposed to the local time + var time = m_InternalNetworkManager.CMBServiceConnection || !m_InternalNetworkManager.IsServer ? m_NetworkBehaviour.NetworkManager.ServerTime.Time : m_NetworkBehaviour.NetworkManager.NetworkTimeSystem.LocalTime; + var timeSinceLastUpdate = time - LastUpdateSent; return ( UpdateTraits.MaxSecondsBetweenUpdates > 0 && @@ -201,7 +203,8 @@ internal bool CanSend() internal void UpdateLastSentTime() { - LastUpdateSent = m_NetworkBehaviour.NetworkManager.NetworkTimeSystem.LocalTime; + // When connected to a service or not the server, always use the synchronized server time as opposed to the local time + LastUpdateSent = m_InternalNetworkManager.CMBServiceConnection || !m_InternalNetworkManager.IsServer ? m_NetworkBehaviour.NetworkManager.ServerTime.Time : m_NetworkBehaviour.NetworkManager.NetworkTimeSystem.LocalTime; } internal static bool IgnoreInitializeWarning; diff --git a/Runtime/SceneManagement/NetworkSceneManager.cs b/Runtime/SceneManagement/NetworkSceneManager.cs index 9756978..6522200 100644 --- a/Runtime/SceneManagement/NetworkSceneManager.cs +++ b/Runtime/SceneManagement/NetworkSceneManager.cs @@ -2550,17 +2550,6 @@ private void HandleSessionOwnerEvent(uint sceneEventId, ulong clientId) // At this point the client is considered fully "connected" if ((NetworkManager.DistributedAuthorityMode && NetworkManager.LocalClient.IsSessionOwner) || !NetworkManager.DistributedAuthorityMode) { - if (NetworkManager.DistributedAuthorityMode && !NetworkManager.DAHost) - { - // DANGO-EXP TODO: Remove this once service is sending the synchronization message to all clients - if (NetworkManager.ConnectedClients.ContainsKey(clientId) && NetworkManager.ConnectionManager.ConnectedClientIds.Contains(clientId) && NetworkManager.ConnectedClientsList.Contains(NetworkManager.ConnectedClients[clientId])) - { - EndSceneEvent(sceneEventId); - return; - } - NetworkManager.ConnectionManager.AddClient(clientId); - } - // Notify the local server that a client has finished synchronizing OnSceneEvent?.Invoke(new SceneEvent() { @@ -2575,6 +2564,20 @@ private void HandleSessionOwnerEvent(uint sceneEventId, ulong clientId) } else { + // Notify the local server that a client has finished synchronizing + OnSceneEvent?.Invoke(new SceneEvent() + { + SceneEventType = sceneEventData.SceneEventType, + SceneName = string.Empty, + ClientId = clientId + }); + + // Show any NetworkObjects that are: + // - Hidden from the session owner + // - Owned by this client + // - Has NetworkObject.SpawnWithObservers set to true (the default) + NetworkManager.SpawnManager.ShowHiddenObjectsToNewlyJoinedClient(clientId); + // DANGO-EXP TODO: Remove this once service distributes objects // Non-session owners receive this notification from newly connected clients and upon receiving // the event they will redistribute their NetworkObjects @@ -2589,9 +2592,6 @@ private void HandleSessionOwnerEvent(uint sceneEventId, ulong clientId) // At this time the client is fully synchronized with all loaded scenes and // NetworkObjects and should be considered "fully connected". Send the // notification that the client is connected. - // TODO 2023: We should have a better name for this or have multiple states the - // client progresses through (the name and associated legacy behavior/expected state - // of the client was persisted since MLAPI) NetworkManager.ConnectionManager.InvokeOnClientConnectedCallback(clientId); if (NetworkManager.IsHost) @@ -2664,9 +2664,14 @@ internal void HandleSceneEvent(ulong clientId, FastBufferReader reader) EventData = sceneEventData, }; // Forward synchronization to client then exit early because DAHost is not the current session owner - NetworkManager.MessageManager.SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, NetworkManager.CurrentSessionOwner); - EndSceneEvent(sceneEventData.SceneEventId); - return; + foreach (var client in NetworkManager.ConnectedClientsIds) + { + if (client == NetworkManager.LocalClientId) + { + continue; + } + NetworkManager.MessageManager.SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, client); + } } } else diff --git a/Runtime/Serialization/FastBufferWriter.cs b/Runtime/Serialization/FastBufferWriter.cs index b2a43a0..66d0da5 100644 --- a/Runtime/Serialization/FastBufferWriter.cs +++ b/Runtime/Serialization/FastBufferWriter.cs @@ -700,7 +700,7 @@ public unsafe void WriteBytes(byte* value, int size, int offset = 0) } if (Handle->Position + size > Handle->AllowedWriteMark) { - throw new OverflowException($"Attempted to write without first calling {nameof(TryBeginWrite)}()"); + throw new OverflowException($"Attempted to write without first calling {nameof(TryBeginWrite)}(), Position+Size={Handle->Position + size} > AllowedWriteMark={Handle->AllowedWriteMark}"); } #endif UnsafeUtility.MemCpy((Handle->BufferPointer + Handle->Position), value + offset, size); @@ -729,7 +729,7 @@ public unsafe void WriteBytesSafe(byte* value, int size, int offset = 0) if (!TryBeginWriteInternal(size)) { - throw new OverflowException("Writing past the end of the buffer"); + throw new OverflowException($"Writing past the end of the buffer, size is {size} bytes but remaining capacity is {Handle->Capacity - Handle->Position} bytes"); } UnsafeUtility.MemCpy((Handle->BufferPointer + Handle->Position), value + offset, size); Handle->Position += size; diff --git a/Runtime/Spawning/NetworkSpawnManager.cs b/Runtime/Spawning/NetworkSpawnManager.cs index 3f6720c..d3b2f9a 100644 --- a/Runtime/Spawning/NetworkSpawnManager.cs +++ b/Runtime/Spawning/NetworkSpawnManager.cs @@ -1581,20 +1581,46 @@ internal void HandleNetworkObjectShow() { foreach (var entry in ClientsToShowObject) { - SendSpawnCallForObserverUpdate(entry.Value.ToArray(), entry.Key); + if (entry.Key != null && entry.Key.IsSpawned) + { + try + { + SendSpawnCallForObserverUpdate(entry.Value.ToArray(), entry.Key); + } + catch (Exception ex) + { + if (NetworkManager.LogLevel <= LogLevel.Developer) + { + Debug.LogException(ex); + } + } + } } ClientsToShowObject.Clear(); ObjectsToShowToClient.Clear(); return; } - // Handle NetworkObjects to show + // Server or Host handling of NetworkObjects to show foreach (var client in ObjectsToShowToClient) { ulong clientId = client.Key; foreach (var networkObject in client.Value) { - SendSpawnCallForObject(clientId, networkObject); + if (networkObject != null && networkObject.IsSpawned) + { + try + { + SendSpawnCallForObject(clientId, networkObject); + } + catch (Exception ex) + { + if (NetworkManager.LogLevel <= LogLevel.Developer) + { + Debug.LogException(ex); + } + } + } } } ObjectsToShowToClient.Clear(); @@ -1883,5 +1909,55 @@ internal void NotifyNetworkObjectsSynchronized() networkObject.InternalNetworkSessionSynchronized(); } } + + /// + /// Distributed Authority Only + /// Should be invoked on non-session owner clients when a newly joined client is finished + /// synchronizing in order to "show" (spawn) anything that might be currently hidden from + /// the session owner. + /// + internal void ShowHiddenObjectsToNewlyJoinedClient(ulong newClientId) + { + if (!NetworkManager.DistributedAuthorityMode) + { + if (NetworkManager == null || !NetworkManager.ShutdownInProgress && NetworkManager.LogLevel <= LogLevel.Developer) + { + Debug.LogWarning($"[Internal Error] {nameof(ShowHiddenObjectsToNewlyJoinedClient)} invoked while !"); + } + return; + } + + if (!NetworkManager.DistributedAuthorityMode) + { + Debug.LogError($"[Internal Error] {nameof(ShowHiddenObjectsToNewlyJoinedClient)} should only be invoked when using a distributed authority network topology!"); + return; + } + + if (NetworkManager.LocalClient.IsSessionOwner) + { + Debug.LogError($"[Internal Error] {nameof(ShowHiddenObjectsToNewlyJoinedClient)} should only be invoked on a non-session owner client!"); + return; + } + var localClientId = NetworkManager.LocalClient.ClientId; + var sessionOwnerId = NetworkManager.CurrentSessionOwner; + foreach (var networkObject in SpawnedObjectsList) + { + if (networkObject.SpawnWithObservers && networkObject.OwnerClientId == localClientId && !networkObject.Observers.Contains(sessionOwnerId)) + { + if (networkObject.Observers.Contains(newClientId)) + { + if (NetworkManager.LogLevel <= LogLevel.Developer) + { + // Track if there is some other location where the client is being added to the observers list when the object is hidden from the session owner + Debug.LogWarning($"[{networkObject.name}] Has new client as an observer but it is hidden from the session owner!"); + } + // For now, remove the client (impossible for the new client to have an instance since the session owner doesn't) to make sure newly added + // code to handle this edge case works. + networkObject.Observers.Remove(newClientId); + } + networkObject.NetworkShow(newClientId); + } + } + } } } diff --git a/TestHelpers/Runtime/NetcodeIntegrationTest.cs b/TestHelpers/Runtime/NetcodeIntegrationTest.cs index 2165693..70ed784 100644 --- a/TestHelpers/Runtime/NetcodeIntegrationTest.cs +++ b/TestHelpers/Runtime/NetcodeIntegrationTest.cs @@ -850,6 +850,12 @@ protected void ClientNetworkManagerPostStartInit() protected virtual bool LogAllMessages => false; + protected virtual bool ShouldCheckForSpawnedPlayers() + { + return true; + } + + /// /// This starts the server and clients as long as /// returns true. @@ -938,7 +944,12 @@ protected IEnumerator StartServerAndClients() AssertOnTimeout($"{nameof(CreateAndStartNewClient)} timed out waiting for all sessions to spawn Client-{m_ServerNetworkManager.LocalClientId}'s player object!\n {m_InternalErrorLog}"); } } - ClientNetworkManagerPostStartInit(); + + if (ShouldCheckForSpawnedPlayers()) + { + ClientNetworkManagerPostStartInit(); + } + // Notification that at this time the server and client(s) are instantiated, // started, and connected on both sides. yield return OnServerAndClientsConnected(); @@ -1030,7 +1041,10 @@ protected void StartServerAndClientsWithTimeTravel() } } - ClientNetworkManagerPostStartInit(); + if (ShouldCheckForSpawnedPlayers()) + { + ClientNetworkManagerPostStartInit(); + } // Notification that at this time the server and client(s) are instantiated, // started, and connected on both sides. diff --git a/TestHelpers/Runtime/NetcodeIntegrationTestHelpers.cs b/TestHelpers/Runtime/NetcodeIntegrationTestHelpers.cs index ed41fc1..8758db8 100644 --- a/TestHelpers/Runtime/NetcodeIntegrationTestHelpers.cs +++ b/TestHelpers/Runtime/NetcodeIntegrationTestHelpers.cs @@ -326,21 +326,31 @@ public static void Destroy() s_IsStarted = false; - // Shutdown the server which forces clients to disconnect - foreach (var networkManager in NetworkManagerInstances) + try { - networkManager.Shutdown(); - s_Hooks.Remove(networkManager); - } + // Shutdown the server which forces clients to disconnect + foreach (var networkManager in NetworkManagerInstances) + { + if (networkManager != null && networkManager.IsListening) + { + networkManager?.Shutdown(); + s_Hooks.Remove(networkManager); + } + } - // Destroy the network manager instances - foreach (var networkManager in NetworkManagerInstances) - { - if (networkManager.gameObject != null) + // Destroy the network manager instances + foreach (var networkManager in NetworkManagerInstances) { - Object.DestroyImmediate(networkManager.gameObject); + if (networkManager != null && networkManager.gameObject) + { + Object.DestroyImmediate(networkManager.gameObject); + } } } + catch (Exception ex) + { + Debug.LogException(ex); + } NetworkManagerInstances.Clear(); diff --git a/Tests/Runtime/ConnectionApproval.cs b/Tests/Runtime/ConnectionApproval.cs index ae60675..6fd311f 100644 --- a/Tests/Runtime/ConnectionApproval.cs +++ b/Tests/Runtime/ConnectionApproval.cs @@ -1,65 +1,135 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Text; using NUnit.Framework; using Unity.Netcode.TestHelpers.Runtime; -using UnityEngine; using UnityEngine.TestTools; namespace Unity.Netcode.RuntimeTests { - internal class ConnectionApprovalTests + [TestFixture(PlayerCreation.Prefab)] + [TestFixture(PlayerCreation.PrefabHash)] + [TestFixture(PlayerCreation.NoPlayer)] + [TestFixture(PlayerCreation.FailValidation)] + internal class ConnectionApprovalTests : NetcodeIntegrationTest { + private const string k_InvalidToken = "Invalid validation token!"; + public enum PlayerCreation + { + Prefab, + PrefabHash, + NoPlayer, + FailValidation + } + private PlayerCreation m_PlayerCreation; + private bool m_ClientDisconnectReasonValidated; + + private Dictionary m_Validated = new Dictionary(); + + public ConnectionApprovalTests(PlayerCreation playerCreation) + { + m_PlayerCreation = playerCreation; + } + + protected override int NumberOfClients => 1; + private Guid m_ValidationToken; - private bool m_IsValidated; - [SetUp] - public void Setup() + protected override bool ShouldCheckForSpawnedPlayers() + { + return m_PlayerCreation != PlayerCreation.NoPlayer; + } + + protected override void OnServerAndClientsCreated() { - // Create, instantiate, and host - Assert.IsTrue(NetworkManagerHelper.StartNetworkManager(out _, NetworkManagerHelper.NetworkManagerOperatingMode.None)); + m_ClientDisconnectReasonValidated = false; + m_BypassConnectionTimeout = m_PlayerCreation == PlayerCreation.FailValidation; + m_Validated.Clear(); m_ValidationToken = Guid.NewGuid(); + var validationToken = Encoding.UTF8.GetBytes(m_ValidationToken.ToString()); + m_ServerNetworkManager.ConnectionApprovalCallback = NetworkManagerObject_ConnectionApprovalCallback; + m_ServerNetworkManager.NetworkConfig.PlayerPrefab = m_PlayerCreation == PlayerCreation.Prefab ? m_PlayerPrefab : null; + if (m_PlayerCreation == PlayerCreation.PrefabHash) + { + m_ServerNetworkManager.NetworkConfig.Prefabs.Add(new NetworkPrefab() { Prefab = m_PlayerPrefab }); + } + m_ServerNetworkManager.NetworkConfig.ConnectionApproval = true; + m_ServerNetworkManager.NetworkConfig.ConnectionData = validationToken; + + foreach (var client in m_ClientNetworkManagers) + { + client.NetworkConfig.PlayerPrefab = m_PlayerCreation == PlayerCreation.Prefab ? m_PlayerPrefab : null; + if (m_PlayerCreation == PlayerCreation.PrefabHash) + { + client.NetworkConfig.Prefabs.Add(new NetworkPrefab() { Prefab = m_PlayerPrefab }); + } + client.NetworkConfig.ConnectionApproval = true; + client.NetworkConfig.ConnectionData = m_PlayerCreation == PlayerCreation.FailValidation ? Encoding.UTF8.GetBytes(Guid.NewGuid().ToString()) : validationToken; + if (m_PlayerCreation == PlayerCreation.FailValidation) + { + client.OnClientDisconnectCallback += Client_OnClientDisconnectCallback; + } + } + + base.OnServerAndClientsCreated(); } - [UnityTest] - public IEnumerator ConnectionApproval() + private void Client_OnClientDisconnectCallback(ulong clientId) + { + m_ClientNetworkManagers[0].OnClientDisconnectCallback -= Client_OnClientDisconnectCallback; + m_ClientDisconnectReasonValidated = m_ClientNetworkManagers[0].LocalClientId == clientId && m_ClientNetworkManagers[0].DisconnectReason == k_InvalidToken; + } + + private bool ClientAndHostValidated() { - NetworkManagerHelper.NetworkManagerObject.ConnectionApprovalCallback = NetworkManagerObject_ConnectionApprovalCallback; - NetworkManagerHelper.NetworkManagerObject.NetworkConfig.ConnectionApproval = true; - NetworkManagerHelper.NetworkManagerObject.NetworkConfig.PlayerPrefab = null; - NetworkManagerHelper.NetworkManagerObject.NetworkConfig.ConnectionData = Encoding.UTF8.GetBytes(m_ValidationToken.ToString()); - m_IsValidated = false; - NetworkManagerHelper.NetworkManagerObject.StartHost(); - - var timeOut = Time.realtimeSinceStartup + 3.0f; - var timedOut = false; - while (!m_IsValidated) + if (!m_Validated.ContainsKey(m_ServerNetworkManager.LocalClientId) || !m_Validated[m_ServerNetworkManager.LocalClientId]) { - yield return new WaitForSeconds(0.01f); - if (timeOut < Time.realtimeSinceStartup) + return false; + } + if (m_PlayerCreation == PlayerCreation.FailValidation) + { + return m_ClientDisconnectReasonValidated; + } + else + { + foreach (var client in m_ClientNetworkManagers) { - timedOut = true; + if (!m_Validated.ContainsKey(client.LocalClientId) || !m_Validated[client.LocalClientId]) + { + return false; + } } } + return true; + } - //Make sure we didn't time out - Assert.False(timedOut); - Assert.True(m_IsValidated); + [UnityTest] + public IEnumerator ConnectionApproval() + { + yield return WaitForConditionOrTimeOut(ClientAndHostValidated); + AssertOnTimeout("Timed out waiting for all clients to be approved!"); } private void NetworkManagerObject_ConnectionApprovalCallback(NetworkManager.ConnectionApprovalRequest request, NetworkManager.ConnectionApprovalResponse response) { var stringGuid = Encoding.UTF8.GetString(request.Payload); + if (m_ValidationToken.ToString() == stringGuid) { - m_IsValidated = true; + m_Validated.Add(request.ClientNetworkId, true); + response.Approved = true; + } + else + { + response.Approved = false; + response.Reason = "Invalid validation token!"; } - response.Approved = m_IsValidated; - response.CreatePlayerObject = false; + response.CreatePlayerObject = ShouldCheckForSpawnedPlayers(); response.Position = null; response.Rotation = null; - response.PlayerPrefabHash = null; + response.PlayerPrefabHash = m_PlayerCreation == PlayerCreation.PrefabHash ? m_PlayerPrefab.GetComponent().GlobalObjectIdHash : null; } @@ -78,13 +148,6 @@ public void VerifyUniqueNetworkConfigPerRequest() Assert.True(currentHash != newHash, $"Hashed {nameof(NetworkConfig)} values {currentHash} and {newHash} should not be the same!"); } - - [TearDown] - public void TearDown() - { - // Stop, shutdown, and destroy - NetworkManagerHelper.ShutdownNetworkManager(); - } - } } + diff --git a/Tests/Runtime/DistributedAuthority/ExtendedNetworkShowAndHideTests.cs b/Tests/Runtime/DistributedAuthority/ExtendedNetworkShowAndHideTests.cs new file mode 100644 index 0000000..1ebb0f0 --- /dev/null +++ b/Tests/Runtime/DistributedAuthority/ExtendedNetworkShowAndHideTests.cs @@ -0,0 +1,154 @@ +using System.Collections; +using NUnit.Framework; +using Unity.Netcode.TestHelpers.Runtime; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests +{ + [TestFixture(HostOrServer.DAHost, true)] + [TestFixture(HostOrServer.DAHost, false)] + public class ExtendedNetworkShowAndHideTests : NetcodeIntegrationTest + { + protected override int NumberOfClients => 3; + private bool m_EnableSceneManagement; + private GameObject m_ObjectToSpawn; + private NetworkObject m_SpawnedObject; + private NetworkManager m_ClientToHideFrom; + private NetworkManager m_LateJoinClient; + private NetworkManager m_SpawnOwner; + + public ExtendedNetworkShowAndHideTests(HostOrServer hostOrServer, bool enableSceneManagement) : base(hostOrServer) + { + m_EnableSceneManagement = enableSceneManagement; + } + + protected override void OnServerAndClientsCreated() + { + if (!UseCMBService()) + { + m_ServerNetworkManager.NetworkConfig.EnableSceneManagement = m_EnableSceneManagement; + } + + foreach (var client in m_ClientNetworkManagers) + { + client.NetworkConfig.EnableSceneManagement = m_EnableSceneManagement; + } + + m_ObjectToSpawn = CreateNetworkObjectPrefab("TestObject"); + m_ObjectToSpawn.SetActive(false); + + base.OnServerAndClientsCreated(); + } + + private bool AllClientsSpawnedObject() + { + if (!UseCMBService()) + { + if (!s_GlobalNetworkObjects.ContainsKey(m_ServerNetworkManager.LocalClientId)) + { + return false; + } + if (!s_GlobalNetworkObjects[m_ServerNetworkManager.LocalClientId].ContainsKey(m_SpawnedObject.NetworkObjectId)) + { + return false; + } + } + + foreach (var client in m_ClientNetworkManagers) + { + if (!s_GlobalNetworkObjects.ContainsKey(client.LocalClientId)) + { + return false; + } + if (!s_GlobalNetworkObjects[client.LocalClientId].ContainsKey(m_SpawnedObject.NetworkObjectId)) + { + return false; + } + } + return true; + } + + private bool IsClientPromotedToSessionOwner() + { + if (!UseCMBService()) + { + if (m_ServerNetworkManager.CurrentSessionOwner != m_ClientToHideFrom.LocalClientId) + { + return false; + } + } + + foreach (var client in m_ClientNetworkManagers) + { + if (!client.IsConnectedClient) + { + continue; + } + if (client.CurrentSessionOwner != m_ClientToHideFrom.LocalClientId) + { + return false; + } + } + return true; + } + + protected override void OnNewClientCreated(NetworkManager networkManager) + { + m_LateJoinClient = networkManager; + networkManager.NetworkConfig.EnableSceneManagement = m_EnableSceneManagement; + networkManager.NetworkConfig.Prefabs = m_SpawnOwner.NetworkConfig.Prefabs; + base.OnNewClientCreated(networkManager); + } + + /// + /// This test validates the following NetworkShow - NetworkHide issue: + /// - During a session, a spawned object is hidden from a client. + /// - The current session owner disconnects and the client the object is hidden from is prommoted to the session owner. + /// - A new client joins and the newly promoted session owner synchronizes the newly joined client with only objects visible to it. + /// - Any already connected non-session owner client should "NetworkShow" the object to the newly connected client + /// (but only if the hidden object has SpawnWithObservers enabled) + /// + [UnityTest] + public IEnumerator HiddenObjectPromotedSessionOwnerNewClientSynchronizes() + { + // Get the test relative session owner + var sessionOwner = UseCMBService() ? m_ClientNetworkManagers[0] : m_ServerNetworkManager; + m_SpawnOwner = UseCMBService() ? m_ClientNetworkManagers[1] : m_ClientNetworkManagers[0]; + m_ClientToHideFrom = UseCMBService() ? m_ClientNetworkManagers[NumberOfClients - 1] : m_ClientNetworkManagers[1]; + m_ObjectToSpawn.SetActive(true); + + // Spawn the object with a non-session owner client + m_SpawnedObject = SpawnObject(m_ObjectToSpawn, m_SpawnOwner).GetComponent(); + yield return WaitForConditionOrTimeOut(AllClientsSpawnedObject); + AssertOnTimeout($"Not all clients spawned and instance of {m_SpawnedObject.name}"); + + // Hide the spawned object from the to be promoted session owner + m_SpawnedObject.NetworkHide(m_ClientToHideFrom.LocalClientId); + + yield return WaitForConditionOrTimeOut(() => !m_ClientToHideFrom.SpawnManager.SpawnedObjects.ContainsKey(m_SpawnedObject.NetworkObjectId)); + AssertOnTimeout($"{m_SpawnedObject.name} was not hidden from Client-{m_ClientToHideFrom.LocalClientId}!"); + + // Promoted a new session owner (DAHost promotes while CMB Session we disconnect the current session owner) + if (!UseCMBService()) + { + m_ServerNetworkManager.PromoteSessionOwner(m_ClientToHideFrom.LocalClientId); + } + else + { + sessionOwner.Shutdown(); + } + + // Wait for the new session owner to be promoted and for all clients to acknowledge the promotion + yield return WaitForConditionOrTimeOut(IsClientPromotedToSessionOwner); + AssertOnTimeout($"Client-{m_ClientToHideFrom.LocalClientId} was not promoted as session owner on all client instances!"); + + // Connect a new client instance + yield return CreateAndStartNewClient(); + + // Assure the newly connected client is synchronized with the NetworkObject hidden from the newly promoted session owner + yield return WaitForConditionOrTimeOut(() => m_LateJoinClient.SpawnManager.SpawnedObjects.ContainsKey(m_SpawnedObject.NetworkObjectId)); + AssertOnTimeout($"Client-{m_LateJoinClient.LocalClientId} never spawned {nameof(NetworkObject)} {m_SpawnedObject.name}!"); + } + } +} diff --git a/Tests/Runtime/DistributedAuthority/ExtendedNetworkShowAndHideTests.cs.meta b/Tests/Runtime/DistributedAuthority/ExtendedNetworkShowAndHideTests.cs.meta new file mode 100644 index 0000000..5d22751 --- /dev/null +++ b/Tests/Runtime/DistributedAuthority/ExtendedNetworkShowAndHideTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a6389d04d9080b24b99de7e6900a064c \ No newline at end of file diff --git a/Tests/Runtime/Messaging/NamedMessageTests.cs b/Tests/Runtime/Messaging/NamedMessageTests.cs index 541fbb2..4c2d0d8 100644 --- a/Tests/Runtime/Messaging/NamedMessageTests.cs +++ b/Tests/Runtime/Messaging/NamedMessageTests.cs @@ -239,5 +239,45 @@ public void WhenSendingNamedMessageToNullClientList_ArgumentNullExceptionIsThrow }); } } + + [Test] + public unsafe void ErrorMessageIsPrintedWhenAttemptingToSendNamedMessageWithTooBigBuffer() + { + // First try a valid send with the maximum allowed size (this is atm 1264) + var msgSize = m_ServerNetworkManager.MessageManager.NonFragmentedMessageMaxSize - FastBufferWriter.GetWriteSize() - sizeof(ulong)/*MessageName hash*/ - sizeof(NetworkBatchHeader); + var bufferSize = m_ServerNetworkManager.MessageManager.NonFragmentedMessageMaxSize; + var messageName = Guid.NewGuid().ToString(); + var messageContent = new byte[msgSize]; + var writer = new FastBufferWriter(bufferSize, Allocator.Temp, bufferSize * 2); + using (writer) + { + writer.TryBeginWrite(msgSize); + writer.WriteBytes(messageContent, msgSize, 0); + m_ServerNetworkManager.CustomMessagingManager.SendNamedMessage(messageName, new List { FirstClient.LocalClientId }, writer); + m_ServerNetworkManager.CustomMessagingManager.SendNamedMessage(messageName, FirstClient.LocalClientId, writer); + } + + msgSize++; + messageContent = new byte[msgSize]; + writer = new FastBufferWriter(bufferSize, Allocator.Temp, bufferSize * 2); + using (writer) + { + writer.TryBeginWrite(msgSize); + writer.WriteBytes(messageContent, msgSize, 0); + var message = Assert.Throws( + () => + { + m_ServerNetworkManager.CustomMessagingManager.SendNamedMessage(messageName, new List { FirstClient.LocalClientId }, writer); + }).Message; + Assert.IsTrue(message.Contains($"Given message size ({msgSize} bytes) is greater than the maximum"), $"Unexpected exception: {message}"); + + message = Assert.Throws( + () => + { + m_ServerNetworkManager.CustomMessagingManager.SendNamedMessage(messageName, FirstClient.LocalClientId, writer); + }).Message; + Assert.IsTrue(message.Contains($"Given message size ({msgSize} bytes) is greater than the maximum"), $"Unexpected exception: {message}"); + } + } } } diff --git a/Tests/Runtime/Messaging/UnnamedMessageTests.cs b/Tests/Runtime/Messaging/UnnamedMessageTests.cs index 61333ad..8b0c854 100644 --- a/Tests/Runtime/Messaging/UnnamedMessageTests.cs +++ b/Tests/Runtime/Messaging/UnnamedMessageTests.cs @@ -194,5 +194,44 @@ public void WhenSendingNamedMessageToNullClientList_ArgumentNullExceptionIsThrow }); } } + + [Test] + public unsafe void ErrorMessageIsPrintedWhenAttemptingToSendUnnamedMessageWithTooBigBuffer() + { + // First try a valid send with the maximum allowed size (this is atm 1272) + var msgSize = m_ServerNetworkManager.MessageManager.NonFragmentedMessageMaxSize - FastBufferWriter.GetWriteSize() - sizeof(NetworkBatchHeader); + var bufferSize = m_ServerNetworkManager.MessageManager.NonFragmentedMessageMaxSize; + var messageContent = new byte[msgSize]; + var writer = new FastBufferWriter(bufferSize, Allocator.Temp, bufferSize * 2); + using (writer) + { + writer.TryBeginWrite(msgSize); + writer.WriteBytes(messageContent, msgSize, 0); + m_ServerNetworkManager.CustomMessagingManager.SendUnnamedMessage(new List { FirstClient.LocalClientId }, writer); + m_ServerNetworkManager.CustomMessagingManager.SendUnnamedMessage(FirstClient.LocalClientId, writer); + } + + msgSize++; + messageContent = new byte[msgSize]; + writer = new FastBufferWriter(bufferSize, Allocator.Temp, bufferSize * 2); + using (writer) + { + writer.TryBeginWrite(msgSize); + writer.WriteBytes(messageContent, msgSize, 0); + var message = Assert.Throws( + () => + { + m_ServerNetworkManager.CustomMessagingManager.SendUnnamedMessage(new List { FirstClient.LocalClientId }, writer); + }).Message; + Assert.IsTrue(message.Contains($"Given message size ({msgSize} bytes) is greater than the maximum"), $"Unexpected exception: {message}"); + + message = Assert.Throws( + () => + { + m_ServerNetworkManager.CustomMessagingManager.SendUnnamedMessage(FirstClient.LocalClientId, writer); + }).Message; + Assert.IsTrue(message.Contains($"Given message size ({msgSize} bytes) is greater than the maximum"), $"Unexpected exception: {message}"); + } + } } } diff --git a/Tests/Runtime/NetworkTransform/NetworkTransformBase.cs b/Tests/Runtime/NetworkTransform/NetworkTransformBase.cs index da35c76..4413b73 100644 --- a/Tests/Runtime/NetworkTransform/NetworkTransformBase.cs +++ b/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/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs b/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs index 7b9ac99..95ca7e0 100644 --- a/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs +++ b/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/Tests/Runtime/NetworkTransformAnticipationTests.cs b/Tests/Runtime/NetworkTransformAnticipationTests.cs index 44530a7..c4921d1 100644 --- a/Tests/Runtime/NetworkTransformAnticipationTests.cs +++ b/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/Tests/Runtime/NetworkVisibilityTests.cs b/Tests/Runtime/NetworkVisibilityTests.cs index f81eb29..a19f856 100644 --- a/Tests/Runtime/NetworkVisibilityTests.cs +++ b/Tests/Runtime/NetworkVisibilityTests.cs @@ -14,9 +14,11 @@ namespace Unity.Netcode.RuntimeTests internal class NetworkVisibilityTests : NetcodeIntegrationTest { - protected override int NumberOfClients => 1; + protected override int NumberOfClients => 2; private GameObject m_TestNetworkPrefab; private bool m_SceneManagementEnabled; + private GameObject m_SpawnedObject; + private NetworkManager m_SessionOwner; public NetworkVisibilityTests(SceneManagementState sceneManagementState, NetworkTopologyTypes networkTopologyType) : base(networkTopologyType) { @@ -27,7 +29,11 @@ protected override void OnServerAndClientsCreated() { m_TestNetworkPrefab = CreateNetworkObjectPrefab("Object"); m_TestNetworkPrefab.AddComponent(); - m_ServerNetworkManager.NetworkConfig.EnableSceneManagement = m_SceneManagementEnabled; + if (!UseCMBService()) + { + m_ServerNetworkManager.NetworkConfig.EnableSceneManagement = m_SceneManagementEnabled; + } + foreach (var clientNetworkManager in m_ClientNetworkManagers) { clientNetworkManager.NetworkConfig.EnableSceneManagement = m_SceneManagementEnabled; @@ -38,7 +44,8 @@ protected override void OnServerAndClientsCreated() protected override IEnumerator OnServerAndClientsConnected() { - SpawnObject(m_TestNetworkPrefab, m_ServerNetworkManager); + m_SessionOwner = UseCMBService() ? m_ClientNetworkManagers[0] : m_ServerNetworkManager; + m_SpawnedObject = SpawnObject(m_TestNetworkPrefab, m_SessionOwner); yield return base.OnServerAndClientsConnected(); } @@ -46,13 +53,49 @@ protected override IEnumerator OnServerAndClientsConnected() [UnityTest] public IEnumerator HiddenObjectsTest() { + var expectedCount = UseCMBService() ? 2 : 3; #if UNITY_2023_1_OR_NEWER - yield return WaitForConditionOrTimeOut(() => Object.FindObjectsByType(FindObjectsSortMode.None).Where((c) => c.IsSpawned).Count() == 2); + yield return WaitForConditionOrTimeOut(() => Object.FindObjectsByType(FindObjectsSortMode.None).Where((c) => c.IsSpawned).Count() == expectedCount); #else - yield return WaitForConditionOrTimeOut(() => Object.FindObjectsOfType().Where((c) => c.IsSpawned).Count() == 2); + yield return WaitForConditionOrTimeOut(() => Object.FindObjectsOfType().Where((c) => c.IsSpawned).Count() == expectedCount); #endif Assert.IsFalse(s_GlobalTimeoutHelper.TimedOut, "Timed out waiting for the visible object count to equal 2!"); } + + [UnityTest] + public IEnumerator HideShowAndDeleteTest() + { + var expectedCount = UseCMBService() ? 2 : 3; +#if UNITY_2023_1_OR_NEWER + yield return WaitForConditionOrTimeOut(() => Object.FindObjectsByType(FindObjectsSortMode.None).Where((c) => c.IsSpawned).Count() == expectedCount); +#else + yield return WaitForConditionOrTimeOut(() => Object.FindObjectsOfType().Where((c) => c.IsSpawned).Count() == expectedCount); +#endif + AssertOnTimeout("Timed out waiting for the visible object count to equal 2!"); + + var sessionOwnerNetworkObject = m_SpawnedObject.GetComponent(); + var clientIndex = UseCMBService() ? 1 : 0; + sessionOwnerNetworkObject.NetworkHide(m_ClientNetworkManagers[clientIndex].LocalClientId); +#if UNITY_2023_1_OR_NEWER + yield return WaitForConditionOrTimeOut(() => Object.FindObjectsByType(FindObjectsSortMode.None).Where((c) => c.IsSpawned).Count() == expectedCount - 1); +#else + yield return WaitForConditionOrTimeOut(() => Object.FindObjectsOfType().Where((c) => c.IsSpawned).Count() == expectedCount - 1); +#endif + AssertOnTimeout($"Timed out waiting for {m_SpawnedObject.name} to be hidden from client!"); + var networkObjectId = sessionOwnerNetworkObject.NetworkObjectId; + sessionOwnerNetworkObject.NetworkShow(m_ClientNetworkManagers[clientIndex].LocalClientId); + sessionOwnerNetworkObject.Despawn(true); + + // Expect no exceptions + yield return s_DefaultWaitForTick; + + // Now force a scenario where it normally would have caused an exception + m_SessionOwner.SpawnManager.ObjectsToShowToClient.Add(m_ClientNetworkManagers[clientIndex].LocalClientId, new System.Collections.Generic.List()); + m_SessionOwner.SpawnManager.ObjectsToShowToClient[m_ClientNetworkManagers[clientIndex].LocalClientId].Add(null); + + // Expect no exceptions + yield return s_DefaultWaitForTick; + } } } diff --git a/Tests/Runtime/TransformInterpolationTests.cs b/Tests/Runtime/TransformInterpolationTests.cs index 9c402ef..4e0cea8 100644 --- a/Tests/Runtime/TransformInterpolationTests.cs +++ b/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/package.json b/package.json index 961f571..049964c 100644 --- a/package.json +++ b/package.json @@ -2,23 +2,23 @@ "name": "com.unity.netcode.gameobjects", "displayName": "Netcode for GameObjects", "description": "Netcode for GameObjects is a high-level netcode SDK that provides networking capabilities to GameObject/MonoBehaviour workflows within Unity and sits on top of underlying transport layer.", - "version": "2.0.0-pre.4", + "version": "2.0.0", "unity": "6000.0", "dependencies": { "com.unity.nuget.mono-cecil": "1.11.4", "com.unity.transport": "2.3.0" }, "_upm": { - "changelog": "### Added\n\n- Added `NetworkVariable.CheckDirtyState` that is to be used in tandem with collections in order to detect whether the collection or an item within the collection has changed. (#3004)\n\n### Fixed\n\n- Fixed issue where nested `NetworkTransform` components were not getting updated. (#3016)\n- Fixed issue by adding null checks in `NetworkVariableBase.CanClientRead` and `NetworkVariableBase.CanClientWrite` methods to ensure safe access to `NetworkBehaviour`. (#3012)\n- Fixed issue where `FixedStringSerializer` was using `NetworkVariableSerialization.AreEqual` to determine if two bytes were equal causes an exception to be thrown due to no byte serializer having been defined. (#3009)\n- Fixed Issue where a state with dual triggers, inbound and outbound, could cause a false layer to layer state transition message to be sent to non-authority `NetworkAnimator` instances and cause a warning message to be logged. (#3008)\n- Fixed issue using collections within `NetworkVariable` where the collection would not detect changes to items or nested items. (#3004)\n- Fixed issue where `List`, `Dictionary`, and `HashSet` collections would not uniquely duplicate nested collections. (#3004)\n- Fixed issue where `NotAuthorityTarget` would include the service observer in the list of targets to send the RPC to as opposed to excluding the service observer as it should. (#3000)\n- Fixed issue where `ProxyRpcTargetGroup` could attempt to send a message if there were no targets to send to. (#3000)\n\n### Changed\n\n- Changed `NetworkAnimator` to automatically switch to owner authoritative mode when using a distributed authority network topology. (#3021)\n- Changed permissions exception thrown in `NetworkList` to exiting early with a logged error that is now a unified permissions message within `NetworkVariableBase`. (#3004)\n- Changed permissions exception thrown in `NetworkVariable.Value` to exiting early with a logged error that is now a unified permissions message within `NetworkVariableBase`. (#3004)" + "changelog": "### Added\n\n- Added tooltips for all of the `NetworkObject` component's properties. (#3052)\n- Added message size validation to named and unnamed message sending functions for better error messages. (#3049)\n- 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)\n- 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)\n- 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)\n- Added `NetworkObject.AllowOwnerToParent` property to provide the ability to allow clients to parent owned objects when running in a client-server network topology. (#3013)\n- 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)\n- Added `NetcodeEditorBase` editor helper class to provide easier modification and extension of the SDK's components. (#3013)\n\n### Fixed\n\n- Fixed issue where `NetworkAnimator` would send updates to non-observer clients. (#3057)\n- Fixed issue where an exception could occur when receiving a universal RPC for a `NetworkObject` that has been despawned. (#3052)\n- Fixed issue where a NetworkObject hidden from a client that is then promoted to be session owner was not being synchronized with newly joining clients.(#3051)\n- Fixed issue where clients could have a wrong time delta on `NetworkVariableBase` which could prevent from sending delta state updates. (#3045)\n- Fixed issue where setting a prefab hash value during connection approval but not having a player prefab assigned could cause an exception when spawning a player. (#3042)\n- 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)\n- Fixed issue where the `NetworkManagerHelper` was continuing to check for hierarchy changes when in play mode. (#3026)\n- Fixed issue with newly/late joined clients and `NetworkTransform` synchronization of parented `NetworkObject` instances. (#3013)\n- Fixed issue with smooth transitions between transform spaces when interpolation is enabled (requires `NetworkTransform.SwitchTransformSpaceWhenParented` to be enabled). (#3013)\n\n### Changed\n\n- Changed `NetworkTransformEditor` now uses `NetworkTransform` as the base type class to assure it doesn't display a foldout group when using the base `NetworkTransform` component class. (#3052)\n- Changed `NetworkAnimator.Awake` is now a protected virtual method. (#3052)\n- Changed when invoking `NetworkManager.ConnectionManager.DisconnectClient` during a distributed authority session a more appropriate message is logged. (#3052)\n- Changed `NetworkTransformEditor` so it now derives from `NetcodeEditorBase`. (#3013)\n- Changed `NetworkRigidbodyBaseEditor` so it now derives from `NetcodeEditorBase`. (#3013)\n- Changed `NetworkManagerEditor` so it now derives from `NetcodeEditorBase`. (#3013)" }, "upmCi": { - "footprint": "48286e9f7b0e053fe7f7b524bafc69a99c2906fc" + "footprint": "f1ef7566b7a89b1ee9c34cc13400735ae63964d4" }, "documentationUrl": "https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@2.0/manual/index.html", "repository": { "url": "https://github.com/Unity-Technologies/com.unity.netcode.gameobjects.git", "type": "git", - "revision": "2802dfcd13c3be1ac356191cc87d1559203d2db3" + "revision": "8a7ae9f91a53bdcabe5e7df783dd1884c07bcd6f" }, "samples": [ {