Skip to content

feat: add pre and post spawn methods #2906

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

NoelStephensUnity
Copy link
Collaborator

@NoelStephensUnity NoelStephensUnity commented Apr 29, 2024

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 all NetworkObjects 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.

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.OnNetworkPreSpawn and NetworkBehaviour.OnNetworkPostSpawn methods that provide the ability to handle pre and post spawning actions during the NetworkObject spawn sequence.

  • Added: A client-side only NetworkBehaviour.OnNetworkSessionSynchronized convenience method that is invoked on all NetworkBehaviours after a newly joined client has finished synchronizing with the network session in progress.

  • Added: NetworkBehaviour.OnInSceneObjectsSpawned convenience method that is invoked when all in-scene NetworkObjects have been spawned after a scene has been loaded or upon a host or server starting.

  • Fixed: Issue where a NetworkObject component's associated NetworkBehaviour components would not be detected if scene loading is disabled in the editor and the currently loaded scene has in-scene placed NetworkObjects.

Testing and Documentation

  • Includes integration tests:
    • NetworkBehaviourPrePostSpawnTests.OnNetworkPreAndPostSpawn (validates dynamically spawned)
    • NetworkBehaviourSessionSynchronized.InScenePlacedSessionSynchronized (validates in-scene placed)
  • Includes public documentation updates: PR-1260

adding pre and post spawn methods.
clearing the m_ChildNetworkBehaviours on Awake.
adding change log entries
Needed to add a NetworkManager ref into the pre spawn method.
Needed to account for locally spawning.
Adding test that validates the pre spawn and post spawn methods are invoked and that order of operations for OnNetworkSpawn invocation is not an issue with OnNetworkPostSpawn invocation.
updating comments
updating comments a bit more
Migrating OnDynamicNetworkPreAndPostSpawn into its own class to avoid interfering with the generic tests.
removing property no longer used and remove LF/CR
Added NetworkBehaviour.OnNetworkSessionSynchronized and NetworkBehaviour.OnInSceneObjectsSpawned methods.

Added test to validate the above methods.

Added assets for the test to validate the above methods.
@zachstronaut
Copy link

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).

  1. 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 OnNetworkSpawn race condition in NetworkTransform for CanCommitToTransform #2870)
  2. 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)
  3. NetworkObj X (in the scene) wants to call an RPC on NetworkObj Y, but Y (also in the scene) hasn't net spawned yet
  4. X wants to call an RPC that has a NetworkObjectRef to Y, but Y hasn't spawned yet
  5. X has NetworkBehaviour's B1 and B2, and B1 wants to call an RPC on B2 but B2 hasn't spawned yet.
  6. 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.
  7. X wants to re-parent itself to Y, but Y hasn't spawned yet.
  8. 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!

@NoelStephensUnity
Copy link
Collaborator Author

NoelStephensUnity commented May 2, 2024

@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.

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.
  • 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).

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.

@zachstronaut
Copy link

zachstronaut commented May 2, 2024

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.

@NoelStephensUnity
Copy link
Collaborator Author

NoelStephensUnity commented May 2, 2024

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 method...which that all would be considered a "breaking change" and is outside of the possibilities for v1.9

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).

@zachstronaut
Copy link

Thanks for the insights!

@NoelStephensUnity NoelStephensUnity merged commit cd6ead3 into develop May 8, 2024
24 checks passed
@NoelStephensUnity NoelStephensUnity deleted the feat/add-pre-and-post-spawn-methods-mtt-8470 branch May 8, 2024 15:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
4 participants