Skip to content

fix: hybrid prefab component gating and distributed authority rejection - #4184

Merged
NoelStephensUnity merged 8 commits into
develop-3.x.xfrom
fix/hybrid-prefab-component-gating
Oct 7, 2026
Merged

NoelStephensUnity merged 8 commits into
develop-3.x.xfrom
fix/hybrid-prefab-component-gating

Conversation

@NoelStephensUnity

@NoelStephensUnity NoelStephensUnity commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

Purpose of this PR

This PR assures the NetworkBehaviourId values on hybrid prefabs are maintained by keeping NetworkTransform and NetworkRigidbody effectively a "nop" on the instance instead of destroying them at runtime like the POC was doing. Included in this PR, hybrid prefabs are rejected when using a distributed authority network topology.

  • NetworkRigidbodyBase derived classes were originally destroyed in the POC during runtime on hybrid prefab instances. This PR keeps the components but turns them into "nop components". This fixes some edge cases:

    • The component was removed from ChildNetworkBehaviours while Destroy is deferred to the end of the frame.
    • NetworkVariable synchronization is NetworkBehaviourId relative, so any deviation from the original indices could break delta synchronization.
    • Rpcs on the runtime adjusted could be impacted the same way due to the change in NetworkBehaviour indices.
    • NetworkRigidbodyBase now early-exits on HasGhost similar to NetworkTransform. The body is made kinematic on every peer except the server at spawn, and its authored kinematic state is restored on destroy.
    • An ownership change no longer re-applies the authority kinematic rule to a hybrid prefab's body (with an owner authoritative NetworkTransform, the new owner's body would become dynamic).
  • NetworkTransform needed additional HasGhost gating within OnSynchronize, InternalOnNetworkPostSpawn, InternalOnNetworkSessionSynchronized and InternalOnNetworkObjectParentChanged. A hybrid object no longer writes a full transform state to every late joining client.

    • NetworkTransform.UnregisterRigidbody had no call sites. It is now called from the gate, so reparenting a hybrid object that has a NetworkRigidbody no longer dereferences a destroyed component.
  • [RequireComponent(typeof(Rigidbody))] and [RequireComponent(typeof(Rigidbody2D))] are unconditional again. Under UNIFIED_NETCODE they were dropped from every NetworkRigidbody in the project, hybrid or not.

  • The final suggested pattern, if a user decides to keep the hybrid prefab setting, will be to completely remove NetworkTransform and NetworkRigidbody.

  • The UNIFIED_NETCODE_DESTROY block in NetworkObject is removed (that define is never used and was a left over artifact from the POC).

  • Distributed authority does not support hybrid prefabs:

    • Before this PR, if a user were to try and start a distributed authority session and had hybrid prefab(s) registered, it would replaced the transport with UnifiedNetcodeTransport and attempt to start the session.
    • Users attempting to do this will now see an error message and the NetworkManager will shutdown before attempting to start the session.
    • Any hybrid prefab added during a distributed authority session is rejected.
  • NetworkPrefab overrides are not supported for hybrid prefabs (yet):

    • N4E instantiates the server's prefab from the ghost type, so an NGO override is never applied on clients.
    • NetworkPrefab.Validate now treats an override with a hybrid source or target prefab as invalid (in any network topology).
    • Users will see an error message naming the override, and the entry is ignored like any other invalid override.
  • NetworkPrefabs subscribed to each NetworkPrefabsList again every time it was initialized, so a prefab added to a list during a session was registered more than once. It now keeps a single subscription, and HasGhostPrefabs is recomputed each time the prefabs are initialized.

  • NetworkPrefab.GetDebugName names a prefab, including its override target, for the prefab logs and the debugger (DebuggerDisplay).

  • NetcodeIntegrationTest.CreateNetworkObjectPrefab(string, bool) creates a prefab without a GhostObject in a unified test.

PR Scope:

NetworkObject, NetworkTransform, NetworkRigidbodyBase, NetworkRigidbody, NetworkRigidbody2D, NetworkManager, NetworkConfig, NetworkPrefab, NetworkPrefabs and NetworkPrefabHandler. All changes are within UNIFIED_NETCODE paths except the NetworkPrefabsList subscription fix and GetDebugName. No wire format changes.

Out of scope:

  • Supporting NetworkPrefab overrides for hybrid prefabs (needs N4E support for instantiating a different prefab per client).
  • NetworkRigidbody integration coverage (NetworkRigidbodyTest never compiles because the test assembly is missing the physics2d versionDefine. This will be fixed in a separate PR using the fix/rigidbody-tests-never-compiled branch).
  • As an example, pre-existing issues noticed but not changed here:
    • The transport replaced by UnifiedNetcodeTransport is not restored after a hybrid session ends.
    • NetworkClient.SetRole leaves IsServer set when it rejects a distributed authority StartServer, so destroying that NetworkManager throws (plain NGO, both develop lines; will be fixed in a separate PR).

Jira ticket

N/A

Changelog

  • Fixed: Issue where a prefab added to a NetworkPrefabsList while a session was running was registered more than once and logged a duplicate GlobalObjectIdHash error.

Documentation

  • No documentation changes or additions were necessary (NGO hybrid prefab documentation does not exist yet. "Distributed authority does not support hybrid prefabs" should be clearly stated when this is put together).

Testing & QA (How your changes can be verified during release Playtest)

Fixture Passed Failed
UnifiedHybridPrefabBehaviourIdTests (UnifiedHost) 1 0
UnifiedHybridPrefabValidationTests (UnifiedHost) 4 0
Full unified pass (UNIFIED_TESTS=true, all runtime tests) 237 0 (non-unified fixtures skipped by design, plus 4 pre-existing ignores)
Non-unified regression: DAHost, ownership, prefab, NetworkTransform, NetworkRigidbody and NetworkManagerStartExceptionTests fixtures 4186 0 (40 skipped: hybrid-only fixtures in the non-unified run, plus pre-existing ignores)

Functional Testing

Manual testing :

  • Manual testing done

Automated tests:

  • Covered by existing automated tests
  • Covered by new automated tests

Does the change require QA team to:

  • Review automated tests?
  • Execute manual tests?
  • Provide feedback about the PR?

Up-port

Not needed. This PR targets develop-3.x.x.

Backports

The hybrid prefab changes are not needed on develop-2.0.0 (hybrid prefabs exist on develop-3.x.x only). The NetworkPrefabsList subscription fix applies to develop-2.0.0 as well, and a port will follow.

A hybrid prefab's NetworkRigidbodyBase derivatives were destroyed at runtime so a user could
try hybrid mode without first stripping components from the prefab. Removing an entry from
ChildNetworkBehaviours is not safe: InitializeChildNetworkBehaviours rebuilds unconditionally
and has five call sites, two of them the public GetNetworkBehaviourOrderIndex and
GetNetworkBehaviourAtOrderIndex, while Destroy is deferred to end of frame. A rebuild in a
later frame therefore renumbers every behaviour after the removed one, and the NetworkVariable
half of object synchronization is positional with no id, so a one-entry disagreement corrupts
the whole buffer for that object.

NetworkTransform was already handled by early-returning on HasGhost rather than being
destroyed - the removal behind UNIFIED_NETCODE_DESTROY was dead, that define exists nowhere.
This extends the same treatment to NetworkRigidbodyBase and closes four NetworkTransform paths
that had no gate: OnSynchronize, InternalOnNetworkPostSpawn,
InternalOnNetworkSessionSynchronized and InternalOnNetworkObjectParentChanged. The first of
those means a hybrid object no longer writes a full teleport state to every joining client.

The rigidbody is now forced kinematic on non-server peers rather than destroyed, restored from
m_OriginalKinematicState on destroy as the class already provided for. That decision is made at
spawn, not during Awake, because Initialize runs from Awake where an in-scene placed instance
has no session to ask whether it is the server. UnregisterRigidbody had no call sites and is
now called from the gate, which is what keeps a second Initialize from leaving a registration
pointing at a component that no longer drives anything.

[RequireComponent(typeof(Rigidbody))] is unconditional again. Behind #if !UNIFIED_NETCODE it
was stripped from every NetworkRigidbody in the project the moment N4E was installed, hybrid or
not, after which NetworkRigidbodyBase dereferences a null rigidbody in Awake.
Distributed authority does not support hybrid prefabs. A start with any GhostObject prefab
registered now fails in CanStart with an error naming each prefab, before Initialize replaces
the transport with UnifiedNetcodeTransport. A hybrid prefab added during a distributed
authority session is refused, so HasGhostPrefabs cannot switch the running session into
hybrid mode and stop its send queue.
@NoelStephensUnity
NoelStephensUnity requested a review from a team as a code owner October 5, 2026 15:02
@u-pr

u-pr Bot commented Oct 5, 2026

Copy link
Copy Markdown

⚠️ Some settings were ignored in '.pr_agent.toml (repository root)' repo settings

Check the configuration's TOML syntax, permitted settings, and size limit. See the error below.


Error message:
Ignored prohibited settings: github_app.handle_pr_actions. Permitted settings were applied.

Configuration content:
# This is the configuration for the u-pr bot
# https://github.cds.internal.unity3d.com/unity/u-pr
# For configuration of this file:
# https://developer.portal.internal.unity.com/catalog/default/component/u-pr/docs/configuration/automatic_runs/

[github_app]
handle_pr_actions = ['opened', 'ready_for_review']  # PR events that auto-run pr_commands
pr_commands       = ["/harness-review"]             # what runs on those events
reviewer_commands = ["/harness-review"]             # what runs when @u-pr is added as reviewer

@u-pr u-pr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Harness Review

Needs changes

The change keeps hybrid components present to preserve behaviour IDs and adds distributed authority prefab checks, but the checks and rigidbody gating miss several reachable paths.

Reviewed commit 03ba4e1

🤖 Helpful? 👍/👎

Comment thread com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs Outdated
Comment thread com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs Outdated
Comment thread com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs Outdated
Comment thread com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs Outdated
- NetworkRigidbodyBase no longer re-applies the authority kinematic rule on an ownership change for a hybrid prefab.
- NetworkPrefabs recomputes HasGhostPrefabs on initialize, and keeps a single subscription to each NetworkPrefabsList
  (a prefab added to a list during a session was registered more than once).
- RejectGhostPrefabs is set in Initialize, after SetRole accepts the start, so a failed start cannot leave it set.
- A NetworkPrefab override with a hybrid source or target prefab is detected as a hybrid registration. N4E
  instantiates the server's prefab from the ghost type, so overrides are not supported for hybrid prefabs yet: a start
  with one registered fails in any topology, and one added during a session is rejected.
@codecov-github-com

codecov-github-com Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Codecov Report

Attention: Patch coverage is 37.50000% with 10 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...gameobjects/Runtime/Configuration/NetworkPrefab.cs 10.00% 9 Missing ⚠️
...meobjects/Runtime/Spawning/NetworkPrefabHandler.cs 0.00% 1 Missing ⚠️
@@                Coverage Diff                @@
##           develop-3.x.x    #4184      +/-   ##
=================================================
- Coverage          78.19%   78.17%   -0.03%     
=================================================
  Files                153      153              
  Lines              26272    26286      +14     
=================================================
+ Hits               20544    20549       +5     
- Misses              5728     5737       +9     
Flag Coverage Δ
NGOv2_project_testproject_ubuntu 76.62% <37.50%> (-0.03%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...objects/Runtime/Components/NetworkRigidBodyBase.cs 41.35% <100.00%> (+0.24%) ⬆️
...gameobjects/Runtime/Components/NetworkRigidbody.cs 100.00% <ø> (ø)
...meobjects/Runtime/Components/NetworkRigidbody2D.cs 0.00% <ø> (ø)
...gameobjects/Runtime/Components/NetworkTransform.cs 88.75% <ø> (ø)
...gameobjects/Runtime/Configuration/NetworkConfig.cs 63.12% <ø> (ø)
...ameobjects/Runtime/Configuration/NetworkPrefabs.cs 81.81% <100.00%> (+0.17%) ⬆️
...netcode.gameobjects/Runtime/Core/NetworkManager.cs 79.97% <ø> (ø)
....netcode.gameobjects/Runtime/Core/NetworkObject.cs 75.31% <ø> (ø)
...meobjects/Runtime/Spawning/NetworkPrefabHandler.cs 66.66% <0.00%> (ø)
...gameobjects/Runtime/Configuration/NetworkPrefab.cs 64.10% <10.00%> (-5.06%) ⬇️
Components Coverage Δ
com.unity.netcode.gameobjects 78.17% <37.50%> (-0.03%) ⬇️

ℹ️ Need help interpreting these results?

#if UNIFIED_NETCODE
// An override is a hybrid registration when either its source or its target prefab has a GhostObject.
HasGhost = (Override == NetworkPrefabOverride.Prefab && SourcePrefabToOverride.GetComponent<NetworkObject>().HasGhost)
|| (OverridingTargetPrefab.TryGetComponent(out NetworkObject targetNetworkObject) && targetNetworkObject.HasGhost);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, I have two thoughts here:

  1. I'm not sure that inside the Validate method is the right place to be setting this value. Validate is supposed to be "Is this valid to be a prefab or not", not "Is this valid to be a prefab and also please finish setting this prefab up for me".
  2. If we do leave setting this value in this function, I think it should be set in the various switch statements. We're already doing the TryGetComponent(out NetworkObject networkObject) in each of the branches of this function, GetComponent is expensive enough that we should be re-using the value we've already got.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call on both points. Validate no longer sets HasGhost for overrides at all: a hybrid override is now invalid, so it logs an error and returns false like the other invalid override cases, reusing the NetworkObject already fetched in the Prefab branch. The only HasGhost assignment left in Validate is the one already on develop-3.x.x for non-override prefabs.

@EmandM EmandM Oct 6, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HasGhost is still being set in Validate on line 181. I don't think the new logic here is fixing the issue. The structure of the code here should allow validation without having to make an additional GetComponent call.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only HasGhost assignment left in Validate is the one already on develop-3.x.x for non-override prefabs.

I put that there months ago and it is not part of this PR.

If we want to discuss changing this flow, then that would be a separate PR.

Comment thread com.unity.netcode.gameobjects/Runtime/Configuration/NetworkPrefab.cs Outdated
Comment thread com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs Outdated
Comment on lines +155 to +156
// SetRole keeps IsServer set when it rejects the start, which makes the NetworkManager throw when destroyed.
m_StandaloneNetworkManager.ConnectionManager.LocalClient.SetRole(false, false);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a bug! We should fix it!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, it's a bug in plain NGO on both develop lines. Keeping it out of this PR since it's unrelated to hybrid prefabs: it'll be its own PR on develop-2.0.0 with a test and a CHANGELOG entry, then up-ported. The test resets the role by hand until then.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or I could fix this here and do a back port... your call?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really depends on the size of the fix. If the fix is 10 lines or less, I'd rather it goes into this PR. If it's more complicated, happy for it to go on a separate PR.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is targeted for another PR that will make it cleaner to review.

- Move the distributed authority hybrid prefab check and its session flag into NetworkPrefabs, called through NetworkConfig.InitializePrefabsForStart.
- NetworkPrefab.Validate treats an override with a hybrid source or target as invalid, so it is logged and ignored in any topology.
- Add NetworkPrefab.GetDebugName, used by the prefab logs and the debugger.
- Add NetcodeIntegrationTest.CreateNetworkObjectPrefab(string, bool).
- Trim the hybrid prefab tests to the behaviour id and kinematic checks, and rework the validation tests onto the harness helpers.
// Owner authority makes an ungated NetworkRigidbody change the kinematic state on an ownership change.
m_Prefab.AddComponent<NetworkTransform>().AuthorityMode = NetworkTransform.AuthorityModes.Owner;
m_Prefab.AddComponent<Rigidbody>();
m_Prefab.AddComponent<NetworkRigidbody>();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth maybe adding a GhostRigidbody component for this test? It'd be like we did for the DisconnectTests, just a dummy component that basically checks that everything replicates like we expect.

Suggested change
m_Prefab.AddComponent<NetworkRigidbody>();
m_Prefab.AddComponent<NetworkRigidbody>();
m_Prefab.AddComponent<GhostRigidbody>();

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good thought. Reading N4E, GhostRigidbody only replicates to predicted clients and rewrites isKinematic from the server every prediction tick, so for a predicted hybrid prefab N4E would override the kinematic state NetworkRigidbody sets at spawn (interpolated clients don't get that data). #4176 adds predicted hybrid prefabs to CreateHybridPrefab, so I've logged it as a follow-up to test once both PRs land, along with skipping our kinematic override when a GhostRigidbody is present.

KinematicStateSurvivesOwnershipChange already runs the same behaviour table and gated component checks.
@NoelStephensUnity
NoelStephensUnity enabled auto-merge (squash) October 7, 2026 02:46
@NoelStephensUnity
NoelStephensUnity merged commit 15166f7 into develop-3.x.x Oct 7, 2026
28 of 29 checks passed
@NoelStephensUnity
NoelStephensUnity deleted the fix/hybrid-prefab-component-gating branch October 7, 2026 03:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants