11#if UNIFIED_NETCODE
22using Unity . NetCode ;
33using UnityEditor ;
4- using UnityEditor . SceneManagement ;
54using UnityEngine ;
6- using UnityEngine . SceneManagement ;
75
86namespace 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}
0 commit comments