How Beacon Rush Works

Where the Beacon Rush sample calls the PingCore SDK: one player's path from Find match to a seat, the game server's start, its admission gate and its optional hooks.

This page shows where Beacon Rush calls the PingCore SDK, so you know which files to copy from. The SDK calls themselves are explained on The Client SDK and The Game Server SDK.

Paths are relative to SampleGame/ in the repository. The game server and the code both sides share are in Assets/Game/. The player build is in Assets/Client/, which never goes into a dedicated server build.

One player's path

This is the path of a player who presses Find match, from the first call to playing on a game server.

Screenshot: the Beacon Rush menu in the Editor, with Quick play, Find match and Browse game servers.

  1. The client creates its Discovery client. Assets/Client/ClientBootstrap.cs holds Settings/PingCoreClientSettings.asset, which Connect in the PingCore window fills with your fleet's public app id. Assets/Client/Flows/ClientServices.cs creates one DiscoveryClient per Discovery app with DiscoveryClient.Create, on first use.

  2. It checks that something is there to join. At launch, Assets/Client/UI/ClientUi.Infrastructure.cs runs InfrastructureCheck.RunAsync on the fleet app and shows the report on the menu until it passes. It runs again on Check again and after Discovery refuses a join.

  3. It submits a matchmaking ticket. FindMatchAsync in Assets/Client/UI/ClientUi.Matchmaking.cs measures latency with MeasureLatencyAsync, then calls SubmitTicketAsync. The first call that needs a player token asks Discovery for an anonymous one, which is why the fleet's Discovery app must allow anonymous player tokens.

  4. It waits for a match. TicketHandle.WaitAsync returns once the ticket is matched. By then the matchmaker has allocated a game server, and the game server has opened a session for the match.

  5. It connects with a join ticket. JoinMatch builds the join ticket from the matched handle. Connect in Assets/Client/UI/ClientUi.Session.cs encodes it with JoinTicketCodec.TryEncode, and Assets/Client/Flows/GameConnection.cs puts it in Netcode for GameObjects' connection data and calls StartClient.

  6. The game server admits the player. PingCoreConnectionApproval checks the join ticket against the match's roster, then asks BeaconRushAdmissionGate (see The admission gate).

  7. The player plays. The client shows the lobby until the match starts. When the match is over, the game server ends the session and goes back to ready.

The ticket asks for 4 players and accepts 2 after 10 seconds, on the queue rush-p2. From Assets/Client/Models/MatchmakingModel.cs:

return new TicketOptions
{
    Queue = Queue,
    SessionSize = SessionSize,
    MinSessionSize = MinSessionSize,
    RelaxAfterSeconds = RelaxAfterSeconds,
    JoinInProgress = joinInProgress,
};

A ticket sent with a player token must ask for at least 2 players, so one player alone keeps searching on Find match until a second player queues.

Once the ticket is matched, JoinMatch connects with the join ticket the handle builds:

Connect(endpoint, matched.CreateJoinTicket(BeaconRushProtocol.Version, JoinName), MatchmakingModel.SessionSize);

The other ways in

The other menu entries differ only in steps 3 to 5:

  • Quick play calls QuickJoinAsync, then connects with JoinTicket.ForReservation. Assets/Client/Models/QuickPlayPlan.cs decides what happens when that fails. With no free seat anywhere it queues on Find match instead. When the game server was taken between the hold and the join, it tries once more.

  • Browse game servers lists game servers with ListServersAsync, and Join on a row calls ReserveAsync, then connects with JoinTicket.ForReservation (Assets/Client/UI/ClientUi.Browse.cs).

  • Host a game and Direct connect start and join a listen host. See Self-Hosted Game Servers and Listen Hosts.

On a refusal, Assets/Client/Models/ConnectionText.cs turns the reason into a sentence. For server_full the player reads "Could not join: That game server is full. Pick another one or try again shortly."

The game server's start

Assets/Game/Scenes/Server.unity holds one object with DedicatedServer, GameServerRuntime and BeaconRushNetwork on it. DedicatedServer.Start runs the boot.

  1. Assets/Game/Hosting/ServerArgs.cs reads -port from the command line (7777 when it is missing) and refuses a malformed value.

  2. DedicatedServer creates the SDK with FleetSdk.Create. On a PingCore-hosted game server it calls StartAsync, which talks to the local SDK endpoint inside the container.

  3. Assets/Game/Hosting/HostingModeSelector.cs picks the hosting mode once, from what the process finds.

  4. GameServerRuntime.RunAsync runs that mode.

From Assets/Game/Hosting/DedicatedServer.cs:

fleet = FleetSdk.Create(new FleetSdkOptions { Log = FleetEventBridge.OnFleetLog });

bool answered = false;
if (fleet.IsHosted)
{
    bridge.Attach(fleet);
    answered = await fleet.StartAsync(token);
}

HostingSelection selection = HostingModeSelector.Select(fleet.IsHosted, answered, HostingEnvironment.DiscoveryToken());

Hosting mode

  • Hosted: the local SDK endpoint is there and answered. This is the mode on a fleet.

  • Endpoint silent: the endpoint is configured but never answered. The game server quits with exit code 1 and never falls back to another mode.

  • Self-hosted: no local SDK endpoint, and PINGCORE_DISCOVERY_TOKEN holds a heartbeat token.

  • Local: neither. The game server listens and admits LAN joins only.

The self-hosted and listen modes are covered on Self-Hosted Game Servers and Listen Hosts.

Hosted mode

Assets/Game/Hosting/HostedMode.cs holds everything a fleet game server does:

  • Before listening, it writes 0 to the players counter through Assets/Game/Hosting/PlayersCounterWriter.cs, which calls SetCounterAsync. That first write tells PingCore the game uses the SDK, so the game server stays out of rotation until it calls ready.

  • It installs connection approval and listens. GameServerRuntime.RunAsync creates PingCoreConnectionApproval with HostedAdmissionEvidence and the game's admission gate, calls Install, then StartServer.

  • After listening, it calls ReadyAsync. The matchmaker can now allocate the game server.

  • On every join and leave, it writes the new player count. It never writes the sessions counter.

  • On an allocation, OnAllocationReceived reads the roster with MatchContext.Parse and opens a session that waits for the roster's players. A quick-play player on an idle game server claims a session of its own, with a 10 second lobby.

  • When PingCore clears an allocation the game did not end, the session closes and its players are disconnected.

Ending sessions

Assets/Game/Session/SessionDirector.cs runs the session as Lobby, Match and Results:

  1. The lobby waits for the roster's players. After 60 seconds it starts with whoever is there, or ends the session if nobody came.

  2. The match moves to Results when a player reaches 10 points, or after 3 minutes.

  3. Results ends the session after 10 seconds. The last player leaving a running match also ends it.

When a session ends, the game server disconnects its players and HostedMode.EndSessionAsync calls the SDK:

FleetCallResult ended = await fleet.EndSessionAsync(command.AllocationId, cancellationToken);

The game server is then ready for the next match. Beacon Rush ends sessions and does not recycle its process. A game that wants a fresh process calls ShutdownAsync instead.

On quit, DedicatedServer stops admitting players, calls NotifyProcessStopping and shuts down Netcode for GameObjects.

The admission gate

PingCoreConnectionApproval checks the evidence for each join ticket (the hold, the roster or the backfill). Assets/Game/Admission/BeaconRushAdmissionGate.cs then applies the game's own rule, in this order:

  1. The game server was allocated to a match between a quick-play hold and its session claim: refused_by_game. The client tries quick play once more.

  2. No open session: not_in_session. The exception is a quick-play player on an idle hosted game server, who is admitted and claims a session.

  3. The session is showing results: refused_by_game.

  4. A reservation into the lobby of a matched session: refused_by_game, because counting that player would start the match before the matched players arrive.

  5. All 8 seats taken: server_full.

From BeaconRushAdmissionGate.cs:

if (!current.SessionOpen && !IsSoloJoin(ticket, current, facts))
{
    return AdmissionGateResult.Reject(JoinRejectReason.NotInSession, current.Mode == GameHostingMode.Hosted
        ? "this game server has no open session"
        : "no local session is open");
}

if (current.Phase == SessionPhase.Results)
{
    return AdmissionGateResult.Reject(JoinRejectReason.RefusedByGame, "the match is over; the session is about to end");
}

These rules are Beacon Rush's. Your gate can decide differently, but it can only answer not_in_session, server_full or refused_by_game. See Admitting Players.

Optional instrumentation hooks

Beacon Rush has two extension points for your own logging or debug tools. With nothing attached, they change nothing.

  • ServerEvents (Assets/Game/Hosting/ServerEvents.cs) raises a named event at each step, such as boot, ready, allocation, approval and sessionEnded. Subscribe to ServerEvents.Raised.

  • ServerInstrumentation (Assets/Game/Hosting/ServerInstrumentation.cs) is a class of hooks, such as BeforeReadyAsync, ConfigureApproval and AllocatedSessionSettings. Subclass it, override what you need, and set ServerInstrumentation.Current before the first scene loads, from an assembly of your own that references BeaconRush.Game.

A hook can weaken admission, so keep such tools out of every build players get. See Keeping Secrets Out of Your Build.

Next

Adding PingCore to Your Own Game lists what to copy from these files and what to leave.