Skip to content

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.

Placeholder: GameMode inspector and scene actor

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:

  1. build a ConnectionRequest from the PlayerRef and connection token;
  2. await GameModeBase.AuthorizePlayerAsync;
  3. disconnect the player if authorization is rejected;
  4. call CreatePlayerController with the returned AuthorizedPlayerData;
  5. assign the player's input authority to the new controller;
  6. call OnPlayerJoined;
  7. for a non-spectator, call RestartPlayer;
  8. select a PlayerStart, create the character, and attach it to the controller.

Placeholder: player join pipeline

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:

  • CreatePlayerController when projects use different controller classes or need authorization data;
  • OnPlayerJoined for team registration or a lobby phase;
  • RestartPlayer for a custom respawn pipeline;
  • CreateCharacterForPlayerController for role-based character selection;
  • FailedToRestartPlayer and FinishRestartPlayer for 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
                                     └────────────────────────→ Aborted

FixedUpdateNetwork detects changes and dispatches the matching hook:

StateHook
WaitingToStartOnMatchIsWaitingToStart()
InProgressOnMatchHasStarted()
WaitingPostMatchOnMatchHasEnded()
LeavingMapOnLeavingMap()
AbortedOnMatchAborted()

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.

Next: GameInstance, Travel, and Bootstrap.

Updated:

RedEngine Framework documentation