Ramblings of an aging IT geek
← Ramblings of an aging IT geek
gamedev

Finally Understanding Unreal's Gameplay Framework

After fighting Unreal's GameMode, PlayerController and Pawn split for weeks, the moment it clicked was realising which classes live on the server and which the client only borrows.

A game engine viewport on a monitor

I came to Unreal from backend work, which meant I arrived with strong opinions about where state should live and almost no idea where Unreal thinks it should live. For the first few weeks the gameplay framework felt like a pile of classes with overlapping names that all sort of did the same thing. GameMode, GameState, PlayerController, PlayerState, Pawn, Character. Why so many? What owns what?

The thing that finally made it click wasn't a tutorial. It was drawing the network boundary.

Server-authoritative changes everything

Once I accepted that Unreal assumes a server-authoritative model by default, even when you're building a single-player game, the class split stopped looking arbitrary and started looking obvious. Each class exists where it does because of where it can exist on the network.

  • GameMode lives only on the server. It's the rulebook: who can join, where they spawn, when the match ends. Clients never see it, and trying to reach it from client code is one of the classic early mistakes.
  • GameState is the server's view of the match, replicated down to every client. Scoreline, match timer, the things everyone needs to agree on.
  • PlayerController is the bridge between a human and the simulation. The owning client has one, the server has a copy for every connected player.
  • Pawn (or Character) is the physical thing in the world that a controller drives. Controllers can possess and unpossess pawns, which is why the two are separate at all.
  • PlayerState is the per-player data that needs replicating: name, score, team. It outlives the pawn, so when you respawn you don't lose your score.

A diagram of overlapping code structures

Written out like that it reads as common sense. It did not feel like common sense at the time.

A small example that helped

The penny properly dropped when I stopped putting state on the Pawn. I'd been storing health on the Character, which works until the character dies and gets destroyed, taking the value with it. Moving the persistent stuff onto PlayerState and keeping only the in-world, transient state on the Pawn made respawning trivial:

void AMyPlayerState::AddScore(int32 Amount)
{
    // Replicated to all clients via GetLifetimeReplicatedProps
    Score += Amount;
}

The Pawn became disposable, which is exactly what it should be. It's a body. Bodies are replaceable. The identity lives elsewhere.

What I'd tell my past self

Stop asking "which class should hold this". Ask "where does this need to exist on the network, and who is allowed to change it". The answer to that question tells you the class. GameMode for rules the server enforces, GameState for shared truth, PlayerState for per-player truth, PlayerController for intent, Pawn for the puppet.

It took me a frustratingly long time to internalise something the documentation states fairly plainly. In my defence, the documentation states it plainly only once you already understand it. That's the curse of a lot of engine docs, and it's why I'm writing this down: so the next backend refugee who wanders into Unreal has one more plain-English description to bounce off. The framework is good. It's just opinionated in a way that only makes sense from the inside.