For the course Programming 4 in my 2nd year at DAE Howest, I had to make a Game Engine and with that game engine make the game qbert.Repository
-Game Engine specifics and design choices:
The Input System uses commands to execute its assigned events. You set this up by either overriding the “Command” and “Execute” functions. Or by using the “CommandFunc” and passing a function pointer to it with return type ‘void’ and no parameters. If you want parameters already assigned you can bind them when creating the CommandFunc.
The flyweight technique is used in Resource Management. When you request a Texture2D you pass the file path as a parameter, which then returns the desired Texture (if it exists). Internally it holds a map with file paths as key, and the resource as value. This way there is always one allocation of that resource. Saving up heap memory.
If you want to add listeners to certain objects without needing to have them call you directly. You can add a subject to that object. You first inherit from Observer, add your event methods, that class is used with the Subject templated class. When you want to notify you to call notify on the subject with the observer's self-defined function as an object. Because these are member methods you bind a placeholder for the “this pointer”, after that you can bind more parameters.
I have 2 types of singletons. A normal one uses a static allocation of that type on the stack. And a singleton using a static smart pointer for allocation on the heap. This enabled me to use singletons in a more friendly way. And not having to free the instance myself.
The state machine I added uses an object-oriented implementation. You define an inherited class from “State” and choose which functions you want to implement. Options are: “Enter”, "Exit", “HandleInput”, “Update”, “FixedUpdate”. For Initialization, you instantiate your state machine. Then you make instances from your desired overwritten state objects. This uses the state machine and the GameObject user as parameters. And finally, you start your state machine using the “Initialize” methods on the state machine. You pass the start state as a parameter. In the update method of your component, you call “HandleInput” and “Update”, and in your component’s “FixedUpdate” you call “FixedUpdate” of the currentState from the state machine.
For the Game loop, I used the “Play catch up” pattern. This way I could have a Fixed Update good for physics and networking. And a normal Update each frame.
Each GameObject can have components attached to it. You can add unique logic to a component. This enables the possibility to encapsulate implementations and use it as a modular piece, which you can use on other game objects as well. And still, be functional. The Parent Gameobject will call each Component’s “Initialize”, “LateInitialize”, “Start”, “Update”, “FixedUpdate” at the required time.
Initialize and LateInitialize are only called once for a component. The start is called each time an object has become active. E.g. you set another scene active.
Each scene can have a collection of GameObject which can also have Child Gameobject and components attached to them. Since the game loop calls the Update and the FixedUpdate in the SceneManager, this enables the possibility to have a dedicated Update/Fixed Update method for each component in a scene.
Currently, there is not a standalone Event Queue type that you could use. I do use it in the SoundSystem. Because allocating and playing audio files requires a lot of CPU time compared to the rest of the program. I do this on a separate thread. When requesting to play an audio file, this will create a task containing the required information. And add it to the queue. Since the thread the audio queue is handled is locked with a condition variable. I notify it to tell it that there are pending items in the queue. When the queue is empty, the condition variable waits until it gets notified again.
I used a service locator for the sound system implementation. Currently, I use SDL to play sound. But there are plenty of other options out there. Because I don’t want to make a new system for every option. That is where a Service locator comes in handy. This enables the possibility to switch between services without having to change the already existing code using the sound system.
Last but not least, I added the Dirty flag pattern as an optimization extension to the already existing GameObject hierarchy. Because each Gameobject only holds its local transform. There has to be a way to get an object’s world transform. Previously I went down from the root and added all the transformations in a recursive way. But this was happening each time I requested the transform of a GameObject. By using the Dirty Flag pattern I am able to store a copy of that world transform. And every time I update that transform I set the flag to false. Knowing this, the next time I request that the transform. I recalculate the world transform from the root up, Cache it, and set the flag to true.