Yama Game Engine

Introduction

The most important coursework for the first semester of the final year. Our course was tasked to create a C++ game engine that would load a modifiable scene file to create a game level and provide a high-score mechanic, which would include sending the results to the database and also getting top 5 scores. The engine had to be designed in such a way, that every part of it (physics, graphics, etc.) had to be modular, so that it could be easily swapped out, as part of the requirement was to use third party middleware, where appropriate. I also had additionally challenged myself to use C++20 for this project, to further learn about the new standard, and create an editor that would allow level creation without the need to manually edit the scene files. And this is how I managed to build all of this!

General architecture

The architecture was heavily influenced by the Unity game engine and my prior experience with it. That is why I decided to, instead of using an event-based system, like the lecturers suggested, introduce the entity-component system (ECS). This decision allowed for easier game object creation, clearer game data representation within the system and more intuitive data game handling. This resulted in a data creation and delivery pipeline, where game scripts and physics systems provide important data to the graphics system.

Game logic and physics code produce transform data which is then delivered to the graphics system

C++20 modules provided a great way to encapsulate the used middleware (where it was appropriate) with wrappers, as one can define what is exported (i.e. visible to the code outside of the .ixx module) and modules do not pass through the defined macros. During development, I had to change the input handling multiple times, but I did not have to change any of the exported global functions (such as GetPressedKey or IsRMBPRessed), just what was done inside of them and how everything was initialized.

During the development, I also tried to use gained knowledge from my internship, that is why I tried to introduce const correctness to my code and used preprocessor definitions to switch between the game version and the editor version of the engine.

The editor architecture is very similar to the game architecture, that is why in order to increase code reuse everything was merged together and separated by using preprocessor directives.
The main difference is that the game entity components have a different version within the editor which primarily consists of float and string values for easy editing.

The used middleware

Since time was a major limitation (especially since I challenged myself to do more than the original task asked), I could not spend a lot of time trying to integrate some middleware – a couple of days, max. Luckily, most of the systems were easy to integrate or replace by something else that did. Except for the CURL related library, but more on that later.

  • Graphics: Irrlicht. I initially tried playing with Ogre before the start the course, but was dissapointed with some of the tutorial materials, so after further research I tried out Irrlicht, which was great for basic things that I wanted to do. Though, there were some issues. Primarely was its age. It did not load FBX files by default and I had to rely on a third party library that merged Irrlicht and Assimp. While the loading did work, there were some issues with getting animations to play properly.
  • Physics: Bullet. Originally, I wanted to try out Nvidia Physx, but lecturer said that it was not very beginner friendly and that is why I used Bullet. Due to time constraints I only used cube colliders – it fit for all of the needed situations. There were also some limitations, such as, Bullet only provides the data on what the object is currently colliding with, so in order to figure out events like collision enter, collision stay and collision exit I had to cache older data and use set theory.
  • Logging: Loguru. Was chosen because it had a combination of asserts, file creation, simplistic calls and coloured text (which was quite helpful in spotting the errors within the console window).
  • ECS: entt. Chosen primarly based on its popularity and use in other projects. It was relatively easy to use it in ways I wanted to.
  • GUI: ImGui. Decided to use it because of popularity and use in other projects. I also had to use version 1.46 (which came out at around 2015) due to library with made it compatible with Irrlicht.
  • Input: Used Gainput and OIS, but both of them did not work as expected due to unknown reasons, so had to fall back to use Irrlicht in a separate module.
  • Audio: Audio Stupidly Simple. A simple wrapper around SoLoud that provided exactly what I need – an ability to just play audio file. If there were more requirements this definitelly would not be used, however, since there were no real requirements for audio middleware, except for it to exist.
  • JSON Parser: Since I had already had prior experience with JSON, I wanted to store my scene files in this format. It was also used within the networking code to receive the data from web api.
  • CURL: Initially I used CPR, which looked great and was easy to use, however, whenever I used my web api address that was hosted in MS Azure, I would always get errors that I could not find the cause for. I replaced it with CURLPP, which was a bit more difficult to use, but still did the job quite well. Well, until the last day when with the help from a friend an issue was found when posting data in release configuration. I tried to match the debug config by disabling optimizations, but it still did not work.

Optimizations

While writing code I tried to optimize it as well!

  • I created a very lightweight 3D Vector struct that had only 3 floats and nothing more. All of the operations related to this struct was stored as global functions that do not modify the passed in vectors. The returns were left for RVO to handle.
  • More complex objects were passed in with const& in order reduce object copying.
  • Where appropriate I used aliases to refer to parts of a complex object that were called multiple times.
  • Wherever I saw fit I tried to use noexcept, consteval, constexpr attributes.
  • Tried to prefer moving data instead of copying it.
  • I choose appropriate data collections based on a very informative flowchart.
  • Used provided observer pattern implementation by the ECS to update only the changed trasnforms.

C++ 20

It was quite exciting to use all the shiny new stuff provided with the C++20 standard. The most used features were the .ixx modules, std::optional to show that some functions might return empty values and std::format, which allowed for a very easy string formatting. I also used std::any to pass data from one scene into another one. For some time I used std::variant to hold editor component values, which was also easy to represent in GUI, however, when some more complex components were created the std::variant was replaced with individual (to the component) data containers and their representation in GUI.
I also tried to use std::source_location::current() in the logging functions, but due to the issues with the compiler, I had to abandon it.

My thoughts and opinions

When considering the time limitations and the fact that there were other coursework projects to work on, I am very happy with how everything turned out and what middleware I used.
Using C++20 did create some unexpected issues, but I am glad I could try new things out!
If I were to work on a similar project, but with a bigger time budget, I would try out using Ogre for graphics, PhysX for physics, then maybe try out using YAML for scene data storage as it seems to be more compact than JSON. Also, using SoLoud for audio would be preferred.
I would also like to add features that I did not have the time to add, such as Lua scripting support, so that the game logic can be edited without the need to recompile the engine code. Also, I would like to have a dedicated middleware for AI pathfinding. Of course, it would be nice to have a scene hierarchy, but I am unsure where I would start with that!

Overall, this was a great experience!

GitHub