Skip to content

Commit 3474768

Browse files
fix and test
Fixing some things u-pr caught and adding a test to validate the update.
1 parent 49e605e commit 3474768

2 files changed

Lines changed: 220 additions & 37 deletions

File tree

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

Lines changed: 129 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
#if UNIFIED_NETCODE
22
using Unity.NetCode;
33
using UnityEditor;
4+
using UnityEditor.SceneManagement;
45
using UnityEngine;
6+
using UnityEngine.SceneManagement;
57

68
namespace Unity.Netcode.GameObjects.Editor.Configuration
79
{
@@ -20,6 +22,11 @@ private static void OnApplicationStart()
2022
// Cross-assembly ordering between the two is not a documented contract.
2123
// Defer rather than racing it.
2224
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;
2330
}
2431

2532
private static void OnDelayCall()
@@ -28,6 +35,11 @@ private static void OnDelayCall()
2835
Apply(false);
2936
}
3037

38+
private static void OnSceneOpened(Scene scene, OpenSceneMode mode)
39+
{
40+
Apply(false);
41+
}
42+
3143
/// <summary>
3244
/// Adjusts <see cref="NetCodeConfig"/> for NGO hybrid mode.
3345
/// </summary>
@@ -61,6 +73,11 @@ internal static void Apply(bool applyRecommended)
6173
{
6274
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);
6375
}
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();
6481
}
6582
else
6683
{
@@ -80,48 +97,92 @@ internal static void Apply(bool applyRecommended)
8097
return;
8198
}
8299

83-
settings.HybridDefaultsVersion = HybridNetcodeDefaults.Version;
84-
settings.SaveSettings();
85-
86100
EditorUtility.SetDirty(config);
87101
AssetDatabase.SaveAssetIfDirty(config);
88102
}
89103

90104
/// <summary>
91-
/// True when any <see cref="NetworkManager"/> in the project has a registered prefab carrying a ghost.
105+
/// True when a registered network prefab carries a ghost.
92106
/// </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>
93112
internal static bool IsHybridProject()
94113
{
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.
95125
foreach (var networkManager in Resources.FindObjectsOfTypeAll<NetworkManager>())
96126
{
97-
var prefabs = networkManager.NetworkConfig?.Prefabs;
98-
if (prefabs == null)
127+
if (HasGhost(networkManager))
99128
{
100-
continue;
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;
101155
}
156+
}
102157

103-
foreach (var prefab in prefabs.Prefabs)
158+
foreach (var prefabsList in prefabs.NetworkPrefabsLists)
159+
{
160+
if (HasGhost(prefabsList))
104161
{
105-
if (HasGhost(prefab))
106-
{
107-
return true;
108-
}
162+
return true;
109163
}
164+
}
165+
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)
177+
{
178+
return false;
179+
}
110180

111-
foreach (var prefabsList in prefabs.NetworkPrefabsLists)
181+
foreach (var prefab in prefabsList.PrefabList)
182+
{
183+
if (HasGhost(prefab))
112184
{
113-
if (prefabsList == null)
114-
{
115-
continue;
116-
}
117-
118-
foreach (var prefab in prefabsList.PrefabList)
119-
{
120-
if (HasGhost(prefab))
121-
{
122-
return true;
123-
}
124-
}
185+
return true;
125186
}
126187
}
127188

@@ -152,9 +213,13 @@ internal static NetCodeConfig ResolveGlobalConfig()
152213

153214
/// <summary>
154215
/// Returns either the current N4E tick rate or the NGO <see cref="NetworkConfig.TickRate"/>.
155-
/// If no NetworkManager is found yet, it returns N4E's tick rate.
156-
/// If a NetworkManager is found, then it returns NGO's tick rate.
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.
157218
/// </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>
158223
/// <param name="config">The config, used as the fallback when no NetworkManager can be found.</param>
159224
/// <returns>The tick rate to write into the config.</returns>
160225
private static uint ResolveTickRate(NetCodeConfig config)
@@ -163,6 +228,11 @@ private static uint ResolveTickRate(NetCodeConfig config)
163228
var diverged = false;
164229
foreach (var networkManager in Resources.FindObjectsOfTypeAll<NetworkManager>())
165230
{
231+
if (!HasGhost(networkManager))
232+
{
233+
continue;
234+
}
235+
166236
var tickRate = networkManager.NetworkConfig?.TickRate ?? 0u;
167237
if (tickRate == 0)
168238
{
@@ -175,36 +245,58 @@ private static uint ResolveTickRate(NetCodeConfig config)
175245

176246
if (diverged)
177247
{
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);
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);
179249
}
180250

181-
// No NetworkManager to read from (a prefab-only project, or one mid-import) leaves the config as it is.
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.
182253
return found != 0 ? found : (uint)config.ClientServerTickRate.SimulationTickRate;
183254
}
184255
}
185256

186257
/// <summary>
187-
/// Re-runs the hybrid config pass when a prefab import could have turned this into a hybrid project.
258+
/// Re-runs the hybrid config pass when an import could have turned this into a hybrid project.
188259
/// </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>
189264
internal class HybridNetcodeConfigPostprocessor : AssetPostprocessor
190265
{
191266
private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
192267
{
193268
foreach (var assetPath in importedAssets)
194269
{
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)
270+
if (ImportReachesHybridMode(assetPath))
202271
{
203272
HybridNetcodeConfigApplier.Apply(false);
204273
return;
205274
}
206275
}
207276
}
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))
294+
{
295+
return HybridNetcodeConfigApplier.HasGhost(AssetDatabase.LoadAssetAtPath<NetworkPrefabsList>(assetPath));
296+
}
297+
298+
return false;
299+
}
208300
}
209301
}
210302
#endif

com.unity.netcode.gameobjects/Tests/Editor/HybridNetcodeDefaultsTests.cs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using NUnit.Framework;
33
using Unity.NetCode;
44
using Unity.Netcode.GameObjects.Editor.Configuration;
5+
using UnityEditor;
56
using UnityEngine;
67

78
namespace Unity.Netcode.EditorTests
@@ -26,6 +27,17 @@ public void TearDown()
2627
Object.DestroyImmediate(m_Config);
2728
}
2829

30+
/// <summary>
31+
/// A config that already matches reports no change, which is why the applier records the version marker
32+
/// independently of whether anything was written.
33+
/// </summary>
34+
[Test]
35+
public void ApplyRecommendedReportsNoChangeWhenConfigAlreadyMatches()
36+
{
37+
Assert.IsTrue(HybridNetcodeDefaults.ApplyRecommended(m_Config, 30u), "Expected the first apply to report a change.");
38+
Assert.IsFalse(HybridNetcodeDefaults.ApplyRecommended(m_Config, 30u), "Applying an already matching config should report no change.");
39+
}
40+
2941
[Test]
3042
public void ApplyRequiredCorrectsBothSettings()
3143
{
@@ -66,6 +78,29 @@ public void ApplyTickRateLocksSimulationAndNetworkRates(uint tickRate)
6678
Assert.IsFalse(HybridNetcodeDefaults.ApplyTickRate(m_Config, tickRate));
6779
}
6880

81+
/// <summary>
82+
/// The one-shot can run before the hybrid <see cref="NetworkManager"/>'s scene is open, in which case it writes
83+
/// N4E's tick rate rather than NGO's. Opening that scene runs a tick rate only pass, which has to correct the
84+
/// rate without disturbing the tuned values.
85+
/// </summary>
86+
[Test]
87+
public void TickRateOnlyPassCorrectsTheRateAndLeavesTheTunedValuesAlone()
88+
{
89+
const int n4eTickRate = 60;
90+
const uint ngoTickRate = 30;
91+
92+
HybridNetcodeDefaults.ApplyRecommended(m_Config, n4eTickRate);
93+
Assume.That(m_Config.ClientServerTickRate.SimulationTickRate, Is.EqualTo(n4eTickRate), "The one-shot should have written N4E's tick rate.");
94+
95+
Assert.IsTrue(HybridNetcodeDefaults.ApplyTickRate(m_Config, ngoTickRate));
96+
97+
Assert.AreEqual((int)ngoTickRate, m_Config.ClientServerTickRate.SimulationTickRate);
98+
Assert.AreEqual((int)ngoTickRate, m_Config.ClientServerTickRate.NetworkTickRate);
99+
Assert.AreEqual(HybridNetcodeDefaults.SnapshotPacketSize, m_Config.GhostSendSystemData.DefaultSnapshotPacketSize, "A tick rate pass must not disturb the tuned values.");
100+
Assert.AreEqual(HybridNetcodeDefaults.InterpolationTimeMS, m_Config.ClientTickRate.InterpolationTimeMS);
101+
Assert.AreEqual(HybridNetcodeDefaults.InterpolationTimeScaleMax, m_Config.ClientTickRate.InterpolationTimeScaleMax);
102+
}
103+
69104
[Test]
70105
public void ApplyRecommendedProducesTheTunedValues()
71106
{
@@ -146,6 +181,62 @@ public void IsHybridProjectOnlyDetectsPrefabsCarryingAGhost()
146181
Object.DestroyImmediate(managerObject);
147182
}
148183
}
184+
185+
/// <summary>
186+
/// Once the one-shot has been recorded, every later pass still drives the tick rate from the hybrid
187+
/// <see cref="NetworkManager"/>. This is what corrects the rate when the manager's scene opens after the
188+
/// defaults were already applied.
189+
/// </summary>
190+
[Test]
191+
public void ApplyDrivesTheTickRateAfterTheOneShotHasBeenRecorded()
192+
{
193+
const uint managerTickRate = 45;
194+
195+
var config = HybridNetcodeConfigApplier.ResolveGlobalConfig();
196+
Assume.That(config, Is.Not.Null, "This project has no NetCodeConfig to adjust.");
197+
Assume.That(HybridNetcodeConfigApplier.IsHybridProject(), Is.False, "Another loaded NetworkManager already registers a ghost prefab.");
198+
199+
var settings = NetcodeForGameObjectsProjectSettings.instance;
200+
var restoreVersion = settings.HybridDefaultsVersion;
201+
var restoreSimulation = config.ClientServerTickRate.SimulationTickRate;
202+
var restoreNetwork = config.ClientServerTickRate.NetworkTickRate;
203+
204+
var managerObject = new GameObject(nameof(ApplyDrivesTheTickRateAfterTheOneShotHasBeenRecorded));
205+
var prefabObject = new GameObject("GhostPrefab");
206+
var prefabsList = ScriptableObject.CreateInstance<NetworkPrefabsList>();
207+
try
208+
{
209+
var networkManager = managerObject.AddComponent<NetworkManager>();
210+
networkManager.NetworkConfig = new NetworkConfig { TickRate = managerTickRate, };
211+
prefabObject.AddComponent<NetworkObject>().HasGhost = true;
212+
prefabsList.Add(new NetworkPrefab { Prefab = prefabObject });
213+
networkManager.NetworkConfig.Prefabs.NetworkPrefabsLists.Add(prefabsList);
214+
215+
// Past the one-shot, so this exercises the required plus tick rate path rather than ApplyRecommended.
216+
settings.HybridDefaultsVersion = HybridNetcodeDefaults.Version;
217+
config.ClientServerTickRate.SimulationTickRate = 60;
218+
config.ClientServerTickRate.NetworkTickRate = 60;
219+
220+
HybridNetcodeConfigApplier.Apply(false);
221+
222+
Assert.AreEqual((int)managerTickRate, config.ClientServerTickRate.SimulationTickRate, "The tick rate should have been driven from the hybrid NetworkManager.");
223+
Assert.AreEqual((int)managerTickRate, config.ClientServerTickRate.NetworkTickRate);
224+
}
225+
finally
226+
{
227+
config.ClientServerTickRate.SimulationTickRate = restoreSimulation;
228+
config.ClientServerTickRate.NetworkTickRate = restoreNetwork;
229+
EditorUtility.SetDirty(config);
230+
AssetDatabase.SaveAssetIfDirty(config);
231+
232+
settings.HybridDefaultsVersion = restoreVersion;
233+
settings.SaveSettings();
234+
235+
Object.DestroyImmediate(prefabsList);
236+
Object.DestroyImmediate(prefabObject);
237+
Object.DestroyImmediate(managerObject);
238+
}
239+
}
149240
}
150241
}
151242
#endif

0 commit comments

Comments
 (0)