Cant start client with Relay

Hi everyone!

I just started to explore all of those Netcode and Unity Services to understand how they work and how to use them.

So I created A simple demo of Bomberman concept without any Netcode, and then I started to integrate it when as I thought it was the correct time for this.

I successfully installed NGO, Relay and Authorization services. I get Hosting and Relay connection work, but I stuck at the moment when I need to connect client with Relay. What is interesting, is that I can connect to Relay and Even can see how the host is walking around, but player is not instanced and Networck manager says that there is already instance of the client.

There is link to Git: LitvinenkoMan/BomberMan at dev (github.com)

///

public class HostCreator : MonoBehaviour
{
    [Header("Buttons to create room")] [SerializeField]
    private Button CreateHostButton;
    
    [SerializeField]
    private TMP_InputField RoomPassword;
    [SerializeField]
    private TMP_Text RoomJoinCodeText;
    [SerializeField]
    private TMP_Dropdown LevelSelection;

    
    [Space(20)]
    [Header("Hosting Events:")]
    [Space(10)]
    [SerializeField, Tooltip("This will fire at moment when host is only trying to launch.")]
    public UnityEvent OnHostLaunched;
    [SerializeField]
    public UnityEvent OnHostStarted;

    private void OnEnable()
    {
        CreateHostButton.onClick.AddListener(CreateHost);
        NetworkManager.Singleton.Shutdown();
    }

    private void OnDisable()
    {
        CreateHostButton.onClick.RemoveListener(CreateHost);
    }

    private async void CreateHost()
    {
        OnHostLaunched?.Invoke();
        
        Allocation allocation = await RelayManager.Instance.CreateRelay(8);
        
        if (allocation != null)
        {
            string joinCode = await RelayManager.Instance.GetJoinCode(allocation);
            RoomJoinCodeText.text = joinCode;
            if (!string.IsNullOrEmpty(joinCode))
            {
                NetworkManager.Singleton.GetComponent<UnityTransport>().SetRelayServerData(
                    allocation.RelayServer.IpV4,
                    (ushort)allocation.RelayServer.Port,
                    allocation.AllocationIdBytes,
                    allocation.Key,
                    allocation.ConnectionData
                );

                NetworkManager.Singleton.StartHost();
                ServerStartedRpc();
                
            }
        }
    }

    [Rpc(SendTo.ClientsAndHost)]
    private void ServerStartedRpc()
    {
        PlayerSpawner.Instance.SpawnPlayer();
        OnHostStarted?.Invoke();
    }
}

///

public class ClientConnector : MonoBehaviour
{
    [Header("Buttons to join the room")]
    [SerializeField]
    private Button JoinHostButton;
    
    [SerializeField]
    private TMP_InputField RoomName;


    [Space(20)]
    [Header("Connection Events:")]
    [Space(10)]
    public UnityEvent OnClientConnectionLaunched;
    public UnityEvent OnClientConnected;

    //private string _joinCodeResult;
    
    private void OnEnable()
    {
        JoinHostButton.onClick.AddListener(JoinHost);
    }

    private void OnDisable()
    {
        JoinHostButton.onClick.RemoveListener(JoinHost);
    }
    
    public async void JoinHost()
    {
        OnClientConnectionLaunched?.Invoke();

        string joinCode = RoomName.text;
        if (!string.IsNullOrEmpty(joinCode))
        {
            JoinAllocation joinAllocation = await RelayManager.Instance.JoinRelay(joinCode);
            
            if (joinAllocation != null)
            {
                NetworkManager.Singleton.GetComponent<UnityTransport>().SetRelayServerData(
                    joinAllocation.RelayServer.IpV4,
                    (ushort)joinAllocation.RelayServer.Port,
                    joinAllocation.AllocationIdBytes,
                    joinAllocation.Key,
                    joinAllocation.ConnectionData,
                    joinAllocation.HostConnectionData
                );
                
                NetworkManager.Singleton.OnClientStarted += ClientConnected;
                NetworkManager.Singleton.StartClient();   // There it telling me that I cant start client
            }
        }
    }

    private void ClientConnected()
    {
        Debug.Log("PlayerConnected");
        PlayerSpawner.Instance.SpawnPlayer();
        OnClientConnected?.Invoke();
    }
}

You call it like this:

transport.SetRelayServerData(new RelayServerData(relayConfig.HostAllocation, "dtls"));

Much less room for errors. :wink:
dtls if you use encryption, otherwise leave empty

That won’t work. You can’t StartXxxxx and directly afterwards send an RPC. The object isn’t spawned yet! You have to implement OnNetworkSpawn and that’ll be the very first place you can call an RPC.

Also note that you put this RPC in a MonoBehaviour object. It needs to be a NetworkBehaviour. If you change that and leave the code as is, I’m sure it’ll give you a warning or error.

Nothing about the relay needs “managing”. You create an allocation and get the code, respectively join an allocation with a code. There’s no state when it comes to relay so a separate component for it seems like overkill.

This is essentially all the relay code I have in my Write Better Netcode project:

			if (role == NetcodeRole.Server || role == NetcodeRole.Host)
			{
				var allocation = await relay.CreateAllocationAsync(relayConfig.MaxConnections, relayConfig.Region);
				var joinCode = await relay.GetJoinCodeAsync(allocation.AllocationId);
				relayConfig.SetHostAllocation(allocation, joinCode);
			}
			else
			{
				var joinAlloc = await relay.JoinAllocationAsync(relayConfig.JoinCode);
				relayConfig.SetJoinAllocation(joinAlloc);
			}

The relayConfig is a data object that provides the transport access to the allocation respectively for the client to enter the join code.

Ah finally someone who doesn’t name every class a Manager. :slight_smile:
But since you call this from ClientConnected callback I hope you know that only the server can spawn objects. So the PlayerSpawner would have to be a NetworkBehaviour that sends an RPC to the server to spawn the player instance.

But personally I would prefer to enable connection approval because then you can send a payload through which the server gets to know which kind of player prefab to spawn for that client the instance the player connects. Like an index, say 3 could be the mage, 4 would be the paladin, etc.

Thaks for reply!

I Changed to have this:

transport.SetRelayServerData(new RelayServerData(relayConfig.HostAllocation, "dtls"));

on both of Client connection and Hosting sides, thanks

About the Relay Manager. Yes, It’s might be overkill, but it does the same thing you showed me on your example, I decided to put it in other class to have a lot of logs of what is going on. I am pretty shure I will change this as soon as I will have a success on multiplayer (when Client is connected and walking)

PlayerSpawner:

public class PlayerSpawner : NetworkBehaviour
{
    [SerializeField] private GameObject Player;              // PlayerPrefab, 
    [SerializeField] private GameObject Camera;
    [SerializeField] private bool SpawnOnStart;

    public static PlayerSpawner Instance;

    private LevelSectionsDataHolder _currentLevelDataHolder;

    private void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject); 
        }
    }

    private void OnEnable()
    {
        if (SpawnOnStart)
        {
            SpawnPlayerRpc();
        }
    }

    [Rpc(SendTo.ClientsAndHost)]
    public void SpawnPlayerRpc()
    {
        GameObject player = Instantiate(Player, Vector3.zero, new Quaternion(0, 0, 0, 0));
        if (IsOwner)
        {
            GameObject camera = Instantiate(Camera, Vector3.zero, new Quaternion(0, 0, 0, 0));
            
            if (camera.GetComponentInChildren<CinemachineTargetGroup>())
            {
                camera.GetComponentInChildren<CinemachineTargetGroup>().AddMember(player.transform, 1, 0);
            }                   
        }

        player.name = $"Player {AuthenticationService.Instance.PlayerId}";
        player.transform.position = GetRandomSpawnGameObject().transform.position;
        player.GetComponent<NetworkObject>().Spawn();
    }

    private GameObject GetRandomSpawnGameObject()
    {
        int chosenNumber = Random.Range(0, _currentLevelDataHolder.SpawnPlaces.Count);
        return _currentLevelDataHolder.SpawnPlaces[chosenNumber];
    }

    public void SetUpCurrentDataHolder(LevelSectionsDataHolder dataHolder)
    {
        _currentLevelDataHolder = dataHolder;
    }
}

So I have this PlayerSpawner on host and client in DontDestroyOnLoad scene, I call SpawnPlayer() at the moment when client is started, also I call this method at moment when Host is started.
Should I create two methods of Spawning: one for server and second for clients?

Those appears when I try to Spawn player from client side. As I understand this is problem of not correct usage of an RPC method. But how should I?

also can it be that cause of problem goes from that I didn’t used NetworkObject.Spawn() on Player spawner?

You should not call RPCs in OnEnable or Start. Can’t recall the sequence but it depends on whether the prefab is instantiated or in the scene. The object may not be “spawned” from the network’s perspective. You can check with the IsSpawned flag.

Always use OnNetworkSpawn to run any network initialization code because then the code will work regardless of the object being spawned or placed in the scene.

The PlayerSpawner doesn’t seem to be registered with the network prefabs list according to the error (if its not the “not spawned” issue above). It has to be a prefab and in the network prefabs list, whether you instantiate it or manually place it in the scene.

Yeah, I get it now.

Okey, so I solved this and now I have Player spawner which one spawns Player for Host and Client. But now I have problem that When Client is connecting to Host, both of them have Player spawner which one is listening to OnClientConnectedCallBack, And this leads to spawn 2 identical Clients with same ClientID

how Can I Avoid this?

Updated PlayerSpawner

public class PlayerSpawner : NetworkBehaviour
{
    [SerializeField] private GameObject Player;

    public static PlayerSpawner Instance;

    private LevelSectionsDataHolder _currentLevelDataHolder;

    private void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject); 
        }
    }

    public override void OnNetworkSpawn()
    {
        NetworkManager.OnClientConnectedCallback += SpawnClient;
    }

    public override void OnNetworkDespawn()
    {
        NetworkManager.OnClientConnectedCallback -= SpawnClient;
    }

    private void SpawnClient(ulong clientId)
    {
        if (IsClient && !IsServer)
        {
            SpawnPlayerRpc(clientId);
        }
    }

    [Rpc(SendTo.Server)]
    public void SpawnPlayerRpc(ulong clientId)
    {
        GameObject player = Instantiate(Player, Vector3.zero, new Quaternion(0, 0, 0, 0));

        player.name = $"Player {clientId}";
        player.transform.position = GetRandomSpawnGameObject().transform.position;
        player.GetComponent<NetworkObject>().SpawnAsPlayerObject(clientId);
    }

    private GameObject GetRandomSpawnPosition()
    {
        int chosenNumber = Random.Range(0, _currentLevelDataHolder.SpawnPlaces.Count);
        return _currentLevelDataHolder.SpawnPlaces[chosenNumber];
    }

    public void SetUpCurrentDataHolder(LevelSectionsDataHolder dataHolder)
    {
        _currentLevelDataHolder = dataHolder;
    }
}

Edit: Okey I get it, I can just add this:

public override void OnNetworkSpawn()
    {
        if (IsHost)
        {
            NetworkManager.OnClientConnectedCallback += SpawnClient;
        }
    }

    public override void OnNetworkDespawn()
    {
        if (IsHost)
        {
            NetworkManager.OnClientConnectedCallback -= SpawnClient;
        }
    }

Be careful with those events! There’s no guarantee that client connection will occur AFTER that object has spawned!

Always follow this flow:

  • register all NetworkManager events that you need
  • call StartServer/StartHost/StartClient

You mean once the other player joins, you actually get to have 2 player objects each?

You likely want to change that check to:

    private void SpawnClient(ulong clientId)
    {
        if (IsOwner)
        {
            SpawnPlayerRpc(clientId);
        }
    }

Thank you for help, thanks to you I managed to solve my issue and I made what I wanted. I had script for players which gives them ability to walk, I changed it to be NetworkBehaviour and added RPC on Move(), and now host getting messages that some of players trying to move and allows them to.

Once again, Thanks!

1 Like