I figured early on that this project would be my biggest one yet, so I quickly realized that I had to put in a lot more
effort this time in the quality of the project architecture and long-term performance. However, the fact that I didn't have much experience
in C++ or large project management was pretty damning to say the least. To get past this, I took a lot of help from game engine development
tutorials on youtube, specifically
TheCherno's tutorials. At the beginning,
I treated a lot of it as a follow-along type of walkthrough, which helped me break through really well! Watching a veteran programmer at
work helped me develop habits and forward thinking for the architecture far better than if I were to just go about it alone. Of course at some point,
I branched onto my own to develop my own ideas and implementations and walked back through the past implementations to properly understand everything.
Early on, I have to admit that it was difficult to stay motivated. With most of the beginning being just figuring out how to
properly compile C++ projects and setting up architecture and tools for the future, there wasn't a lot of "satisfying results" that rewarded
a long stream of work. It was a lot of setting up basic rendering components, the logging system, and platform compatibility.
System compatibility was a huge thing to consider in the beginning, as I wanted to leave room in the future to not just be cross compatible,
but to support different rendering backends such as using Vulkan, Metal, or DirectX rather than just OpenGL. This was especially important for
rendering, since I didn't want to have to rewrite the entire rendering backend when I was done with OpenGL.
On a side note, this was also when I really learned about C/C++ macros! Macros have quickly become one of my favorite features of C/C++ due
to how versatile and powerful they are to use! When I first started using and understanding them, it felt like I could basically create my own
minor programming language within C/C++ to customize my programming experience while at the same time optimizing performance! Every time I start
a new C/C++ project I thank Alan Snyder (who I believe introduced the C preprocessor?) for making macros a reality. 🙏
Way back long ago, when baby Jason was like 12, I tried to create a windowed program in C++. After being met with youtube videos
telling me to use C++ Windows Forms, I was immediately repelled and never tried to touch it again. Starting this project up again and needing
to create a window frightened me, as I could only imagine how horrible it could be. However, using GLFW was quite the delight! I have no why I saw
Windows Forms before this, but GLFW was the perfect amount of control and customizability I needed to create a game window. Thankfully, it also
had a nice event system for grabbing inputs!
The rendering system was the first large-scale implementation that I had to do. I've never known how graphics work prior to this,
so implementing an entire customized graphics pipeline was definitely quite a challenge. To be fair, it was an OpenGL graphics pipeline, but
it was still a new area to work in nonetheless. Even after following youtube tutorial after youtube tutorial, I struggled to understand what I was
even doing that made the funny triangle appear on my screen. In fact, I can say that I truly understood the graphics pipeline only after I finished
implementing it, in which I attempted to create a similar renderer using Vulkan. Vulkan is, to say the least, to OpenGL as C is to Python. Considerably
harder to understand and use, but far more powerful in terms of potential. Ironically, it was only after I finished creating the Vulkan pipeline was I
able to look back on my OpenGL pipeline and understand it.
As one might expect, it's not just enough to draw a colorful triangle on a window to create an entire game engine. To make objects appear
in 3D or orthographic space, there is a slight degree of 3D math involved. However, back when I was first starting work on this, I did not have much knowledge
in the subject matter, since I didn't take a linear algebra class yet. However, I was enrolled in a computer vision course, which taught the basics of 3D math
for the application of image processing and analyzation. Because of this, learning and applying these concepts to create view-projection cameras with both
projection and orthographic modes were pretty fun to do!
Debugging this project had its ups and downs; when it came to debugging the graphics pipeline, I was fairly clueless on how to
detect errors due to my inexperience. I didn't know how to properly log the information I needed, nor did I know what information I was exactly looking for.
For the rest of the project, however, things were suprisingly pleasant.
Debugging Flora is really where the architecture I set up in the beginning truly shined. This is also where I truly think object-oriented programming
shines the most. When a bug appears, it was so easy to immediately identity what systems/classes are linked to it, which ones could cause it, and which
one the irregular behavior likely stemmed from. In fact, 9 times out of 10, I could immediately pinpoint which function from memory (my brain's memory, not
the computer lol) the bug was located in. Compared to debugging previous projects, this was like a wave of relaxation and relief. In fact, I rarely worried
or stressed over bugs and issues, and I can only thank my past self for putting the time and effort into creating a clear and purposeful architecture.
An honorable mention here is actually for Visual Studio! I know that people have their gripes with Visual Studio as a bloated and overengineered piece of
proprietary software from Microsoft, but as a first C++ IDE, I found it delightful! Even though I've switched away from it since, I still hold a spot for it
in my heart, and this is primarily due to its debugging capabilities. Adding in breakpoints, navigating the codebase, and monitoring memory locations were
so intuitive and easy, I never really had to "learn" how to use the debugger officially. It came naturally, and made debugging issues just that much easier!
Serialization for Flora was one of the more interesting things to think about, despite it not exactly being the most visually exciting function.
Saving information between scenes and even entities was indeed a pain to implement (no thanks to C++ lacking reflection support...), but boy did it pay off!
Being able to completely save a scene and even an entity, and load it easily was such a nice feature, especially for collaborative game development projects!
This was because of the fact that the serialization system uses YAML! Although this is definitely not the most efficient system, especially for larger scenes,
the tradeoff here is that it becomes very human readable. Looking at scene content and modifying it becomes incredibly easy without having to touch or compile
code at all! And because of this, it becomes easy for collaborating developers to share resources, such as entities across scenes and projects. Have a cool game
object? Just serialize it and send it over! As long as any assets such as textures or scripts are also sent over, another developer can immediately load it and
use it without any issues!
To make a game, a developer is going to need game objects. A game engine's job is to provide extremely abstracted game objects that can be customized
to such a degree that the developer will have total freedom over the implementation of their creative vision. To do this, I decided on a scene and entity system.
Entities are essentially empty pointers which a scene handles, and then the developer is then able to customize their entity to their hearts desire through entity
components. Components package basic values such as a transform or a texture to render. Some prebuilt components are precompiled with Flora, such as a transformcomponent,
spritecomponent, audiocomponent, etc. However, the true customizability lies in the scriptcomponent. While the Flora backend can handle the precompiled components in
a specialized manner, what happens if the user wants to handle their custom component? This is done with the scriptcomponent. Each entity that has a scriptcomponent
holds packaged and customized code that can handle any properties the entity has. Pretty cool!
As to avoid cutting down several years of my life trying to make my own embedded runtime that wouldn't explode, I chose to integrate mono as a
C# scripting engine! This was also my first introduction really to how .dll files worked and what exactly C# was in terms of a language. Communicating between
the C++ backend and a C# frontend was incredibly cool to do. This was really the first time I saw 2 different languages actually intertwine and communicate
between each other outside of just communicating via sockets or modifying a file. That being said, mono did not come without its difficulties. Sharing entities
and structures between the two languages resulted in some...memory issues, specifically because of the C# garbage collector. Unless I designated an object that was
created in C++ to be garbage collected, it would just stay there forever, eventually bloating and crashing the C# runtime. Unfortunately, I did not realize this for
about... 3 weeks of debugging. As embarassing as that is, I definitely am more versed in debugging via memory monitoring and addresses now.
For physics, I opted to use box2D. Before you start shouting "aw weak! If you were a REAL programmer you would make your own physics engine!",
know that I've made physics engines before, and I've had QUITE ENOUGH. I'm not great at making them super efficient, and box2D works out of the box (haha pun)
like a dream. That being said, box2D has pretty much every feature I could ask for, so it was super easy to integrate. However, one large challenge was
integrating box2D bodies with subentities.
The way Flora handles subentities is that subentities can inherit properties from their parents, namely their transformation. When this happens, a subentity's
transform is handled in reference to the parent's transform as the origin. When this is mixed with binding a rigid body to the transform, it has...unintented
consequences. When subentities who try to bind their transform to their parents also try to bind themselves to the world of physics, the two constraints collide.
This results in what I have deduced to be them fighting and then exponentially increasing their control to win over the subentity. This results in a lot of shaking,
then expanding, and then dissapearance. Because of this, I "locked" binding physics bodies to subentities that inherit transforms under a VERY heavy warning.
Stats for flora included mainly resource usage and rendering system statistics. However, the pain that doing resource usage put me through
merits the existence of this section in the first place.
This was because I used the "pdh.h" header to monitor system resources
(exclusively on windows). Memory, disk, and CPU were not very hard at all with this. However, trying to monitor GPU usage has been and still is
something that is simply just beyond me. Although there is an implemented attempt at it, Flora still lacks a working reliable GPU usage monitor. I'm not sure
if its because of some bullcrap between AMD and NVIDIA cards having different support names or drivers, but I could just never really find out how to detect
the usage behind them. Not only this, the documentation and support for this library header is either non-existant or just plain wrong. In terms of the official
Microsoft documentation, the actual usage outlined is just plain wrong in terms of syntax and functionality I've found. If this is due to incompetence or if
it just hasn't been updated in years, I will never know. When turning to forums such as slack overflow however, it seems that everyone is collectively scratching
their head on how to use this library.
For audio, I chose to use OpenAL! Although it was a little harder than expected to link and use at first,
I eventually got it to work! OpenAL was my first choice because, well, it was open source, cross platform, and had lots of support!
Another thing that I loved about OpenAL was that it came prepackaged with the capability of 3D sound! This was perfect for a game
engine, and perfect for audio "source" and "listener" components. Overall, OpenAL was a delight to work with! The only caviat was the audio file support.
Loading audio files came with 2 main obstacles. The first one, was learning the structure of a .wav file. APPARENTLY, some .wav files are
just structured...differently? Depending on the .wav file, it can just not load correctly if you're not meticulous enough with the preprocessing.
It took a while for me to realize this, which was... frustrating to say the least. The second obstacle, however, was even worse.
To my surprise, common software developers just... can't use .mp3 audio files! After hours of searching and disbelief, it appears that
.mp3 files are just... impossible to decode?? You apparently need to pay for a software license to decode them?? This seemed to be just
utterly ridiculous to me that such a widely used format...is pay to win. I'm hoping that I'm just wrong, and there is indeed an mp3 decoding
library out there somewhere.
Implementing text was a treat. This was because I chose to implement MSDF text rendering. For those who don't know what MSDF rendering is,
read
this paper! For those of you who
don't want to read another 64 pages after sifting though all my rambling, here's the basics:
In typical font rendering, a .ttf file is used to generate an atlas with 2 color channels. The TLDR of what this means is that
when you zoooooooooom in a lot, the font will get blurry. This can be seen in a lot of applications if you want to dick around with that
kind of stuff. The reason behihnd this is because the 2 channels can only create an "outline" with a specific resolution, as it is not stored as
a vector format when being rendered. However, MSDF uses multiple color channels! This allows fonts to "blob" parts of a glyph so the renderer can
identify points where these blobs cross, indicating a clean point. Because of this, you can zoom in infinitely, and there will be zero loss of quality!
Really the only downside of using MSDF rendering is that you need to generate a MSDF font atlas from the .ttf file, which can take several seconds
on startup. However, this problem is easily made insignificant by making a user "import" the font and caching the atlas for quick loading.