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.
Much less room for errors.
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.
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.
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?
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.
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;
}
}
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.