Best practices for dynamic initialization of network objects

In my current code, I’m doing some initial setting of network values before spawning a prefab.

GamePlayerController controller = Instantiate(_playerControllerPrefab);

if (controller && controller.NetworkObject)
{
    controller.AssignToPlayer(joinedPlayer.NetworkObjectId);
    controller.NetworkObject.SpawnWithOwnership(joinedPlayer.OwnerClientId);
}

In this case, I’m telling the controller object to assign itself to the given player, which is either an AI owned by the server or connected client player. However this is producing the following warning:

“NetworkVariable is written to, but doesn’t know its NetworkBehaviour yet. Are you modifying a NetworkVariable before the NetworkObject is spawned?”

After a bit of searching, it seems like the recommendation from unity is that we don’t write to NetworkVariables before the object has been spawned (see Adding a value to a NetworkList before spawning its networkObject results in a null ref exception · Issue #2159 · Unity-Technologies/com.unity.netcode.gameobjects · GitHub).

So with all that in mind, what is considered the best practice for doing dynamic network initialization like this? Is there a way to dynamically set the initial state of a network variable without needing to send an additional command like an RPC or having some spawn queueing system?

set the NetworkVariable values of the spawned object in its OnNetworkSpawn method to whatever initial values desired

you can use an instance of a struct or scriptableobject as a temporary value holder if you really really have to assign values right after calling Instantiate and before spawning (but I would question that kind of design … it feels flawed on first thought)

Thanks for the info! This is what I’m thinking at the moment, but the issue then becomes I need to implement some kind of initialization data caching to apply later.

For example if I spawn a controllable object for a client, I spawn that object with the remote client as the owner. However, only the server knows the initialization data, so I need to send a second init command after the spawn to get that data across. This feels like it’ll be problematic as I’ll need to have 2 round trips to create a new object. I’m currently doing all my spawning on the server (maybe this isn’t necessary?), but for objects owned by remote clients initialization is pretty cumbersome.

The other option I’m toying with is doing all my spawning on the server in an initialization step. Then implement some kind of pooling once all the network data has been synced, but that also has issues where I potentially have a bunch of unnecessary data going across the wire.

No there aren‘t necessarily two round trips. Have you tried the following on the server?

  • Instantiate prefab
  • Spawn object with ownership
  • call an Init method on the spawned object, passing in the init values
  • Init() on object => assigns init values to the corresponding NetworkVariables

This should in theory synchronize the spawn and the network variables for the client all at once. Give it a try, maybe it works.

I think this should work if you call Init after spawning but it will fail with the error you mentioned if you call it right after Instantiate.

Thanks for the tip!

For anyone curious, here is the solution I ended up with. This function is responsible for spawning a controller object (i.e. character, spectator, etc.) to a player in the game. This logic assumes clients own their controller objects, while the server owns all AI.

GameMode class

GamePlayerController controller = Instantiate(_playerControllerPrefab);
if (controller && controller.NetworkObject)
{
    if (joinedPlayer.IsOwnedByServer)
    {
        controller.NetworkObject.Spawn();
    }
    else
    {
        controller.NetworkObject.SpawnWithOwnership(joinedPlayer.NetworkObjectId);
    }

    controller.AssignToPlayer(joinedPlayer.NetworkObjectId);
}

From here, I have logic in the OnGainedOwnership and value changed events of the network variable set in AssignToPlayer to register the controller with the player object.

GamePlayer class

public override void OnGainedOwnership()
{
    base.OnGainedOwnership();

    if (!IsServer)
    {
        GamePlayer player = GamePlayerManager.GetClientPlayer<GamePlayer>(OwnerClientId);
        player.SetController(this);
    }
}

private void OnAssignedToPlayer(ulong previousValue, ulong newValue)
{
    name = string.Format("{0}: {1}", GetType().Name, _assignedPlayerObjectId.Value);

    if (GamePlayerManager.GetPlayer(previousValue, out GamePlayer previousPlayer))
    {
        previousPlayer.SetController(null);
    }

    if (GamePlayerManager.GetPlayer(newValue, out GamePlayer newPlayer))
    {
        newPlayer.SetController(this);
    }
}

While I haven’t tested extensively yet, it seems to be working. Thanks again for the help!

1 Like