Four patterns from Nystrom (2014), applied to the Snap-It architecture.
Primary reference: Nystrom, R. (2014). Game Programming Patterns. Additional references: Millington, I. (2019). AI for Games. Shiffman, D. (2012). The Nature of Code.
The problem this solves: NPC movement logic embedded directly in the NPC class becomes unmaintainable the moment you need more than one movement mode. Every conditional branch is a tighter coupling.
The fix: NPCBehaviour is an abstract class with one pure virtual method,
calculateMovement(float deltaTime, float moveSpeed).
Five concrete subclasses implement it: WanderBehavior, FleeBehavior, FlockBehavior,
AngryNPCBehavior, JournalistBehavior. The NPC class contains no movement logic whatsoever.
It calls m_behaviour->calculateMovement() and applies the returned velocity vector.
The practical result: adding a new behaviour during development required writing one new class.
No existing NPC code was touched. setBehaviour(std::unique_ptr) handles ownership
transfer and safe swapping at runtime - for example, assigning FleeBehavior to an NPC the moment
it enters the radius of a hat-wearing NPC in the FirstCapture phase.
Applied at two levels. First: the GameState enum drives the entire game loop.
Game::update() and Game::render() switch on m_currentState,
ensuring the right logic is active in each phase (Menu, Playing, Capturing, ShowingPhoto, NPCReaction, Paused, GameOver).
Second: AngryNPCBehavior has its own internal state enum: Chasing, Screaming, Wandering.
This is a state machine inside a strategy class. The Chasing state moves toward hat-wearing NPCs at 1.3×
speed until within screaming range. Screaming locks movement for 2 seconds. Wandering uses a random angle
for 2 seconds before returning to Chasing. Timer-driven transitions prevent simultaneous movement and screaming
- a bug encountered during early development when the state check wasn't in place.
A separate GamePhase enum tracks narrative progression. This is managed by PhaseManager
and read by the Game class - the game loop never writes it directly.
The problem: the Game class shouldn't know how to construct phase-specific entities. Putting entity construction in the game loop means changing the construction logic requires touching game loop code.
PhaseManager::createSpecialEntity() returns a std::unique_ptr<SpecialEntity>
with the correct type for the current phase. It constructs a peaceful entity for Introduction, an AngryNPC for
FirstCapture, and a Journalist for Tension. Ownership transfers to the Game class which stores it as
m_specialEntity, m_angryNpc, or m_journalist.
Once created, PhaseManager holds no reference to them - ownership is clean.
The practical result: adding a new phase entity type means adding a new case in createSpecialEntity().
The Game class and game loop don't change.
Entity provides a pure virtual update(sf::Time) contract.
The fixed-timestep game loop at 60Hz calls update on every entity. Each entity is entirely
responsible for its own state - the loop never inspects type or casts.
For NPCs, update() calls m_behaviour->calculateMovement() and applies
the result to position. It also advances the animation timer, checks proximity flags (statue collision,
world wrapping), and handles the highlight overlay timer after capture.
The loop never knows which behaviour is active.
NPC doesn't inherit behaviour or animation - it owns them as components. NPCBehaviour
is stored as a std::unique_ptr and swapped at runtime. Animation is a
value member that manages sprite sheet state independently.
The practical benefit: behaviour and animation evolve independently. During development, new behaviour
types were added without touching the NPC class. Animation state changes (texture reload on state change)
are self-contained in the Animation class. The only exception is SpecialEntity inheriting
from NPC - justified because SpecialEntity is genuinely a type of NPC, using the same movement pipeline,
animation system, and behaviour framework.