Hey,
So… I’m technically in vacation. But since this problem is quite important to be understood, I spent some time to dig into it.
The result, as I expected, is mostly game specific. However, the inner reasons are generic enough.
Following a big and long post. This is going to describe in details how things work and what you ( @optimise ) should look at.
At the same time, this is important to everyone to understand some important details.
Here we go:
in the project I looked into, after a client connect, the game scene is loaded and then the client communicate to the server that is ready to and the connection is set to be in game.
Pretty standard setup.
Then… What happen: both server and the client start processing ghost prefabs loaded from the game scene, agreed upon the loaded data and then the server stream ghost to client. This at a very high level.
The GhostCollectionSystem is responsible, for both client and server, to process and setup the ghost prefabs and create at runtime the information necessary to serialise the ghost. In particular two buffers are populated:
- The GhostCollectionPrefabSerializer buffer. This buffer contains the serialization information for each prefab. See the component documentation for further info.
- The GhostCollectionComponentIndex, that contains for each serialised component some metadata.
An hash is calculated and assigned to the processed ghost, and it is used to verify that client and server agree on how the ghost is serialised.
The server will process the prefabs as soon as there is a connection that is game. It will add the currently used ghost types to the GhostCollectionPrefab and setup a serialiser for the ghost.
The server will send to the “in-game”, clients, as part of the ghost snapshot, the GhostCollectionPrefab list. Until the list (or a part of it) is not “acked” by the client, no ghost are sent.
If the list is long, the list is sent in multiple frames (up to 32 ghosts per tick IIRC). The list is sent sorted by ghost hash (THIS IS VERY IMPORTANT).
The client should load the same ghosts the server and ack the ones he has loaded (at any point in time). This information is sent as part of the command stream.
As soon as the client receive a GhostCollectionPrefab list update, it will complete the prefab processing, verify that the hash match, and reply to the server how many ghosts from the begin of the list he has loaded. This information is reflected on the server by the SnapshotAckComponent.NumLoadedPrefabs field.
The server uses the number of loaded prefabs to determine the subset of ghosts that he can stream to the client (the one he is able to deerialise).
Because the list is sorted, depending when and how the data is loaded, the client may have only some of the required ghosts present in the list. For example:
The server has loaded 1,2,3,4,5,6,7. At the moment the client receives the list, he has loaded 1,2,3,6,7.
The client will report to the server that is has currently have loaded 3 prefabs (1,2 and 3). This even though it has loaded also 6 and 7. When the client load more ghosts, i.e 4,5, it will report the server tha the num of loaded prefabs is 7 (or any other partials, depending on the case).
In such a case, an hole or a partial number of ghosts are availble, the client should inform the GhostCollectionSystem that the required resource (the ghost prefab) is going to the be loaded soon.
To do that, the client should set the missing entries the GhostCollectinPrefab buffer as loading, by setting the LoadingState to LoadingActive.
The GhostCollectionSystem will not further check or validates other ghosts until the missing resource (in the list order) are loaded (the entry has a prefab entity associated to it).
The client must continue to set the LoadingState to LoadingActive as long as the prefab is not loaded or its state is set to LoadingNotActive.
Failing to do so, will result in a validation error. The GhostCollectionSystem will trigger an error “The ghost collection contains a ghost which does not have a valid prefab on the client” and disconnect the client.
NOTE: This logic is completely missing in the game I checked. And in at least one case I saw an holes in the list (I can give more info privately). If you are loading multiple scenes, at the same time, that may happen depending on the loading order.
The game also have something like 201 ghost types. The first ones in the list are usually player characters, the remaining, are ability and other things. The ability, for what I saw, are ghosts.And in particular they are predictively spawned".
The problem that @optimise isseeing the in is due to the predictively spawned abilities, in conjunction with the the long list of prefabs.
The server spawn the player character and start streaming the ghost data. As soon as the hero is available on the client, when the game re-connect an ability is triggered. And because they use predicted spawning, the client will create a new entity for it. So far so good.
However, because of that, the ghost will processed by the PredictedGhostSpawnSystem, that will try to setup and initialize the new ghost (client only). And this REQUIRES that the ghost type (the prefab) has been already processed by the GhostCollectionSystem. The GhostCollectionPrefabSerializer must contain an entry for that ghost.
If the list of prefab is long enough, and the ability trigger before the ghost has been processed, then the entry may be not present.
This is exactly the error you are seeing: “Could not find ghost type in the collection”. That does not tell you all that (sorry about it, we will add more verbose info).
How to solve this?
Solving the prefab loading issue
If you are experiencing the problem that some prefabs are not available when the client start receiving the ghost type list (and so the GhostCollectionSystem trigger error and disconnect the client) I would suggest to add a system that:
- MUST run before the GhostCollectionSystem
- set the LoadingState to LoadingActive for all the prefab in the GhostCollectionPrefab buffer.
That system should continue to set the loading state as active as long as the the GhostCollectionPrefab.prefab field not assigned (different than Entity.Null).
Solving the predicted spawn problem.
This issue is a little more tricky, depending on how we would like to deal with it.
Ideally, on the client side a ghost should be predictively spawned only if the prefab has been alredy processed and a serialiser has been setup for it.
Unfortunately, we don’t add tags or mark the prefab in any way, so discnerning that on the application side become harder than it should be.
So… what you can do?
A robust solution, in general, involve adding some modification to the package. I have two hot-fix/patch idea here,that aren’t ideal but they are fast to implement.
- Add to the prefab a tag or use a set/dictionary to track which prefab has been already processed by GhostCollectionSystem. That involve modifying the GhostCollectionSystem.ProcessGhostPrefab method a little bit (very very simple)
- Modify the InitGhostJob inside the PredictedGhostSpawnSystem and insted of triggering an exception, just return (wait for the serialisation data to be ready). This is definitiively the easiest. However, there is some downside: The ghost is spawned, so it get simulated, but it may requires some time before the real instance is sent from the server (some round trip). That may be ok, depending on the situation.
I definitively tried the second one (single line fix) and it work. Then there was an issue trying disconnecting again (a missing singleton in your game) but that seemed to me completely unrelated.
We should definively provide a more robust solution for this problem in general, but at the moment we are speaking these are some workaround.
(my apologies for the insane long post)