The Gameplay Framework
Every Unreal game session gets the same standard cast of objects: a GameMode, a GameState, a PlayerController and PlayerState per player, a GameInstance, and a World to hold them. You never spawn these yourself — you subclass them. This guide explains what each one is for, who creates what, and the single idea that makes the whole design click: Unreal was built for multiplayer first, and every one of these classes exists on a specific side of the network for a reason.
1. The Big Picture
In a networked game there is one server that owns the true state of the match, and any number of clients that each see a partial, replicated copy of it. The framework classes are split across that boundary deliberately: some exist only on the server, some are mirrored to every client, and some are private to one machine. The diagram shows the server and one client side by side — who creates what, and what crosses the wire.
One server, one of N clients. In single-player the same picture holds — your machine simply plays both columns at once, which is why everything "just works" until you go multiplayer.
You don't create any of these objects yourself. The engine spawns them at level load and
player join, using the subclasses you registered — in Project Settings → Maps &
Modes, per-map World Settings, or on the GameMode itself (§4). Your job is to subclass and configure, never to
SpawnActor a GameMode.
2. Designed for Multiplayer From Day One
Unreal's gameplay framework descends directly from Unreal Tournament — a competitive online shooter. Its networking model was baked in from the start, and it shapes the design of every class on this page, even if your game is single-player. Three rules explain it:
Rule 1 — the server is the authority
One machine — the server — owns the true state of the game. Clients are, in effect, sophisticated viewers: they render the replicated state they receive and send input. A client never decides "I hit him for 50 damage"; it asks, and the server decides. This is what stops cheating and keeps every player's world consistent. Single-player and play-in-editor are just a listen server with one local player — the same machinery runs, with the network latency set to zero. That's why the framework looks identical either way, and why netcode bugs hide until you actually test with two clients.
Rule 2 — replication flows one way: server → clients
Actors and properties marked for replication are copied from the server to clients, automatically, whenever they change. Clients cannot push a replicated value back — if a client writes to a replicated variable, only its local copy changes, and the next server update overwrites it.
Rule 3 — RPCs are the only conversation across the wire
Besides replicated state, the only way code on one machine can make something happen on
another is a remote procedure call — a
UFUNCTION tagged with where it should run:
// Replicated state: server writes, every client receives. One-way.
UPROPERTY(Replicated) // or ReplicatedUsing=OnRep_Health for a change callback
float Health;
// RPCs — the three directions:
UFUNCTION(Server, Reliable) // client asks the server to do something
void ServerFireWeapon();
UFUNCTION(Client, Reliable) // server tells one owning client something private
void ClientShowHitMarker();
UFUNCTION(NetMulticast, Unreliable) // server broadcasts a cosmetic event to everyone
void MulticastPlayExplosion(); Now the framework's class splits stop looking arbitrary and start looking inevitable:
- GameMode is server-only because it is the authority: it holds the rules, spawn logic, and win conditions — exactly the things clients must never be able to read or tamper with.
- GameState and PlayerState exist purely as the replicated, readable mirrors of the facts clients do need — the scoreboard, the timer, everyone's names. They are the GameMode's public noticeboard.
- PlayerController is each player's private channel: it exists on the owning client and the server, and nowhere else. It's where input becomes Server RPCs, and where the server sends private replies.
3. The Cast at a Glance
The reference table. Each class gets a deeper section below; the "Exists on" column is the one to memorise.
| Class | How many | Exists on | Job |
|---|---|---|---|
| AGameModeBase | 1 per match | Server only | The rules: who can join, what pawn to spawn, when the match ends |
| AGameStateBase | 1 per match | Everywhere (replicated) | Shared match state every client can see: score, timer, phase |
| APlayerController | 1 per player | Owning client + server | Input, camera, and UI for one human |
| APlayerState | 1 per player | Everywhere (replicated) | Per-player facts everyone can see: name, score, ping |
| APawn | 1 per player (usually) | Everywhere (replicated) | The body in the world — disposable; respawns spawn a new one |
| UGameInstance | 1 per game process | Each machine, locally | Survives level changes — settings, session data, persistent systems |
| UWorld | 1 per loaded map | Each machine | The container everything above (except GameInstance) lives in |
GetGameMode() returns null
on every client in a networked game. Anything clients need to read belongs in GameState or
PlayerState — that's the entire reason those classes exist. (In single-player everything runs
"on the server", which is why this trap only bites once you go multiplayer.)
The order these objects get created in — and where in that sequence your players spawn — is charted in the Execution Order guide §11.
4. GameMode & GameState — Rules vs Scoreboard
AGameModeBase is the referee. It runs only
on the server and answers the authority-only questions: may this player join
(PreLogin)? What pawn class do they get, and where does it
spawn (RestartPlayer, spawn points)? Has someone won? It's
also the registry for the rest of the cast — one GameMode subclass picks the whole team:
AMyGameMode::AMyGameMode()
{
DefaultPawnClass = AMyCharacter::StaticClass();
PlayerControllerClass = AMyPlayerController::StaticClass();
GameStateClass = AMyGameState::StaticClass();
PlayerStateClass = AMyPlayerState::StaticClass();
HUDClass = AMyHUD::StaticClass();
} Which GameMode runs is set in Project Settings → Maps & Modes (the default) or overridden per-map in World Settings — which is how your menu level runs a different GameMode than your gameplay levels.
AGameStateBase is the scoreboard: the
GameMode's decisions, published. It replicates to every client and carries the match facts
everyone renders — elapsed time, match phase, team scores — plus
PlayerArray, the replicated list of every player's
PlayerState (the only way a client can enumerate who's in the match). It also provides
server-synchronised time via GetServerWorldTimeSeconds().
The division of labour: GameMode decides, GameState displays.
AGameModeBase/AGameStateBase are
the modern minimal pair. Their subclasses AGameMode/AGameState add a
classic match-state machine (WaitingToStart → InProgress → WaitingPostMatch, ready
checks, spectating) inherited from shooter games. Start from Base; reach for the non-Base pair
only if you want that match flow.
5. PlayerController & PlayerState — One Human, Two Objects
Each human connected to the game is represented by a pair of actors that both outlive any particular pawn: a private one and a public one.
APlayerController is the player's will — the persistent, private half:
- Turns input into actions, owns the camera (
PlayerCameraManager), and is the usual owner of UMG widgets — HUD and menus are per-player concerns. - Exists only on the owning client and the server. Other players' machines have no copy of your controller at all — you cannot iterate "all PlayerControllers" on a client and expect more than your own.
- Is the client's voice: gameplay requests travel as Server RPCs on the PlayerController (or on the pawn it owns), because it's the actor the connection owns.
- Survives pawn death. When your character dies, the controller stays connected and simply possesses the next body (§6).
APlayerState is the player's public record — the replicated half everyone can see:
- Name (
GetPlayerName()), score, ping, team — the scoreboard row for one player. - Replicated to every client, and listed for everyone in
GameState->PlayerArray. - The right home for any per-player fact that must survive respawns and be visible to other players: kills, deaths, character selection, ready status.
One more split-screen wrinkle: on a single machine, each local human is a
ULocalPlayer (owned by the GameInstance) that
pairs up with one PlayerController in the current world. You'll mostly meet it as the thing
CreateWidget and Enhanced Input subsystems hang off.
6. Pawns and Possession
A APawn is a body: an actor that can be possessed by a controller and driven around the world. The controller–pawn split is the framework's cleanest idea — the mind is persistent, the body is disposable:
// Respawn, on the server: new body, same mind.
APawn* NewPawn = GetWorld()->SpawnActor<APawn>(PawnClass, SpawnTransform);
Controller->Possess(NewPawn); // old pawn (if any) is unpossessed first - Possession is a server decision —
Possess()only works with authority. The GameMode'sRestartPlayer()does exactly the snippet above for you at spawn time. - NPCs use the same machinery with an AAIController possessing the pawn instead of a player.
- The pawn replicates to everyone (people need to see your character), but only the owning client sends input for it. Whether to derive from Pawn or ACharacter — which adds walking, jumping and networked movement — is covered in Core Classes §6.
The exact callback sequence when possession happens (and which side each callback runs on) is charted in the Execution Order guide §12.
7. GameInstance & World — The Containers
UGameInstance is the outermost box: one per running process, created before any level and destroyed only when the game quits. Everything else on this page is torn down and rebuilt when you change levels — the GameInstance is what survives the trip. That makes it the home for settings, the party you formed in the menu, save-game handles, and long-lived services. For those services, prefer subclassing UGameInstanceSubsystem over stuffing the GameInstance itself — subsystems are auto-created per GameInstance and keep systems decoupled. Note it is local: the server and every client each have their own, and nothing in it replicates.
UWorld is the loaded map: the container all the
framework actors live in (GameMode, controllers, states, pawns — everything except
GameInstance). One world is current at a time in a running game, but more exist than you'd
think: the editor viewport is a world, and each play-in-editor client is another. That's why
engine code is scrupulous about GetWorld() instead of any
global — "which world am I in?" has more than one answer, especially during PIE. Level travel
replaces the current world wholesale; anything you need on the far side goes in the
GameInstance.
8. Where Should This Variable Live?
The practical payoff of the whole framework. Classify the fact, and the class picks itself:
| The fact is… | It lives on | Why |
|---|---|---|
| A rule, spawn decision, or win condition | GameMode | Authority-only; clients must not read or tamper with it |
| Match state every client renders (timer, phase, team scores) | GameState | One copy, replicated to all |
| A per-player fact everyone sees (name, kills, team, ready) | PlayerState | Replicated to all, and it survives respawns |
| Per-player private state (input mode, camera, open menus) | PlayerController | Only the owning player and the server need it |
| Physical state of the body (health, ammo in the equipped gun) | Pawn / components | Dies with the body — for these facts, that's correct |
| Anything that must survive level travel (settings, party, save data) | GameInstance | The only thing here that outlives the world |
9. Common Gotchas
"GetGameMode() returns null"
You're on a client — this is by design, not a bug (§2). Whatever you wanted from the GameMode, the replicated version of it belongs in GameState or PlayerState. If it isn't there yet, that's the fix — don't try to reach the GameMode from client code.
"GameState / PlayerState is null in BeginPlay"
On clients, framework actors arrive over the network and there's no guarantee they exist
before your actor's BeginPlay runs. Null-check and
defer (a short timer, or better: react in an OnRep_
callback when the data actually arrives) instead of assuming order. The
Execution Order guide covers what
is and isn't guaranteed.
"It worked fine until we tested multiplayer"
Single-player runs everything as the server, so authority checks, replication, and RPC
direction are never exercised. Habitually run PIE with 2+ players and "Play as Client",
and use network emulation (Project Settings, or Net PktLag)
to surface timing assumptions before your players do.
"I stored the score on my Character"
The pawn is disposable — respawn spawns a new pawn and your data dies with the old one. The pawn is not the player; the PlayerState is. Persistent per-player facts go on APlayerState (visible to all) or the PlayerController (private). See the table in §8.
"I set a replicated variable on the client and nobody saw it"
Replication is strictly one-way, server → client. A client writing a replicated property changes only its local copy, which the server's next update silently overwrites. To change shared state from a client, send a Server RPC and let the server make the change — that's the green arrow in the §1 diagram.