Skip to content

Commit 9c7fb1a

Browse files
refactor
Refactoring when the NetcodeConfig default settings are written. Assuring that the NetcodeConfig tick rate is always the currently set tick rate when starting a session.
1 parent 392159f commit 9c7fb1a

7 files changed

Lines changed: 221 additions & 309 deletions

File tree

com.unity.netcode.gameobjects/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +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.
13+
- Added an "Enable the experimental unified netcode API" opt-in under Project Settings > Multiplayer > Netcode for GameObjects, shown when Netcode for Entities is installed. Enabling it writes the `NetCodeConfig` snapshot, interpolation and transport values recommended for hybrid mode, once. They can be changed freely afterwards and restored from the same page.
14+
- Added alignment of the Netcode for Entities tick rates with `NetworkConfig.TickRate` when a session with `GhostObject` prefabs is started, so ghost updates land on the same interval as the rest of Netcode for GameObjects.
1415

1516

1617
### Changed

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

Lines changed: 52 additions & 231 deletions
Original file line numberDiff line numberDiff line change
@@ -1,204 +1,106 @@
11
#if UNIFIED_NETCODE
22
using Unity.NetCode;
33
using UnityEditor;
4-
using UnityEditor.SceneManagement;
54
using UnityEngine;
6-
using UnityEngine.SceneManagement;
75

86
namespace Unity.Netcode.GameObjects.Editor.Configuration
97
{
108
/// <summary>
11-
/// Keeps the project's <see cref="NetCodeConfig"/> aligned with NGO needs whenever the project is running in
12-
/// hybrid mode (N4E installed and at least one registered NGO network prefab has a GhostObject component).
9+
/// Writes the <see cref="NetCodeConfig"/> values NGO recommends for hybrid mode, once, the first time a
10+
/// <see cref="NetCodeConfig"/> is available.
1311
/// </summary>
1412
/// <remarks>
1513
/// This does not create <see cref="NetCodeConfig"/>. This finds the one N4E created and modifies it.
14+
/// Nothing tracks the project after that write. The defaults are inert in a project with no hybrid prefabs, and
15+
/// <see cref="NetworkManager"/> re-aligns the tick rate at start-up in a project that has them, so there is no
16+
/// reason to scan for ghost prefabs from the editor.
1617
/// </remarks>
1718
internal static class HybridNetcodeConfigApplier
1819
{
20+
/// <summary>
21+
/// Whether the user has to opt into the experimental unified netcode API before NGO writes anything.
22+
/// </summary>
23+
/// <remarks>
24+
/// TODO-RELEASE: Set this to true before the 6000.7.0 release manifest submission if Netcode for Entities
25+
/// ships the unified API as experimental and its scripting defines.
26+
/// Note: This is deliberately not a const: IDE0035 (remove unreachable code) is an error in this repository, so a
27+
/// const would fail the standards job as soon as it was set to false.
28+
/// </remarks>
29+
internal static readonly bool RequiresExperimentalOptIn = false;
30+
31+
private static NetCodeConfig s_ScannedConfig;
32+
private static bool s_ConfigScanned;
33+
1934
[InitializeOnLoadMethod]
2035
private static void OnApplicationStart()
2136
{
2237
// Cross-assembly ordering between the two is not a documented contract.
2338
// Defer rather than racing it.
2439
EditorApplication.delayCall += OnDelayCall;
25-
26-
// A NetworkManager in an unopened scene is not loaded, so its tick rate cannot be read at this point.
27-
// Rescan when a scene opens to pick it up.
28-
EditorSceneManager.sceneOpened -= OnSceneOpened;
29-
EditorSceneManager.sceneOpened += OnSceneOpened;
3040
}
3141

3242
private static void OnDelayCall()
3343
{
3444
EditorApplication.delayCall -= OnDelayCall;
35-
Apply(false);
36-
}
37-
38-
private static void OnSceneOpened(Scene scene, OpenSceneMode mode)
39-
{
40-
Apply(false);
45+
ApplyDefaults(false);
4146
}
4247

4348
/// <summary>
44-
/// Adjusts <see cref="NetCodeConfig"/> for NGO hybrid mode.
49+
/// Writes the NGO hybrid mode defaults into the project's <see cref="NetCodeConfig"/>.
4550
/// </summary>
46-
/// <param name="applyRecommended">
51+
/// <param name="force">
4752
/// Driven by the button in Project Settings:
48-
/// - When true: it re-applies the full tuned set even if this project has already had it applied once.
49-
/// - When false: default NGO settings are only written once, the first time they are applied. From that
50-
/// point forward, the user's edits are not overwritten.
53+
/// - When true: re-applies the full tuned set even though this project has already had it applied once.
54+
/// - When false: writes only if this project has never had them written. From that point forward, the user's
55+
/// edits are not overwritten.
5156
/// </param>
52-
internal static void Apply(bool applyRecommended)
57+
internal static void ApplyDefaults(bool force)
5358
{
5459
if (EditorApplication.isPlayingOrWillChangePlaymode)
5560
{
5661
return;
5762
}
5863

59-
var config = ResolveGlobalConfig();
60-
if (config == null || !IsHybridProject())
61-
{
62-
return;
63-
}
64-
6564
var settings = NetcodeForGameObjectsProjectSettings.instance;
66-
var isFirstApply = settings.HybridDefaultsVersion < HybridNetcodeDefaults.Version;
67-
var changed = false;
68-
69-
if (applyRecommended || isFirstApply)
70-
{
71-
changed = HybridNetcodeDefaults.ApplyRecommended(config, ResolveTickRate(config));
72-
if (changed)
73-
{
74-
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);
75-
}
76-
77-
// Recorded even when the config already matched and nothing was written. Leaving it unrecorded would
78-
// make the next domain reload a first application again, which would revert the user's next edit.
79-
settings.HybridDefaultsVersion = HybridNetcodeDefaults.Version;
80-
settings.SaveSettings();
81-
}
82-
else
83-
{
84-
// Outside the one-shot, only the settings hybrid mode cannot run without are enforced, plus the tick
85-
// rate, which NGO owns.
86-
changed = HybridNetcodeDefaults.ApplyRequired(config);
87-
if (changed)
88-
{
89-
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);
90-
}
91-
92-
changed |= HybridNetcodeDefaults.ApplyTickRate(config, ResolveTickRate(config));
93-
}
94-
95-
if (!changed)
65+
if (RequiresExperimentalOptIn && !settings.EnableUnifiedNetcodeApi)
9666
{
9767
return;
9868
}
9969

100-
EditorUtility.SetDirty(config);
101-
AssetDatabase.SaveAssetIfDirty(config);
102-
}
103-
104-
/// <summary>
105-
/// True when a registered network prefab carries a ghost.
106-
/// </summary>
107-
/// <remarks>
108-
/// The <see cref="NetworkPrefabsList"/> assets are scanned rather than the loaded <see cref="NetworkManager"/>s
109-
/// because a manager living in an unopened scene is not loaded and would not be found. A ghost prefab sitting
110-
/// in a list is treated as intent to run hybrid mode even if no manager references that list yet.
111-
/// </remarks>
112-
internal static bool IsHybridProject()
113-
{
114-
foreach (var guid in AssetDatabase.FindAssets($"t:{nameof(NetworkPrefabsList)}"))
115-
{
116-
var prefabsList = AssetDatabase.LoadAssetAtPath<NetworkPrefabsList>(AssetDatabase.GUIDToAssetPath(guid));
117-
if (HasGhost(prefabsList))
118-
{
119-
return true;
120-
}
121-
}
122-
123-
// Prefabs added directly to a NetworkManager never reach a list asset, so the loaded managers still have
124-
// to be checked.
125-
foreach (var networkManager in Resources.FindObjectsOfTypeAll<NetworkManager>())
126-
{
127-
if (HasGhost(networkManager))
128-
{
129-
return true;
130-
}
131-
}
132-
133-
return false;
134-
}
135-
136-
/// <summary>
137-
/// True when this <see cref="NetworkManager"/> registers a prefab carrying a ghost, either directly or through
138-
/// one of its assigned <see cref="NetworkPrefabsList"/> assets.
139-
/// </summary>
140-
/// <param name="networkManager">The manager to inspect.</param>
141-
/// <returns>Whether this manager takes part in hybrid mode.</returns>
142-
private static bool HasGhost(NetworkManager networkManager)
143-
{
144-
var prefabs = networkManager == null ? null : networkManager.NetworkConfig?.Prefabs;
145-
if (prefabs == null)
146-
{
147-
return false;
148-
}
149-
150-
foreach (var prefab in prefabs.Prefabs)
151-
{
152-
if (HasGhost(prefab))
153-
{
154-
return true;
155-
}
156-
}
157-
158-
foreach (var prefabsList in prefabs.NetworkPrefabsLists)
70+
if (!force && settings.HybridDefaultsVersion >= HybridNetcodeDefaults.Version)
15971
{
160-
if (HasGhost(prefabsList))
161-
{
162-
return true;
163-
}
72+
return;
16473
}
16574

166-
return false;
167-
}
168-
169-
/// <summary>
170-
/// True when this <see cref="NetworkPrefabsList"/> holds a prefab carrying a ghost.
171-
/// </summary>
172-
/// <param name="prefabsList">The list to inspect.</param>
173-
/// <returns>Whether this list takes part in hybrid mode.</returns>
174-
internal static bool HasGhost(NetworkPrefabsList prefabsList)
175-
{
176-
if (prefabsList == null)
75+
// A project with no config yet leaves the marker unrecorded so that the next domain reload tries again.
76+
// N4E creates one on any domain reload that finds none.
77+
var config = ResolveGlobalConfig();
78+
if (config == null)
17779
{
178-
return false;
80+
return;
17981
}
18082

181-
foreach (var prefab in prefabsList.PrefabList)
83+
if (HybridNetcodeDefaults.ApplyRecommended(config, HybridNetcodeDefaults.DefaultTickRate))
18284
{
183-
if (HasGhost(prefab))
184-
{
185-
return true;
186-
}
85+
EditorUtility.SetDirty(config);
86+
AssetDatabase.SaveAssetIfDirty(config);
87+
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);
18788
}
18889

189-
return false;
190-
}
191-
192-
private static bool HasGhost(NetworkPrefab prefab)
193-
{
194-
return prefab?.Prefab != null
195-
&& prefab.Prefab.TryGetComponent<NetworkObject>(out var networkObject)
196-
&& networkObject.HasGhost;
90+
// Recorded even when the config already matched and nothing was written. Leaving it unrecorded would make
91+
// the next domain reload a first application again, which would revert the user's next edit.
92+
settings.HybridDefaultsVersion = HybridNetcodeDefaults.Version;
93+
settings.SaveSettings();
19794
}
19895

19996
/// <summary>
20097
/// Resolves the config N4E considers global, falling back to a project scan when N4E has not assigned one yet.
20198
/// </summary>
99+
/// <remarks>
100+
/// The scan is done at most once per domain reload, including when it finds nothing, because this is also
101+
/// reached from OnGUI and <see cref="AssetDatabase.FindAssets"/> walks the entire project. A config created
102+
/// after the scan is picked up on the next domain reload.
103+
/// </remarks>
202104
/// <returns>The config to adjust or null if no config exists.</returns>
203105
internal static NetCodeConfig ResolveGlobalConfig()
204106
{
@@ -207,95 +109,14 @@ internal static NetCodeConfig ResolveGlobalConfig()
207109
return NetCodeConfig.Global;
208110
}
209111

210-
var guids = AssetDatabase.FindAssets($"t:{nameof(NetCodeConfig)}");
211-
return guids.Length == 1 ? AssetDatabase.LoadAssetAtPath<NetCodeConfig>(AssetDatabase.GUIDToAssetPath(guids[0])) : null;
212-
}
213-
214-
/// <summary>
215-
/// Returns either the current N4E tick rate or the NGO <see cref="NetworkConfig.TickRate"/>.
216-
/// If no NetworkManager taking part in hybrid mode is loaded, it returns N4E's tick rate.
217-
/// If one is loaded, then it returns NGO's tick rate.
218-
/// </summary>
219-
/// <remarks>
220-
/// Only managers registering a ghost prefab are considered. A conventional NGO manager running at a different
221-
/// tick rate has no bearing on the interval N4E should synchronize ghosts at.
222-
/// </remarks>
223-
/// <param name="config">The config, used as the fallback when no NetworkManager can be found.</param>
224-
/// <returns>The tick rate to write into the config.</returns>
225-
private static uint ResolveTickRate(NetCodeConfig config)
226-
{
227-
var found = 0u;
228-
var diverged = false;
229-
foreach (var networkManager in Resources.FindObjectsOfTypeAll<NetworkManager>())
230-
{
231-
if (!HasGhost(networkManager))
232-
{
233-
continue;
234-
}
235-
236-
var tickRate = networkManager.NetworkConfig?.TickRate ?? 0u;
237-
if (tickRate == 0)
238-
{
239-
continue;
240-
}
241-
242-
diverged |= found != 0 && found != tickRate;
243-
found = tickRate;
244-
}
245-
246-
if (diverged)
247-
{
248-
Debug.LogWarning($"[Netcode] Found hybrid mode {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 network prefabs carrying a ghost.", config);
249-
}
250-
251-
// Nothing to read from (a prefab-only project, one mid-import, or the manager's scene is not open yet)
252-
// leaves the config as it is. Opening that scene runs this again.
253-
return found != 0 ? found : (uint)config.ClientServerTickRate.SimulationTickRate;
254-
}
255-
}
256-
257-
/// <summary>
258-
/// Re-runs the hybrid config pass when an import could have turned this into a hybrid project.
259-
/// </summary>
260-
/// <remarks>
261-
/// Both a prefab gaining a GhostObject and a prefab list gaining an existing ghost prefab reach hybrid mode, so
262-
/// both imports are watched.
263-
/// </remarks>
264-
internal class HybridNetcodeConfigPostprocessor : AssetPostprocessor
265-
{
266-
private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
267-
{
268-
foreach (var assetPath in importedAssets)
269-
{
270-
if (ImportReachesHybridMode(assetPath))
271-
{
272-
HybridNetcodeConfigApplier.Apply(false);
273-
return;
274-
}
275-
}
276-
}
277-
278-
/// <summary>
279-
/// Cheap check for whether an imported asset could have introduced a ghost, so that the full project scan in
280-
/// <see cref="HybridNetcodeConfigApplier.Apply"/> is only paid when it might matter.
281-
/// </summary>
282-
/// <param name="assetPath">The imported asset.</param>
283-
/// <returns>Whether the import is worth a rescan.</returns>
284-
private static bool ImportReachesHybridMode(string assetPath)
285-
{
286-
var assetType = AssetDatabase.GetMainAssetTypeAtPath(assetPath);
287-
if (assetType == typeof(GameObject))
288-
{
289-
var gameObject = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath);
290-
return gameObject != null && gameObject.TryGetComponent<NetworkObject>(out var networkObject) && networkObject.HasGhost;
291-
}
292-
293-
if (assetType == typeof(NetworkPrefabsList))
112+
if (!s_ConfigScanned)
294113
{
295-
return HybridNetcodeConfigApplier.HasGhost(AssetDatabase.LoadAssetAtPath<NetworkPrefabsList>(assetPath));
114+
s_ConfigScanned = true;
115+
var guids = AssetDatabase.FindAssets($"t:{nameof(NetCodeConfig)}");
116+
s_ScannedConfig = guids.Length == 1 ? AssetDatabase.LoadAssetAtPath<NetCodeConfig>(AssetDatabase.GUIDToAssetPath(guids[0])) : null;
296117
}
297118

298-
return false;
119+
return s_ScannedConfig;
299120
}
300121
}
301122
}

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,17 @@ private void OnEnable()
3838
public bool GenerateDefaultNetworkPrefabs = true;
3939

4040
#if UNIFIED_NETCODE
41+
/// <summary>
42+
/// Whether the user has opted into the experimental unified netcode API.
43+
/// </summary>
44+
/// <remarks>
45+
/// Only consulted while <see cref="HybridNetcodeConfigApplier.RequiresExperimentalOptIn"/> holds. Turning it
46+
/// off again hides the hybrid section and leaves the NetCodeConfig exactly as it is; the marker below is what
47+
/// keeps turning it back on from overwriting anything.
48+
/// </remarks>
49+
[SerializeField]
50+
public bool EnableUnifiedNetcodeApi;
51+
4152
/// <summary>
4253
/// The hybrid mode default values already applied to this project's NetCodeConfig.
4354
/// </summary>

0 commit comments

Comments
 (0)