Prefabs not loading on client builds?

“The ghost collection contains a ghost which does not have a valid prefab on the client! Ghost: ‘PrespawnSceneList’”

I receive this error when I attempt to run a client in a build (I use a tool to draw the console in builds), followed by the client immediately getting dropped. Oddly, the game runs fine if the client is in the editor, or if a build runs as the host. It will occasionally work as normal in builds too, but these events are rare and I haven’t noticed a pattern. I tried running the builds on a separate machine to see if it had anything to do with my machine, but the error still popped up.

Prior research found that the issue might be caused by client side ghosts failing to load before a connection is established, but I don’t know how to test that or how to fix it.

2 Likes

Bumping this, I’ve yet to find any meaningful response to this issue, and all staff members who do respond, neglect details and don’t answer follow-ups. Very frustrating.

Hey, this sounds like a common gotcha with subscene prefab loading. I.e.

  • If the client connection is established with the server before the clients subscene (containing the ghost prefab entity) has loaded, you’ll get this error.
  • If the client connection is established after the ghost prefab Entity has been loaded as part of the subscene, you won’t.

Nuances:

  • In the Editor, when entering play-mode, the current scene’s subscenes are loaded in a synchronous, blocking manner, thus they avoid this timing issue.
  • In host mode builds, I expect the server subscene[s] load on the same tick as the client subscene[s], which again accidentally bypasses this issue.

The fix is to check that subscene loading has completed before beginning to establish a connection, using something like this:

using var scenesQuery = world.EntityManager.CreateEntityQuery(ComponentType.ReadOnly<SceneReference>());
            using var scenesLeftToLoad = scenesQuery.ToEntityListAsync(Allocator.Persistent, out var handle);
            handle.Complete();

            float count = scenesLeftToLoad.Length;
            while (scenesLeftToLoad.Length > 0)
            {
                for (var i = 0; i < scenesLeftToLoad.Length; i++)
                {
                    var sceneEntity = scenesLeftToLoad[i];
                    if (SceneSystem.IsSceneLoaded(world.Unmanaged, sceneEntity))
                    {
                        scenesLeftToLoad.RemoveAt(i);
                        var numLoaded = count - scenesLeftToLoad.Length;
                        var loadingProgress = numLoaded / count;
                        LoadingData.Instance.UpdateLoading(step, loadingProgress);
                        i--;
                    }
                }

                await Awaitable.NextFrameAsync();
            }

Pulled from the Competitive Action Template’s ScenesLoader.cs.

Frustratingly, the NetcodeSamples Frontend.cs does not make use of this, but it should.


Also note: This same issue can show up as missing IComponentData Singletons, as it’s easy to assume a singleton will exist in OnCreate (or the first few OnUpdate calls), but it won’t have in builds.


EDIT: Another option is to mark the affected prefabs in the client’s ghost collection as “loading” every tick, but that’s a much more obscure API. Example from our test:

            var collectionEntity = SystemAPI.GetSingletonEntity<GhostCollection>();
            var ghostCollection = EntityManager.GetBuffer<GhostCollectionPrefab>(collectionEntity);

            // This must be done on the main thread for now, for read/write safety reasons.
            for (int i = 0; i < ghostCollection.Length; ++i)
            {
                ref var ghost = ref ghostCollection.ElementAt(i);
                if (ghost.GhostPrefab == Entity.Null) // I.e. A ghost prefab received from the server is not yet recognised by this client.
                {
                    ghost.Loading = GhostCollectionPrefab.LoadingState.LoadingActive; // Tell Netcode that you (the developer) know that the prefab is still loading, thus, do not err.
                    // IMPORTANT NOTE 1: You should probably add a time limit to this!
                    // IMPORTANT NOTE 2: The server will not send any ghosts of this prefab type until the client reports that it has loaded said ghost prefab. The report happens automatically, upon successful load.
                }
            }

I’m already waiting for subscenes to be fully loaded first using almost identical code to the one you provided, and it doesn’t help. Furthermore, the introduction of your more obscure trick also doesn’t help (I’m assuming that code is to be used to prevent adding NetworkStreamInGame from the client). Through my observation and of another forum poster (sorry I couldn’t find the post), it appears that the client doesn’t get access to any prefabs to load at all before adding that NetworkStreamInGame, where the error occurs, preventing the prefabs from ever being sent to the client. I have sent a bug report and it is currently being reviewed by a developer now, so whatever the issue is, whether on my side or on Unity’s, it will be resolved.

Ah okay.

This part is by design, at least. NetworkStreamInGame must be added to both the client & server connection entities for snapshot synchronization (including ghost prefab resolution) to work, so maybe its a timing issue there. Can you post the bug incident number so I can fast track it?

IN-118499, this one I believe.

I fixed this error by manually and synchronously loading subscene before the connection request.

public SubScene gameScene;

public async void StartRelayClient()
{
    processingUI.SetActive(true);
    DisposeAllGameWorlds();

    await relayClient.ConnectAsync(code.text);

    var relayClientData = relayClient.RelayClientData;
    var relayServerData = relayServer.RelayServerData;

    var oldConstructor = NetworkStreamReceiveSystem.DriverConstructor;
    NetworkStreamReceiveSystem.DriverConstructor = new RelayDriverConstructor(relayServerData, relayClientData);
    var client = ClientServerBootstrap.CreateClientWorld("ClientWorld");
    NetworkStreamReceiveSystem.DriverConstructor = oldConstructor;

    LoadRequiredSceneToWorld(client);

    World.DefaultGameObjectInjectionWorld = client;

    var networkStreamEntity = client.EntityManager.CreateEntity(ComponentType.ReadWrite<NetworkStreamRequestConnect>());
    client.EntityManager.SetComponentData(networkStreamEntity, new NetworkStreamRequestConnect { Endpoint = relayClientData.Endpoint });

    SwapUI();
}

private void LoadRequiredSceneToWorld(World world)
{
    SceneSystem.LoadSceneAsync(world.Unmanaged, gameScene.SceneGUID, new SceneSystem.LoadParameters 
    { 
        Flags = SceneLoadFlags.BlockOnImport | SceneLoadFlags.BlockOnStreamIn,
        AutoLoad = true,
        Priority = 100,
    });
    Debug.Log($"{world.Name} loaded scene with guid {gameScene.SceneGUID}");
}