Skip to main content

Limiting the maximum number of players

Netcode for GameObjects provides a way to customize the connection approval process that can reject incoming connections based on any number of user-specific reasons.

Boss Room provides one example of how to handle limiting the number of players through the connection approval process:

Assets/Scripts/ConnectionManagement/ConnectionState/HostingState.cs
using System;
using Unity.BossRoom.Infrastructure;
using Unity.BossRoom.UnityServices.Lobbies;
using Unity.Multiplayer.Samples.BossRoom;
using Unity.Multiplayer.Samples.Utilities;
using Unity.Netcode;
using UnityEngine;
using VContainer;

namespace Unity.BossRoom.ConnectionManagement
{
/// <summary>
/// Connection state corresponding to a listening host. Handles incoming client connections. When shutting down or
/// being timed out, transitions to the Offline state.
/// </summary>
class HostingState : OnlineState
{
[Inject]
LobbyServiceFacade m_LobbyServiceFacade;
[Inject]
IPublisher<ConnectionEventMessage> m_ConnectionEventPublisher;

// used in ApprovalCheck. This is intended as a bit of light protection against DOS attacks that rely on sending silly big buffers of garbage.
const int k_MaxConnectPayload = 1024;

public override void Enter()
{
//The "BossRoom" server always advances to CharSelect immediately on start. Different games
//may do this differently.
SceneLoaderWrapper.Instance.LoadScene("CharSelect", useNetworkSceneManager: true);

if (m_LobbyServiceFacade.CurrentUnityLobby != null)
{
m_LobbyServiceFacade.BeginTracking();
}
}

public override void Exit()
{
SessionManager<SessionPlayerData>.Instance.OnServerEnded();
}

public override void OnClientConnected(ulong clientId)
{
m_ConnectionEventPublisher.Publish(new ConnectionEventMessage() { ConnectStatus = ConnectStatus.Success, PlayerName = SessionManager<SessionPlayerData>.Instance.GetPlayerData(clientId)?.PlayerName });
}

public override void OnClientDisconnect(ulong clientId)
{
if (clientId != m_ConnectionManager.NetworkManager.LocalClientId)
{
var playerId = SessionManager<SessionPlayerData>.Instance.GetPlayerId(clientId);
if (playerId != null)
{
var sessionData = SessionManager<SessionPlayerData>.Instance.GetPlayerData(playerId);
if (sessionData.HasValue)
{
m_ConnectionEventPublisher.Publish(new ConnectionEventMessage() { ConnectStatus = ConnectStatus.GenericDisconnect, PlayerName = sessionData.Value.PlayerName });
}
SessionManager<SessionPlayerData>.Instance.DisconnectClient(clientId);
}
}
}

public override void OnUserRequestedShutdown()
{
var reason = JsonUtility.ToJson(ConnectStatus.HostEndedSession);
for (var i = m_ConnectionManager.NetworkManager.ConnectedClientsIds.Count - 1; i >= 0; i--)
{
var id = m_ConnectionManager.NetworkManager.ConnectedClientsIds[i];
if (id != m_ConnectionManager.NetworkManager.LocalClientId)
{
m_ConnectionManager.NetworkManager.DisconnectClient(id, reason);
}
}
m_ConnectionManager.ChangeState(m_ConnectionManager.m_Offline);
}

public override void OnServerStopped()
{
m_ConnectStatusPublisher.Publish(ConnectStatus.GenericDisconnect);
m_ConnectionManager.ChangeState(m_ConnectionManager.m_Offline);
}

/// <summary>
/// This logic plugs into the "ConnectionApprovalResponse" exposed by Netcode.NetworkManager. It is run every time a client connects to us.
/// The complementary logic that runs when the client starts its connection can be found in ClientConnectingState.
/// </summary>
/// <remarks>
/// Multiple things can be done here, some asynchronously. For example, it could authenticate your user against an auth service like UGS' auth service. It can
/// also send custom messages to connecting users before they receive their connection result (this is useful to set status messages client side
/// when connection is refused, for example).
/// Note on authentication: It's usually harder to justify having authentication in a client hosted game's connection approval. Since the host can't be trusted,
/// clients shouldn't send it private authentication tokens you'd usually send to a dedicated server.
/// </remarks>
/// <param name="request"> The initial request contains, among other things, binary data passed into StartClient. In our case, this is the client's GUID,
/// which is a unique identifier for their install of the game that persists across app restarts.
/// <param name="response"> Our response to the approval process. In case of connection refusal with custom return message, we delay using the Pending field.
public override void ApprovalCheck(NetworkManager.ConnectionApprovalRequest request, NetworkManager.ConnectionApprovalResponse response)
{
var connectionData = request.Payload;
var clientId = request.ClientNetworkId;
if (connectionData.Length > k_MaxConnectPayload)
{
// If connectionData too high, deny immediately to avoid wasting time on the server. This is intended as
// a bit of light protection against DOS attacks that rely on sending silly big buffers of garbage.
response.Approved = false;
return;
}

var payload = System.Text.Encoding.UTF8.GetString(connectionData);
var connectionPayload = JsonUtility.FromJson<ConnectionPayload>(payload); // https://docs.unity3d.com/2020.2/Documentation/Manual/JSONSerialization.html
var gameReturnStatus = GetConnectStatus(connectionPayload);

if (gameReturnStatus == ConnectStatus.Success)
{
SessionManager<SessionPlayerData>.Instance.SetupConnectingPlayerSessionData(clientId, connectionPayload.playerId,
new SessionPlayerData(clientId, connectionPayload.playerName, new NetworkGuid(), 0, true));

// connection approval will create a player object for you
response.Approved = true;
response.CreatePlayerObject = true;
response.Position = Vector3.zero;
response.Rotation = Quaternion.identity;
return;
}

response.Approved = false;
response.Reason = JsonUtility.ToJson(gameReturnStatus);
if (m_LobbyServiceFacade.CurrentUnityLobby != null)
{
m_LobbyServiceFacade.RemovePlayerFromLobbyAsync(connectionPayload.playerId, m_LobbyServiceFacade.CurrentUnityLobby.Id);
}
}

ConnectStatus GetConnectStatus(ConnectionPayload connectionPayload)
{
if (m_ConnectionManager.NetworkManager.ConnectedClientsIds.Count >= m_ConnectionManager.MaxConnectedPlayers)
{
return ConnectStatus.ServerFull;
}

if (connectionPayload.isDebug != Debug.isDebugBuild)
{
return ConnectStatus.IncompatibleBuildType;
}

return SessionManager<SessionPlayerData>.Instance.IsDuplicateConnection(connectionPayload.playerId) ?
ConnectStatus.LoggedInAgain : ConnectStatus.Success;
}
}
}

The code below shows an example of an over-capacity check that would prevent more than a certain pre-defined number of players from connecting.

if( m_Portal.NetManager.ConnectedClientsIds.Count >= CharSelectData.k_MaxLobbyPlayers )
{
return ConnectStatus.ServerFull;
}

In connection approval delegate, Netcode for GameObjects doesn't support sending anything more than a Boolean back.

Boss Room shows a way to offer meaningful error code to the client by invoking a client RPC in the same channel that Netcode for GameObjects uses for its connection callback.

When using Relay, ensure the maximum number of peer connections allowed by the host satisfies the logic implemented in the connection approval delegate.