This README is a short summary of my Bsc Thesis project, the task for the project was to develop game engine components for an FPS game in C++. The task and the components were lightly inspired by the book Game Engine Architecture by Jason Gregory. Part of the task was to implement one or more components more complexly, the components of my choice were the Low Level Renderer and the Physics & Collision System. There are other components in the project as well such as a Scene system, a HUD system and a Resource Manager, however these subsystems are all very bare bones.
For the graphics API I chose Vulkan, this was because FPS games make high demands for performance, and Vulkan is very well optimized, so it can create much faster applications. Another upside is that Vulkan is very complicated, but by abstracting these complex parts away with a game engine we can have a tool that's easy to use, but still quite powerful. Finally I have worked previously with Vulkan, however, I wanted to practice it more, so I could have a better grasp of it.
One final important aspect of the engine is that it was made with usability in mind. This means the appearance of the game objects can be edited in real time, while the engine is running. For this I created an editor (see above), where you can select what mesh, texture or shader the game object will use for rendering. You can also create new game objects for the virtual world with previously loaded meshes and textures, or you can load entirely new ones from files! For the editor I used imGui since it can create complex guis, and it can be easily used in tandem with Vulkan.
The job of the Low Level Renderer is to create the necessary Vulkan components we need, and to create structures that the game objects will use for drawing. Since many Vulkan classes need each other for initialization, I put them into bigger wrapper classes, with each wrapper class having an init function initializing the needed instances of the Vulkan classes. These Wrappers are also responsible for destroying the wrapped classes, this is usually done in the destructor. Below is a short summary of the major classes of the Low Level Renderer
- vulkanInstance: Wraps the classes that are necessary for a Vulkan context e.g.: vkInstance, vkDevice, vkPhysicalDevice etc... Furthermore, it contains a validation layer for ease of debugging. It is also used for general tasks, that need some of the previously mentioned classes, such as creating a buffer and uploading data to it. For this, it also contains the CommanPool, so we can issue single time commands easier from within the class.
- vulkanSurface: Contains everything that is related to a rendering surface in Vulkan, such as: vkSwapChain, vkImages, vkImageViews, vkExtent etc... It is also responsible for recreating the swap chain when the window of the application (the reference to it is not in this class) is resized. It does not contain vkFramebuffers since those are heavily related to the vkRenderPass
- vulkanRenderer: Contains everything that is necessary for maintaining a vkRenderPass, such as the vkFramebuffers and the vkImages that wraps.
- vulkanObject: Contains a vkPipeline and the Descriptor Set Layouts and Pools that the pipeline will need.
- vulkanSceneData: Contains the Descriptor Set that contains the data for the sceneUniformBuffer (so far there is only a view and a projection matrix in it)
- vulkanMeshData: Contains a Descriptor Set that contains the data for the transform of the Mesh, so the model matrix. Every Mesh in the engine is in a single Dynamic Uniform Buffer, this is handled by another wrapper class, the vulkanDynamicUniformBuffer. This class also contains a buffer containing the vertexes of the mesh and another buffer containing the indexes of the mesh.
- vulkanTextureData: Contains a texture, and has the capabilities to send it to the GPU
- vulkanModelData: This contains a pointer to a vulkanMeshData and a vulkanTextureData, since these are pointers it's easy and fast to swap them out in runtime, this provides the "hot swap" functionality to the editor.
- object: This represents a game object in the virtual world. Similar to the vulkanModelData it contains a pointer to a vulkanModelData and a vulkanObject. As mentioned above, this is for the "hot swap" functionality of the editor.
- vulkanDrawer: This class is responsible for drawing and presenting a frame. For this it needs synchronization, this is done with semaphores and fences. It has a function getNextImage, which gets the next image we need to draw to, or if it can't, it sets a flag in the vulkanSurface that signals that there is no image we can draw to currently. The next function is drawFrame which draws every object in a given scene, binding the correct Descriptor Sets and Pipelines for each game object in the scene. We also have a submitCommandBuffer function, this is separate from the drawFrame, because we use the same commandBuffer to record the draw commands of the Editor. Finally, the presentFrame function presents the results of our draw commands to the screen.
The Physics & Collision System was implemented with less complexity as the Low Level Renderer. The two main classes of this component are the Collider class and the RigidBidy class. The object class contains a pointer for both of these classes, this is how we can attach them to a game object. The Collider class is the class that every collider type inherits from, so the type of the collider doesn't need to be hardcoded into the object class. The Rigid Body class is responsible for moving the object according to the physics simulations.
Adding new colliders is easy since all of the colliders inherit the main Collider class. Currently, there are only two of them implemented. One of them is the sphere collider since it's really fast to calculate its collisions and the other one is the capsule collider since it's also fast, and it's often used for hurt and hitboxes in FPS but mainly arena FPS games. For implementing collision detection, I used this resource. To optimize the detection I first always check if the bounding spheres of the colliders overlap, this bounding sphere is automatically maintained by the given class implementing a given collider.
The Physics system is very simple, using Euler integration to calculate the acceleration, velocity and finally position of the game object given the sum of forces applied to the object. Since we don't want the objects to move indefinitely after a collision, we also apply air resistance to the objects in every frame. Both the mass and the air resistance coefficient of the given object is stored in the RigidBody class attached to it.
Since we don't want to move the objects before we are done with calculating every collision, the collisions only generate "events" that contain which object collided, and a vector that indicates the direction and the amount their colliders overlapped. After we are done with these calculations, we run through every single event created and apply forces to the objects contained in the event according to the stored vector. Only after this when updating the scene, and thus every game object within it, do we move the objects according to the forces applied to them.
Of course this project is very far from an actual game engine, so there are a lot of things missing, I don't wish to list all of these here, however I want to list a few limitations with the current solution of some of the things implemented, focusing on the two components that have had the most work put into them:
- Limited number of game objects: Every single model matrix in the engine is stored into a single Dynamic Uniform Buffer. The size of this Buffer is architecture dependent, which leads to inconsistency, but the bigger issue is that there is a very limited number of game objects the engine can handle. There needs to be a class that manages the model matrixes and loads them into multiple Dynamic Uniform Buffers if necessary.
- No multi mesh models: The engine can only handle models that consist of a single mesh, this is an issue since many freely available game assets have multiple meshes.
- Monolithic game object model: To create a new custom game object, we need to inherit from the object class. This will lead to us having tons of classes in a practical application, which creates unmaintainable and unnecessarily complicated code. The solution for this is to make the game object model component based. This would require a bigger repurposing of the code.
- No box collider: The box collider is a very useful collider that most engines need, its implementation is tricky, however inevitable.
- Only hitboxes, no hurtboxes: In most FPS games there are so called hitscan weapons, which "shoot out a ray" from the tip of the gun and damage an enemy if the enemy's hurtbox is in the way of the ray. The current iteration of the engine only deals with collisions between colliders and not rays. The implementation of this ray collision is not complicated, however it is missing.