Writing Super Mario Bros in C++
I decided to learn C++ as step towards understanding more about the fundamentals of computer science, programming languages and graphics programming in general.
After reading some books on C++ I decided to clone the Italian plumber platformer.
In this post, I want to talk about aspects I found interesting about learning C++ coming from the JVM world. I also want to shed some light into the process of building a “simple” game using the Entity Component System Architecture. If you are not a game developer I hope my article serves as a general introduction to how simple games like this are put together.
Starting out
I love JetBrains IDE’s so the first step for me was downloading CLion. CLion uses CMAKE to build C++ projects by default. CMake is a build system that builds build systems. It’s cross platform but also incredibly unintuitive. Still, it’s your best bet if you want your software to run in multiple platforms. It came as a surprise that a language with this much history doesn’t have a more sophisticated solution in place. Coming from the Java (Kotlin) world, I have a newfound appreciation for Gradle.
I decided to use Simple Direct Media Layer 2 (SDL2) to get access to audio, keyboard, mouse, and graphics hardware. I considered using OpenGL directly for doing graphics, but I think the scope of the project was quickly getting out of hand as it was. (edit: I created a new branch in which I don’t use SDL for graphics and use OpenGL directly, which allows more flexibility to play with fragment shaders)
Next, I set off to define an architecture for the game. Plain Object Oriented design seems like a perfect fit for games. You have a bunch of objects in your game interacting with each other. You can map those objects to classes and take advantage of inheritance to avoid repeating code. Unfortunately, this naive approach brings with it some problems when building games of relative complexity. I don’t want to get too deep into what those problems are but here’s a quick summary:
- Using inheritance usually results in inflexible architectures that sooner or later will lead into the Deadly diamond problem.
- Efficiency. Objects often are scattered around memory and make poor use of CPU cache both in terms of cache coherency and prefetching. (this might not be a huge deal depending on the kind of games you’re trying to make)
- Encapsulation. It’s hard to define where logic should live and what has access to what, resulting in spaghetti code.
I might expand on these points on a following article. For now I want to show you how the architecture I picked works and how it overcomes these problems.
The ECS Architecture
I struggled in the past trying to architect my games in a way that they would scale. When I first learned about the Entity-Component-System architecture I immediately wanted to try it out. Let’s start by defining each term on its name.
Entity
Everything in your game world is represented by an entity. This includes the player, the enemies, sounds, music and and even the camera. An entity is just a bag of components and contains no logic:
Component
Components are plain old structs
that also contain no logic. They are just data holders. Common components you’ll find in most games are PositionComponent
, PlayerComponent
and TextureComponent
.
Notice that some components don’t even hold data (see PlayerComponent
for example) they just serve as “labels” that help identify certain entities in the game world.
Components can be assigned-to and removed from entities at run-time.
The components above are pretty general, but you will most likely be creating components specific to your game. As an example, a SuperMarioComponent
is assigned to the mario entity whenever he eats a mushroom.
System
Lastly, systems are where the action happens. They are very domain specific. They hold logic and keep it encapsulated. I think this will make more sense with some examples of the types of system I defined in my game:
- The render system
- The camera system
- The physics system
- The animation system
- etc.
The system class is not much complicated than the rest of the classes in the architecture. On it most basic form it overrides an update(world* world)
function that takes in a game world as a parameter. We’ll talk about the World
class next.
Take a look at the AnimationSystem in my game for a simple real example of the logic illustrated above.
Putting things together
As hinted above, Systems
, Entities
and Components
interact with each other using the World
class.
As you might have guessed, the world
class models our gameworld, and as such, contains all our game Entities
. We can define what happens to those entities by registering systems to the gameworld. Both entities and systems can be added/removed at run time.
The world class is as straightforward as the rest of the classes in the architecture:
I think you can guess what most of these methods do. The first one is just the default constructor. The next two just add/remove Systems
to to the world. The following two ones do something similar but for Entities
.
The last method, find()
, is perhaps the one that deserves more explanation. It takes care of finding all entities currently in the gameworld that match a specific criteria. That criteria is usually what type of components are assigned to them:
// Find all entities with Texture and Position data.
auto entities = world->find<TextureComponent, PositionComponent>();
The find
function takes advantage of function templates
to define the search. In Java/Kotlin if you want a class or function to operate on a arbitrary class you can make use of generics
. Function and class templates are a bit like generics only more powerful. Function templates are instructions to the compiler to write code for you, also known as metaprogramming. This allows us to create a function template whose functionality can be adapted to more than one type or class.
This is the definition of find
in my code:
There’s a few interesting things going on here. The first one is the use of template<typename… Components>
in the function signature. This is telling the compiler to generate a function called find
at compile-time. The compiler will generate such function each time it encounters a distinct invocation of find
:
For the code above, the compiler will automatically generate the following for us:
void find(const std::function<void(Entity*)>& callback) {
for (auto entity : entities) {
if (entity->has<AnimationComponent, TextureComponent>) {
callback(entity);
}
}
}
As I hinted before this happens at compile time, so there’s no run time penalty. I must admit I didn’t like this C++ feature when I first encounter it. I still don’t, but I understand the performance benefits they bring along. The reason I don’t like them is I think they result code that is hard to read and hard to reason about. I don’t like weak typing systems and this looks awfully like it.
The rest of the function should be relatively easy to follow. Let me know in the comments if this is not the case.
Conclusion
You should now have a rough understanding ECS works. We no longer rely on inheritance to define behavior, we rely on composition instead. This means we got rid of potentially long and inflexible hierarchies in favor of lightweight reusable components that we can add and remove at runtime.
The efficiency problem I mentioned before is also mitigated. Our entities and components are now tightly packed in contiguous memory, so iterating over them makes much better use of CPU caches. There are techniques to improve this even further but I haven’t had the need to implement them for this game.
Our third problem, encapsulation, is also fixed. It is now very clear where the logic should live, how the different parts our architecture should communicate and what should have access to what. As an example, it makes sense for the RenderSystem
to make use of classes such as renderer.cpp
or window.cpp
, but it would be immediately obvious something is off if those classes are required by systems like the PhysicsSystem.cpp
or the SoundSystem.cpp
.
You should know have an idea of how my code is structured. Feel free to poke around and create PR to improve any bug or mistake I might have made. I’m still very new with C++, so I’m sure my code has a lot of room for improvement.
Learning C++ was a fun experience but I believe I have still a long way to go to feel comfortable using it. Future steps for this project include re-writing it using Rust or Kotlin-native. I would also love to compile this project to WASM to get it to run in a browser, if you have any experience with any that I would love to chat with you!
Thank you for reading, and Happy coding!