Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
bc6a99d
test: add hybrid prefab prediction and interop tests
NoelStephensUnity Sep 30, 2026
a683fdc
test: cover NetworkVariable writes from prediction and NGO ownership …
NoelStephensUnity Sep 30, 2026
f6fee1d
fix: hybrid prefab issues found by the interop tests
NoelStephensUnity Oct 1, 2026
5fe98da
fix: set the ghost owner before OnNetworkSpawn
NoelStephensUnity Oct 1, 2026
d4fda26
fix: guard the host world in UpdateGhostOwner and wait for a re-simul…
NoelStephensUnity Oct 1, 2026
5e8e7e7
fix: raise the client's own disconnect event when a hybrid client dis…
NoelStephensUnity Oct 1, 2026
b60f7a1
fix: only register hybrid instances as pending ghosts on clients in i…
NoelStephensUnity Oct 1, 2026
2897835
fix: only send scene migrations to observers, and synchronize late hy…
NoelStephensUnity Oct 1, 2026
423c9e2
fix: spawn a NetworkObject shown after a scene migration in the serve…
NoelStephensUnity Oct 1, 2026
c85e173
Merge branch 'develop-3.x.x' into feat/hybrid-interop-tests
NoelStephensUnity Oct 2, 2026
c642272
Merge branch 'develop-3.x.x' into feat/hybrid-interop-tests
NoelStephensUnity Oct 4, 2026
cbad3c1
Merge branch 'develop-3.x.x' into feat/hybrid-interop-tests
NoelStephensUnity Oct 4, 2026
8c8e908
test: collapse the DisconnectTests owner persistence fixtures into th…
NoelStephensUnity Oct 4, 2026
448fd28
fix: adapt the hybrid tests to the N4E 7.1.0 GameObject API renames
NoelStephensUnity Oct 5, 2026
fd755ee
chore: remove stale comments from NetworkObjectBridge
NoelStephensUnity Oct 5, 2026
410747e
style
NoelStephensUnity Oct 5, 2026
7725811
feat: warn when a hybrid instance sends an RPC or writes a NetworkVar…
NoelStephensUnity Oct 5, 2026
37c1d3d
update
NoelStephensUnity Oct 5, 2026
260b89b
Merge branch 'feat/hybrid-interop-tests' of https://github.com/Unity-…
NoelStephensUnity Oct 5, 2026
db29ca1
update
NoelStephensUnity Oct 5, 2026
4c9db62
test
NoelStephensUnity Oct 5, 2026
2a85f6a
update
NoelStephensUnity Oct 5, 2026
1e8f6bc
fix: apply the scene migration fixes in distributed authority mode
NoelStephensUnity Oct 6, 2026
c409f8f
chore: correct the comment on when the CMB service is sent a scene mi…
NoelStephensUnity Oct 6, 2026
3545d29
test
NoelStephensUnity Oct 6, 2026
8d87bfb
Merge branch 'develop-3.x.x' into feat/hybrid-interop-tests
NoelStephensUnity Oct 6, 2026
297190d
Merge branch 'develop-3.x.x' into feat/hybrid-interop-tests
NoelStephensUnity Oct 6, 2026
2a75457
Merge develop-3.x.x into feat/hybrid-interop-tests
netcode-ci-service Oct 6, 2026
02df7a9
Change UseUnifiedTests to return false
NoelStephensUnity Oct 7, 2026
17414f3
Modify test configuration for unified tests
NoelStephensUnity Oct 7, 2026
0730f26
Fix formatting in NetworkObjectSceneMigrationObserverTests
NoelStephensUnity Oct 7, 2026
13cb82b
Merge branch 'develop-3.x.x' into feat/hybrid-interop-tests
NoelStephensUnity Oct 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions com.unity.netcode.gameobjects/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ Additional documentation and release notes are available at [Multiplayer Documen

### Fixed

- Issue where moving a `NetworkObject` into another scene made the clients that did not observe it log "Trying to synchronize NetworkObjectId but it was not spawned". The scene migration is now only sent to the clients that observe the `NetworkObject`. (#4176)
- Issue where a `NetworkObject` that was moved into another scene while hidden from a client spawned in that client's active scene when it was shown with `NetworkShow`, instead of the scene it is in on the server. (#4176)
- 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. (#4184)

### Security
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,27 @@
namespace Unity.Netcode
{
/// <summary>
/// TODO-UNIFIED: Needs further peer review and exploring alternate ways of handling this.
/// This is a component that is added to the root of all N4E-spawned hybrid prefab instances. It is used to link
/// <see cref="NetworkObject.SerializedObject"/> the N4E-spawned hybrid prefab instances to the incoming <see cref="CreateObjectMessage"/>
/// specific to the N4E-spawned hybrid prefab instance that has the matching <see cref="NetworkObjectId"/>.
/// </summary>

[DefaultExecutionOrder(GhostObject.ExecutionOrder + 1)]
//BREAK --- Fix this on UNIFIED side 1st
// Internal: GhostBehaviour is only public when NETCODE_GAMEOBJECT_BRIDGE_EXPERIMENTAL is defined, and a public
// type cannot derive from an internal one.
internal partial class NetworkObjectBridge : GhostBehaviour
{
// DefaultExecutionOrder
// TODO: Define a const for the value used on GhostObject and use that value
// to set the execution order so if it changes on GhostObject it updates here.
#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<GhostObject>();
if (ghostAdapter != null)
{
ghostAdapter.SupportedGhostModes = GhostModeMask.Interpolated;
}
}

private void OnValidate()
{
hideFlags = HideFlags.HideInInspector;
Expand All @@ -35,10 +40,6 @@ private void OnValidate()
return;
}

// Start users with just interpolation (they can adjust this if they want prediction)
// to make the initial transition less problematic for users.
ghostAdapter.SupportedGhostModes = GhostModeMask.Interpolated;

#if COM_UNITY_MODULES_PHYSICS
var rigidBody = GetComponent<Rigidbody>();
var ghostRigidBody = GetComponent<GhostRigidbody>();
Expand Down Expand Up @@ -88,6 +89,41 @@ internal void ApplyScale(Vector3 scale)
{
Ghost.ApplyPostTransformMatrixScale(scale);
}

/// <summary>
/// Keeps the ghost's owner in step with the <see cref="NetworkObject"/> owner, so an owner-predicted
/// ghost is predicted by the NGO owner.
/// </summary>
/// <remarks>Only the server can assign a ghost owner; N4E replicates it to clients.</remarks>
/// <param name="networkManager">The server's <see cref="NetworkManager"/>.</param>
/// <param name="ownerClientId">The new NGO owner.</param>
internal void UpdateGhostOwner(NetworkManager networkManager, ulong ownerClientId)
{
if (!networkManager.IsServer || !Ghost.HasOwner)
{
return;
}

// Owned by the server with no local client means no client owns the ghost.
var ownerNetworkId = default(NetworkId);
if (ownerClientId == networkManager.LocalClientId)
{
if (networkManager.IsHost && networkManager.NetcodeWorld != null)
{
ownerNetworkId = networkManager.NetcodeWorld.LocalConnection.NetworkId;
Comment thread
NoelStephensUnity marked this conversation as resolved.
}
}
else
{
// The unified transport id is the client's N4E network id.
var (transportId, found) = networkManager.ConnectionManager.ClientIdToTransportId(ownerClientId);
if (found)
{
ownerNetworkId = new NetworkId { Value = (int)transportId };
}
}
Ghost.OwnerNetworkId = ownerNetworkId;
}
}

#if UNIFIED_NETCODE_7_0_0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#endif
#if UNIFIED_NETCODE
using System;
using System.Collections.Generic;
using Unity.Entities;
#if !UNIFIED_NETCODE_7_0_0
using Unity.NetCode;
Expand All @@ -27,6 +28,32 @@ internal class UnifiedBootstrap : ClientServerBootstrap

private static int s_WorldCounter = 0;

// Every ClientServerBootstrap constructor clears N4E's ServerWorlds and ClientWorlds, and each NetworkManager
// creates its own bootstrap, so the worlds created for other NetworkManagers are registered again.
private static readonly List<NetcodeWorld> s_CreatedWorlds = new List<NetcodeWorld>();

private static void RegisterCreatedWorlds()
Comment thread
NoelStephensUnity marked this conversation as resolved.
{
for (int i = s_CreatedWorlds.Count - 1; i >= 0; i--)
{
var world = s_CreatedWorlds[i];
if (!world.IsCreated)
{
s_CreatedWorlds.RemoveAt(i);
continue;
}
// A single world host is registered as both a server and a client world, the same as N4E does.
if (world.IsServer() && !ServerWorlds.Contains(world))
{
ServerWorlds.Add(world);
}
if (world.IsClient() && !ClientWorlds.Contains(world))
{
ClientWorlds.Add(world);
}
}
}

public override bool Initialize(string defaultWorldName)
{
var networkManager = CurrentNetworkManagerForInitialization;
Expand All @@ -44,6 +71,7 @@ public override bool Initialize(string defaultWorldName)

if (networkManager != null)
{
RegisterCreatedWorlds();
Debug.Log($"Starting a world for {(networkManager.IsServer ? "Host" : "Client")}");
s_WorldCounter++;
LastCreatedWorld = networkManager.IsServer ? CreateSingleWorldHost($"HostSingleWorld-{s_WorldCounter}")
Expand All @@ -69,6 +97,7 @@ public override bool Initialize(string defaultWorldName)
}

networkManager.NetcodeWorld = (NetcodeWorld)LastCreatedWorld;
s_CreatedWorlds.Add(networkManager.NetcodeWorld);
}
else
{
Expand Down
57 changes: 57 additions & 0 deletions com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,54 @@ internal enum __RpcExecStage
private const int k_RpcMessageDefaultSize = 1024; // 1k
private const int k_RpcMessageMaximumSize = 1024 * 64; // 64k

#if UNIFIED_NETCODE
private bool m_WarnedRpcInPredictionLoop;
private bool m_WarnedNetworkVariableInPredictionLoop;

/// <summary>
/// Whether this hybrid instance's ghost is currently inside the Netcode for Entities prediction loop.
/// </summary>
private bool IsInGhostPredictionLoop()
{
if (m_NetworkObject == null || !m_NetworkObject.HasGhost)
{
return false;
}
var ghostObject = m_NetworkObject.GhostObject;
return ghostObject != null && ghostObject.IsSpawned && ghostObject.NetworkTime.IsInPredictionLoop;
}

// RPCs and NetworkVariables are not rolled back or tick aligned, so a re-simulated tick sends or writes them again.
// Warned once per NetworkBehaviour.
private void WarnIfRpcSentInPredictionLoop()
{
if (m_WarnedRpcInPredictionLoop || !IsInGhostPredictionLoop())
{
return;
}
m_WarnedRpcInPredictionLoop = true;
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning($"[{name}][{GetType().Name}] An RPC was sent from inside the Netcode for Entities prediction loop, which is not supported. " +
"RPCs are not part of prediction and are sent again for every re-simulated tick.");
}
}

internal void WarnIfNetworkVariableWrittenInPredictionLoop(string variableName)
{
if (m_WarnedNetworkVariableInPredictionLoop || !IsInGhostPredictionLoop())
{
return;
}
m_WarnedNetworkVariableInPredictionLoop = true;
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning($"[{name}][{GetType().Name}][{variableName}] A NetworkVariable was written from inside the Netcode for Entities prediction loop, which is not supported. " +
"NetworkVariables are not rolled back, so re-simulated ticks write it again and its value can move backwards.");
}
}
#endif

#pragma warning disable IDE1006 // disable naming rule violation check
// RuntimeAccessModifiersILPP will make this `protected`
internal FastBufferWriter __beginSendServerRpc(uint rpcMethodId, ServerRpcParams serverRpcParams, RpcDelivery rpcDelivery)
Expand All @@ -83,6 +131,9 @@ internal FastBufferWriter __beginSendServerRpc(uint rpcMethodId, ServerRpcParams
{
throw new RpcException("The NetworkBehaviour must be spawned before calling this method.");
}
#if UNIFIED_NETCODE
WarnIfRpcSentInPredictionLoop();
#endif

return new FastBufferWriter(k_RpcMessageDefaultSize, Allocator.Temp, k_RpcMessageMaximumSize);
}
Expand Down Expand Up @@ -161,6 +212,9 @@ internal FastBufferWriter __beginSendClientRpc(uint rpcMethodId, ClientRpcParams
{
throw new RpcException("The NetworkBehaviour must be spawned before calling this method.");
}
#if UNIFIED_NETCODE
WarnIfRpcSentInPredictionLoop();
#endif

return new FastBufferWriter(k_RpcMessageDefaultSize, Allocator.Temp, k_RpcMessageMaximumSize);
}
Expand Down Expand Up @@ -339,6 +393,9 @@ internal FastBufferWriter __beginSendRpc(uint rpcMethodId, RpcParams rpcParams,
{
throw new RpcException("This RPC can only be sent by its owner.");
}
#if UNIFIED_NETCODE
WarnIfRpcSentInPredictionLoop();
#endif
return new FastBufferWriter(k_RpcMessageDefaultSize, Allocator.Temp, k_RpcMessageMaximumSize);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1843,8 +1843,11 @@ internal void ShutdownInternal()
{
try
{
// Dispose of all worlds
World.DisposeAllWorlds();
// Dispose only this NetworkManager's world. Other NetworkManagers in the same process keep theirs.
if (NetcodeWorld != null && NetcodeWorld.IsCreated)
{
NetcodeWorld.Dispose();
}
// Clear the world assigned from previous session
NetcodeWorld = null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,13 @@ internal static void CreateObject(ref NetworkManager networkManager, ulong sende
networkManager.Log.ErrorServer(new Context(LogLevel.Developer, $"Failed to deserialize {nameof(NetworkObject)}.").AddInfo(nameof(NetworkObject.GlobalObjectIdHash), serializedObject.Hash).AddInfo(nameof(NetworkObject.NetworkObjectId), serializedObject.NetworkObjectId));
return;
}

// A NetworkObject shown to this client after it migrated into another scene was instantiated in the
// active scene, and the migration was only sent to the clients observing it at the time.
if (networkManager.NetworkConfig.EnableSceneManagement)
{
networkManager.SceneManager.SynchronizeNetworkObjectScene(networkObject);
}
}
else
{
Expand Down Expand Up @@ -225,6 +232,12 @@ internal static void CreateObject(ref NetworkManager networkManager, ulong sende
networkManager.Log.ErrorServer(new Context(LogLevel.Developer, $"Failed to deserialize {nameof(NetworkObject)}.").AddInfo(nameof(NetworkObject.GlobalObjectIdHash), serializedObject.Hash).AddInfo(nameof(NetworkObject.NetworkObjectId), serializedObject.NetworkObjectId));
return;
}

// Same as client-server: the migration was only sent to the clients observing it at the time
if (networkManager.NetworkConfig.EnableSceneManagement)
{
networkManager.SceneManager.SynchronizeNetworkObjectScene(networkObject);
}
}

// DA - NGO CMB SERVICE NOTES:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,12 @@ public virtual void SetDirty(bool isDirty)

if (m_IsDirty)
{
#if UNIFIED_NETCODE
if (m_NetworkBehaviour != null)
{
m_NetworkBehaviour.WarnIfNetworkVariableWrittenInPredictionLoop(Name);
}
#endif
MarkNetworkBehaviourDirty();
}
}
Expand Down
Loading
Loading