OnNetworkSpawn and IsLocalPlayer

Hi,

I’m trying to make it so that when a player disconnects, their player object remains in the world and when they reconnect it is given back to them. I found some strange behaviour while doing this and not sure it’s the expected behaviour from NGO.

I broke this down into the most simple setup I could and created a 2d project where the player object is just a circle with a networkobject and a network behaviour class attached to it.

Then I run the server like this:

using Unity.Netcode;
using UnityEngine;

public class StartHost : MonoBehaviour
{
private NetworkObject despawnedPo;

void Start()
{
NetworkManager.Singleton.StartHost();
}

private void Update()
{
if (Input.GetKeyUp(KeyCode.Z))
{
despawnedPo = NetworkManager.Singleton.ConnectedClients[0].PlayerObject;
despawnedPo.Despawn(destroy: false);
}
else if (despawnedPo != null && Input.GetKeyUp(KeyCode.X))
{
despawnedPo.SpawnAsPlayerObject(NetworkManager.ServerClientId);
despawnedPo = null;
}
}
}

With a player object class like this:

using Unity.Netcode;
using UnityEngine;

public class Controls : NetworkBehaviour
{
private Transform _transform;
private CharacterController _characterController;

private void Awake()
{
_transform = GetComponent<Transform>();
_characterController = GetComponent<CharacterController>();
}

public override void OnNetworkSpawn()
{
Debug.Log($"Spawned! IsLocalPlayer={IsLocalPlayer}");
base.OnNetworkDespawn();
}

public override void OnNetworkDespawn()
{
Debug.Log($"Despawned! IsLocalPlayer={IsLocalPlayer}");
base.OnNetworkDespawn();
}

void Update()
{
if (!IsLocalPlayer)
{
return;
}
if (Input.GetKeyDown(KeyCode.LeftArrow))
{
_characterController.Move(Vector3.right * -1);
}
else if (Input.GetKeyDown(KeyCode.RightArrow))
{
_characterController.Move(Vector3.right);
}
}
}

Running as the host, this leads to very strange behaviour, and not what you’d expect.

The server starts and the log will say:
“Spawned! IsLocalPlayer=true”

You press z and you get
“Despawned! IsLocalPlayer=true”

All good so far. But the first bit of weird behaviour: the host still has control of the Controls object, despite ownership being taken away. OK that’s fine, just some weird behaviour with hosts being “LocalPlayer” for despawned objects.

But it gets stranger, and this is where I can’t make any sense of the behaviour anymore:

You press x to respawn and you get the message:
“Spawned! IsLocalPlayer=false”

Only at this point do you lose control of the player object, which is super strange because, well… I’m the owner of it now since it just spawned it with “SpawnAsPlayerObject”? But looking at the object closely, it is no longer marked as a PlayerObject at all, which is why IsLocalPlayer is false.

I would have expected the behaviour:

On despawn => IsPlayerObject becomes false and IsLocalPlayer becomes false.
On respawn => IsPlayerObject becomes true and IsLocalPlayer becomes true.

in OnNetworkSpawn you call base.OnNetworkDespawn();
Shouldn’t it be
base.OnNetworkSpawn();?

You’re right that it is backward, but it actually makes no difference to the behaviour. It’s the same regardless, base.OnNetworkSpawn() (and despawn) are empty methods.

I got it working by basically doing this:

  • Set the NetworkObject to DontDestroyWithOwner = true
  • When the client disconnects, don’t do anything special. Don’t call Despawn on it.
  • When (if) the client reconnects, run this code in the same frame
var oldPlayerObj = SomeCodeToFindTheNetworkObject(clientId);
oldPlayerObj.Despawn(destroy: false);
oldPlayerObj.SpawnAsPlayerObject(clientId);

As far as I’m aware, there is no way to assign a player object to a client without it first having been despawned. ChangeOwnership() is not good enough because it doesn’t assign the PlayerObject to the client. Despawning/respawning is a good idea anyway because then you can still have the client setup their controls in OnNetworkSpawn.

As of yet, I have not found any problems with doing this, other than some wacky IsLocalPlayer rules, which you can get around with special checks.

The reason you don’t want to call Despawn(destroy: false) when the client disconnects, is that the networkobject will become invisible to everyone except the host. The NetworkObject component also cannot be referenced with NetworkObjectReference while despawned, which causes all kinds of problems.