11#if UNIFIED_NETCODE
22using Unity . NetCode ;
33using UnityEditor ;
4+ using UnityEditor . SceneManagement ;
45using UnityEngine ;
6+ using UnityEngine . SceneManagement ;
57
68namespace 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
0 commit comments