A top-down 2D survival game.
Thorns is a top-down 2D survival game built in C++17 with SFML 3.0. The research question was simple: can the Game Programming Patterns catalogue (Nystrom, 2014) be applied systematically to produce a demonstrably better-structured codebase.
Seven patterns were implemented, each chosen to solve a specific, real problem in the code, not just to tick boxes. The project also implements a multi-phase procedural generation pipeline and a SAT polygon collision system with profiled performance results.
Player is composed from independent components: SpriteComponent, HealthComponent, StatComponent (x3), Inventory, HUDComponent, CursorComponent. Nothing inherits from Player. Adding a feature touches only the relevant component.
A two-phase pipeline. Phase 1 uses Voronoi regionalization with Bridson's Poisson disk sampling for site placement. Phase 2 applies octave Perlin noise for world object placement. Phases 3 and 4 (Cellular Automata, Dijkstra) were cut during development.
AABB broad-phase pre-check followed by SAT narrow-phase for convex polygons loaded from Tiled .tmx files. Collision shape data is Flyweight-shared, thousands of world objects reference one shape.
A Pushdown Automaton in GameStateManager handles five states. Push/pop semantics let Settings overlay correctly from both the main menu and the pause menu, returning to the right prior state each time.
InputController maps InputAction enums to physical keys. Game logic queries InputAction::Sprint, never sf::Keyboard::Key::LShift. Full runtime rebinding via the Settings menu. wasJustPressed/Released handled via memcpy state snapshots.
Two enemy types, each with a state machine. SavageEnemy: Idle -> Chase -> Lost. ChomperEnemy adds a Leap state, a velocity locked lunge that aborts on wall contact. Both use line-of-sight raycasting. Incomplete but functional.
Three games informed what Thorns is trying to be. Not in mechanics copied wholesale, but in the feeling each one creates, the atmosphere, the tension, the way the world feels like it belongs to something other than the player.
The architectural foundation was established early and stayed stable throughout. The interface hierarchy was flexible enough to accommodate everything added during development without modification, which is the whole point of doing it that way.
The profiling workflow produced clear, actionable data. Identifying VoronoiDiagram::renderDebug as a 28.93% CPU hotspot wasn't obvious before running the profiler. The TMX-based collision template system made adjusting collision shapes straightforward without recompilation.
The view separation fix (m_gameView vs m_uiView) solved a whole class of HUD drift bugs cleanly. Identifying the root cause and applying a structural fix was more satisfying than hacking around it.
The enemy system took longer than expected and ended up incomplete. In hindsight, a simpler enemy, just directional movement and contact damage, should have been done earlier as a proof of the IGameEntity interface. The architectural groundwork was there, the time wasn't.
There's an anonymous struct in CollisionType.h that's a leftover from an abandoned circle collision type. A reminder that exploratory code should live on a branch, not main.
The first attempt at map rendering created 16,384 individual sprites for a 128x128 map, producing a 25,000ms generation time.
State management, collision, input, rendering, inventory, enemy AI. What each system does and how they connect.
Seven patterns from the Nystrom catalogue, each with the specific problem it solves and where it lives in the codebase.
The four-phase pipeline. Voronoi, Poisson disk, Perlin noise, and the two phases that aren't implemented yet.
Profiling data from a 27-second session. Generation timing comparison, CPU breakdown, memory results.
In-engine screenshots. World view, Voronoi debug, items, inventory, HUD. Click any image to open the lightbox viewer.