
Posted on • Edited on
New Year, New Game Engine - Nikola Engine Devlog 0
Me And Game Engines
I madeso many game engines. Some were complex, others were simple, but all were horrible.
These engines always lacked a greater focus and a more scalable model. While I do not care much for abstractions or planning big from the beginning, I do have to say that the engines I created in the past always had a lifetime of one use and then were immediately discarded. The general design of these engines was more "old-school" than anything. By that I mean, the engine itself was embedded into the game. Many of the engine's systems were soldered onto the die (the game in this case). There is a greattalk by Bill Clark that goes in-depth about the differences betweenan engine and a capitalA engine. Essentially, from my point of view,an engine is just one that is built with the game itself. The engine is not a separate entity, but rather, itis the game. While, on the other hand, a capitalA engine is your typical Unity, Unreal, and CryEngine (you thought I would say Godot, didn't you?). And, for the longest time, I builtan engine within a game.
And why wouldn't I? I wanted to make a game, after all. And, perhaps, with the scale of the games I was making at the time, it worked. It did serve me well for a bit. However, as my ambitions grew to create games on a bigger scale,an engine was not sufficient anymore. I could not just loadall of the resources that I would ever need in the game at initialization and keep them alive for the entire runtime of the game. My levels were bigger now. They hadway more resources. They could not simply live in memory all at once without problems. On top of that, I needed a better and more intuitive way of creating and prototyping levels. I could not just place all of the objects into the world programmatically. I had to find a serialization system of some sort, and one that would handle hundreds, if not, thousands of entities. Suffice it to say,an engine had to leave and a new capitalA engine had to take its place. One that would ease the creation process of my games while still being "out of my way" enough not to annoy me. I'll get into more about that later.
And, beforetruly starting to work on the engine, I had to decide my goals, ambitions, and, most importantly, myintentions with this engine. And, as you would come to know, my goals and ambitions with this engine are, quite frankly, fairly selfish. Well, let me back up a bit.
The Intentions
Previously, whenever I started a new game project, I would startcompletely from scratch. From zero. I would not carryany line of code from my previous projects. The reason is that I, simply, could not. The systems I "designed" at the time were very dependent on each other. Moreover, they were dependent onspecific data structures only created inthat game. While I could have perhaps changed some code around or written my systems to be game-agnostic from the beginning, I chose not to. You could call it inexperience since it was. I was constantly learning at the time. I would make a "renderer" only to find a better and more efficient way to make another renderer. I would make an entity system and then scrap it all because I wanted to experiment with another system. On top of that, I liked to switch genres all the time. Yet, if you go to my itch page (which I will not link out of shame), you can see that all my games are, ironically, one genre. Arcade-y games. It is not because I love arcade games so much. I would simply give up halfway through my original idea and decide to make an arcade-style game instead. The "engines" I made at the time were not thought through, as I have said before. They were lackluster at best. A mish-mash of different ideas and implementations that I had suddenly decided to add. But that was only a testament to my lack of experience and lack of patience.
While Icould have used something like Godot or Unity, I did not really want to. I will not go into the reasons why I dislike game engines (because I already did) but just know that I dislike the GUI nature and the restricted workflow that all game engines seem to share. Understand, I amnot trying to bash on game engines. I think they are fantastic tools for game developers. However, they are certainly not for me.
I believe that not all games are equal. Does every game have common components? Of course. That is quite obvious. However, not every game has the samegame creation flow. The flow of prototyping and creating levels for a racing game is not the same as for a real-time strategy game. They sharetechnical components, yes, but noteverything is the same. I think that lumping all games in a general sense might hurt experimentation and might hinder the innovation of game design. Many game studios end up creating specialized tools for their games anyway even if they are using an already-existing engine like Unreal. And so, after somewhat of a long-winded rant, my intention with this project isnot to create a general-purpose game engine. At the same time, as I said before, makingan engine with each game might, in the long run, be inefficient as well. So, what then? Well, let's take a trip down memory lane, shall we? There's no special hat for that, don't worry. Just hop in.
You see, back in the day, developers used toalso start from scratch. Unlike me, their reason was more valid. The hardware scene at the time was rapidly changing. CPUs were getting stronger and faster every clock cycle (or what seemed like it, at least). Every new gamehad to be rewritten since a new and better CPU entered the scene. Now, developers did not removeevery piece of code they wrote and startliterally from zero. Yet,most of the code was thrown out and replaced with code that adhered to the newer CPU's architecture. Code that could, in some way, bend the CPU to the developer's will by using some exploit or some special feature the CPU had. Not all the code was like that, of course.
But, as the growth and speed of CPUs started to plateau, developers started to keep their old tools and code to be reused. Every project now had a base to comfortably be built upon. Level editors, graphics tools and renderers, and even multiplayer code. Yet, these tools were never in one place. They were always separate programs that had some kind of output that would be fed into the game itself. Thegame was the main hub these tools would output to. Compare this to game engines of today, where most of the tools are builtinto the engine while the game is just aresult of all of these tools working together. The engine is like the main hub for everything. The overworld, if you will. The engine will import the assets, compress the textures, edit the world, handle all of the gameplay scripting, and do other things like create animations or particle effects.
There is a reason for this--it is, simply,much easier for everyone involved. But, it does come with drawbacks. Mainly, as I said before, it locks you into an ecosystem that you areheavily relying upon. It somewhat limits you to what you can do. That is just not what I wanted to create. I wanted to create an engine to have all the basicneeds and the setup to get a game up and running, but I didnot want to limit any experimentation or any, perhaps, additional features or tweaks that can be made on the fly. I wanted the engine to be lightweight yet still have a robust set of features. I wanted thegame to be the central hub of everything and not theengine. The engine is only there in the passenger seat, helping the game with directions rather than being the driver and ordering the game around.
Now, of course, the gamestill needed a set of tools to help speed up development. Level editors, for example, would have to be created. The meaning of a "level editor" differs from game to game. For an open-world RPG, that might mean a whole-world editor, while for a Mario-style game, that would mean a top-down 2D tile editor. Do understand that this is notthe perfect way of making a game. But, for my use cases, it might as well be. It allows me to reuse tools and code while still giving me the freedom to experiment with different ideas and genres. It does not limit my flow in any way.
My intentions for this game engine are quite selfish now that I think about it. I do not care if other people use it or not. I do not care if it has the best and prettiest GUI editor. I do not want to use things like ECS (Entity Component System) for better entity management. I, frankly, only want to make this game engine for myself. I want to make games at the end of the day. But I want to make games the wayI like to make them. More work, yes, but also way more fun.
The purposes for this engine's existence are a) Make it easier for my style of game development, b) Learn a great deal from the technical challenges, and c) A good show off for my portfolio. Of course. Why not?
In order to capture my vision for this engine, however, I have to use the right tools for the job. I did not want to choose a tool that would end up wasting my time and energy while not benefiting my intentions with this engine whatsoever. And so...
The Tools And Dependencies
You see, for the past several years I have used many programming languages and many more game frameworks and libraries. Programming languages like Java, C#, C++, and even, sadly, JavaScript (I know...). Game frameworks likeLWJGL,SDL2,Raylib,MonoGame,SFML, and many more. Essentially, I have seen it all. Out of all of them, I think SDL2 was closer to what I was looking for, though, Raylib was the one I used the most at the beginning. And the reason I liked SDL more was because it was more"lower-level" than Raylib or SFML. Additionally, it had that C-style of programming that I have always been fond of. However, despite that, I decided to goagainst any of these libraries.
The one thing Iknew I wanted to do in this engine was to reduce the amount of dependencies as much as possible. I would not dare to go fullHandmade Hero-style, but I still wanted to use the minimum amount of required dependencies to get me started. I wanted the engine to be as lightweight as I could make it be. And while SDL does have a multitude of systems that would be beneficial for me, it still included a 2D renderer that I would for surenever use. I wanted to design and implement my own renderer, and having a "ghost" dependency in there just felt wrong to me. Besides that, SDL had its own way of handling textures, fonts, and audio files. While useful, it was very much unnecessary for me. I had in mind to create my own resource binary format which would, essentially, deprecate any need for.png
files or the sort. Now, of course, I still need loaders to decode these image and audio files, but I already had a way better and smaller dependency for that in mind.
So what did I use, then? Well, I separated the engine dependencies into five categories. The categories are laid out as such:
Operating System Dependencies: This is for things like window creation, input handling, the file system, console logging, and any operating system-specific operation
Graphics: This is, as the name implies, anything to do with rendering and graphics. So the graphics API (OpenGL in this case), any UI libraries, and so on.
Audio: Obviously, it has anything to do with audio. Notdecoding orencoding, butplaying audio by giving the audio card samples and having an audio thread active in the background.
Math: This might be a stretch but, besides the obvious math library, I also added physics dependencies in this category.
Resource Loaders: Image loaders, audio files decoders, 3D model format parsers, and so on.
Out of all of these, the first category--the operating system dependencies--is probably the one I thought about the most. Since SDL was out of the picture, I sawGLFW as a potential choice for handling window creation and input. An obvious choice, by many. And, seeing how I already had used it before, I thought it was obvious to me as well. Yet, there was a feeling I did not need it. After all, I decided from the start that I wouldonly support Windows and Linux. Not for any particular reason other than I use Linux on a daily basis and Windows has the bigger market. I did not have a Mac machine lying around somewhere (poor. I'm poor, basically). And as for consoles, well, that was a long stretch. I did not see the possibility of meever needing to port my games to consoles. At least not for the time being. And so, that means I only had to deal with the Win32, X11, and Wayland APIs. I say it as though it is an easy affair. It is not. Far from it. Especially if you had never dealt with these APIs before and had to start learning them, which was my case. So, instead, I picked a middle ground. I would use GLFW but I would design my API in such a way that would be easier to switch away from it in the future if needed. I'll write a more in-depth article about the window system and whatnot in the future.
As for the graphics, well, that was a point of contention, too. The only graphics API Itruly know by heart is OpenGL. I had used it plenty of times before so it would not be difficult for me to integrate it into the engine. Yet, like the case with GLFW, I was torn. OpenGL is not, well, the mostmodern of graphics APIs out there at the moment. A better and more "modern" choice would, of course, be something like Vulkan. But Vulkan is abig beast. One that would take quite a long time to deal with. And, frankly, I had the itch to make a game for a long time. I did not want to be held by Vulkan any longer. So, once again, I decided to find a middle ground. I would create a more robust and "open-ended" graphics API that would wrap around OpenGL so that I can, in the future, substitute it if needed. That was the intention at least. I did, briefly, try to integrate DirectX11 at some point, but I ultimately failed. Miserably, I might add. You can still see some remnants of my attempts at implementing DirectX11 in the engine, in fact. But, again, I will go more in-depth about that in a future article.
Audio is another similar issue to graphics. There are plenty of audio APIs out there. Different APIs support different audio cards. I, however, had made up my mind on this one a long time ago. I decided to useMiniAudio, which is the audio library used by Raylib under the hood and one that I used plenty of times before. It is a single-header library that issuper easy to integrate.
As for math, that was the easiest choice as of yet. No doubt,GLM is a "gold standard" at this point. For OpenGL it is, at least. But, like with a lot of the other APIs, I decided to build a wrapper around it rather than directly reference the library in the engine's code. And for physics, well, I had not come upon that answer just yet. I did try to make my own physics logic at some point. And while it was, surprisingly, successful, I wantedmore than just a simple physics layer. I wanted something more complex and, more importantly,faster than my implementation. I have not decided upon a physics library yet. But I'll cross that bridge when I come to it.
And, finally, resource loaders. I have worked with many resource loaders in the past. That's to say that I know my way around them. For image loaders, I decided to go with the obviousstb_image. It is small, easy to use, and has only a single header. Meaning, like MiniAudio, I can easily integrate it into the engine. Besides that, it supports a wide array of image formats. EvenHDR
which was surprising. Audio file loaders are next. For this one, I decided to use a few small libraries. Specifically, the Mp3 and WAV loaders fromdr_libs and the OGG loader from STB once again (Sean Barret to the rescue!) Each of these are a single-header library as well. Once again, very small and very easy to integrate. As for 3D models, I decided to use a huge dependency namedASSIMP. For me, that was a very hard sell. ASSIMP ishuge but it supports plenty of 3D model formats. And besides that, none of the resource loaders are going to be present in the engine itself. But, rather, there is going to be a separate tool--one that I dubbed NBR (Nikola Binary Resource)--that would take only the minimum required data from these loaders and save it into a binary file (.nbr
) which then would be read accordingly by the engine.
If you think that's weird, well...
The Design
In my dependencies talk earlier, I mentioned a lot of libraries that were "single-header". I'm sure you noticed I used a lot of them. But, what are they exactly?
In the most simple of terms, a "single-header" library is what it sounds like: There isonly one header file (.h
or.hpp
) in thewhole project. These libraries also have their implementation code (.c
or.cpp
) in the header file itself. Usually, the implementation code is hidden behind a#ifdef
. So, in the case ofstb_image
, you would create a translation unit (.c
or.cpp
... you should know this by now) which only has the following code:
#define STB_IMAGE_IMPLEMENTATION #include<stb_image.h>
The implementation code would effectively becopied into the translation unit and then compiled normally. Ilove these kind of libraries. They are usually easy to use, very easy to integrate, and a lightweight dependency overall. I even created alibrary ortwo in the same vain just for fun. But why am I talking about single-header libraries now? Besides the fact thatmost of my dependencies are single-header, I wanted tosomewhat imitate thespirit of single-header libraries while avoiding the need to jumble all my code into one header file. While it is convenient to have all the code in one place and, once again, it would be very easy to integrate by other folks, but, seeing how I am already an unorganized person, I will refrain from the complexity that comes with such a design. Instead, I wanted to have separate translation units for every module, but keep the idea of a single-header file for thedefinitions. Let me explain.
This engine really has three parts.
Core: This is thebase of the whole engine. This is where the window, input, and event-handling systems live. The graphics API wrapper exists here as well. The logger, the asserts, many core typedefs, and so on. It is more of a "game engine-maker" if you will.
- Engine: This is where mostly all of the code that will be used directly by the user (me) lives. The entities, the scenes, the resource manager, the application callbacks, the camera, the renderer, and so on.
- UI: I have yet to make this part of the engine but it is essentially a wrapper around ImGui to handle the UI for any custom editors and such.
For each part of the engine, there exists a.hpp
equivalent. There is anikola_core.hpp
, anikola_engine.hpp
, and a yet-to-be-created,nikola_ui.hpp
. Thenikola_engine.hpp
depends onnikola_core.hpp
, andnikola_ui.hpp
depends on both.
Now, thegreat thing about this is that all the definitions of the project live in three places. I do not have to includetwenty or so header files in each translation unit when I want to do such a trivial thing as rendering a cube mesh for example. However, there are, as I came to discover, two very important cons to this approach. The first is the obviouslength (be mature, damn it) of these header files. For example, thenikola_core.hpp
header file alone is2000 lines of code. And there arestill features to be added to it. Yet, with some thorough documentation and some separator comments (one of these bad boys,/// ----------------------
), it could be managed pretty well. The main issue and the one that annoys me themost is that if there isany small change in any of the header files it means, potentially, a whole recompile of the engine. Now, thenikola_core.hpp
does not have any heavy dependencies that would require a long time to recompile (in fact, this is the only section of the engine that compiles surprisinglyfast). However,nikola_engine.hpp
does have heavy dependencies. Specifically, since GLM is exclusively templated, the "engine" part of the project takes quite some time to compile. It isn'tslow by any means. But it is frustrating to change something in the renderer definitions only to have the whole math section be recompiled. Perhaps I could have handled it in a better way, but, honestly, I do not mind it. The ease of use this approach gives me is well worth the recompilations. And, besides, the header files are not often changed anyway. So I do not have to recompileall the time.
The other important design decision I had to make was with resources. As I talked about before, I created a custom binary resource format specifically for the engine to use instead of the so-called "middle-man" formats. Or, the PNGs, JPEGs, OGGs, and so on. It is quite a common practice in the game engine space to handle resources that way. It keeps the engine separated from any need for these formats while giving the engineers room to approve the load times and compression of these resources. Quake used its proprietary format.mdl
for model files. The Source Engine used the format.vtf
for handling textures. And, of course, every modern engine packages its resources in some way or another so that the runtime (the exported game, essentially) can use it as efficiently as possible.
Now, I am not smart. Far from it. However, I decided toalso partake in this "tradition" and make my own resource binary format that will, in the future, be subject to better optimizations for loading at runtime. Of course, as discussed before, there is no "runtime" concept in my engine. The runtime in these engines usually means the final exported game. Well, in my case, the engineis the game. So, effectively, I am always in runtime. So, I decided to make a small command line program to convert any given supported resources into the.nbr
format. Keep in mind, the engine doesnot understand nor does it care about any other resource format. It can only parse and decode the.nbr
resource format.
I will talk in-depth about the whole resource management system of my engine in a future article. But, for now, know that it is very primitive in its current state. I do not claim that.nbr
compresses files to seemingly nothing. In fact,.nbr
files dono compressing whatsoever. But, it is a start. And, surprisingly, I have noticed better startup times whenever I'm loading.nbr
files as opposed to regular.obj
files for example. But, I would have to test that theory later in the future.
Current Progress And The Future
So, where am I? And where am I going?
Currently, as I'm sure you can tell, the engine is still in its infant state. It can do a lot. Currently, it can open a window, accept input, render pixels, load models, and images, and render them even. But there is still along way to go. For example, audio and fonts are still not fully implemented. While things like entities are not even a thing yet. However, if you are interested I do have some interesting showcases in theprojects
section of this website. You can also go to theengine's repo to check the code for yourself if you are interested.
It is hard to say what the future of this project is. I can say, though, that I am planning to stay with this engine for awhile. Once again, my intentions are fairly selfish when it comes to this engine. And, by extension, the planned future for this engine is quite selfish as well. While you can use the engine for your own projects, I will not advertise it as such. In fact, I will not advertise it at all. Show some interesting demos and progress reports here and there, sure. But I will not go out of my way to market this engine as a product. At the end of the day, I'm making this engine solely for my own enjoyment and based entirely on my own philosophies. And, naturally, not everyone will share these sentiments.
Thanks for reading and have a good day/night
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse