From 472ba7fbb2581b3908d092e56d3e3d0ae38694ca Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Thu, 22 Oct 2026 16:43:01 -0500 Subject: [PATCH 1/9] fix: remove the NetworkObjectBridge when a hybrid prefab stops being hybrid Removing GhostObject from a hybrid prefab now removes only the hidden NetworkObjectBridge and any GhostRigidbody, instead of every GhostBehaviour, and restores SynchronizeTransform. Removing NetworkObject from a hybrid prefab also removes the bridge. The bridge now sets FullTickSmoothing in place of the obsolete SingleWorldHostInterpolationSmoothing. --- .../Editor/NetworkObjectEditor.cs | 12 ++-- .../Components/Helpers/NetworkObjectBridge.cs | 18 +++++- .../Runtime/Core/NetworkObject.cs | 60 +++++++++++++++---- 3 files changed, 69 insertions(+), 21 deletions(-) diff --git a/com.unity.netcode.gameobjects/Editor/NetworkObjectEditor.cs b/com.unity.netcode.gameobjects/Editor/NetworkObjectEditor.cs index 312e875850..56833a679f 100644 --- a/com.unity.netcode.gameobjects/Editor/NetworkObjectEditor.cs +++ b/com.unity.netcode.gameobjects/Editor/NetworkObjectEditor.cs @@ -36,20 +36,16 @@ private static void OnApplicationStart() } /// - /// Callback to remove the GhostBehaviours prior to removing GhostObject. + /// Callback to remove the NetworkObjectBridge prior to removing GhostObject. /// /// The with the component being removed. private static void OnGhostObjectPreRemoval(GameObject gameObject) { - var ghostBehaviours = gameObject.GetComponentsInChildren(); - for (int i = ghostBehaviours.Length - 1; i >= 0; i--) + var networkObject = gameObject.GetComponent(); + if (networkObject != null) { - DestroyImmediate(ghostBehaviours[i], true); + networkObject.OnRemoveNetworkObjectBridge(); } - var networkObject = gameObject.GetComponent(); - networkObject.GhostObject = null; - networkObject.HasGhost = false; - networkObject.HadBridge = true; } #endif diff --git a/com.unity.netcode.gameobjects/Runtime/Components/Helpers/NetworkObjectBridge.cs b/com.unity.netcode.gameobjects/Runtime/Components/Helpers/NetworkObjectBridge.cs index 1e8f1f0673..db4e411781 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/Helpers/NetworkObjectBridge.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/Helpers/NetworkObjectBridge.cs @@ -1,6 +1,9 @@ #if UNIFIED_NETCODE using Unity.Mathematics; using Unity.Transforms; +#if UNITY_EDITOR +using UnityEditor; +#endif using UnityEngine; namespace Unity.Netcode @@ -37,6 +40,19 @@ private void OnValidate() return; } + var networkObject = GetComponent(); + if (networkObject == null) + { + EditorApplication.delayCall += () => + { + if (this != null) + { + DestroyImmediate(this, true); + } + }; + return; + } + #if COM_UNITY_MODULES_PHYSICS var rigidBody = GetComponent(); var ghostRigidBody = GetComponent(); @@ -44,7 +60,7 @@ private void OnValidate() { // This must be enabled when replicating the rigid body. - ghostAdapter.SingleWorldHostInterpolationSmoothing = SingleWorldHostInterpolationMode.Interpolate; + ghostAdapter.FullTickSmoothing = FullTickSmoothingMode.Interpolate; // TODO: Currently, this is added only if you enable replication of the rigid body. // There is a bug where if you don't add this component it doesn't synchronize the transform. // Remove this once the issue is resolved. diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index d3951a2732..0c207577ca 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -369,14 +369,43 @@ private void CheckForInScenePlaced() [SerializeField] internal bool HadBridge; #if UNITY_EDITOR - private void OnApplicationUpdate() + private void OnAddNetworkObjectBridge() { NetworkObjectBridge = gameObject.AddComponent(); HadBridge = true; // Transform synchronization is handled by unified netcode SynchronizeTransform = false; + EditorApplication.update -= OnAddNetworkObjectBridge; + } - EditorApplication.update -= OnApplicationUpdate; + internal void OnRemoveNetworkObjectBridge() + { + var networkObjectBridge = gameObject.GetComponent(); + if (networkObjectBridge != null) + { + DestroyImmediate(networkObjectBridge, true); + } + var ghostRigidbodies = gameObject.GetComponentsInChildren(true); + for (int i = 0; i < ghostRigidbodies.Length; i++) + { + DestroyImmediate(ghostRigidbodies[i], true); + } + + NetworkObjectBridge = null; + + HadBridge = false; + // NGO synchronizes the transform again once the prefab is no longer hybrid + SynchronizeTransform = true; + EditorApplication.update -= OnRemoveNetworkObjectBridge; + } + + private void OnRemoveGhostObject() + { + OnRemoveNetworkObjectBridge(); + DestroyImmediate(GhostObject); + GhostObject = null; + HasGhost = false; + EditorApplication.update -= OnRemoveGhostObject; } internal void UnifiedValidation() @@ -390,22 +419,29 @@ internal void UnifiedValidation() //TODO: Needs to be validated once develop-2.0.0 is merged. if (InScenePlaced) { - Debug.LogError($"This experimental version of NGO does not support hybrid in-scene placed objects."); - Destroy(GhostObject); - HasGhost = false; + Debug.LogError($"In-scene placed Hybrid instances are not allowed."); + EditorApplication.update -= OnRemoveGhostObject; + EditorApplication.update += OnRemoveGhostObject; return; } - - if (NetworkObjectBridge == null) + else if (NetworkObjectBridge == null) { - EditorApplication.update -= OnApplicationUpdate; - EditorApplication.update += OnApplicationUpdate; + EditorApplication.update -= OnAddNetworkObjectBridge; + EditorApplication.update += OnAddNetworkObjectBridge; } } - else if (HadBridge && !HasGhost && !NetworkObjectBridge) + else if (HadBridge && !HasGhost) { - HadBridge = false; - SynchronizeTransform = true; + if (NetworkObjectBridge != null) + { + EditorApplication.update -= OnRemoveNetworkObjectBridge; + EditorApplication.update += OnRemoveNetworkObjectBridge; + } + else + { + HadBridge = false; + SynchronizeTransform = true; + } } } #endif From e81a81c6066cf982a69e0fa23f67f6f3424372b1 Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Thu, 22 Oct 2026 16:59:30 -0500 Subject: [PATCH 2/9] update Clearing HasGhost. --- com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 0c207577ca..36121168b7 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -392,8 +392,9 @@ internal void OnRemoveNetworkObjectBridge() } NetworkObjectBridge = null; - HadBridge = false; + HasGhost = false; + // NGO synchronizes the transform again once the prefab is no longer hybrid SynchronizeTransform = true; EditorApplication.update -= OnRemoveNetworkObjectBridge; From 6c444570d8bac87566f0a7cb82725aaa4b2f37c6 Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Fri, 23 Oct 2026 12:50:22 -0500 Subject: [PATCH 3/9] fix: revalidate after GhostObject removal and guard deferred editor callbacks The GhostObject removal hook now re-runs UnifiedValidation on the next editor tick, so GhostObject and HasGhost match the components on the prefab stage root, and the bridge comes back when the hook fired for a different GhostBehaviour. The deferred add/remove callbacks unsubscribe first and return when their NetworkObject was destroyed. --- .../Editor/NetworkObjectEditor.cs | 8 ++++++++ .../Components/Helpers/NetworkObjectBridge.cs | 8 +------- .../Runtime/Core/NetworkObject.cs | 19 ++++++++++++++++--- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/com.unity.netcode.gameobjects/Editor/NetworkObjectEditor.cs b/com.unity.netcode.gameobjects/Editor/NetworkObjectEditor.cs index 56833a679f..d57bfc04e9 100644 --- a/com.unity.netcode.gameobjects/Editor/NetworkObjectEditor.cs +++ b/com.unity.netcode.gameobjects/Editor/NetworkObjectEditor.cs @@ -45,6 +45,14 @@ private static void OnGhostObjectPreRemoval(GameObject gameObject) if (networkObject != null) { networkObject.OnRemoveNetworkObjectBridge(); + // Revalidate once the removal completes: OnValidate is skipped on the prefab stage root, and this hook fires for any GhostBehaviour. + EditorApplication.delayCall += () => + { + if (networkObject) + { + networkObject.UnifiedValidation(); + } + }; } } #endif diff --git a/com.unity.netcode.gameobjects/Runtime/Components/Helpers/NetworkObjectBridge.cs b/com.unity.netcode.gameobjects/Runtime/Components/Helpers/NetworkObjectBridge.cs index db4e411781..8e79485ebf 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/Helpers/NetworkObjectBridge.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/Helpers/NetworkObjectBridge.cs @@ -43,13 +43,7 @@ private void OnValidate() var networkObject = GetComponent(); if (networkObject == null) { - EditorApplication.delayCall += () => - { - if (this != null) - { - DestroyImmediate(this, true); - } - }; + EditorApplication.delayCall += () => DestroyImmediate(this, true); return; } diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 36121168b7..e1db16746a 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -369,17 +369,27 @@ private void CheckForInScenePlaced() [SerializeField] internal bool HadBridge; #if UNITY_EDITOR + // The update callbacks unsubscribe first so one whose NetworkObject was destroyed doesn't throw every frame. private void OnAddNetworkObjectBridge() { + EditorApplication.update -= OnAddNetworkObjectBridge; + if (this == null) + { + return; + } NetworkObjectBridge = gameObject.AddComponent(); HadBridge = true; // Transform synchronization is handled by unified netcode SynchronizeTransform = false; - EditorApplication.update -= OnAddNetworkObjectBridge; } internal void OnRemoveNetworkObjectBridge() { + EditorApplication.update -= OnRemoveNetworkObjectBridge; + if (this == null) + { + return; + } var networkObjectBridge = gameObject.GetComponent(); if (networkObjectBridge != null) { @@ -397,16 +407,19 @@ internal void OnRemoveNetworkObjectBridge() // NGO synchronizes the transform again once the prefab is no longer hybrid SynchronizeTransform = true; - EditorApplication.update -= OnRemoveNetworkObjectBridge; } private void OnRemoveGhostObject() { + EditorApplication.update -= OnRemoveGhostObject; + if (this == null) + { + return; + } OnRemoveNetworkObjectBridge(); DestroyImmediate(GhostObject); GhostObject = null; HasGhost = false; - EditorApplication.update -= OnRemoveGhostObject; } internal void UnifiedValidation() From a182f27ab5f8943f78b05cc3a4e6674e20470356 Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Fri, 23 Oct 2026 13:41:07 -0500 Subject: [PATCH 4/9] chore: retrigger CI From 3454143d8d5282d79f50860b9cac224584703d38 Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Fri, 23 Oct 2026 15:58:01 -0500 Subject: [PATCH 5/9] fix: drop the GhostObject removal callback and keep the user's ghost modes UnifiedValidation removes a leftover NetworkObjectBridge, so the GhostObjectEditor pre-removal callback is no longer needed. It also fired when the inspection component was removed, and the rebuilt bridge reset SupportedGhostModes. The bridge no longer forces SupportedGhostModes to Interpolated. --- .../Documentation~/ghostobjects.md | 2 +- .../Editor/NetworkObjectEditor.cs | 35 ------------------- .../Components/Helpers/NetworkObjectBridge.cs | 12 ------- .../Runtime/Core/NetworkObject.cs | 2 +- 4 files changed, 2 insertions(+), 49 deletions(-) diff --git a/com.unity.netcode.gameobjects/Documentation~/ghostobjects.md b/com.unity.netcode.gameobjects/Documentation~/ghostobjects.md index 5e135b699a..333d3e6f8d 100644 --- a/com.unity.netcode.gameobjects/Documentation~/ghostobjects.md +++ b/com.unity.netcode.gameobjects/Documentation~/ghostobjects.md @@ -19,7 +19,7 @@ A hybrid object synchronizes its data through two separate systems: Because Netcode for GameObjects data isn't part of the snapshot, Unity Netcode doesn't roll back or replay `NetworkVariable`s, RPCs, or spawn data as part of its prediction loop. To use prediction on a hybrid object, write your prediction code in `GhostBehaviour`s. Prediction code in `GhostBehaviour`s on hybrid objects works the same way as on any other GhostObject. -When you convert a NetworkObject into a hybrid object, Netcode for GameObjects sets the GhostObject's supported ghost modes to interpolated only. To use prediction, change the `SupportedGhostModes` and `DefaultGhostMode` settings on the **GhostObject** component to include a predicted mode. For more information about ghost modes, refer to the [Unity Netcode documentation](https://docs.unity3d.com/Packages/com.unity.netcode@latest). +When you convert a NetworkObject into a hybrid object, Netcode for GameObjects doesn't change the GhostObject's ghost mode settings. By default, a GhostObject spawns interpolated and supports all ghost modes. To use prediction, set `DefaultGhostMode` on the **GhostObject** component to a predicted mode. To reduce bandwidth, limit `SupportedGhostModes` to the modes the object uses. For more information about ghost modes, refer to the [Unity Netcode documentation](https://docs.unity3d.com/Packages/com.unity.netcode@latest). ### Effect on existing transform and physics components diff --git a/com.unity.netcode.gameobjects/Editor/NetworkObjectEditor.cs b/com.unity.netcode.gameobjects/Editor/NetworkObjectEditor.cs index d57bfc04e9..256baa72c6 100644 --- a/com.unity.netcode.gameobjects/Editor/NetworkObjectEditor.cs +++ b/com.unity.netcode.gameobjects/Editor/NetworkObjectEditor.cs @@ -1,7 +1,4 @@ using System.Collections.Generic; -#if UNIFIED_NETCODE -using Unity.Netcode.Editor; -#endif using UnityEditor; using UnityEngine; using UnityEngine.Scripting.APIUpdating; @@ -25,38 +22,6 @@ public class NetworkObjectEditor : UnityEditor.Editor private static readonly string[] k_HiddenFields = { "m_Script" }; -#if UNIFIED_NETCODE - /// - /// Register for the GhostObject removal event. - /// - [InitializeOnLoadMethod] - private static void OnApplicationStart() - { - GhostObjectEditor.OnGhostObjectPreRemoval = OnGhostObjectPreRemoval; - } - - /// - /// Callback to remove the NetworkObjectBridge prior to removing GhostObject. - /// - /// The with the component being removed. - private static void OnGhostObjectPreRemoval(GameObject gameObject) - { - var networkObject = gameObject.GetComponent(); - if (networkObject != null) - { - networkObject.OnRemoveNetworkObjectBridge(); - // Revalidate once the removal completes: OnValidate is skipped on the prefab stage root, and this hook fires for any GhostBehaviour. - EditorApplication.delayCall += () => - { - if (networkObject) - { - networkObject.UnifiedValidation(); - } - }; - } - } -#endif - private void Initialize() { if (m_Initialized) diff --git a/com.unity.netcode.gameobjects/Runtime/Components/Helpers/NetworkObjectBridge.cs b/com.unity.netcode.gameobjects/Runtime/Components/Helpers/NetworkObjectBridge.cs index 8e79485ebf..de8c5172fb 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/Helpers/NetworkObjectBridge.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/Helpers/NetworkObjectBridge.cs @@ -18,18 +18,6 @@ namespace Unity.Netcode internal partial class NetworkObjectBridge : GhostBehaviour { #if UNITY_EDITOR - private void Reset() - { - // Start users with just interpolation (they can adjust this if they want prediction) - // to make the initial transition less problematic for users. - // Only set when the bridge is first added, so a user's prediction setting is kept. - var ghostAdapter = GetComponent(); - if (ghostAdapter != null) - { - ghostAdapter.SupportedGhostModes = GhostModeMask.Interpolated; - } - } - private void OnValidate() { hideFlags = HideFlags.HideInInspector; diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index e1db16746a..236afec35a 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -383,7 +383,7 @@ private void OnAddNetworkObjectBridge() SynchronizeTransform = false; } - internal void OnRemoveNetworkObjectBridge() + private void OnRemoveNetworkObjectBridge() { EditorApplication.update -= OnRemoveNetworkObjectBridge; if (this == null) From 110f8bc851e1ef1b6867df6c3158571d5a7cec5d Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Mon, 26 Oct 2026 15:43:05 -0500 Subject: [PATCH 6/9] Clean up NetworkObjectBridge by removing physics code Removed unused physics-related code and comments. --- .../Components/Helpers/NetworkObjectBridge.cs | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Components/Helpers/NetworkObjectBridge.cs b/com.unity.netcode.gameobjects/Runtime/Components/Helpers/NetworkObjectBridge.cs index de8c5172fb..d89a8c1694 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/Helpers/NetworkObjectBridge.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/Helpers/NetworkObjectBridge.cs @@ -32,29 +32,7 @@ private void OnValidate() if (networkObject == null) { EditorApplication.delayCall += () => DestroyImmediate(this, true); - return; } - -#if COM_UNITY_MODULES_PHYSICS - var rigidBody = GetComponent(); - var ghostRigidBody = GetComponent(); - if (rigidBody != null) - { - // This must be enabled when replicating the rigid body. - - ghostAdapter.FullTickSmoothing = FullTickSmoothingMode.Interpolate; - // TODO: Currently, this is added only if you enable replication of the rigid body. - // There is a bug where if you don't add this component it doesn't synchronize the transform. - // Remove this once the issue is resolved. - if (ghostRigidBody == null) - { - gameObject.AddComponent(); - } - } -#endif -#if COM_UNITY_MODULES_PHYSICS2D - // TODO: Fill out a similar script as above but for the 2D version -#endif } #endif From 68bab5e82b822b0c26f7c94a02aa73b76bcbc060 Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Mon, 26 Oct 2026 15:48:27 -0500 Subject: [PATCH 7/9] Refactor HybridParentUpdate and remove ApplyScale method Removed the ApplyScale method and commented out the current position and rotation logging. --- .../Runtime/Components/Helpers/NetworkObjectBridge.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Components/Helpers/NetworkObjectBridge.cs b/com.unity.netcode.gameobjects/Runtime/Components/Helpers/NetworkObjectBridge.cs index d89a8c1694..4f82321d52 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/Helpers/NetworkObjectBridge.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/Helpers/NetworkObjectBridge.cs @@ -52,17 +52,12 @@ private void OnValidate() /// instance's entity's PostTransformMatrix. internal void HybridParentUpdate(Vector3 scale) { - var current = Ghost.GetPositionAndRotation(); + //var current = Ghost.GetPositionAndRotation(); //Debug.Log($"---- Current LT: {current.Position} | {current.Rotation.eulerAngles}"); //Debug.Log($"---- New LT: {transform.localPosition} | {transform.localRotation}"); Ghost.ApplyPostTransformMatrixScale(scale); } - internal void ApplyScale(Vector3 scale) - { - Ghost.ApplyPostTransformMatrixScale(scale); - } - /// /// Keeps the ghost's owner in step with the owner, so an owner-predicted /// ghost is predicted by the NGO owner. From f8e8e75c669e4763b5a58595ebddb0a4d7f5c6f8 Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Tue, 27 Oct 2026 12:36:07 -0500 Subject: [PATCH 8/9] style remove whitespaces --- .../Runtime/Components/Helpers/NetworkObjectBridge.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Components/Helpers/NetworkObjectBridge.cs b/com.unity.netcode.gameobjects/Runtime/Components/Helpers/NetworkObjectBridge.cs index fd542e0c90..94dc52ceb2 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/Helpers/NetworkObjectBridge.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/Helpers/NetworkObjectBridge.cs @@ -27,7 +27,7 @@ private void OnValidate() { return; } - + var networkObject = GetComponent(); if (networkObject == null) { From fac0d6ff4c2545daa3d2ee8a3dc2c442c1b98e63 Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Tue, 27 Oct 2026 13:36:22 -0500 Subject: [PATCH 9/9] fix: keep the bridge when a script adds it before the NetworkObject NetworkObjectBridge.OnValidate queued a DestroyImmediate whenever no NetworkObject was present yet. Test helpers (and user scripts) add the bridge first, so the bridge was destroyed after the prefab was set up, breaking every unified hybrid test. The deferred call now checks again before destroying. --- .../Runtime/Components/Helpers/NetworkObjectBridge.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Components/Helpers/NetworkObjectBridge.cs b/com.unity.netcode.gameobjects/Runtime/Components/Helpers/NetworkObjectBridge.cs index 94dc52ceb2..e54543af5d 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/Helpers/NetworkObjectBridge.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/Helpers/NetworkObjectBridge.cs @@ -31,8 +31,14 @@ private void OnValidate() var networkObject = GetComponent(); if (networkObject == null) { - // Remove the bridge if there is no NetworkObject - EditorApplication.delayCall += () => DestroyImmediate(this, true); + // Check again once the current operation finishes, since a script can add the bridge before the NetworkObject. + EditorApplication.delayCall += () => + { + if (this != null && GetComponent() == null) + { + DestroyImmediate(this, true); + } + }; } else {