Appearance
GameMode Pipeline
GameModeBase is the server-side coordinator for one gameplay world. It decides who may join, creates the player's controller, chooses a spawn point, creates the character, and owns the replicated match state. Put exactly one game mode actor in each gameplay scene that can run with state authority.
Execution model
- Authority — GameMode mutation is State-Authority-only: admission, spawn, restart, leave, and match transitions.
- Prediction — clients do not predict player admission, controller creation, or match-state transitions.
- Replicated — match state, controllers, characters, ownership, and any networked match deadlines.
- Local only — join UI, countdown presentation, announcements, particles, and audio.
What belongs in a game mode
Use the game mode for rules that describe the session rather than one character:
- player and spectator limits;
- authentication or session admission;
- controller and default-character selection;
- team assignment or other authorization payloads;
- spawn-point selection and respawn policy;
- match start, end, abort, and map-leave transitions.
Do not use it as a global singleton. A game mode belongs to a World, exists only for that world, and is authoritative only on the server or single-player runner.
Player join pipeline
When Fusion reports a joined player, World runs this sequence on state authority:
- build a
ConnectionRequestfrom thePlayerRefand connection token; - await
GameModeBase.AuthorizePlayerAsync; - disconnect the player if authorization is rejected;
- call
CreatePlayerControllerwith the returnedAuthorizedPlayerData; - assign the player's input authority to the new controller;
- call
OnPlayerJoined; - for a non-spectator, call
RestartPlayer; - select a
PlayerStart, create the character, and attach it to the controller.
This ordering gives authorization a clean place to attach validated information before a network actor is spawned. AuthorizedPlayerData.Payload can carry a project-specific profile or team result into an overridden controller factory.
Authorize a player
The base implementation enforces MaxPlayers and MaxSpectators. A Spectator option in the travel request selects the spectator role. Override the method for authentication or matchmaking data, but return a structured failure instead of spawning partial player state.
csharp
using System.Threading.Tasks;
using RedEngine.Core;
public sealed class ArenaGameMode : GameModeBase
{
public override async Task<PlayerAuthorizationResult> AuthorizePlayerAsync(
ConnectionRequest request)
{
PlayerProfile? profile = await PlayerProfiles.FindAsync(request.Player);
if (profile == null)
return PlayerAuthorizationResult.Rejected(
PlayerJoinFailureReasons.AuthorizationFailed);
return PlayerAuthorizationResult.Allowed(new AuthorizedPlayerData(
request.Player,
new PlayerIdentity(profile.Id, profile.DisplayName),
PlayerJoinRole.Player,
payload: profile));
}
}Keep network mutation after the authorization result returns to the main pipeline. The framework disconnects rejected players with the supplied PlayerJoinFailureReason.
Customize controller and character creation
CreatePlayerController spawns PlayerControllerPrefab through World.SpawnActor. The default OnPlayerJoined immediately calls RestartPlayer, which then calls CreateCharacterForPlayerController at the selected PlayerStart. After a replacement character spawns successfully, RestartPlayer destroys the previous character; failed spawns leave the existing character untouched.
Override the narrowest hook:
CreatePlayerControllerwhen projects use different controller classes or need authorization data;OnPlayerJoinedfor team registration or a lobby phase;RestartPlayerfor a custom respawn pipeline;CreateCharacterForPlayerControllerfor role-based character selection;FailedToRestartPlayerandFinishRestartPlayerfor failure handling and post-spawn setup.
The spawn-point check understands CharacterController, capsule, box, and sphere colliders, plus ISpawnCapsule. Occupied starts are classified as empty, partial, or full before the character is created.
Carry authorization data into the controller
Use AuthorizedPlayerData.Payload to pass validated server-side information into your controller factory. The payload is never a substitute for replicated controller state: copy only the fields clients actually need into [Networked] properties during spawn.
csharp
public sealed record ArenaJoinData(byte Team, string LoadoutId);
public sealed class ArenaGameMode : GameModeBase
{
public override PlayerController? CreatePlayerController(
AuthorizedPlayerData playerData)
{
ArenaJoinData join = playerData.GetRequiredPayload<ArenaJoinData>();
ArenaPlayerController? controller = World.SpawnActor(
arenaControllerPrefab,
inputAuthority: playerData.Player,
onBeforeSpawned: spawned =>
{
spawned.Initialize(playerData);
spawned.InitializeArena(join.Team, join.LoadoutId);
});
return controller;
}
}The factory runs on authority before OnPlayerJoined. If it returns null, World disconnects the joining peer with ControllerSpawnFailed and does not continue into character spawning.
Select a spawn point by team
The default selection prefers an empty or partially occupied PlayerStart, farthest from the current character, and only falls back to a fully occupied start. Override ChoosePlayerStart when the game has stronger rules:
csharp
protected override bool ChoosePlayerStart(
PlayerController controller,
out PlayerStart? foundPlayerStart)
{
string teamTag = ((ArenaPlayerController)controller).Team == 0
? "Blue"
: "Red";
foundPlayerStart = World.GetComponentsOfType<PlayerStart>()
.Where(start => start.PlayerStartTag == teamTag)
.FirstOrDefault(start =>
GetLocationOccupancy(start, controller) != StartLocationOccupancy.Full);
return foundPlayerStart != null;
}Keep the occupancy check unless overlapping spawns are intentional. RestartPlayer remembers the previous StartSpot, creates the replacement first, and destroys the old character only after the new spawn succeeds.
Match-state pipeline
ReplicatedMatchState is networked and uses these states:
text
EnteringMap → WaitingToStart → InProgress → WaitingPostMatch → LeavingMap
└────────────────────────→ AbortedFixedUpdateNetwork detects changes and dispatches the matching hook:
| State | Hook |
|---|---|
WaitingToStart | OnMatchIsWaitingToStart() |
InProgress | OnMatchHasStarted() |
WaitingPostMatch | OnMatchHasEnded() |
LeavingMap | OnLeavingMap() |
Aborted | OnMatchAborted() |
Use ReadyToStartMatch() and ReadyToEndMatch() for authoritative conditions. Use MatchStateChanged(from, to) for shared reactions. Replicated match deadlines should be Fusion TickTimer values, never wall-clock timers or Unity coroutines.
Build a warm-up and timed round
Store deadlines in replicated state so prediction, late join, and rollback all see the same result:
csharp
public sealed class TimedArenaGameMode : GameModeBase
{
[Networked] private TickTimer Warmup { get; set; }
[Networked] private TickTimer Round { get; set; }
protected override void OnMatchIsWaitingToStart()
{
if (Object.HasStateAuthority && !Warmup.IsRunning)
Warmup = TickTimer.CreateFromSeconds(Runner, 5f);
}
protected override bool ReadyToStartMatch() =>
World.PlayerControllers.Any() && Warmup.Expired(Runner);
protected override void OnMatchHasStarted()
{
if (Object.HasStateAuthority)
Round = TickTimer.CreateFromSeconds(Runner, 180f);
}
protected override bool ReadyToEndMatch() => Round.Expired(Runner);
protected override void MatchStateChanged(MatchState from, MatchState to)
{
Logger.LogInfo($"Match state: {from} -> {to}");
}
}FixedUpdateNetwork calls the readiness methods only in their corresponding states. The base class then performs the transition, and every peer receives the matching state hook when ReplicatedMatchState changes.
Respawn after a character dies
Keep the respawn deadline on an authoritative replicated object. Once it expires, resolve the controller and return to the standard pipeline:
csharp
if (Object.HasStateAuthority && RespawnAt.Expired(Runner))
{
RespawnAt = TickTimer.None;
gameMode.RestartPlayer(playerController);
}Calling RestartPlayer preserves spawn selection, spectator checks, replacement safety, and the FailedToRestartPlayer / FinishRestartPlayer extension points.
Player leave pipeline
When a player leaves, World finds the controller by input authority, removes that authority, calls OnPlayerLeft, and destroys the controller. Override OnPlayerLeft to release team slots or save session results; owned gameplay actors should still be despawned through World.