C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
153: Working with a 2D Graphics Library
You've likely reached the point where you're tired of the console window and want to actually see something happening. You've downloaded a library—maybe SFML, SDL, or Raylib—and your first instinct is to get a character moving on the screen as fast as possible. It's an exciting moment, but it's also where most developers accidentally bake a massive architectural mistake into their project that they'll spend the next three months regretting.
The Temptation of the Self-Drawing Sprite
When I first started with 2D graphics, I did what felt intuitive: I created a Player class and gave it a member variable for the library's sprite object. I gave the Player a draw() method that took a reference to the render window and called the library's draw function internally. It looked like this:
class Player {
sf::Sprite sprite; // SFML specific
float health;
public:
void draw(sf::RenderWindow& window) {
window.draw(sprite);
}
// ... game logic ...
};
On the surface, this is clean. It's object-oriented. The player knows how to draw itself. But here's the problem: your Player class is no longer a representation of a player in a game; it's now a wrapper for a specific graphics library. I call this "leaking the abstraction." The moment you put sf::Sprite inside your game entity, you've married your game logic to that specific library for life.
The Wall of Dependencies
You might think, "Why does it matter? I'm not planning on switching libraries." Believe me, it's not about switching. It's about the "Wall of Dependencies." When your Player, Enemy, and Projectile classes all depend on the graphics library, every single file in your project now needs to include those heavy graphics headers. Your compile times start to crawl. Even worse, you can't write a unit test for your player's movement logic without initializing a GPU-accelerated window, because the Player object can't exist without its sf::Sprite.
It also makes handling "z-order" (which object is on top of which) a nightmare. If every object draws itself whenever it feels like it, you lose centralized control over the rendering pipeline. You end up with flickering sprites or backgrounds that accidentally draw over your UI because the call order in your main loop was slightly off.
Decoupling State from Pixels
The better way—the way we do this in professional engines—is to treat graphics as a service, not a property. Your game entities should only hold "Plain Old Data" (POD): a position, a size, and perhaps an ID or a string indicating which texture they use. They shouldn't even know a graphics library exists.
Instead, you create a dedicated Renderer or RenderSystem. This system is the only place in your entire codebase that knows about the graphics library. It takes the state of your game objects and translates that into draw calls. It looks more like this:
struct Player {
float x, y;
std::string textureId = "player_ship";
// No graphics library code here!
};
class RenderSystem {
std::map<std::string, sf::Texture> textures;
public:
void render(sf::RenderWindow& window, const std::vector<Player>& players) {
for (const auto& p : players) {
sf::Sprite s(textures[p.textureId]);
s.setPosition(p.x, p.y);
window.draw(s);
}
}
};
Now, look at the trade-off. Yes, you've added a bit more boilerplate. You have to manage a map of textures and pass your entities into a system. But in exchange, your Player class is now a lightweight struct. You can instantiate a thousand players in a unit test without opening a single window. If you decide that SFML isn't performing well enough and you want to move to SDL, you only have to rewrite one class—the RenderSystem—rather than hunting through every single entity file in your project to swap out sprite types.
📋 Practical Task
Refactoring the Decoupled Sprite Manager
You have been given a small project where the developer committed the "Naive Way" described in the lesson. There is a GameObject class that contains both the x, y coordinates and an SDL_Texture* pointer, with a draw() method that calls SDL_RenderCopy.
Your Goal: Refactor this code to separate the data from the rendering.
- Step 1: Strip the
GameObjectclass of all SDL-related members and methods. It should only contain the position and astd::string textureName. - Step 2: Create a
TextureManagerclass that loadsSDL_Textureobjects into astd::map<std::string, SDL_Texture*>. - Step 3: Implement a
Rendererclass with a methoddrawEntity(SDL_Renderer* renderer, const GameObject& obj). This method should look up the texture in theTextureManagerusing the object'stextureNameand then perform the actual SDL draw call. - Step 4: Update the main game loop so that instead of calling
object.draw(), it passes the object into theRenderer.
There are no comments for now.