com.unity.netcode.gameobjects
com.unity.netcode.gameobjects copied to clipboard
feat: add pre and post spawn methods
This PR focuses on providing users with additional NetworkBehaviour virtual methods that remove the need to subscribe to scene events or client connected callbacks while also removing the need to know or track order of operations when it comes to cross NetworkObject access in problematic areas (scene loading and synchronization).
Adding pre and post spawn methods to NetworkBehaviour components. This can be useful for:
- PreSpawn:
- Assigning an owner write NetworkVariable on the server side prior to the NetworkObject being spawned.
- Doing any other form of pre-spawn initialization prior to beginning the OnNetworkSpawn pass.
- PostSpawn:
- Accessing another NetworkBehaviour component's values after OnNetworkSpawn has been invoked (i.e. order of OnNetworkSpawn invocation is no longer an issue).
- Doing any other form of post-spawn processing immediately after all NetworkBehaviour components associated with the same NetworkObject have run through the OnNetworkSpawn pass.
Adding NetworkBehaviour.OnNetworkSessionSynchronized and NetworkBehaviour.OnInSceneObjectsSpawned convenience methods that provide users with the ability to handle post-synchronization and/or post-scene loading actions.
NetworkBehaviour.OnNetworkSessionSynchronized: A client-side only convenience method that is invoked on every NetworkBehaviour once a newly connected client has finished synchronizing and allNetworkObjects are spawned.NetworkBehaviour.OnInSceneObjectsSpawned: A convenience method (requires scene management to be enabled) that is invoked when:- A server or host first starts up after all in-scene placed
NetworkObjects in the currently loaded scene(s) have been spawned. - A client finishes synchronizing.
- On the server and client side after a scene has been loaded and all newly instantiated in-scene placed
NetworkObjects have been spawned.
- A server or host first starts up after all in-scene placed
Clearing the m_ChildNetworkBehaviours on Awake to resolve an issue with not completely resetting a NetworkObject component's child NetworkBehaviour components.
Implements portions of epic: MTT-8470 MTT-8368 MTT-8400
Also: fix: #2892 fix: #2870
Changelog
-
Added:
NetworkBehaviour.OnNetworkPreSpawnandNetworkBehaviour.OnNetworkPostSpawnmethods that provide the ability to handle pre and post spawning actions during theNetworkObjectspawn sequence. -
Added: A client-side only
NetworkBehaviour.OnNetworkSessionSynchronizedconvenience method that is invoked on allNetworkBehaviours after a newly joined client has finished synchronizing with the network session in progress. -
Added:
NetworkBehaviour.OnInSceneObjectsSpawnedconvenience method that is invoked when all in-sceneNetworkObjects have been spawned after a scene has been loaded or upon a host or server starting. -
Fixed: Issue where a
NetworkObjectcomponent's associatedNetworkBehaviourcomponents would not be detected if scene loading is disabled in the editor and the currently loaded scene has in-scene placedNetworkObjects.
Testing and Documentation
- Includes integration tests:
NetworkBehaviourPrePostSpawnTests.OnNetworkPreAndPostSpawn(validates dynamically spawned)NetworkBehaviourSessionSynchronized.InScenePlacedSessionSynchronized(validates in-scene placed)
- Includes public documentation updates: PR-1260
Thanks!!
The limitations of OnNetworkPreSpawn concern me about its utility a bit, but I haven't thought it all the way through. I was kind of expecting it to be more akin to Awake/Start where OnNetworkPreSpawn is fired for ALL NetworkObjects' NetworkBehaviours immediately before OnNetworkSpawn is fired for ALL NetworkObjects' NetworkBehaviours. (When I say "all" I mean every scene net object... obviously dynamically spawned network objects have no relationship to each other in that way.)
Is there a use case I'm missing where it is useful to have OnNetworkPreSpawn not wait for IsOwner etc to be set? Is there a use case where it is harmful for OnNetworkPreSpawn to wait like that?
There's a fair amount of complexity and nuance (and maybe even overlap?) between OnNetworkSessionSynchronized, OnInSceneObjectsSpawned, and OnNetworkPostSpawn... let me try to make sure we both understand some common things that I want to do in code during the network spawn stage of a scene that cannot use OnNetworkSpawn alone (because other objects may not be spawned yet).
- NetworkObj X (in the scene) has NetworkBehaviour's B1 and B2, and B1 wants to do some operation on B2, but B2 isn't initialized yet because it isn't net spawned yet (this is #2870)
- NetworkObj X (in the scene) has NetworkBehaviour's B1 that wants to do some operation on a different NetworkBehaviour that is on NetworkObj Y (in the scene) but Y hasn't net spawned yet so that other NetworkBehaviour isn't initialized yet (Similar to number 1 but spans two network objects)
- NetworkObj X (in the scene) wants to call an RPC on NetworkObj Y, but Y (also in the scene) hasn't net spawned yet
- X wants to call an RPC that has a NetworkObjectRef to Y, but Y hasn't spawned yet
- X has NetworkBehaviour's B1 and B2, and B1 wants to call an RPC on B2 but B2 hasn't spawned yet.
- X has NetworkBehaviour's B1 and B2, and B1 wants to call its own RPC with a NetworkBehaviourRef to B2, but B2 hasn't spawned yet.
- X wants to re-parent itself to Y, but Y hasn't spawned yet.
- X wants to change the parent of Y, but Y hasn't spawned yet.
It's ambiguous to me right now if OnInSceneObjectsSpawned will run multiple times on the same scene load...
- A server or host first starts up after all in-scene placed NetworkObjects in the currently loaded scene(s) have been spawned.
- A client finishes synchronizing.
- On the server and client side after a scene has been loaded and all newly instantiated in-scene placed NetworkObjects have been spawned.
Will it only run exactly one time on the server and each client for the loading of a given scene? Or is this bulleted list saying that it will fire multiple times?
It looks like OnNetworkSessionSynchronized clearly only runs once per client when all NetworkObjects are ready (the host being a client too). This seems like it would solve a lot of my problems!! I think I'm not quite straight in my head yet on what I would use OnInSceneObjectsSpawned for instead.
As for OnNetworkPostSpawn, I see this in the documentation:
"Gets called after the
is spawned. All NetworkBehaviours associated with the NetworkObject will have had invoked."
So that would mean that OnNetworkPostSpawn will fire on one NetworkObject in a loaded scene before potentially any Pre/Spawn/Post stuff has fired at all on some OTHER NetworkObject in that loaded scene? I go back to my Awake/Start analogy, and I think this is a bit of a gotchya for programmers. I guess I was thinking OnNetworkPostSpawn would be what OnNetworkSessionSynchronized is?
I appreciate you!
@zachstronaut
The limitations of OnNetworkPreSpawn concern me about its utility a bit, but I haven't thought it all the way through. I was kind of expecting it to be more akin to Awake/Start where OnNetworkPreSpawn is fired for ALL NetworkObjects' NetworkBehaviours immediately before OnNetworkSpawn is fired for ALL NetworkObjects' NetworkBehaviours. (When I say "all" I mean every scene net object... obviously dynamically spawned network objects have no relationship to each other in that way.)
Is there a use case I'm missing where it is useful to have OnNetworkPreSpawn not wait for IsOwner etc to be set? Is there a use case where it is harmful for OnNetworkPreSpawn to wait like that?
When deserializing from the inbound message stream buffer, everything is processed in a synchronous fashion. This means that each NetworkObject is:
- Instantiated or Linked to:
- Dynamically spawned NetworkObjects are instantiated
- In-Scene placed are linked to
- Spawned
This happens as the buffer is being parsed, so it would be a complete re-working of that entire process to first instantiate and/or link to NetworkObjects, invoked the prespawn, then spawn the NetworkObjects, then invoke the postspawn (as you describe above).
There's a fair amount of complexity and nuance (and maybe even overlap?) between OnNetworkSessionSynchronized, OnInSceneObjectsSpawned, and OnNetworkPostSpawn... let me try to make sure we both understand some common things that I want to do in code during the network spawn stage of a scene that cannot use OnNetworkSpawn alone (because other objects may not be spawned yet).
Here is the order of operations of spawning:
- For each NetworkObject:
- OnNetworkPreSpawn is invoked prior to being spawned
- Until the NetworkObject has been spawned, there is no ownership or identifier assigned (i.e. it is just an instance that is not yet spawned). You can do things like instantiate NetworkVariables or other "non-spawn" dependent actions here.
- OnNetworkSpawn is invoked:
- At this point spawn dependent properties are set and can be accessed, but it does not mean that any other component associated with the NetworkObject has run through the spawn process yet.
- OnNetworkPostSpawn is invoked:
- All NetworkBehaviour components associated with the NetworkObject have run through the OnNetworkSpawn process.
- You know that all spawn dependent properties have been set and are safe to access them. The pre and post spawn methods help with order of operation issues for a single NetworkObject and its associated NetworkBehaviours.
- OnNetworkPreSpawn is invoked prior to being spawned
As you pointed out in your scenarios, there are other circumstances where you will want to be assured that everything is spawned and/or at a minimum In-Scene placed NetworkObjects have been spawned (needs a bit of context which is below). In reality there are two conditions where you need to know "when something is spawned" in a client-server topology:
- When a new client is first connecting and synchronizing (OnNetworkSessionSynchronized)
- OnNetworkSessionSynchronized is invoked on all spawned NetworkObjects when a client has finished synchronizing.
- This includes all in-scene placed NetworkObjects
- This is triggered just before the SceneEventType.SynchronizeComplete.
- OnNetworkSessionSynchronized is invoked on all spawned NetworkObjects when a client has finished synchronizing.
- When all in-scene placed NetworkObjects have been spawned (OnInSceneObjectsSpawned)
- For the host-server:
- This is useful when first starting with a scene pre-loaded that already has in-scene placed NetworkObjects in it.
- This is useful when loading a new scene.
- (anything dynamically spawned you control and know when you are done spawning things)
- For clients:
- When first synchronizing, if you need to access another in-scene placed NetworkObject
- Otherwise, if you need dynamically spawned NetworkObject to access an In-Scene placed NetworkObject (or vice versa) you can just place your script in an overridden version of the OnNetworkSessionSynchronized method.
- When a scene is finished loading.
- This is for already connected clients where you aren't trying to spawn NetworkObjects dynamically while in the middle of a scene loading event (not recommended, but you can do this with additional scripting to handle that unique scenario).
- The recommended way to handle spawning NetworkObjects post loading of a scene is to have the server/host wait for the SceneEventType.LoadEventCompleted which denotes that all clients have finished loading the scene.
- If you dynamically spawn a series of NetworkObjects and need them to access NetworkBehaviours in other NetworkObjects within the series of NetworkObjects spawned, you just need to make sure that the action/script is invoked by the last NetworkObject spawned.
- Example: You spawn NetworkObjects A, B, C, D and want to perform some logical operation between NetworkObject A and NetworkObject C as well as NetworkObject B and NetworkObject D.
- Your scripts to perform those actions should be on C and D and not A and B (or spawn them such that you achieve that order).
- Example: You spawn NetworkObjects A, B, C, D and want to perform some logical operation between NetworkObject A and NetworkObject C as well as NetworkObject B and NetworkObject D.
- If you dynamically spawn a series of NetworkObjects and need them to access NetworkBehaviours in other NetworkObjects within the series of NetworkObjects spawned, you just need to make sure that the action/script is invoked by the last NetworkObject spawned.
- The recommended way to handle spawning NetworkObjects post loading of a scene is to have the server/host wait for the SceneEventType.LoadEventCompleted which denotes that all clients have finished loading the scene.
- This is for already connected clients where you aren't trying to spawn NetworkObjects dynamically while in the middle of a scene loading event (not recommended, but you can do this with additional scripting to handle that unique scenario).
- When first synchronizing, if you need to access another in-scene placed NetworkObject
- For the host-server:
- NetworkObj X (in the scene) has NetworkBehaviour's B1 and B2, and B1 wants to do some operation on B2, but B2 isn't initialized yet because it isn't net spawned yet (this is https://github.com/Unity-Technologies/com.unity.netcode.gameobjects/issues/2870)
You would place your script that handles accessing B2 within B1's OnNetworkPostSpawn (B2 will have already run through OnNetworkSpawn at that point).
- NetworkObj X (in the scene) has NetworkBehaviour's B1 that wants to do some operation on a different NetworkBehaviour that is on NetworkObj Y (in the scene) but Y hasn't net spawned yet so that other NetworkBehaviour isn't initialized yet (Similar to number 1 but spans two network objects)
You would place your script action to access the NetworkObject-Y's NetworkBehaviour within one of the NetworkObject-X's NetworkBehaviour's OnInSceneObjectsSpawned overridden method. All in-scene placed NetworkObjects are spawned at that time.
- NetworkObj X (in the scene) wants to call an RPC on NetworkObj Y, but Y (also in the scene) hasn't net spawned yet
- X wants to call an RPC that has a NetworkObjectRef to Y, but Y hasn't spawned yet
- X has NetworkBehaviour's B1 and B2, and B1 wants to call an RPC on B2 but B2 hasn't spawned yet.
- X has NetworkBehaviour's B1 and B2, and B1 wants to call its own RPC with a NetworkBehaviourRef to B2, but B2 hasn't spawned yet.
- X wants to re-parent itself to Y, but Y hasn't spawned yet.
- X wants to change the parent of Y, but Y hasn't spawned yet.
All of the above you would handle within an overridden OnInSceneObjectsSpawned method (assuming all of the above are in-scene placed). Otherwise:
- If the context is a client newly synchronizing then you would want to use OnNetworkSessionSynchronized.
- If the context is the server dynamically spawning NetworkObjects and all clients are already connected, you just need to spawn the NetworkObject(s) in the proper order where the last NetworkObject spawned performs the action (i.e. all spawned NetworkObjects before it will be already spawned).
The caveat is if you load scene A and then upon loading scene A you then load Scene B... when handling "cross scene" in-scene placed NetworkObjects you want to wait for the SceneEventType.LoadEventCompleted event for Scene B on say an in-scene placed NetworkObject's NetworkBehaviour contained in scene A.
Does this help further clarify the usage? Not as detailed as above, but the current updated documentation for this can be found here.
Thanks for the long response!
Hmmm... would it be simpler for everyone involved if there was just Pre/Spawn/Post where:
- PreSpawn and Spawn work as you have implemented.
- PostSpawn fires on all NetworkObjects' NetworkBehaviours after Spawn has fired on ALL NetworkObjects' NetworkBehaviours after a scene has loaded for a given client... they receive all NetworkObjects from the new scene or from the in-progress game that they are joining as a new client... Pre/Spawn fire on those as they are deserialized from the stream. After that is all done then PostSpawn is invoked for each.
The way PostSpawn is described to work now, and the nitty gritty of OnInSceneObjectsSpawned vs OnNetworkSessionSynchronized, is quite a lot to describe and keep in one's brain.
PostSpawn seems like a trap and unsafe compared to OnNetworkSessionSynchronized given the descriptions/docs.
PostSpawn fires on all NetworkObjects' NetworkBehaviours after Spawn has fired on ALL NetworkObjects' NetworkBehaviours after a scene has loaded for a given client... they receive all NetworkObjects from the new scene or from the in-progress game that they are spawning into... Pre/Spawn fire on those as they are deserialized from the stream. After that is all done then PostSpawn is invoked for each.
So, I looked into this and the issues I ran into were the distinct different scenarios:
- When a NetworkObject is just dynamically spawned (i.e. server spawns a single NetworkObject)
- When NetworkObjects are being synchronized for a newly joined client
- When in-scene placed NetworkObjects are spawned (i.e. scene loaded)
It is the "knowing when everything that should be spawned has been spawned" issue based on the 3 above scenarios. My "best solution" would be to make the NetworkManager a full fledged state machine and then I could use the current "state" of the NetworkManager to determine when it was finished "spawning everything that needed to be spawned".
This would require me to do a bit more "heavy lifting" of NetworkManager and the over-all flow from not started to started to shutting down.
That I might be able to pull off in v2.0.0, but for v1.x it wouldn't really be possible... well... it would be possible but it wouldn't be done "correctly"... as in I would want to migrate a large portion of stand alone events over to "states" and then have a single event (i.e. NetworkManager.StateUpdated) that would include a state with additional information that most likely would implement some form of interface to access some basic info with additional state specific information that could be acquired via a generic
For v2 I have more flexibility and could decide to do that (as long as I have enough time relative to all other things needed to be done).
Thanks for the insights!