In my previous blog post, I went over the basic most fundamental concepts of Entity Component System (ECS) without really providing larger concrete examples. As such I will write a series of blog posts documenting the development of a video game using ECS. I will gradually write a Survivor-like game, similar to Vampire Survivors, but I want to use the Entity Component System design pattern, and I want to emphasise clean code using a myriad of tools I have learned from classes (mostly design patterns). For this, I will be using the Flecs framework for ECS, as well as Raylib, a game library, to create this game.
In this installment of the blog series we look at setting up the project. Last I wrote, I said I would use the C# bindings of Flecs and Raylib. I said that because I never understood C++ build systems, which is not a great reason to be honest. Therefore, I will set up this project using the original C++ implementations of the libraries with the CLion IDE.
First things first how do we build with CMake? Well if you are looking for a complete and accurate answer I recommend you look for answers elsewhere as I am trying to learn the CMake build system and am not proficient with it. However, I took one of those Udemy courses about CMake and recommend it if you are having a hard time building C++ projects (See the course here). Essentially, I am building one executable (main.cpp) in my app folder, and the game/engine logic will be in the src folder built as a library.
As for my CMakeLists.txt root, I copied over part of the Raylib template for fetching the content online and reused that process to fetch the Flecs framework.
set(RAYLIB_VERSION 5.5)
find_package(raylib ${RAYLIB_VERSION} QUIET) # QUIET or REQUIRED
if (NOT raylib_FOUND) # If there's none, fetch and build raylib
include(FetchContent)
FetchContent_Declare(
raylib
DOWNLOAD_EXTRACT_TIMESTAMP OFF
URL https://github.com/raysan5/raylib/archive/refs/tags/${RAYLIB_VERSION}.tar.gz
)
FetchContent_GetProperties(raylib)
if (NOT raylib_POPULATED) # Have we downloaded raylib yet?
set(FETCHCONTENT_QUIET NO)
FetchContent_MakeAvailable(raylib)
set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) # don't build the supplied examples
endif()
endif()
It was essential for me to build this project for the web so I could easily showcase the latest build for anyone wanting to play. Thankfully, Raylib has a great resource available that guides you step-by-step through doing just that. I did come across a few issues when setting this up, I had to change CMAKE_C_FLAGS for CMAKE_EXE_LINKER_FLAGS. I also had to add a few extra arguments for the build to work with Flecs the following is all that is required for building for web assembly. I don't want to get too deep into the CMake setup since the examples from Raylib and the CMake course already cover what I did in here.
if (EMSCRIPTEN)
set(CMAKE_EXE_LINKER_FLAGS "\
${CMAKE_EXE_LINKER_FLAGS} \
-s USE_GLFW=3 \
-s ASSERTIONS=1 \
-s WASM=1 \
-s ASYNCIFY \
-s GL_ENABLE_GET_PROC_ADDRESS=1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s STACK_SIZE=131072 \
-s EXPORTED_RUNTIME_METHODS=cwrap")
set(CMAKE_EXECUTABLE_SUFFIX ".html")
endif ()
Applying Software Engineering to Games
Now that I've introduced the basics of the building system, I want to talk about how we will organise this project. From playing around with the Flecs framework in previous projects, I noticed it had a class called "Module". A module is a container for components and systems specific to it. For example, a physics module would contain a velocity component as well as systems that query velocity. And if you properly decouple your modules, you could possibly import and delete modules at runtime. One case I could see this useful in is if I were to work with 2D physics, and decided to instead use a 3D. I could remove the 2D module and import the 3D one at runtime. Using the modules would force me to keep them decoupled and my code a bit cleaner.
Context
I determined I wanted to use the Flecs modules to build my game and engine systems, and according to the documentation of Flecs and my own experience, ECS is very dependent on the order in which things are called, especially for systems. If I want my collision detection system to run before my collision resolution system, then I should declare the former before the latter. However, Flecs provides an alternative way through system pipelines. I could add a .kind(Step1) modifier to one system and .kind(Step2) on the other, that way the declaration order does not matter anymore, well a bit. To achieve this, a module would need to:
- Register components
- Register the systems pipeline
- Register the systems
I want to ensure that when a module is imported, all of these steps are performed and always in the same order. For this, I will use the Template design pattern. In a base class, I will create the three required steps (more can be added later) and the sub-classes will only have to define the implementation of the methods. There is one small caveat.
Implementation
For Flecs to import a module, it requires the module class to have a constructor with a flecs::world parameter, and Flecs uses this as a template method. Here's the problem with this:
BaseModule(flecs::world &world) {
std::cout << "Creating Module " << typeid(T).name() << std::endl;
world.module<T>();
register_components(world);
register_pipeline(world);
register_systems(world);
}
If I try to call register_component(world) in the BaseModule's constructor, BaseModule has still not finished constructing thus the sub-class's methods cannot be found during runtime. Even if I cast the module to template type it still cannot find the virtual methods
BaseModule(flecs::world &world) {
std::cout << "Creating Module " << typeid(T).name() << std::endl;
world.module<T>();
static_cast<T*>(this)->register_components(world);
static_cast<T*>(this)->register_pipeline(world);
static_cast<T*>(this)->register_systems(world);
}
I was eventually able to bypass this restriction by altogether removing the virtual keyword, now we do call the sub-class methods. However, now we have another issue. How do I enforce the sub-classes to implement the regirster_... methods. Well, there is no pretty way to do it, but what I found to be the most effective is to throw an exception in the methods that are required to be implemented.
void register_components(flecs::world &world) {
std::cout << "Base class register component" << std::endl;
throw std::runtime_error("Undefined Component Registration: Module does not define \"register_components\"");
}
void register_systems(flecs::world &world) {
std::cout << "Base systems" << std::endl;
throw std::runtime_error("Undefined System Registration: Module does not define \"register_systems\"");
}
void register_pipeline(flecs::world &world) {
std::cout << "No pipeline registration implemented" << std::endl;
}
We now have a working Template design pattern in a constructor and we have a way to organise all the different modules we will add to the game. This should give a good enough foundation to start working on the game. Let's setup a few modules and get a couple simple systems going.
Let's write a physics module, for now, it will take care of moving entities according to their velocities. First we will register the components, in this case there is only Velocity2D. While Position2D could be in the physics module, eventually a rendering module will need the position to display the entities, and a renderable object is will not necessarily have physics applied to it. Therefore, Position2D will belong in the Core module.
In our core module, let's track the entities' positions and print them every frame to validate that the physics system works properly.
Now that our systems are in place we need to create entities that will match the systems filters. First we import the required modules (Core and Phsysics) then we add a position and a velocity component to the entity. Let hard code the velocity to (1, 1) so the player moves 1owards that direction.
For our systems to run we need to get a main loop going to constantly class them.
Now if we build and run our game, the console will print the entity's position, which translates 1.41 pixels per second in direction (1,1).
Conclusion
We went through the basic build system that is in place which will allow us to build for desktop as well as for web, and implemented the Template design pattern we will use to register components, pipelines, and systems. Well also created sample systems to test our architecture and to give us a taste of what's to come. In the future blog posts, we will implement more game logic, starting by player and enemy locomotion, we will cleanup our run function and abstract most methods in systems thereby introducing the rendering module, as well as adding system pipelines. There's a lot to do! :)
You can play the latest build of ECS-Survivors here, and see the source code on GitHub