Netcode for GameObjects - handling PlayerObjects on scene change

I want to open something up for discussion, which I am not sure is a question, bug report or simply an FYI to everyone who may be similarly confused about the underlying functionality. Anyways, I came up with a solution for what I am about to share, but I am not 100% sure if it’s the best way to handle. So please share your thoughts on any of this and let’s discuss!

To give you some context: Our research team is working on a networking abstraction and unity plugin toolkit built on top of netcode for gameobjects, to kickstart connection management and basic interaction for various XR devices in a multi-user environment. Most importantly for this post, we are expecting the users of our plugin to build applications where player objects for each client may change between scenes.

Overview of how scene switching and user handling is done
(Unity Version 2021.3.26f1 running NGO v1.4.0, Lobby v1.03 & Relay v1.0.5)

Lobby Scene
We start out in a Lobby scene, where all initial connection handling is done, players will connect to a lobby for message relay and device type selection can be made. This is also where the NetworkManager get’s created which exists across all scenes. I won’t go into further detail here, because it’s not the focus of this post.

NetworkSceneSwitch
We wrote a NetworkSceneSwitch, which handles various kinds of scene transitions. It is derived from NetworkBehaviour. Thus, after the initial connection setup (in the Lobby scene) is done*,* it’ll load the first distributed scene OnNetworkSpawn(). Additionally, you can add an offline scene, which will be loaded OnNetworkDespawn(). This si useful to supply a way back into lobby for implicit or explicit disconnects. Of course we guard against the case of going into the lobby when despawn is called due to another kind of scene change. Last, you can use this class to change to any networked by manually calling a public interface. NetworkSceneSwitch instances get destroyed on scene switch, so each scene can have its own scene changes (multiple scene switches are supported per scene as well).

public class NetworkSceneSwitch : NetworkBehaviour
{
    // ...
    public override void OnNetworkSpawn()
    {
        base.OnNetworkSpawn();
        if (IsServer && !string.IsNullOrEmpty(sceneToLoadOnSpawn))
            LoadScene(sceneToLoadOnSpawn, isNetworkScene: true);
    }

    public override void OnNetworkDespawn()
    {
        base.OnNetworkDespawn();
        if (string.IsNullOrEmpty(sceneToLoadOnDespawn))
            return;
        LoadScene(sceneToLoadOnDespawn, isNetworkScene: false);
    }

    private void LoadScene(string name, bool isNetworkScene)
    {
        if (sceneIsLoading)
            return;
        sceneIsLoading = true;
        if (isNetworkScene)
            LoadSceneServerRpc(name);
        else
            SceneManager.LoadScene(name, LoadSceneMode.Single);
    }

    [ServerRpc(RequireOwnership = false)]
    private void LoadSceneServerRpc(FixedString64Bytes name)
    {
        SceneAboutToChangeClientRpc();
        foreach(var client in NetworkManager.Singleton.ConnectedClients.Values)
        {
            if (client.PlayerObject != null)
                client.PlayerObject.Despawn();
        }
        NetworkManager.SceneManager.LoadScene(name.ToString(), LoadSceneMode.Single);
    }

    [ClientRpc]
    private void SceneAboutToChangeClientRpc()
    {
        sceneIsLoading = true;
    }
    // ...
}

NetworkUserSetup
Parses the device specifications derived from the lobby scene config and spawns a respective player object via server rpc called inside OnNetworkSpawn. Any scene spawning a player object can have it’s on NetworkUserSetup, with different prefabs:

public class NetworkUserSetup : NetworkBehaviour
{
    //...
    public override void OnNetworkSpawn()
    {
        int prefabIndex = userRolePrefabs.FindIndex(urp => (urp.userRole == properties.userRole));
        SpawnUserPrefabServerRPC(prefabIndex);
    }

    [ServerRpc(RequireOwnership = false)]
    public void SpawnUserPrefabServerRPC(int prefabIndex, ServerRpcParams serverRpcParams = default)
    {
        GameObject user = (GameObject)Instantiate(userRolePrefabs[prefabIndex].userPrefab);
        user.GetComponent<NetworkObject>().SpawnAsPlayerObject(serverRpcParams.Receive.SenderClientId, destroyWithScene: false);
    }
    //...
}

What is confusing about this?

Note that the function SpawnUserPrefabServerRPC of NetworkUserSetup spawns the user with destroyWithScene = false. Then, the scene LoadSceneServerRpc of NetworkSceneSwitch manually despawns the player object prior to loading the new scene. I know this seems awkward, but I could not rely on any other implementation.

Previously, I had destroyWithScene = true and would not care about despawning. After all, this is what the flag say’s, right? Wrong… apparently. I kept getting an error on scene change on all clients, telling me client-side despawn/destroy is not allowed and should always be handled by the server. I made extra sure that this was in fact produced by the player object and eventually ended up with a prefab that was just a sphere, with a NetworkObject attached. After a bunch of trial and error I arrived at the above implementation, with only “Synchronize Transform” and “Auto Object Parent Sync” set true on the player objects NetworkObject component.

Is it a bug that destroyWithScene = true does not work on player objects? Is it all expected to be like this? Am I missing something here?

I’m experiencing the same issue of destroyWithScene = true doing nothing for my player objects.