I have several calls to await Awaitable.NextFrameAsync()
in a multiplayer game. These calls exist directly after calling NetworkObject.Despawn(false)
followed by returning them to a networked pool (using Unity’s provided example of a networked pool). I recall adding these “because”, but a year later I am looking at them again and wondering if they are or were actually necessary. I wonder if I added them before I was even using pools and was outright destroying objects. Either way, I still wonder if it is necessary in either scenario.
It depends upon what code follows the await Awaitable.NextFrameAsync() call.
However, it is not recommended to use asynchronous tasks/methods as NGO handles updates and timing in a synchronous fashion.
The recommended way to handle things after a NetworkObject has been despawned is to add code to a either the OnNetworkDespawn and/or OnDestroy NetworkBehaviour virtual methods, create a coroutine to check for a specific status of a NetworkObject instance, or to handle clean up in a NetworkUpdateLoop registered method and handling cleanup at a specific point in time relative to the player loop.
Without context to why you were waiting until the next frame it is hard to provide any other advice.
It was probably to prevent some error, but I honestly don’t know. However, here is the context:
- Game is created
- Networked game object pool is created and game objects are spawned
- Game is played
- Some player reaches the score condition
- Game is over
- Despawn all pooled game objects one at a time
- Return all pooled game objects one at a time
- Call
Awaitable.NextFrameAsync()
- Show the score screen, etc
Previously, instead of returning to the pool, I was outright destroying the objects, so I wonder if that’s why I did that…
Now, when you say it is not recommended to use asynchronous tasks/methods
, in what way? I also recently switched all methods that returned a Task to an Awaitable, so surely you don’t mean we cannot use asnyc/await in general if the game uses NGO? Or do you mean specifically in handling of NGOs?
I meant that it isn’t recommended to use await within things that are invoked by messages and the await depends upon something else that might be pending other state or message changes. The reason I don’t recommend it is that you can easily run into issues where you might do something like receiving an RPC (server side) that you then change a server-write permission NetworkVariable and then await for the client to make a change to an owner permission NetworkVariable based on the changes you just made to the server-write permission NetworkVariable… which, depending upon how you are handlingt he await task, could block the primary thread which halts message processing. Also, using await can end up “hiding” exceptions if not used properly…which that too can make it hard to track down an issue.
So, I don’t “recommend” it since NGO depends on the primary player loop to handle message processing and anything that can potentially block the primary thread or can inject an additional layer of complexity will just increase the chance that somewhere, down the road, you are going to be debugging a really complex issue that has a series of nested awaits that all have various dependencies… and so I just “don’t recommend” adding/using awaits in places that are being called from within the NGO stack as that can become problematic.
Now…it doesn’t mean you can’t use them when you need to use them. So… I guess… I should change that to “be cautious” when using tasks or awaitables…
Taking the example in the Awaitable Introduction into consideration it notes towards the end that it can lead to general performance issues depending upon how many GameObject instances that indefinite while loop is running.
If you are using an Awaitable to get a response from a service then that would make sense as you most likely wouldn’t have multiple instances waiting for a response from a service.
If you use something like this: await Awaitable.BackgroundThreadAsync();
If you then start accessing components or other NGO related things that could be changing in the main thread you could run into issues.
I think you can use Awaitables but you should be cautious.
If you are using an Awaitable to check for a change in some netcode driven state then I would say you should think about whether you really want a “poll driven” or “event driven” netcode design.
The “poll driven” design pattern could use something like an Awaitable that waits for some netcode update (NetworkVariable, RPC, change in ownership, etc) which by itself as a single instance can look innocent but if you start using it on many spawned objects and NetworkBehaviour derived classes it can impact over-all performance and become difficult to debug. This kind of design could be handled with a Coroutine as well and yield the same kind of impact.
The “event driven” design pattern invokes scripts or even invokes an event that other systems subscribe to when a netcode state changes. While you are really talking about “running the same script in the end”, it all depends upon how much processing time is consumed waiting for the state change…which event driven there is no processing time consumed waiting as the script is invoked only when the state changes. The advantage of an event design pattern is that you don’t need to worry about canceling anything as there is nothing to be canceled.
Of course, you can run into issues here as well if you have a whole bunch of subscribers to an event and each subscriber invokes a block of script that each by itself seems harmless but the total combined ends up consuming more time than you would like.
But… you can use await, awaitables, coroutines, and even jobs under various conditions… just be cautious.
Regarding the context… it might be you wanted to assure the object was destroyed which can sometimes not be completed until the next frame if you were using “Destroy” as opposed to “DestroyImmediate”.