MatchmakingManager.cs
using Sandbox;
using Sandbox.Network;
using System.Collections.Generic;
using System.Linq;
using System;
using System.Threading.Tasks;
public sealed class MatchmakingManager : Component
{
[Property, Category( "Settings" )]
public List<SceneFile> GameScenes { get; set; } = new();
[Property, Category( "Settings" )]
public int RequiredPlayers { get; set; } = 6;
public bool IsSearching { get; private set; }
public string StatusMessage { get; private set; } = "Waiting...";
public int ConnectedPlayers => Connection.All.Count;
[Sync] public NetList<Guid> ReadyPlayers { get; set; } = new();
protected override void OnStart()
{
IsSearching = false;
if ( Networking.IsHost )
{
ReadyPlayers.Clear();
}
}
/// <summary>
/// Start matchmaking (auto-join or host lobby)
/// </summary>
public async void StartMatchmaking()
{
if ( IsSearching ) return;
IsSearching = true;
StatusMessage = "Searching for active games...";
try
{
// 1. Query for all active lobbies of our game
var lobbies = await Networking.QueryLobbies();
if ( !IsSearching ) return; // Safety check in case search was cancelled during async query
foreach ( var lobby in lobbies )
{
// If lobby has empty slots
if ( lobby.Members < RequiredPlayers )
{
StatusMessage = $"Connecting to lobby {lobby.LobbyId}...";
Networking.Connect( lobby.LobbyId );
StatusMessage = "Waiting for players...";
return;
}
}
// 2. If no open lobbies found, host our own lobby
StatusMessage = "Creating a new lobby...";
Networking.CreateLobby( new LobbyConfig { MaxPlayers = RequiredPlayers } );
StatusMessage = "Waiting for players...";
}
catch ( System.Exception ex )
{
Log.Error( $"Matchmaking error: {ex.Message}" );
StatusMessage = "Connection error!";
IsSearching = false;
}
}
/// <summary>
/// Cancel search or disconnect from lobby
/// </summary>
public void CancelMatchmaking()
{
IsSearching = false;
StatusMessage = "Matchmaking cancelled";
if ( Networking.IsActive )
{
Networking.Disconnect();
}
if ( Networking.IsHost )
{
ReadyPlayers.Clear();
}
}
[Rpc.Broadcast]
public void ToggleReady( Guid connectionId )
{
// Only host processes the state to avoid client desync
if ( !Networking.IsHost ) return;
if ( ReadyPlayers.Contains( connectionId ) )
{
ReadyPlayers.Remove( connectionId );
}
else
{
ReadyPlayers.Add( connectionId );
}
}
protected override void OnUpdate()
{
if ( !IsSearching ) return;
// 3. If connected to network lobby
if ( Networking.IsActive )
{
if ( ConnectedPlayers < RequiredPlayers )
{
StatusMessage = $"Waiting for players: {ConnectedPlayers} of {RequiredPlayers}";
// Optional: Clear ready states if a player leaves and drops us below requirement
if ( Networking.IsHost && ReadyPlayers.Count > 0 )
{
ReadyPlayers.Clear();
}
}
else
{
int readyCount = ReadyPlayers.Count;
StatusMessage = $"Players ready: {readyCount} / {RequiredPlayers}";
// 4. Start game only if EVERYONE is ready
if ( Networking.IsHost && readyCount >= RequiredPlayers && readyCount == ConnectedPlayers )
{
StatusMessage = "Game starting!";
StartGame();
// Disable searching so we don't spam StartGame
IsSearching = false;
}
}
}
}
private void StartGame()
{
Log.Info( "Starting game!" );
if ( GameScenes == null || GameScenes.Count == 0 )
{
Log.Error( "No game scenes assigned in MatchmakingManager!" );
return;
}
// Randomly select one of the configured scenes
int randomIndex = Random.Shared.Int( 0, GameScenes.Count - 1 );
SceneFile chosenScene = GameScenes[randomIndex];
Log.Info( $"Chosen map: {chosenScene.ResourceName}" );
var options = new SceneLoadOptions();
options.SetScene( chosenScene );
Game.ChangeScene( options );
}
}