Nov 10, 2020

U.R.U.K

Team of 4
February 2020 - July 2020
Skills Utilized: C++

Introduction

In this semester, the goal of the GAM class was to form a group and make a complete game. The main mechanic of this game was a gate that opens when the count of creatures matches a certain number. Given the substantial scope of the game and the restriction on using pre-existing game engines, I took the lead to develop a custom game engine.

Showcase




↑ State machine editor showcase

↑ Controls of the state machine editor




↑ Automatically generated state machine codes

↑ The state machine of the main character used in production


↑ A screenshot of the map editor

Controls
- How to Play will teach you the basic controls.
- ` : (Cheat) Press ` while on a Fish Gate to skip to the next floor.

Responsibilities

  • Technical Director
  • Core of the game engine
  • Entity-component based object management
  • Custom memory manager
  • State machine editor
  • Map editor
  • Minimap editor
  • UI-related components
  • Camera
  • Many other gameplay programming

Technical Highlights

Entity-component structure custom engine

In this project, I was in charge of designing and implementing the game engine. We built our own since the use of existing ones were restricted.
A game is a singleton, it has multiple levels in it. Levels contain objects, and objects conceptually contain components. I used the word conceptually because they are actually stored separately.

Engine Reference Document


↑ The technical structure of the game (better resolution)

It's the components where the most of the logic resides in. The objects are there just to bundle them up abstractly.

↑ A portion of the interface of the component class

↑ A part of the function that adds a component to a level

Custom memory manager

I implemented a custom memory manager for this project. I know it would be redundant for a student-size project, but I just wanted to give it a try. We used it just for storing the components.
The structure of the memory manager is as follows: allocate a huge chunk of initial memory, and divide it on demand. If a new type of component requests a memory, assign a large, contiguous part of it for that type of components. Later, when the same type of a component requests a memory, it'll give one from the preassigned part. So after some uses, it'll look like this:


A notable challenge was handling component removal. To optimize reference locality by ensuring contiguous components, I efficiently filled gaps after a component was removed. This was achieved by swapping the last element with the removed one, while considering component's memcpy-ability.


The reason I couldn't use std::swap was the fact that swapping with the "deleted" ones whose destructor was called caused problems. Now I come to think of it, it would have been sufficient to swap the elements beforehand and then to call the destructor of the to-be-removed one. 

Aside from that, moving the components within the memory implies one another problem: we cannot use the normal pointers to access to the components. To address this issue, I chose handles to give access to the components to the outside, which is in this case, an id.


Virtual static functions for deserialization

During the late stages of development, a demand for the serialization support emerged. Implementing the serialization was straightforward – just add a virtual function that gets called when serialization happens. However, deserialization posed a challenge. Many components relied on non-default constructors, so forcing them to have a default constructors was not viable. How could we call a virtual function when the object had not yet been instantiated?
There is a technique called "static polymorphism". In short, you call a static function of the child class in parent class using CRTP. Derived from this idea, I devised an approach: Components with non-default constructors were to declare a static function with a specific signature that could be invoked during deserialization, returning an instance of themselves. These functions were stored in a std::map. The store happens automatically if the component has a static variable of a special type designed for this purpose.
So yes, it's not "virtual static", it's rather a dispatcher. But the users felt like as if it were virtual :)



Text alignment

The graphics library provided by the school didn't have any of the text alignment functionalities. So I took the charge and implmented horizontal and vertical text alignment, in addition to line wrapping.
To achieve this, I analyzed the file format used by the library reading the document. Based on the information, I wrote the code to simulate the library's calculation of the width and the height of a given text. However, the calculation was a bit off even though I strictly followed the documentation. After some serious investigation, I reached out to the professor who wrote the library and got the actual calculation used in the library. It turns out it was actually the library that didn't follow the documentation. To accommodate these differences, I implemented appropriate workarounds and successfully finalized the implementation.



Command-based architecture of the state machine editor

The structure of the state machine editor mirrored that of existing engines, functioning as a node-link editor. To accommodate undo and redo functionality for enhanced user experience, I implemented a command-based architecture. Every operations were encapsulated as distinct commands, each comprising OnExecute and OnUndo functions. The program maintained a series of commands, executed the appropriate functions when necessary.


Conclusion

This project represents a significant investment of effort on my part. With 382 commits, I added 34,682 lines and deleted 13,419 lines, excluding library and resource changes. The experience gained was extensive, highlighting the rigorous process of building an engine. Despite the challenges, the project was enjoyable, I had the chance to apply programming patterns and patterns, realized the importance of patience and determination in implementing complex functionality. It also provided an opportunity to devise solutions for intricate problems.
In summary, this project has been instrumental in my growth as a software engineer, and the skills developed will undoubtedly shape my future work.

No comments:

Post a Comment