Skip to content

Commit 69a99a9

Browse files
feat: auto-configure NetcodeConfig for hybrid mode
When Netcode for Entities is installed and a registered network prefab carries a GhostObject, the project's NetCodeConfig is now aligned with what NGO needs rather than leaving it to the user to discover. The two settings hybrid mode cannot run without (single world hosting, and automatic bootstrapping disabled since NetworkManager owns world creation) are corrected whenever they drift. The Netcode for Entities tick rates are driven from NetworkConfig.TickRate so that ghost transform updates land on the same interval as everything else. The snapshot, interpolation and transport values tuned against the 2000 instance stress test are applied once and then left alone, so a user's own edits survive; Project Settings > Multiplayer > Netcode for GameObjects can restore them. The config is never created here. Netcode for Entities already creates one unconditionally from its own InitializeOnLoadMethod, and creating a second lands the project in its multiple-config error path. Also: - UnifiedIsConfiguredCorrectly now validates EnableClientServerBootstrap and warns when the two tick rates diverge. Its two log messages were missing their string interpolation prefixes. - m_TempStreamSize returns to the Netcode for Entities default of 8192. GhostSendSystem takes max(TempStreamInitialSize, dataStream.Capacity), and capacity is DefaultSnapshotPacketSize, so 4192 had no effect. - Unity.Netcode.Editor.Tests gains the UNIFIED_NETCODE version define. Without it any hybrid editor test compiles away and reports zero cases rather than failing. - Adds a measurement fixture for sizing DefaultSnapshotPacketSize. It is marked Explicit so its 24 cases stay out of the suites.
1 parent 82bbda0 commit 69a99a9

15 files changed

Lines changed: 874 additions & 5 deletions

com.unity.netcode.gameobjects/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Additional documentation and release notes are available at [Multiplayer Documen
1010

1111
### Added
1212

13+
- Added automatic `NetCodeConfig` configuration for hybrid mode. When Netcode for Entities is installed and a registered network prefab has a `GhostObject`, the settings hybrid mode requires are corrected automatically and the Netcode for Entities tick rates are driven from `NetworkConfig.TickRate`. The recommended snapshot, interpolation and transport values are applied once, and can be restored from Project Settings > Multiplayer > Netcode for GameObjects.
14+
1315

1416
### Changed
1517

@@ -32,6 +34,7 @@ Additional documentation and release notes are available at [Multiplayer Documen
3234
- Issue with not being able to spawn initially disabled in-scene placed objects. (#4093)
3335
- Issue with pre-instantiated network prefab instances being marked as in-scene placed. Now pre-instantiated network prefabs are dynamically spawned. (#4093)
3436
- Issue where a user could spawn runtime created `NetworkObject` that has a GlobalObjectIdHash of zero. These are not valid instances and will no longer be allowed to spawn. (#4093)
37+
- Issue where the hybrid mode `NetCodeConfig` validation messages were not interpolated and did not check that automatic bootstrapping was disabled.
3538

3639

3740
### Security
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
#if UNIFIED_NETCODE
2+
using Unity.NetCode;
3+
using UnityEditor;
4+
using UnityEngine;
5+
6+
namespace Unity.Netcode.GameObjects.Editor.Configuration
7+
{
8+
/// <summary>
9+
/// Keeps the project's <see cref="NetCodeConfig"/> aligned with what NGO needs whenever the project is running in
10+
/// hybrid mode (Netcode for Entities installed, and at least one registered network prefab carrying a ghost).
11+
/// </summary>
12+
/// <remarks>
13+
/// This never creates a <see cref="NetCodeConfig"/>. Netcode for Entities already creates one unconditionally from
14+
/// its own <see cref="InitializeOnLoadMethodAttribute"/>, and creating a second lands the project in N4E's
15+
/// multiple-config error path. We find the one N4E settled on and correct it.
16+
/// </remarks>
17+
internal static class HybridNetcodeConfigApplier
18+
{
19+
[InitializeOnLoadMethod]
20+
private static void OnApplicationStart()
21+
{
22+
// N4E creates and assigns the global config from its own InitializeOnLoadMethod. Cross-assembly ordering
23+
// between the two is not a documented contract, so defer rather than racing it.
24+
EditorApplication.delayCall += OnDelayCall;
25+
}
26+
27+
private static void OnDelayCall()
28+
{
29+
EditorApplication.delayCall -= OnDelayCall;
30+
Apply(false);
31+
}
32+
33+
/// <summary>
34+
/// Corrects the global <see cref="NetCodeConfig"/> for hybrid mode, if this is a hybrid project.
35+
/// </summary>
36+
/// <param name="applyRecommended">
37+
/// When true, re-applies the full tuned set even if this project has already had it applied once. Driven by the
38+
/// button in Project Settings. When false, the tuned values are only written the first time, so that a user's
39+
/// own edits are not repeatedly overwritten.
40+
/// </param>
41+
internal static void Apply(bool applyRecommended)
42+
{
43+
if (EditorApplication.isPlayingOrWillChangePlaymode)
44+
{
45+
return;
46+
}
47+
48+
var config = ResolveGlobalConfig();
49+
if (config == null || !IsHybridProject())
50+
{
51+
return;
52+
}
53+
54+
var settings = NetcodeForGameObjectsProjectSettings.instance;
55+
var isFirstApply = settings.HybridDefaultsVersion < HybridNetcodeDefaults.Version;
56+
var changed = false;
57+
58+
if (applyRecommended || isFirstApply)
59+
{
60+
changed = HybridNetcodeDefaults.ApplyRecommended(config, ResolveTickRate(config));
61+
if (changed)
62+
{
63+
Debug.Log($"[Netcode] Applied the NGO hybrid mode defaults to '{config.name}'. These are tuned for NGO and can be changed freely; they will not be re-applied automatically. Use Project Settings > Multiplayer > Netcode for GameObjects to restore them.", config);
64+
}
65+
}
66+
else
67+
{
68+
// Outside the one-shot, only the settings hybrid mode cannot run without are enforced, plus the tick
69+
// rate, which NGO owns.
70+
changed = HybridNetcodeDefaults.ApplyRequired(config);
71+
if (changed)
72+
{
73+
Debug.LogWarning($"[Netcode] Corrected required hybrid mode settings on '{config.name}'. Netcode for GameObjects owns world creation and requires single world hosting, so these two cannot be changed while ghost prefabs are registered.", config);
74+
}
75+
76+
changed |= HybridNetcodeDefaults.ApplyTickRate(config, ResolveTickRate(config));
77+
}
78+
79+
if (!changed)
80+
{
81+
return;
82+
}
83+
84+
settings.HybridDefaultsVersion = HybridNetcodeDefaults.Version;
85+
settings.SaveSettings();
86+
87+
EditorUtility.SetDirty(config);
88+
AssetDatabase.SaveAssetIfDirty(config);
89+
}
90+
91+
/// <summary>
92+
/// True when any <see cref="NetworkManager"/> in the project has a registered prefab carrying a ghost.
93+
/// </summary>
94+
/// <returns>Whether this project is configured for hybrid mode.</returns>
95+
internal static bool IsHybridProject()
96+
{
97+
foreach (var networkManager in Resources.FindObjectsOfTypeAll<NetworkManager>())
98+
{
99+
var prefabs = networkManager.NetworkConfig?.Prefabs;
100+
if (prefabs == null)
101+
{
102+
continue;
103+
}
104+
105+
foreach (var prefab in prefabs.Prefabs)
106+
{
107+
if (HasGhost(prefab))
108+
{
109+
return true;
110+
}
111+
}
112+
113+
foreach (var prefabsList in prefabs.NetworkPrefabsLists)
114+
{
115+
if (prefabsList == null)
116+
{
117+
continue;
118+
}
119+
120+
foreach (var prefab in prefabsList.PrefabList)
121+
{
122+
if (HasGhost(prefab))
123+
{
124+
return true;
125+
}
126+
}
127+
}
128+
}
129+
130+
return false;
131+
}
132+
133+
private static bool HasGhost(NetworkPrefab prefab)
134+
{
135+
return prefab?.Prefab != null
136+
&& prefab.Prefab.TryGetComponent<NetworkObject>(out var networkObject)
137+
&& networkObject.HasGhost;
138+
}
139+
140+
/// <summary>
141+
/// Resolves the config N4E considers global, falling back to a project scan when N4E has not assigned one yet.
142+
/// </summary>
143+
/// <returns>The config to correct, or null when none exists yet.</returns>
144+
internal static NetCodeConfig ResolveGlobalConfig()
145+
{
146+
if (NetCodeConfig.Global != null)
147+
{
148+
return NetCodeConfig.Global;
149+
}
150+
151+
var guids = AssetDatabase.FindAssets($"t:{nameof(NetCodeConfig)}");
152+
return guids.Length == 1 ? AssetDatabase.LoadAssetAtPath<NetCodeConfig>(AssetDatabase.GUIDToAssetPath(guids[0])) : null;
153+
}
154+
155+
/// <summary>
156+
/// The tick rate N4E should be driven at. NGO owns this, so it comes from <see cref="NetworkConfig.TickRate"/>.
157+
/// </summary>
158+
/// <param name="config">The config, used as the fallback when no NetworkManager can be found.</param>
159+
/// <returns>The tick rate to write into the config.</returns>
160+
private static uint ResolveTickRate(NetCodeConfig config)
161+
{
162+
var found = 0u;
163+
var diverged = false;
164+
foreach (var networkManager in Resources.FindObjectsOfTypeAll<NetworkManager>())
165+
{
166+
var tickRate = networkManager.NetworkConfig?.TickRate ?? 0u;
167+
if (tickRate == 0)
168+
{
169+
continue;
170+
}
171+
172+
diverged |= found != 0 && found != tickRate;
173+
found = tickRate;
174+
}
175+
176+
if (diverged)
177+
{
178+
Debug.LogWarning($"[Netcode] Found {nameof(NetworkManager)}s with differing {nameof(NetworkConfig.TickRate)} values. '{config.name}' has been set to {found}; hybrid mode expects a single tick rate across the project.", config);
179+
}
180+
181+
// No NetworkManager to read from (a prefab-only project, or one mid-import) leaves the config as it is.
182+
return found != 0 ? found : (uint)config.ClientServerTickRate.SimulationTickRate;
183+
}
184+
}
185+
186+
/// <summary>
187+
/// Re-runs the hybrid config pass when a prefab import could have turned this into a hybrid project.
188+
/// </summary>
189+
internal class HybridNetcodeConfigPostprocessor : AssetPostprocessor
190+
{
191+
private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
192+
{
193+
foreach (var assetPath in importedAssets)
194+
{
195+
if (AssetDatabase.GetMainAssetTypeAtPath(assetPath) != typeof(GameObject))
196+
{
197+
continue;
198+
}
199+
200+
var gameObject = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath);
201+
if (gameObject != null && gameObject.TryGetComponent<NetworkObject>(out var networkObject) && networkObject.HasGhost)
202+
{
203+
HybridNetcodeConfigApplier.Apply(false);
204+
return;
205+
}
206+
}
207+
}
208+
}
209+
}
210+
#endif

com.unity.netcode.gameobjects/Editor/Configuration/HybridNetcodeConfigApplier.cs.meta

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

com.unity.netcode.gameobjects/Editor/Configuration/NetcodeForGameObjectsProjectSettings.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,18 @@ private void OnEnable()
3535
[SerializeField]
3636
public bool GenerateDefaultNetworkPrefabs = true;
3737

38+
#if UNIFIED_NETCODE
39+
/// <summary>
40+
/// The version of the NGO hybrid mode defaults already applied to this project's NetCodeConfig.
41+
/// </summary>
42+
/// <remarks>
43+
/// Zero means they have never been applied. Recording it is what keeps the tuned values a one-shot, so that a
44+
/// user who deliberately changes them does not have them overwritten on the next domain reload.
45+
/// </remarks>
46+
[SerializeField]
47+
public int HybridDefaultsVersion;
48+
#endif
49+
3850
internal void SaveSettings()
3951
{
4052
Save(true);

com.unity.netcode.gameobjects/Editor/Configuration/NetcodeSettingsProvider.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
using System.Collections.Generic;
22
using System.IO;
3+
#if UNIFIED_NETCODE
4+
using Unity.NetCode;
5+
#endif
36
using UnityEditor;
47
using UnityEngine;
58
using Directory = UnityEngine.Windows.Directory;
@@ -192,6 +195,10 @@ private static void OnGuiHandler(string obj)
192195
networkPrefabsPath,
193196
GUILayout.Width(s_MaxLabelWidth + 270));
194197
GUILayout.EndVertical();
198+
199+
#if UNIFIED_NETCODE
200+
DrawHybridSettings(settings);
201+
#endif
195202
}
196203
EditorGUILayout.EndFoldoutHeaderGroup();
197204
GUILayout.EndVertical();
@@ -205,6 +212,48 @@ private static void OnGuiHandler(string obj)
205212
settings.SaveSettings();
206213
}
207214
}
215+
216+
#if UNIFIED_NETCODE
217+
/// <summary>
218+
/// Surfaces the state of the project's NetCodeConfig, and offers a way back to the NGO defaults for anyone who
219+
/// has since changed them.
220+
/// </summary>
221+
/// <param name="settings">The project settings holding the applied-defaults marker.</param>
222+
private static void DrawHybridSettings(NetcodeForGameObjectsProjectSettings settings)
223+
{
224+
GUILayout.BeginVertical("Box");
225+
GUILayout.Label("Hybrid (Netcode for Entities)", EditorStyles.boldLabel);
226+
227+
if (!HybridNetcodeConfigApplier.IsHybridProject())
228+
{
229+
EditorGUILayout.HelpBox("No registered network prefab has a GhostObject, so this project is not using hybrid mode. Netcode for GameObjects leaves the NetCodeConfig alone until one does.", MessageType.Info);
230+
GUILayout.EndVertical();
231+
return;
232+
}
233+
234+
var config = HybridNetcodeConfigApplier.ResolveGlobalConfig();
235+
if (config == null)
236+
{
237+
EditorGUILayout.HelpBox("No NetCodeConfig could be resolved. Open Project Settings > Multiplayer, which creates one, then return here.", MessageType.Warning);
238+
GUILayout.EndVertical();
239+
return;
240+
}
241+
242+
EditorGUILayout.ObjectField(new GUIContent("Applied to", "The NetCodeConfig that Netcode for GameObjects keeps aligned for hybrid mode."), config, typeof(NetCodeConfig), false);
243+
244+
if (settings.HybridDefaultsVersion < HybridNetcodeDefaults.Version)
245+
{
246+
EditorGUILayout.HelpBox("The Netcode for GameObjects hybrid defaults have not been applied to this config yet.", MessageType.Info);
247+
}
248+
249+
if (GUILayout.Button(new GUIContent("Apply Recommended Hybrid Defaults", "Restores the snapshot, interpolation and transport values Netcode for GameObjects recommends for hybrid mode, and re-syncs the tick rate from your NetworkManager. Applied automatically once; use this to get back to them after changing them.")))
250+
{
251+
HybridNetcodeConfigApplier.Apply(true);
252+
}
253+
254+
GUILayout.EndVertical();
255+
}
256+
#endif
208257
}
209258

210259
internal class NetcodeSettingsLabel : NetcodeGUISettings

0 commit comments

Comments
 (0)