A simple 2D dungeon crawler where the player fights waves of orcs. Built to explore the inner workings of a game engine using SFML and C++.
main.cpp was written on day one and is frozen. It will not be modified.
This was a deliberate choice by me. Before I began designing a game with proper architecture, I first needed to understand what I was working with. I had no experience with SFML, and I hadn't written a program in C++ in about a year, so I started with a brain dump in one file. The file contains global maps, free functions, hardcoded string keys, magic numbers, the works; I did this to practice the basics: loading textures, slicing sprite sheets, building state machines, and properly structuring a game loop. Through this I actually found that writing messy code that worked was the fastest way to learn what I was building. The result was a game that I wanted to continue working on. Every refactor and every change since then has been a direct response to sometihng that hurt from that original file.
The first real architectural component. Replaces the two global unordered_map<string, sf::Texture> maps and the free loadTexture / getFrames functions scattered across main.cpp.
What I built:
- A
struct AssetKeywithCharacterTypeandActionenum fields as a composite map key, replacing magic strings like"IDLE"and"WALK" operator<implemented on the struct to satisfystd::map's ordering requirement — a lexicographic comparison that sorts by character first, then action- A
std::map<AssetKey, sf::Texture>andstd::map<AssetKey, std::vector<sf::IntRect>>as the backing caches populateFramesMap()driven by iterating the already-loaded texture map — so frames can never exist without a corresponding texture, and adding a new character topopulateTexturesMap()automatically generates its frames for free- The Meyers singleton pattern: a
static AssetManager instanceinsidegetInstance(), which is a lazy, thread-safe solution in C++11+, and automatically destroyed at program exit - Deleted copy constructor and assignment operator to prevent duplication
Why singleton here: An asset manager owns a single global cache. Multiple instances would mean multiple copies of the same textures in memory, and every system in the game (Player, Orc, UI) needs to pull from the same source. The singleton enforces this at the type level.
Where industry has a leg up: Production engines use asset handles (lightweight IDs) instead of direct references, reference counting for unloading, async background loading, and data-driven manifests instead of hardcoded paths. The hardcoded frameWidth = 100 is the most obvious gap — a real solution maps frame dimensions per asset key.
- Initializes
sf::SpritefromAssetManager::getInstance().getTexture()in the member initializer list - Sets
textureRectto the first idle frame immediately on construction - Owns animation state:
currentAction,currentFrame,animTimer,frameInterval update(float deltaTime)advances the frame on a timer and calls back intoAssetManager::getFrames()— the player doesn't own or copy frame data, it just reads from the cache
Duplication is obvious: Both Player and Orc have identical animation logic copy-pasted between them. This is the next thing that will be pulled out.
Next step — Animator class:
- Will own all animation states (
currentAction,currentFrame,animationTimer,frameInterval) - Entities will delegate animation responsibly:
animator.animate(sprite, deltaTime)insidePlayer::update()orOrc::update() - Later, Orc will gain a behavior state machine (idle, patrol, chase, attack, die) while Player remains input-driven. However, both will share the same animation infrastructure.
Owns the window, the delta time clock, the player, and the orc. The loop is: restart clock → processEvents() → update(dt) → render(). Delta time is passed down so all update logic is frame-rate independent.
- Singleton pattern and when it's appropriate vs. when it becomes a liability
- Composite struct keys with custom
operator<forstd::map - Enum classes as type-safe identifiers instead of magic strings
- Meyers singleton as the modern C++ approach to lazy, safe static initialization
- Separation of loading, caching, and serving as distinct responsibilities
- Member initializer lists for constructing objects that can't be default-initialized (
sf::Spriterequires a texture) - Delta time as the foundation of frame-rate independent game logic
- The general game loop structure: Input → Update State → Render
Extracted animation control from entities, and built a separate universal animator class to animate said entity
What I built:
- Initializes
currentFrame,animTimer,frameInterval - Sets and stores object frames using
const std::vector<sf::IntRect>& frames - Animates an entity input by reference using the game loops delta time value.
- Implemented a parent class for functionality shared by Player and Orc
- Constructor takes in
sf::Texture& textureandCharacterType characterto initialize sprite and set default values for data members
- Constructor now passes
sf::Texture& textureandCharacterType characterto called parent constructor, as well as setting defaultCharacterspecific initialization values Playernow containshandleInput()for input driven behaviorOrccontains a state machine — AI-driven behavior (idle, patrol, chase, attack, die)- Both call personal functions in
update()then delegate animation behavior to parentupdate(dt)
Variable delta time works for animation but causes instability in physics and collision detection — frame rate spikes mean larger time steps, which can cause tunneling or inconsistent behavior.
Replaced the variable dt = clock.restart() approach with a fixed time step accumulator:
- Physics and collision always update in consistent increments (e.g., 1/60th of a second)
- Accumulator stores leftover time between frames
- Rendering can still happen at variable rates, interpolating between states if needed
This is the standard solution for any game with collision or physics.
Next Step — Implemenation of player input, and orc state machine
Now that I have a solid foundation for the game thus far. I can continue my progression by implementing player movement, and setting up the state machine for the Orc's.
SFML Documentation
https://www.sfml-dev.org/
C++ Reference
https://en.cppreference.com/w/
SFML Game Development
Jan Haller, Henrik Vogelius Hansson, and Artur Moreira
Packt Publishing, 2013