Skip to content

GameState and PlayerState in practice

GameState is public match state. PlayerState is public per-player state. If a client needs to see it and it should survive pawn replacement, it probably does not belong only in GameMode, PlayerController, a widget, or the current pawn.

The one-minute version

  • GameMode decides rules on the server. GameState is the replicated public state about the current match/map that clients can read.
  • PlayerState exists for every connected player on the server and clients. It is the normal home for public per-player data such as score, team, display name, ready state, and a replicated loadout summary.
  • A pawn or character can die, be destroyed, and be replaced. PlayerState continues for that connected player, so it is the safer home for state that should survive respawn.
  • A PlayerController is private to the server and owning client. Do not put scoreboard data there if other clients need it.
  • A widget is local presentation. It should read GameState/PlayerState data, not own the multiplayer truth.
  • Use RepNotify on GameState/PlayerState variables when local UI, audio, or materials should refresh as replicated state arrives.

The framework split

The beginner multiplayer split is:

GameMode:
    server-only rules and decisions

GameState:
    replicated game-wide state clients should observe

PlayerState:
    replicated public state for one connected player

PlayerController:
    private server <-> owning-client control path

Pawn / Character:
    current body in the world; can be replaced

Widget:
    local presentation of the state above

If you pick the wrong home, the bug usually sounds like one of these:

"The host sees the timer, but clients see None."
-> It was stored in GameMode, which remote clients do not have.

"My score resets when I die."
-> It was stored only on the pawn that death destroyed.

"Other players cannot see my team color."
-> It was stored in PlayerController or local UI instead of PlayerState.

GameState: public match state

Use GameState for state about the current game session/map that all clients or spectators should know:

State Why GameState fits
Match phase: Waiting, Countdown, InProgress, Finished It is game-wide and every client needs to present it.
Match timer / round time remaining Everyone reads the same public clock or countdown.
Team score It belongs to the match, not one pawn.
Objective state: generators repaired, payload progress, wave number The state describes the shared match.
Public winner/loser result Everyone needs the same final state.
Array/list of public match objectives Clients need a replicated source for UI and world indicators.

The server should set these values. Clients receive them through replication and update local presentation.

Server GameMode:
    decides countdown ended
    -> GameState.MatchPhase = InProgress
    -> GameState.RemainingTime = 300

Clients:
    GameState values replicate
    -> OnRep_MatchPhase / OnRep_RemainingTime
    -> local HUD updates

GameMode can decide. GameState can announce.

PlayerState: public per-player state

Use PlayerState for state about one player that other clients should be able to see:

State Why PlayerState fits
Score / kills / deaths / assists Public per-player scoreboard state.
Team / squad / color Other clients need to classify that player.
Ready state Lobby and match start UI need every player's status.
Display name Every client can show nameplates/scoreboard rows.
Ping or connection-facing public status Already aligned with player identity.
Loadout summary / selected character class Public identity or gameplay-visible choice.
Alive/spectating flag that should outlive the pawn body The body can be destroyed; the player remains connected.

PlayerState is not private player settings and not local-only UI state. If only the owning player should know it, keep it in PlayerController, local UI, GameInstance, or SaveGame as appropriate.

Why pawns are not enough

The pawn is the body in the world:

  • movement;
  • collision;
  • mesh and animation;
  • current body health;
  • weapon/component references attached to the body;
  • abilities whose lifetime is tied to that body.

That body can be destroyed during death, unpossessed during spectating, or replaced during respawn. State stored only there dies with it unless you copy it somewhere else.

Use this split:

Data Better home
Current body health Pawn/Character or HealthComponent.
Score that survives death PlayerState.
Team visible to every player PlayerState.
Current pawn movement mode Pawn/Character.
Whether the connected player is ready PlayerState.
Private selected graphics setting GameInstance / SaveGame, not replicated.
Equipped weapon actor reference for this body Pawn/Character or component.
Public loadout ID shown on scoreboard PlayerState.

If data is about the body, start at the pawn. If data is about the player identity in this match, start at PlayerState.

A practical first setup

GameMode sets the default classes

In your GameMode Blueprint defaults:

GameState Class = BP_MatchGameState
PlayerState Class = BP_MatchPlayerState
Default Pawn Class = BP_PlayerCharacter
Player Controller Class = BP_PlayerController

GameMode exists on the server and chooses these classes. The actual replicated state should live in the GameState/PlayerState classes where clients can read it.

PlayerState variables

In BP_MatchPlayerState:

Score: integer, RepNotify
TeamId: enum or gameplay tag, RepNotify
bReady: boolean, RepNotify
LoadoutId: name/gameplay tag, RepNotify

OnRep functions call local presentation helpers:

OnRep_Score:
    Broadcast local PlayerStateUpdated dispatcher
    scoreboard widget refreshes this row

OnRep_TeamId:
    nameplate/outline/team color refreshes locally

GameState variables

In BP_MatchGameState:

MatchPhase: enum, RepNotify
TeamAScore: integer, RepNotify
TeamBScore: integer, RepNotify
ObjectiveState: struct, RepNotify

If several values must update together, prefer one struct:

FMatchPublicState
    MatchPhase
    RemainingTime
    TeamAScore
    TeamBScore

That avoids racing separate OnRep functions that each assume the other value already arrived.

How clients find PlayerState

Common Blueprint paths:

Local player:
    Get Player Controller
    -> Get Player State
    -> Cast to BP_MatchPlayerState

Current pawn:
    Get Player State
    -> Cast to BP_MatchPlayerState

Scoreboard:
    Get Game State
    -> PlayerArray
    -> ForEach PlayerState
    -> Cast each to BP_MatchPlayerState

Indexed helper:
    Get Player State(Player State Index)

Get Player State(index) reads from the GameState's PlayerArray after initial replication. It is not the same thing as Get Player Controller(index). PlayerState is the public replicated player record; PlayerController is not present for every other player on every client.

How a ready button should work

Owning client's lobby widget:
    Ready button clicked
    -> widget calls PlayerController/owned pawn request
    -> ServerSetReady(true)

ServerSetReady:
    validate the player may ready up
    -> Get PlayerState
    -> bReady = true

All clients:
    bReady replicates on that player's PlayerState
    -> OnRep_Ready
    -> local lobby row refreshes

Server GameMode:
    checks all PlayerStates
    -> if all ready, sets GameState.MatchPhase = Countdown

The widget asks. The server decides. PlayerState shows each player's public ready value. GameState shows the shared phase.

How score should work

Server confirms pickup/kill/objective:
    ScoringPlayerState.Score += Points

Scoring player's client:
    Score replicates
    -> local HUD/scoreboard row updates

Other clients:
    same PlayerState Score replicates
    -> their scoreboard rows update

Do not store the only score in a widget; other machines cannot read your widget. Do not store the only score in the pawn if death destroys the pawn. Do not store public score in PlayerController if other clients need it.

When it fails, and what failure looks like

  1. Reading GameMode on a client returns nothing useful. Remote clients do not have the server's authoritative GameMode instance.
  2. PlayerState has not arrived yet. Early BeginPlay or immediate widget construction can run before all replicated state is ready. Bind refresh events and handle None temporarily.
  3. You cast the wrong class. The active GameMode may still be using the default PlayerState/GameState class.
  4. You changed the value on a client. The local copy changes briefly or only locally, then the server's value wins.
  5. You kept pawn-only state through respawn by accident. Respawn creates a new body; copy persistent player state through PlayerState or another deliberate owner.
  6. You used PlayerController for public data. Other clients usually do not have that controller.
  7. You used one array index as identity forever. Player order can change as players join/leave. Use stable IDs, PlayerState references, or player names as appropriate for your feature.

Print the active GameMode class on the server, the GameState class on clients, the PlayerState class for each row, and whether the value was changed on the server. Most bugs are wrong class, wrong machine, or too early.

Lookalikes - which one do I want?

Object Use when Not for
GameMode Server-only rules, login/start/restart decisions, choosing default classes. Client-readable timers, score, or objective UI.
GameState Public match/session state everyone should know. Per-player public stats that belong to one player.
PlayerState Public per-player state that survives pawn replacement. Private local UI/settings or body-specific health.
PlayerController Local input, private UI, owner-only client messages. Scoreboard data every other client needs.
Pawn / Character Current body movement, collision, health, abilities, animation. Player identity/state that should survive death.
GameInstance Local per-process data across level travel. Replicated match state.
Widget Local presentation and input controls. Multiplayer authority or replicated truth.

The quick path is to put "global" variables in GameMode and "player" variables on the pawn because those are easy to find. The concrete cost is that remote clients cannot read GameMode and respawn can destroy the pawn. GameState and PlayerState exist to avoid exactly those two bugs.

Going deeper