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.
The client creates its Discovery client.
Assets/Client/ClientBootstrap.csholdsSettings/PingCoreClientSettings.asset, which Connect in the PingCore window fills with your fleet's public app id.Assets/Client/Flows/ClientServices.cscreates oneDiscoveryClientper Discovery app withDiscoveryClient.Create, on first use.It checks that something is there to join. At launch,
Assets/Client/UI/ClientUi.Infrastructure.csrunsInfrastructureCheck.RunAsyncon 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.It submits a matchmaking ticket.
FindMatchAsyncinAssets/Client/UI/ClientUi.Matchmaking.csmeasures latency withMeasureLatencyAsync, then callsSubmitTicketAsync. 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.It waits for a match.
TicketHandle.WaitAsyncreturns 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.It connects with a join ticket.
JoinMatchbuilds the join ticket from the matched handle.ConnectinAssets/Client/UI/ClientUi.Session.csencodes it withJoinTicketCodec.TryEncode, andAssets/Client/Flows/GameConnection.csputs it in Netcode for GameObjects' connection data and callsStartClient.The game server admits the player.
PingCoreConnectionApprovalchecks the join ticket against the match's roster, then asksBeaconRushAdmissionGate(see The admission gate).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 withJoinTicket.ForReservation.Assets/Client/Models/QuickPlayPlan.csdecides 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 callsReserveAsync, then connects withJoinTicket.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.
Assets/Game/Hosting/ServerArgs.csreads-portfrom the command line (7777 when it is missing) and refuses a malformed value.DedicatedServercreates the SDK withFleetSdk.Create. On a PingCore-hosted game server it callsStartAsync, which talks to the local SDK endpoint inside the container.Assets/Game/Hosting/HostingModeSelector.cspicks the hosting mode once, from what the process finds.GameServerRuntime.RunAsyncruns 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_TOKENholds 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
playerscounter throughAssets/Game/Hosting/PlayersCounterWriter.cs, which callsSetCounterAsync. 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.RunAsynccreatesPingCoreConnectionApprovalwithHostedAdmissionEvidenceand the game's admission gate, callsInstall, thenStartServer.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
sessionscounter.On an allocation,
OnAllocationReceivedreads the roster withMatchContext.Parseand 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:
The lobby waits for the roster's players. After 60 seconds it starts with whoever is there, or ends the session if nobody came.
The match moves to Results when a player reaches 10 points, or after 3 minutes.
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:
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.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.The session is showing results:
refused_by_game.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.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 asboot,ready,allocation,approvalandsessionEnded. Subscribe toServerEvents.Raised.ServerInstrumentation(Assets/Game/Hosting/ServerInstrumentation.cs) is a class of hooks, such asBeforeReadyAsync,ConfigureApprovalandAllocatedSessionSettings. Subclass it, override what you need, and setServerInstrumentation.Currentbefore the first scene loads, from an assembly of your own that referencesBeaconRush.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.