Can't Spawn Instantiated Network Object After Scene Load

I am kind of blindly making my way to creating a simple multiplayer game for a lesson I’m preparing.

Players join in a lobby (that’s all done), and once the server is ready they hit Start and the game should have all players in the session load the arena and spawn their playable characters.

However, while I have been able to use the Spawn() function in the lobby, I keep getting this error when trying to do so in the arena:

NullReferenceException: Object reference not set to an instance of an object
Unity.Netcode.Components.NetworkTransform.InternalOnNetworkObjectParentChanged (Unity.Netcode.NetworkObject parentNetworkObject) (at ./Library/PackageCache/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs:3469)
Unity.Netcode.NetworkObject.InvokeBehaviourOnNetworkObjectParentChanged (Unity.Netcode.NetworkObject parentNetworkObject) (at ./Library/PackageCache/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs:1820)
Unity.Netcode.NetworkObject.ApplyNetworkParenting (System.Boolean removeParent, System.Boolean ignoreNotSpawned, System.Boolean orphanedChildPass) (at ./Library/PackageCache/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs:2238)
Unity.Netcode.NetworkSpawnManager.SpawnNetworkObjectLocallyCommon (Unity.Netcode.NetworkObject networkObject, System.UInt64 networkId, System.Boolean sceneObject, System.Boolean playerObject, System.UInt64 ownerClientId, System.Boolean destroyWithScene) (at ./Library/PackageCache/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs:1113)
Unity.Netcode.NetworkSpawnManager.SpawnNetworkObjectLocally (Unity.Netcode.NetworkObject networkObject, System.UInt64 networkId, System.Boolean sceneObject, System.Boolean playerObject, System.UInt64 ownerClientId, System.Boolean destroyWithScene) (at ./Library/PackageCache/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs:1021)
Unity.Netcode.NetworkObject.SpawnInternal (System.Boolean destroyWithScene, System.UInt64 ownerClientId, System.Boolean playerObject) (at ./Library/PackageCache/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs:1594)
Unity.Netcode.NetworkObject.Spawn (System.Boolean destroyWithScene) (at ./Library/PackageCache/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs:1706)
SessionManager.SpawnNetworkObject (Unity.Netcode.NetworkObject obj) (at Assets/Scripts/Network/SessionManager.cs:150)
Gamemode_LastOneStanding.SpawnPlayer (Player player) (at Assets/Scripts/Classes/Gamemode_LastOneStanding.cs:88)
Gamemode_LastOneStanding.OnPlayerJoin (System.UInt64 player) (at Assets/Scripts/Classes/Gamemode_LastOneStanding.cs:74)
GamemodeBase.<OnEnable>b__2_0 (System.UInt64 clientId, System.String sceneName, UnityEngine.SceneManagement.LoadSceneMode loadSceneMode) (at Assets/Scripts/Classes/GamemodeBase.cs:17)
Unity.Netcode.NetworkSceneManager.OnSessionOwnerLoadedScene (System.UInt32 sceneEventId, UnityEngine.SceneManagement.Scene scene) (at ./Library/PackageCache/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs:1911)
Unity.Netcode.NetworkSceneManager.OnSceneLoaded (System.UInt32 sceneEventId) (at ./Library/PackageCache/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs:1836)
Unity.Netcode.SceneEventProgress.<SetAsyncOperation>b__37_0 (UnityEngine.AsyncOperation asyncOp2) (at ./Library/PackageCache/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventProgress.cs:272)
UnityEngine.AsyncOperation.InvokeCompletionEvent () (at <0900e0d4bb644dafbfd59eb7fd222a68>:0)

Here are the scripts mentioned:

public abstract class GamemodeBase : NetworkBehaviour
{
    public GamemodeSO gamemodeSO;

    public static GamemodeBase Singleton;

    private void OnEnable()
    {
        NetworkManager.SceneManager.OnLoadComplete += (ulong clientId, string sceneName, LoadSceneMode loadSceneMode) => OnPlayerJoin(clientId);
    }

    private void OnDisable()
    {

        NetworkManager.SceneManager.OnLoadComplete -= (ulong clientId, string sceneName, LoadSceneMode loadSceneMode) => OnPlayerJoin(clientId);
    }

    public abstract void Init();
    public abstract void OnFirstPlayerSpawn(PlayerScript player);

    public abstract void OnLastPlayerSpawn(PlayerScript lastPlayer);

    public abstract void OnPlayerJoin(ulong player);

    public abstract void OnAllPlayersSpawn();

    public abstract void OnPlayerDisconnect(Player player);

    public abstract void OnPlayerConnect(Player player);

    public abstract void Victory(PlayerScript[] winners);

    public abstract void Defeat(PlayerScript[] losers);

    public abstract bool OnPlayerKilled(PlayerScript player, FragDetails details);

    public abstract bool OnTickPassed();

    public abstract void SpawnPlayer(Player player);

    [Rpc(SendTo.Server)]
    public void NotifyPlayerArrivalToGamemodeRpc(ulong playerID)
    {
        Debug.Log(playerID);

        this.OnPlayerJoin(playerID);
    }
     
    public void SetAsSingleton()
    {
        Singleton = this;
    }
}

This is the script throwing the error

public class Gamemode_LastOneStanding : GamemodeBase
{
    public NetworkObject playerPrefab;
    public NetworkVariable<bool> matchStarted = new NetworkVariable<bool>(false);
    public NetworkVariable<bool> readyToStart = new NetworkVariable<bool>(false);

    public override void Init()
    {
    }

    public override void Defeat(PlayerScript[] losers)
    {
        throw new System.NotImplementedException();
    }

    public override void OnAllPlayersSpawn()
    {
        throw new System.NotImplementedException();
    }

    public override void OnFirstPlayerSpawn(PlayerScript player)
    {
        throw new System.NotImplementedException();
    }

    public override void OnLastPlayerSpawn(PlayerScript lastPlayer)
    {
        throw new System.NotImplementedException();
    }

    public override void OnPlayerConnect(Player player)
    {
        throw new System.NotImplementedException();
    }

    public override void OnPlayerDisconnect(Player player)
    {
        throw new System.NotImplementedException();
    }

    public override bool OnPlayerKilled(PlayerScript player, FragDetails details)
    {
        throw new System.NotImplementedException();
    }

    public override bool OnTickPassed()
    {
        throw new System.NotImplementedException();
    }

    public override void Victory(PlayerScript[] winners)
    {
        throw new System.NotImplementedException();
    }

    public override void OnPlayerJoin(ulong player)
    {
        if (!IsServer)
            return;

        Player targetPlayer;

        if (SessionManager.Singleton.TryGetRegisteredPlayer(player, out targetPlayer))
        {
            SpawnPlayer(targetPlayer);
        }

    }

    public override void SpawnPlayer(Player player)
    {
        if (!IsServer)
            return;

        var instance = Instantiate(playerPrefab);
        instance.gameObject.name = "Player: " + player.playerName;

        //V This line is the culprit
        instance.SpawnWithOwnership(player.OwnerClientId, true);
    }
}

And here’s the function where I load the scene

    public void LoadSelectedMap()
    {
        if (HostManager.selectedMap == null)
            Debug.LogError("ERROR: No map selected.");

        var status = m_NetworkManager.SceneManager.LoadScene(HostManager.selectedMap.mapName, UnityEngine.SceneManagement.LoadSceneMode.Single);

        if (status != SceneEventProgressStatus.Started)
        {
            Debug.LogWarning($"Failed to load {HostManager.selectedMap.mapName} " +
                  $"with a {nameof(SceneEventProgressStatus)}: {status}");
        }
    }

The prefab in question has the NetworkObject component assigned to it. It’s an empty game object with two playable objects parented to it (both of which also have NO components, don’t know if that’s good practice)

What am I doing wrong?

What are “playable objects”?
From the callstack it sounds like there’s parenting involved in the spawning process. Which would indicate that perhaps at least one of the child objects contains a NetworkObject and/or NetworkTransform?

Like in teaching? If so, I have a few things to say about the code. :wink:

IMO it should be GameModeBase. Pascal naming. Even if one can argue that a “Gamemode” is just a single word like “Filename” for some reason it reads awful to me, much like NUnit’s “SetUp” is so awkward although correct (it imitates being a Setter for “Up” ugh). Traditionally you’ll find “GameMode” pascal cased much more often than you’ll see “FileName” written in Pascal case.

You shouldn’t make an abstract NetworkBehaviour and subclass from that for game modes. Several of these base class methods are in no way game mode specific, such as player spawn/join but also Victory and Defeat. They are plain and simple callbacks of either mode and you’ll probably notice as you move along that all of these methods are going to be almost identical regardless of game mode.

The abstract class is supposedly a singleton but nowhere in this code is anyone calling SetAsSingleton. Moreover, this method mustn’t be public, you can’t have outside code determine that any given instance should be the singleton!

Also word of caution when using singletons in a networked environment: you are prone to run into execution order issues. A true singleton should be an object in the first scene (that is never loaded again) and put itself in DontDestroyOnLoad and then responds to game events like “OnNetworkSessionStarted/Stopped” to enable/disable its functions. In the same scene you’ll also add NetworkManager because it is that kind of singleton too. Of course that won’t work with a NetworkBehaviour but I see no real reason why this singleton should derive from NetworkBehaviour, all the convenience properties (IsServer etc) can be obtained via NetworkManager too.

I also strongly advice against the common but ill-founded practice of littering methods with early outs like if (!IsServer) and similar. Instead what you really want is separate ServerGameMode and ClientGameMode scripts which enable themselves in OnNetworkSpawn based on the IsServer flag, ie enabled = IsServer;. If there’s one true and very important advice to hand out to students is that you must not mix server and client code in the same script. You will repeatedly brainf…k yourself if you do! :face_with_spiral_eyes:

Doing so guarantees that all of the component code is bound to a specific role, making it much simpler to write and reason about the code. One of the main pitfalls of a combined script is that you could have fields which are only used (have meaningful values) on either the client or server side but you may easily use a server-only field in client code and vice versa. Unless you are strict about prefixing every field and method with “Server” or “Client” but that would only be a crutch.

Like in teaching?

Yes. I’m trying to learn Netcode so I can teach it later this semester. It’d be an introductory class so I don’t feel like I have to be an expert, just enough to get them through the initial hump. I have a lot of experience with Photon, but I was entirely self-taught and haven’t done anything with online code in half a decade, and I thought that Netcode might be a better long-term solution for the students.

From the callstack it sounds like there’s parenting involved in the spawning process. Which would indicate that perhaps at least one of the child objects contains a NetworkObject and/or NetworkTransform?

Yes, that’s what I meant to say when I wrote that, sorry it was late.

IMO it should be GameModeBase. Pascal naming. Even if one can argue that a “Gamemode” is just a single word like “Filename” for some reason it reads awful to me, much like NUnit’s “SetUp” is so awkward although correct (it imitates being a Setter for “Up” ugh). Traditionally you’ll find “GameMode” pascal cased much more often than you’ll see “FileName” written in Pascal case.

That’s fair. English is my second language, and sometimes I think of “Gamemode”, “Videogame” and “Setup” as one word. I usually do Pascal capitalization.

You shouldn’t make an abstract NetworkBehaviour and subclass from that for game modes.

Because it’s bad practice or because Netcode doesn’t support it?

Several of these base class methods are in no way game mode specific, such as player spawn/join but also Victory and Defeat. They are plain and simple callbacks of either mode and you’ll probably notice as you move along that all of these methods are going to be almost identical regardless of game mode.

Yeah, I was just putting everything into the inherited class first and then find out what to move back to the base class once I knew better what I was doing. The idea was for the derived game mode to determine when the game ends, who won, who lost, etc. I realize looking now through non-sleep-deprived eyes that my naming was way too vague haha

The abstract class is supposedly a singleton but nowhere in this code is anyone calling SetAsSingleton. Moreover, this method mustn’t be public, you can’t have outside code determine that any given instance should be the singleton!
Also word of caution when using singletons in a networked environment: you are prone to run into execution order issues. A true singleton should be an object in the first scene (that is never loaded again) and put itself in DontDestroyOnLoad and then responds to game events like “OnNetworkSessionStarted/Stopped” to enable/disable its functions. In the same scene you’ll also add NetworkManager because it is that kind of singleton too. Of course that won’t work with a NetworkBehaviour but I see no real reason why this singleton should derive from NetworkBehaviour, all the convenience properties (IsServer etc) can be obtained via NetworkManager too.

Thank you, you make a really good point. I actually ran into this problem, so I was trying to limit the use of singletons and instances to scripts that wouldn’t ever be put into that situation, I think I forgot to remove the one from GameModeBase after getting sidetracked with this issue.

I also strongly advice against the common but ill-founded practice of littering methods with early outs like if (!IsServer) and similar. Instead what you really want is separate ServerGameMode and ClientGameMode scripts which enable themselves in OnNetworkSpawn based on the IsServer flag, ie enabled = IsServer;. If there’s one true and very important advice to hand out to students is that you must not mix server and client code in the same script. You will repeatedly brainf…k yourself if you do!
Doing so guarantees that all of the component code is bound to a specific role, making it much simpler to write and reason about the code. One of the main pitfalls of a combined script is that you could have fields which are only used (have meaningful values) on either the client or server side but you may easily use a server-only field in client code and vice versa. Unless you are strict about prefixing every field and method with “Server” or “Client” but that would only be a crutch.

Oooh, hadn’t thought of that. All the tutorials I’ve found (including Unity’s own documentation) just use it like that. I was not super comfortable with that solution from the beginning for the reasons you mention, but I was afraid of experimenting this early.

1 Like

This is mainly because it’s the easy route for small code snippets. But nobody tells you that this style doesn’t scale to production while Youtubers are quick to simply copy whatever Unity’s style is, assuming it’s all around correct.

Hence why we still see beginner’s coming in here with GameObject.Find(“…”) code and similarly flawed concepts that have scalable AND easier alternatives.

1 Like

After refactoring the code the way you suggested it works now! Something else I also fixed was that I used to have two child objects in the player with separate Network Transforms that could be disabled and enabled (a quick and dirty approach at game mode selection), but this was causing some issue with network parenting (no idea why). I fixed that up as well and it works now.

Thanks