8 minutes
Reflecting on game engine development
Game engine tech stack
As some of you may know, I have spent a considerable amount of time developing a game engine in C++. Toward the end of the year, I want to take some time to reflect on where I’m at and how I plan to move forward. Let us begin with a list of libraries that are currently used:
- Rendering: OpenGL
- Physics: Jolt
- Audio: FMOD
- GUI: ImGui, ImGuizmo, ImNodes
- Text: msdf-gen
- Window: GLFW
- Serialization: yaml-cpp
- Testing: googletest
- Scripting: sol3
- Navigation: recastnavigation
- Asset Importing: assimp, ufbx
- Logging: spdlog
- Profiling: tracy
- Graphics Debugging: RenderDoc
Feature showreel
Building on these libraries we implemented several features. So let us show some of these.
PBR Rendering
Physically based rendering has become the standard for rendering realistic graphics. Therefore, it is the default rendering method in our game engine. We even support editing the PBR shader using a node based approach which many developers have to come to expect ever since Unreal Engine introduced its blueprints.
Particle Systems
Another fundamental feature of a game engine is a particle system. Even with a relatively small set of configurable parameters, a wide variety of effects can be achieved. Our system allows configuration of texture, size, lifetime, color, velocity, and gravity all over the lifetime of a particle. Additionally, each paramter can have a variance that is applied to each emitted particle individually. This enables effects such as hearts, which move upward and sideways at different speeds while shrinking over time as can be seen below.

The same system can also be used to create a fountain-like effect, where each particle is animated over time, affected by gravity, and initialized with a randomized velocity.

Animations
Implementing animation support has been one of the most time-consuming aspects of reaching the current state of the engine. There are many different asset file formats that support animations, and even within a single format there are often multiple ways to represent movement. Combined with additional data such as scaling calculation based on the node hierarchy, there are many cases to handle animation properly.
When testing a new model, our first step is to verify whether other tools or websites can import it correctly. There is an overwhelming number of free models with animations that simply did not properly work in any other tools we tested. Based on our testing, Sketchfab provides one of the most robust online model viewers. As a result, our goal was to support any model inside the engine that also works correctly on their platform.
In our opinion, it is always worth creating debug visualizations. The following GIF shows bone weight visualization of a dancing stormtrooper driven by skeletal animation.

Additionally, the engine provides visualization of the currently selected mesh, as well as options to display the skeleton and render the mesh in wireframe mode.
Physics
Our Jolt physics integration is still in its early stages. Determinism issues were one of the main reasons we introduced testing. At the moment, the engine supports several collision shapes. In the following video, a mesh collision shape interacts with the floor in multiple scenarios. If the collision response appears incorrect, this is due to poorly configured physics materials and the fact that the simulation body of the mesh does not match the visual representation particularly well.
Text rendering
Text rendering turned out to be a far more involved process than we initially expected. For that reason, there exists a separate blog post that discusses this topic in greater detail. For this post, simply enjoy this text component of our game engine. Text can be freely rotated, translated, and scaled in the editor while remaining crisp at all times thanks to MSDF-based text rendering. Also you basically get an outline for free when using this rendering method.

Limitations
Since this is a solo project, almost none of the engine’s features are fully finished. However, the following are some of the most significant limitations that currently hinder creating and shipping a game with the engine.
Lights
Only a single directional light and up to four point lights are supported. This is mainly due to the renderer being a simple forward renderer and for each mesh rendered each light in the scene has to be iterated over. This does not scale. We do not regret starting this project with such a simple renderer though since the learning curve to graphcis programming is steep enough even when choosing a simple rendering approach.
Shipping
There is no automated packaging or asset cooking pipeline. Shiping a game currently make it the response of the developer to copy all required assets manually to the right folder. This does not scale and is error prone.
Regrets
Our main motivation to write this post is to force ourselves to reflect on issues we encountered during game engine development in the hopes to help others and our future selves.
Testing
By far our biggest regret is introducing testing far too late in the development process. But why?
-
We started this project roughly five years ago. At the time, I was still at university and had little understanding of how to write code that can be maintained and extended over many years. In my opinion, testing is an integral part of writing high-quality software a mindset that has since been reinforced by my day job as a Java software engineer.
-
Testing a game engine is not straightforward. Most of my professional experience is with Java Spring applications, which can be tested quickly and easily using tools such as JUnit. Spinning up an entire game engine as part of a test suite is a new task for me. We also introduced many side effects for example by having too much global state. Therefore, the order in which tests run work differently.
-
Testing is not inherently fun, and this is a hobby project. That said, we have since experienced the satisfaction of being able to make large changes without constantly worrying about breaking core functionality, thanks to a solid test suite. The longer a project lives and grows, the more value and enjoyment you get out of the tests that were previously written.
Moving too quick
A game engine is a really nice hobby project because there area so many things to implement that you can learn a lot of different things and change up the topics you work on when you do feel like it. There is a downside of working on a big software project like a game engine with this mindset though. You (or atleast I)tend to get things to a ‘working’ state, instead of a correct state. The shortcuts you took years ago tend to catch you off guard much later on. Additionally, writing good documentation is crucial. We spent so much time debugging issues that in the end turned out to be just a function that did not properly handle a certain case, had a confusing name or was not implemented at all. Switching topics really encourages such issues to pile up. One thing that could help in such cases is writing tickets with acceptence criteria. At that point it just becomes too close to work for my liking. This is supposed to be a hobby you mostly do for fun after all.
Assimp
Assimp is great because it supports a large number of file formats that can be used to import assets into a game engine. However, assimp is extremely slow in debug mode. Additionally, almost every time we updated to a newer assimp version and re-imported the same .fbx models, something broke. While this is not entirely assimp’s fault (FBX itself is notoriously complex) it was still very frustrating and time-consuming to fix.
Skeleton animations in particular often broke between versions, and debugging issues with assimp is painful due to its terrible performance in debug builds. At this point, we no longer use assimp at all. Instead, we rely on specialized libraries that focus on a single file format and aim to get that one format right, rather than providing a high-level abstraction that has to support many formats at once. So for FBX files we integrated ufbx.
Future
Graphics API
We really want to move to a newer graphics API. The global state of OpenGL and single thread context has been limiting testing aswell as performance. However, we do not regret having started with OpenGL. The learning curve of vulkan is so much steeper and it takes much more upfront work to get the couple first hits of dopamine when you actually see something on screen.
Linux support
We really want to support linux since gaming is the chain that forces us to still run windows. Due to a specific feature of our game engine, linux support has not been feasible with OpenGL. Since we decided to move to Vulkan on our next iteration of the engine we should be able to support linux!
Releasing a game
One of our main goals when creating a game engine is to ship a game that is built with the engine. Sadly, this goal will have to be postponed by a couple of years because of the decision to move to Vulkan alone, since this is a new technology to us.
Game engine 2.0
Over the past couple of years we have learned a lot about game engine development. This allows us to make better design decisions regarding game engine architecture. Therefore, we decided to not only switch from OpenGL to Vulkan but rewrite the entire game engine from scratch. Taking the lessons learned and referencing code of our first version to create a much more robust game engine that can actually be used to release a game.