Overview
Tested with: UE 5.4+
"GameMode, GameState, PlayerState... the names are similar but what's the difference?" — This is one of the first walls many developers hit when starting UE development. In multiplayer games especially, without a solid understanding of which class exists where and what gets replicated, you'll struggle with server-client data synchronization.
Understanding the role separation of these three classes enables you to build robust, extensible game logic for both single-player and multiplayer games.
Comparing the Three Classes
Let's grasp the big picture first.
| Class | Location | Role | Replicated |
|---|---|---|---|
| GameMode | Server only | Define and enforce game rules | No |
| GameState | Server + all clients | Manage overall game state | Yes |
| PlayerState | Server + all clients | Manage per-player state | Yes |
The most critical point is that GameMode exists only on the server. This design prevents clients from tampering with game rules (win conditions, spawn locations, join requirements, etc.). Game state that clients need to know is synchronized through GameState and PlayerState.
GameMode: The Rulebook
GameMode is the class that defines the game's rules themselves. Think of it as the "referee" or "rulebook."
Key responsibilities:
- Handling player join/leave (login/logout)
- Pawn spawn location, timing, and conditions
- Match start/end conditions
- Game mode-specific rules (deathmatch, CTF, etc.)
AGameModeBase vs AGameMode
UE provides two base classes for GameMode. The default for new projects is AGameModeBase.
| Class | Features | Recommended For |
|---|---|---|
AGameModeBase | Simple and lightweight | Single-player, non-match-based games |
AGameMode | Built-in match state machine | Match-based multiplayer (team deathmatch, etc.) |
AGameMode extends AGameModeBase with a built-in state machine managing six match states: EnteringMap → WaitingToStart → InProgress → WaitingPostMatch → LeavingMap (with Aborted for errors). Used for controlling match waiting periods and post-game screens.
GameMode can be set via Project Settings, level World Settings, or URL arguments, and different levels can use different GameModes.
GameState: The Scoreboard
GameState manages the overall current state of the game, synchronized to all clients. If GameMode is the "rulebook," GameState is the "scoreboard" or "timer."
Key responsibilities:
- Game elapsed time (
GetServerWorldTimeSeconds()for server-synchronized accurate time) - Connected player list (
PlayerArrayproperty) - Team scores and other game-wide information not tied to specific players
- Game started state (
HasBegunPlay())
GameState is for managing game-wide state only. Individual player-specific data (personal scores, names, etc.) belongs in PlayerState, covered next.
PlayerState: The Personal Record
PlayerState manages the state of each individual player in the game. Since it's replicated to all clients, it can be used directly for purposes like displaying other players' info on a score ranking UI.
Built-in properties and methods:
- Player name:
GetPlayerName() - Score:
GetScore()/SetScore() - Ping:
GetPingInMilliseconds() - Player ID:
GetPlayerId() - Bot check:
IsABot()
GameState's PlayerArray contains PlayerState instances. The server and each client can reference the state of all connected players through this array.
Information Flow Example
Let's see how the three classes work together through a concrete scenario.
When a player kills an enemy and earns score:
- The player's Pawn kills an enemy
- The PlayerController (or Pawn) notifies the server of the "enemy killed" event
- The GameMode on the server executes "add score" based on the rules
- GameMode retrieves the relevant player's PlayerState and updates the score
- The PlayerState's score variable is marked
Replicated, so the server's change automatically propagates to all clients - Each client's UI detects the PlayerState change and updates the score display
This is the fundamental design pattern: GameMode enforces rules server-side, and communicates results to all clients via GameState/PlayerState.
C++ Implementation Skeletons
Here are minimal implementation examples for customizing all three classes.
Custom GameMode
// MyGameMode.h
#pragma once
#include "GameFramework/GameModeBase.h"
#include "MyGameMode.generated.h"
UCLASS()
class AMyGameMode : public AGameModeBase
{
GENERATED_BODY()
public:
AMyGameMode();
void OnPlayerScored(APlayerController* Scorer, int32 Points);
virtual void PostLogin(APlayerController* NewPlayer) override;
virtual void Logout(AController* Exiting) override;
};
// MyGameMode.cpp
#include "MyGameMode.h"
#include "MyGameState.h"
#include "MyPlayerState.h"
AMyGameMode::AMyGameMode()
{
// Specify custom GameState and PlayerState classes
GameStateClass = AMyGameState::StaticClass();
PlayerStateClass = AMyPlayerState::StaticClass();
}
void AMyGameMode::OnPlayerScored(APlayerController* Scorer, int32 Points)
{
if (AMyPlayerState* PS = Scorer->GetPlayerState<AMyPlayerState>())
{
PS->AddScore(Points);
}
}
Setting GameStateClass and PlayerStateClass in the constructor tells UE to automatically instantiate your custom classes.
Custom GameState (with Replicated Variables)
// MyGameState.h
#pragma once
#include "GameFramework/GameStateBase.h"
#include "MyGameState.generated.h"
UCLASS()
class AMyGameState : public AGameStateBase
{
GENERATED_BODY()
public:
// Variables marked Replicated are automatically synced to all clients
UPROPERTY(Replicated, BlueprintReadOnly, Category = "Score")
int32 TeamAScore = 0;
UPROPERTY(Replicated, BlueprintReadOnly, Category = "Score")
int32 TeamBScore = 0;
virtual void GetLifetimeReplicatedProps(
TArray<FLifetimeProperty>& OutLifetimeProps) const override;
};
// MyGameState.cpp
#include "MyGameState.h"
#include "Net/UnrealNetwork.h"
void AMyGameState::GetLifetimeReplicatedProps(
TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// Register replicated variables with the DOREPLIFETIME macro
DOREPLIFETIME(AMyGameState, TeamAScore);
DOREPLIFETIME(AMyGameState, TeamBScore);
}
Variables marked with UPROPERTY(Replicated) must be registered with the DOREPLIFETIME macro in GetLifetimeReplicatedProps. Don't forget to include Net/UnrealNetwork.h.
Tip: Use
DOREPLIFETIME_CONDITIONfor conditional replication (owner only, initial only, etc.) to save bandwidth.
Custom PlayerState
// MyPlayerState.h
#pragma once
#include "GameFramework/PlayerState.h"
#include "MyPlayerState.generated.h"
UCLASS()
class AMyPlayerState : public APlayerState
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "Score")
void AddScore(int32 Points);
UPROPERTY(Replicated, BlueprintReadOnly, Category = "Score")
int32 KillCount = 0;
virtual void GetLifetimeReplicatedProps(
TArray<FLifetimeProperty>& OutLifetimeProps) const override;
};
// MyPlayerState.cpp
#include "MyPlayerState.h"
#include "Net/UnrealNetwork.h"
void AMyPlayerState::AddScore(int32 Points)
{
// Update score using APlayerState's built-in method
SetScore(GetScore() + Points);
KillCount++;
}
void AMyPlayerState::GetLifetimeReplicatedProps(
TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(AMyPlayerState, KillCount);
}
APlayerState has built-in SetScore() / GetScore(). When adding custom variables (like KillCount here), use the same Replicated + DOREPLIFETIME pattern to register them.
RepNotify: To detect variable changes on the client side for UI updates, combine
UPROPERTY(ReplicatedUsing=OnRep_KillCount)withUFUNCTION() void OnRep_KillCount(). When the value changes on the server, theOnRep_callback is automatically called on clients — the standard place to put score display update logic.
For single-player games: When not targeting multiplayer, it's viable to skip GameState/PlayerState and consolidate most logic into GameMode. However, if there's any chance of multiplayer expansion in the future, separating into three classes from the start dramatically reduces migration cost.
Summary
- GameMode = Rulebook. Exists only on the server, not replicated to clients
- GameState = Scoreboard. Syncs overall game state to all clients
- PlayerState = Personal record. Syncs per-player info to all clients
- Set
GameStateClass/PlayerStateClassin the GameMode constructor to use custom classes - Register replication variables with
UPROPERTY(Replicated)+DOREPLIFETIMEmacro