Score, Objectives, and Win/Loss State¶
Score and objectives are not widget variables. They are gameplay state with an owner, an audience, and a lifetime. Pick those first, then the Blueprint graph gets much smaller.
The one-minute version¶
- GameMode decides rules. It is the server-side place that answers "did this kill count?", "is the objective complete?", and "has the match ended?"
- GameState exposes public match state. Put shared values there when every player, spectator, or HUD needs to know the current phase, team score, wave, objective progress, or winner.
- PlayerState exposes public per-player state. Put score, kills, deaths, assists, team, ready state, and player-facing match identity there when the value belongs to one player and should survive pawn replacement.
- Placed objective or wave managers coordinate level-specific work. A generator set, room puzzle, race checkpoint route, or arena wave can own its local process, then report the public result to GameState or the decision to GameMode.
- Widgets read and request. HUDs, scoreboards, end screens, prompts, and floating labels should display GameState/PlayerState/objective data; they should not be the truth.
- SaveGame is a snapshot, not live state. Copy durable results into a SaveGame object when the run ends, a checkpoint is reached, or the player explicitly saves.
The ownership split¶
The beginner mental model is:
flowchart TD
Event[Kill, pickup, checkpoint, generator, wave clear] --> Rule[GameMode<br/>decides rules on the server]
Event --> Manager[Objective / Wave Manager<br/>coordinates level-specific actors]
Rule --> GS[GameState<br/>public match state]
Rule --> PS[PlayerState<br/>public per-player state]
Manager --> Rule
Manager --> GS
GS --> UI[HUD / scoreboard / end screen<br/>local presentation]
PS --> UI
Rule --> Save[SaveGame write<br/>durable snapshot when appropriate]
GameMode is the authority for rules. GameState and PlayerState are the public records clients can read. A manager actor is useful when the objective is a named thing in the level with placed participants. UI turns those records into bars, rows, markers, text, sounds, and menus.
The quick path is to put every "global" score variable in GameMode and every display value in the widget. The concrete cost is that remote clients cannot read the real GameMode instance, widgets are local only, and pawn/widget lifetime bugs erase state during death, respawn, travel, or late UI creation.
What belongs where¶
| State or behavior | First owner to consider | Why |
|---|---|---|
| Win/loss decision | GameMode | It is a rule decision. In multiplayer, only the server should decide it. |
| Match phase enum: Waiting, Playing, Won, Lost, Finished | GameState | Every player and UI needs the same public phase. |
| Single-player current run score | PlayerState or GameState | Use PlayerState if it belongs to the player; GameState if it is the run/session total. |
| Per-player score, kills, deaths, assists | PlayerState | It belongs to one player and should survive pawn death. |
| Team score | GameState | It is match-wide public state, not one player's body state. |
| Objective progress: 2 of 3 generators | GameState or an objective manager plus GameState summary | The manager can coordinate the generators; GameState exposes the public result. |
| Objective participants: generators, checkpoints, spawners | Placed manager actor or participant actors/components | These are level actors with references, collision, and local behavior. |
| End-screen widget | PlayerController / HUD / local widget path | UI is local presentation; it should read the final public state. |
| High score, unlocked stars, completed mission IDs | SaveGame | This must survive quitting the game. |
| Cross-level temporary run data | GameInstance, then copied into new map owners | It survives Open Level, but it is not replicated and is not saved to disk by itself. |
This table is about ownership, not class purity. A small single-player prototype can start simpler. The rule is still the same: put the value where its lifetime and audience match.
Match phase first, details second¶
Many first projects create several booleans:
bGameStarted
bGameOver
bPlayerWon
bCanRespawn
bShowEndScreen
That looks simple until two booleans disagree. Prefer one phase enum:
EMatchPhase
WaitingToStart
Playing
Won
Lost
Finished
Then put the public phase where the UI can read it:
BP_MatchGameState
MatchPhase: EMatchPhase, RepNotify if networked
WinningTeam: TeamId or None
ObjectiveSummary: FObjectiveSummary
GameMode sets the phase when rules say it changed:
pseudocode - project logic, not engine source
function CheckForEndCondition():
if MatchPhase != Playing:
return
if RequiredObjectivesComplete():
GameState.MatchPhase = Won
GameState.WinningTeam = PlayerTeam
SaveMissionResultIfNeeded()
NotifyPlayersOrLet HUD react to GameState
else if AllPlayersDefeated():
GameState.MatchPhase = Lost
SaveMissionResultIfNeeded()
NotifyPlayersOrLet HUD react to GameState
The phase is the public fact. The end screen is just one local response to that fact.
Score shapes¶
Per-player score¶
Use PlayerState when score belongs to a player:
Enemy death runs once on the server
-> GameMode AwardKill(KillerController, DeadEnemy)
-> KillerController Get PlayerState
-> BP_MatchPlayerState.Score += Points
-> BP_MatchPlayerState.Kills += 1
-> CheckForEndCondition()
The pawn can die and be replaced. The PlayerState remains the public player record for the connected player.
Team score¶
Use GameState when score belongs to the match or team:
Server confirms Team A captured zone
-> GameMode validates capture rule
-> GameState.TeamAScore += 1
-> GameState.ObjectiveSummary = updated public summary
-> CheckForEndCondition()
If one player should also receive credit, update that player's PlayerState in the same server-owned scoring path.
Single-player score¶
Single-player still has lifetime questions:
| Score need | Reasonable first home |
|---|---|
| Score for the current pawn body only | Pawn or component, if losing it on death is intended. |
| Score for the current player/run | PlayerState or GameState. |
| Score visible in HUD after respawn | PlayerState, GameState, or a controller-owned model read by the HUD. |
| Score that survives changing maps | GameInstance during travel, then rebuild into the new map's owner. |
| High score or mission result after quitting | SaveGame. |
Do not store score in a widget because it is easy to see there. The widget may be destroyed and recreated without changing the gameplay truth.
Objective progress¶
Objectives usually have two layers:
- The local process: which levers are on, which enemies are alive, which checkpoints were crossed, which generators are repaired.
- The public summary: "2 / 3 generators", "Wave 4", "Payload 72%", "Objective complete", "Defeat".
A placed objective manager actor often owns the local process:
BP_GeneratorObjectiveManager
Generators: array of BP_Generator references
RequiredCount: 3
CompletedCount: 0
Generator broadcasts OnRepaired
-> manager increments CompletedCount once
-> manager updates GameState.ObjectiveSummary
-> if CompletedCount >= RequiredCount:
manager tells GameMode ObjectiveCompleted(Self)
GameState should expose the summary when HUD, markers, scoreboards, or clients need it. GameMode should decide whether that completion ends the match, awards score, opens travel, or starts a new phase.
End screens¶
An end screen is local UI. The result it presents is gameplay state.
Beginner-safe shape:
Server GameMode decides match ended
-> GameState.MatchPhase = Won or Lost
-> GameState.FinalScore / WinningTeam / ObjectiveSummary updated
Each local PlayerController or HUD:
-> observes GameState phase change
-> creates WBP_EndScreen locally
-> reads GameState and local PlayerState values
-> offers Restart / Continue / Main Menu buttons
In single-player, this may all happen in one process. In multiplayer, the same split prevents a remote client from trying to read server-only GameMode or a host-only widget.
SaveGame and travel¶
SaveGame is for durable values:
FMissionSaveRecord
MissionId
BestScore
bCompleted
StarsEarned
ObjectivesCompleted
Write those values at deliberate points:
- when a checkpoint is accepted;
- when a mission ends;
- when the player confirms a save;
- when returning to a menu;
- when a roguelike run commits its result.
Do not save live actor references, widget references, timer handles, or manager actor pointers. Save stable IDs and plain values. On load, the new map finds or creates the live actors again and feeds them the saved plain data.
GameInstance is the temporary bridge for data across Open Level. It is not a
disk save, and in multiplayer it exists separately on each machine.
Multiplayer authority¶
For networked projects, use the same rule every time:
Client does a thing
-> client sends an owned Run on Server request when gameplay state should change
-> server validates the request
-> GameMode / manager / authoritative actor changes the real state
-> GameState and PlayerState replicate the public facts
-> each client updates local UI from those facts
Clients can play temporary local feedback, but they should not award real score or declare victory because their local graph thought something happened.
Late-joining clients need current replicated state, not a one-time event they missed. That is why final score, phase, objective progress, and player score should be stored as state, not only broadcast as a past message.
Failure modes¶
| Symptom | Likely ownership problem |
|---|---|
| Host sees score, clients see zero | Score was stored in GameMode, a widget, or another non-replicated owner. |
| Score resets on death | It lived only on the pawn that death destroyed. |
| End screen appears only for the host | The host created local UI instead of clients reacting to replicated state or an owning-client message. |
| Score doubles | Death/objective completion ran from more than one path without a one-shot gate. |
| Objective UI says complete, but the door stays locked | Widget state changed instead of the objective owner or server rule state. |
| Player joins late and sees no objective state | Objective was only sent as an event, not stored as replicated current state. |
| High score disappears after restarting the game | It never got copied into a SaveGame object and saved to a slot. |
| Win/loss booleans contradict each other | Several booleans modeled one phase instead of a single enum. |
| Client can cheat score | Client-side UI or pawn graph directly changed authoritative score. |
Debug by printing the actor name, net mode, authority, active GameMode class, GameState class, PlayerState class, old score, new score, and match phase. If the print happened on the wrong machine, fix ownership before changing math.
The pattern everyone actually uses¶
For a first enemy score loop:
BP_Enemy HealthComponent OnDeath
-> BP_Enemy BeginDeath runs once
-> BP_Enemy reports death to GameMode or objective manager with KillerController
GameMode AwardEnemyDeath
-> validate score should count
-> KillerController Get PlayerState
-> BP_MatchPlayerState.Score += EnemyScoreValue
-> GameState.TeamScore += optional team points
-> CheckForEndCondition
HUD / scoreboard
-> reads local PlayerState for personal score
-> reads GameState and PlayerArray for match/team/scoreboard rows
For a first objective:
Generator repaired on server
-> Generator broadcasts OnRepaired once
-> BP_ObjectiveManager updates CompletedCount
-> BP_ObjectiveManager writes public summary to GameState
-> if complete: GameMode CheckForEndCondition
-> HUD refreshes objective text from GameState
Lookalikes - which one do I want?¶
| Owner | Use when | Not for |
|---|---|---|
| GameMode | Server rules: award score, start/end match, decide win/loss, respawn rules. | Client-readable HUD state or durable saves. |
| GameState | Public match state: phase, timer, team score, objective summary, winner. | Private player input or per-player score rows. |
| PlayerState | Public per-player score, team, name, ready, kills/deaths, state through respawn. | Body-specific health or private settings. |
| Objective / wave manager actor | Level-specific coordination of placed generators, doors, spawners, waves, checkpoints. | Whole-project global state or unrelated systems. |
| PlayerController / HUD | Local UI creation, input buttons, owner-only presentation, restart/menu requests. | The authoritative score or objective truth. |
| Pawn / Character | Current body health, movement, weapons, hit reactions, body-specific death. | Score that survives death. |
| GameInstance | Temporary local data across map travel. | Replicated match state or saved-to-disk progress. |
| SaveGame | Durable records that survive quitting. | Live actors, widgets, timers, or current replicated truth. |
Rule of thumb: GameMode decides, GameState announces match state, PlayerState announces player state, managers coordinate local objectives, widgets present, and SaveGame remembers after the live world is gone.
Going deeper¶
- GameMode & friends, single-player edition - framework roles, spawn flow, and why GameMode is not cross-map storage.
- GameState and PlayerState in practice - public match state vs public per-player state in multiplayer.
- HUD state from replicated and local data - combining pawn, PlayerState, GameState, and local-only UI data.
- Death and defeat cleanup flow - awarding score once from a guarded death transition.
- Player Start, Restart Player, and checkpoints - respawn flow and why player state often outlives the pawn.
- Save Game: Create / Save / Load Game to Slot - copying durable results into a SaveGame object.
- RepNotify variables and local presentation - updating local UI when replicated state changes.
- Blueprint Custom Events as RPCs - client requests to server-owned scoring/objective logic.
- Official docs: Game Mode and Game State, Gameplay Framework, and Get Player State.
- Engine source (requires engine access - we do not reproduce it here):
framework declarations under
Engine/Source/Runtime/Engine/Classes/GameFramework/, especiallyGameModeBase.h,GameStateBase.h,GameState.h,PlayerState.h, andPlayerController.h; spawn/login/state flow inEngine/Source/Runtime/Engine/Private/GameModeBase.cpp; PlayerState array handling throughAGameStateBasefunctions inEngine/Source/Runtime/Engine/Private/GameStateBase.cpp.