Game Engine Dev, Explained for Non-Programmers: Choosing the Right Tools
This series can be read out of order, but here are some navigation links for your convenience:
So, I know I’ve made a big deal out of starting from scratch, but nobody actually does that ever for anything1. In my case, there’s a whole bundle of tools I’ll be relying on third-party software for, but to even get started, I’ll need to decide on two things:
A programming language
A game development framework
What is a programming language, anyway?
You can’t just ask me that! Dear reader, to truly answer such a question I would be forced to do you a terrible harm! That is, give you a computer science lesson! No, that wouldn’t do, it simply wouldn’t. But… ah, needs must. Here’s what we’ll do: I’ll give you a unique, very special computer science lesson. It will be so special by being incredibly, magnificently brief! I guarantee, you will learn very little. It’s not ideal, I know, but it will surely be a mercy compared to the alternative. Now then…
What is a programming language? Put simply, it’s a set of logically consistent rules for writing text2, called a program, which can be turned into instructions that your computer can understand.
You might have heard that, fundamentally, your computer only deals in 1s and 0s, but how can it ever read text then? Well, inside pretty much any computer is a device called a Central Processing Unit. In (grossly oversimplified) essence, a CPU is a little machine that takes in and puts out numbers using electrical signals. Each is built with a big set of little tasks3 they can do, called instructions, and each instruction has its own little number to represent it. A program is basically a long list of those little numbers that the CPU reads in order. It’s even possible to write out that list of instructions directly using something called an assembly language, but this incredibly tedious and difficult, even for relatively simple programs.
With that in mind, programming languages can be grouped broadly into 2 buckets:
Compiled languages: The more straightforward approach. Compiled languages use a program called a compiler, which knows the rules of the language, to read the text you wrote and turn it into a list of instructions for your CPU. You can then run the “compiled” program on its own, whenever you want.
Interpreted languages: Instead of turning your text into little numbers ahead of time, interpreted languages use a program called an interpreter, which runs alongside your program, and reads the text to figure out what instructions to give the CPU as your program is running. Interpreted languages tend to be more flexible, since, unlike a compiler, an interpreter doesn’t need to know what to tell the CPU way ahead of time and can figure things out on the fly. Unfortunately though, this means programs written in interpreted languages are significantly slower, since figuring things out on the fly takes time.
Coming back to my own project: my interest in developing my own engine took serious shape about year ago, when I started more closely following the development of Jai (Jonathan Blow’s new programming language that he’s been working on since 2014). Its philosophy and design goals were fascinating. It seeks, in essence, to be a replacement for C++, the current standard for any low-level game development.
Many popular languages nowadays, like Python and Javascript, are interpreted4. This makes them easier to use in many ways than compiled languages like C++, but for tasks like serious game development, where performance is extremely important, the slowness of an interpreted language is unacceptable. In addition to that, many interpreted (and even compiled languages), in pursuit of ease-of-use, tend to obfuscate how your program actually interacts with computer hardware (specifically, for the most part, how your program manages memory). They’re designed to handle that sort of thing automatically in the background while you deal purely with the abstract logic (i.e. they are ‘high-level’, compared to low-level languages which are closer to the hardware). This does make things easier, but usually results in programs and programmers that don’t understand what is going on under the hood, which is quite bad for performance.
C++ is one of the only popular languages that still puts control of the hardware in the hands of the programmer. The language it’s based on, C, is also like this, but C lacks many of the features of C++ that make it feasible to write very large, complex programs. Even then, the way C++ handles that complexity is, in hindsight, less than ideal, and makes programs unduly hard to maintain and reason about.
Learning about Jai and the philosophy behind it exposed me to the true gravity of these problems, problems which it is aiming to fix as a C++ replacement. It was also very exciting, the language itself seemed incredibly fun to use compared to the much older and more bloated C++5, which made the task of serious game engine development under said philosophy seem a lot more manageable.
Unfortunately, Jai is still unavailable to the general public, which left me experimenting with alternatives. But before I talk about that ordeal…
The innovative, portable elephant in the room
As I write this, my Nintendo Switch devkit sits demandingly and NDA-protectedly on the corner of my desk…
The biggest, most glaring condition I set for myself as part of developing this engine was that it needed to be able to run on consoles (in addition to Windows, Mac, and Linux devices) without much additional work. This meant finding a game development framework that supported the major console platforms. What is a game development framework? You can- totally ask me that question, it’s not that complicated. Such a framework is basically a collection of existing code (a ‘library’) that you can include as part of your program that handles common tasks such as drawing to the screen, collecting input, playing audio, etc. It also abstracts that functionality across platforms, which means code written using that framework will work across any platform it supports. For example, say I want to draw a rectangle on the screen:
If I was handling this stuff myself, I would have to write different code that draws a rectangle for every single platform I wanted to support, taking into account all sorts of things that differ like how the CPU is built, the graphics interface, requests to the operating system, etc. Obviously, this is extremely time-consuming and complicated and requires an obscene amount of technical knowledge.
Alternatively, I can simply tell a framework “Draw me a rectangle!”6 and the framework will handle all the complicated stuff for me, changing how it does things under the hood depending on the platform I’m targeting. Not only does this save me the effort of actually writing the code that draws the damn rectangle, it means I only have to worry about the platform when it really matters.
As you can see, frameworks are an essential tool, and even amongst indie studios which write their own engines it’s very, very rare to see someone not using one. So, finding the right framework was a must. And while finding one that supported console development was my main goal, I also had a number of other things in mind when looking:
It should be in line with the philosophy outlined in the previous section, i.e. it should be written in a low-level language and able to be used with a low-level language.
It should be primarily designed for 2D game development. Often, frameworks created for 3D development (or both 3D and 2D development) tend to come with a lot of unnecessary bloat from the perspective of someone like me, who works only in 2D.
It should strike a nice balance between having enough features and still being relatively simple and easy to learn.
It should be primarily designed for PC and console development, as opposed to mobile development.
It should have a track record of quality, commercial indie games being made with it. This is the best proof that a framework is powerful enough to suit the needs of a commercial game developer like me.
So how did it go?
Annoyingly.
To my surprise and dismay, pretty much the only framework that advertised itself as having full support for all consoles (aside from those used by full-on engines) was Monogame. At first, this didn’t seem like the worst thing in the world. It fit most of my criteria and even came with a number of useful tools. And hey, if it was good enough for Terraria and Celeste, surely it would be good enough for me? But Monogame was built on Microsoft’s old XNA framework, which means it’s written almost exclusively in and expects you to use C#, Microsoft’s special little language.
C# is… in a weird middle ground. It uses Just-In-Time Compilation, which means rather than being compiled ahead of time, a program called a runtime, which is sort of like an interpreter, compiles your program as it’s running. Sorta. Look, the details aren’t important; this basically just means that C# is faster than an interpreted language, but more flexible than a compiled language. But, y’know, you could also rephrase that as it being less flexible than an interpreted language and slower than a compiled language. Especially because C# hates the idea of giving you control of memory… but I was blissfully unaware of the full extent of this hatred at the time, and so, with a notepad of full of low-level memory tricks I wanted to try out, I decided to jump in and give Monogame a shot.
Cut to one month later, and I was at my limit. See, C# technically gives you the ability to do low-level memory control, but holy sh— it’s like pulling teeth!! Half the language features aren’t built for it, you have to ask pretty please every time you do it, and whenever I wanted to ask a question about it every forum answer (and half the official documentation… and ChatGPT…) basically just told me “you’re crazy, don’t do it”! They literally call it “unsafe code”! And of course, the runtime is so full of implicit background behavior when it comes to this stuff that there’s never any easy way of knowing if things are working as intended or if you screwed up and wiped out all the performance benefits you were aiming for. The simplest tasks, stretched out into days of infuriating research and fiddling around! What foolery! What nonsense!
Uh… memory…?
Ah. Crap, another computer science lesson. Alright, let’s get through this quick, I believe in you.
You might be familiar with the idea that your computer has memory, i.e. places where it stores information (as little numbers) when the CPU isn’t using it for its little tasks. But did you know that your computer has different kinds of memory? In essence, different memory components on your computer trade-off between how much data they can store, and how quickly the CPU can access them. So a component might be able to store a lot of data, but take a really long time to access; or it might only store a little bit of data, but be really fast to access. Here’s an overview of the different memory components in a typical PC:
Hard Drive (or ‘the disk’): This is basically “cold storage” for your data. Hard drives are huge, with hundreds of Gigabytes or even Terabytes of storage space7, and they store data even if the computer is powered off, but they’re really slow to read from. When a game is “loading”, it’s usually because it has to move a bunch of data from the hard drive to RAM. Speaking of…
RAM (Random Access Memory): This is basically where all the programs running on your computer store themselves and any data they’re currently using, after being loaded in from the hard drive. It’s called that because you can access any part of it just as quickly as any other part, instead of having to wait for a literal spinning disk to move to the right position. RAM is pretty big, usually capable of storing several gigabytes on modern PCs. It’s also much faster to read from than a hard drive, but not as fast as…
CPU cache: Your CPU comes with a number of little memory components called ‘caches’, each one smaller and faster than the last. The smallest ones are usually on the order of a few kilobytes. Whenever a program tries to send a piece of data from RAM to the CPU, it (and some surrounding data8) gets copied to a CPU cache. The next time your program tries to read data from RAM, the CPU will check if it’s in a cache first. If it is, great! It can just read it from there instead (this is called a ‘cache hit’, and if the CPU can’t find the data in a cache, it’s a ‘cache miss’). This is useful because getting data from a cache can be hundreds of times faster than getting it from RAM.
CPU registers: This is where the CPU puts data it’s directly working on. They’re tiny, usually just a few bytes. If your CPU is in the middle of, for example, adding two numbers together, each of them is gonna take up one register.
So, bringing it back to the earlier discussion, how do different programming languages interact with this stuff? Generally, anything that has to do with memory is considered fairly low-level, so many languages do their best to make it so you don’t have to think about it. In C#’s case, it employs a common strategy called ‘garbage-collection’:
Say I want to store a bunch of numbers for later use: in a garbage-collected language, I just need to say “store those numbers somewhere!”, that’s it. Another special program that runs alongside mine, called the ‘garbage collector’ (GC), will hand me a reference I can use to get my numbers back later, and I can go about my merry way. The GC will automatically handle putting and shifting those numbers around in RAM, and cleaning them up when my program doesn’t need them anymore (it does this by continuously checking if the reference it gave me is still in use).
In a ‘manual memory-management’ language like C, I would be in control of memory, but also responsible for it. So for my bunch of numbers, I’d have to explicitly ask the operating system for somewhere (in RAM) to put them, make sure there’s enough space for them, keep track of exactly where they are, move them somewhere else if e.g. I suddenly need to add more numbers than I have space for, and free up the memory as soon as I no longer needed it.
Wow, manual memory management sounds like a pain! Why bother? Well, as mentioned, it all comes down to performance. Running a GC introduces overhead, but worse than that is that in a GC’d language you have no clue how your program is actually laid out in memory. This makes it basically impossible to optimize your program for the cache, since the main way of doing that is to very carefully arrange your data sequentially in RAM, to minimize cache misses. This is a big deal, even simple programs can, in practice, run dozens to hundreds of times faster if properly optimized for the cache. This makes low-level memory control a must-have if you’re serious about performance. But as much as C#’s design insists otherwise, it doesn’t have to be a nightmare…
Alternatives
I can actually remember the specific line of code that shattered my faith in C#9… it was time to try something else. After all, in many ways, C# was diametrically opposed to Jai. It’s a decades-old language designed by committee with a huge emphasis on automatic memory management. Frankly, it’s a bit crazy that I got as far with it as I did. Perfect, guaranteed console support was just not worth having to use it, so I loosened my criteria: if I could get a test app running on my Switch devkit, that would be good enough for now. With my expectations properly humbled, I decided to look closer to “home”.
It was through my research at this point that I realized that we’re truly living in a bit of a renaissance period for C-like languages. Jon Blow wasn’t the only person who saw the need for a new C successor; over the past decade or so, several projects have sprung up trying to be more pleasant alternatives to C/C++ for systems programmers. Go, Rust, Zig, V, and many more have been slowly carving out their own niches. But for my purposes, one language stood out to me: ODIN.
Created by Bill “gingerBill” Hall, ODIN is a C-like language whose design philosophy is squarely in the same camp as Jai’s, and it shows in both the syntax and language features. It was created primarily to help with the creation of the VFX software that Bill works on, and seems to have been a huge success in that regard10. It’s also considered to be incredibly well suited for low-level game development, in-part thanks to its out-of-the-box support for many existing C-based game dev libraries. Most notably: Simple Directmedia Layer 2.
SDL2 is an incredibly popular game development framework with an extremely impressive track record. While I was already aware of it from my previous research and some dabbling years prior, I had glossed over it since it didn’t seem to advertise having out-of-the-box support for consoles. But color me surprised! Upon closer investigation of an old readme document hosted at the back of their website, I uncovered the existence of official Switch and Xbox ports of the framework!11
(Now is probably a good time to mention: getting one’s hands on these sorts of things is a bit of a pain. Console manufacturers are extremely secretive about the inner workings of their machines, so ports like these can’t be hosted freely alongside the normal framework. They can only be made available to licensed developers, on pain of a visit from the ninjas. Fortunately, I have all the right credentials, so getting access to the Switch port of SDL2 was just a matter of a few twitter DMs.)
Hope!
The temptation was too strong, I had to try it! I set up Odin on my PC and hooked up my new project with the SDL2 port. This was it! The moment of truth! Would I be able to get a test app written in Odin using SDL2 working on my devkit?
…
Yes, of course, or else we wouldn’t be here. Did you not read the introduction post?
Still, it was quite the endeavor. I learned a lot about setting up compilers. After a weekend of desperate trial and error I actually leapt out of my chair and cheered once I got my devkit to display an empty red screen. I can’t talk too much about it unfortunately, or else the ninjas will be at my door, but let’s just say that Odin working seamlessly with any existing C code was a big deal.
Great success!
Indeed. I’ve been working on the engine for the past couple of weeks now and ODIN has been delightful to use and learn about, a huge improvement over C#. I feel like almost every day I’m picking up new and better ways of doing things. I didn’t really touch on Object-Oriented vs Data-Oriented programming in this post, but I might cover it later on; my opinions on the matter are seeing quite the rapid evolution thanks to this language.
My plan right now is to hit some basic milestones in terms of engine features, build a small prototype game using those features, and make sure everything still works on Switch. An Xbox port of SDL2 exists… I think… but that will have to wait until I get access to a devkit. As for Playstation, that console will probably be the hardest to support; it uses a platform-specific graphics API, which in simple terms means that a lot of work specific to that platform has to be put in to get a framework working on it. Likely because of this, SDL2 sadly doesn’t seem to have a Playstation port. So… Switch now, Xbox later, and Playstation, uh… maybe never? We’ll see.
Next post, we’ll finally get into actual engine design, so look forward to it! See you then!
Oh? Primitive tech youtubers? Did they build their own cameras too, HMM???
Yes, plain text! You could open up notepad right now, write in any language you want, and it would work just fine as a program.
Individual instructions really are quite simple. Most of them just boil down to moving a single number from one place to another. It’s amazing what you can do if you’re running billions of them per second though.
Game Maker Language is a bit of an interesting case. By default, it’s interpreted, but it can be compiled for a performance boost. As part of its compilation process, it actually gets converted into C++ as an intermediate step! However, the resulting program is still usually quite inefficient compared to one that was written directly in C++.
For reference, C++ was released to the public in 1985 and to this day still receives a steady stream of revisions and new features. C has been around even longer of course, since 1972, but while it’s still being maintained it, by design, hasn’t changed much since then.
Well, more like “SDL_RenderDrawRect(renderer, rect)
”, but you get the idea.
As of 2024 lol.
As in, whatever data happens to be literally, physically stored next to it in RAM.
public ref T RefExample<T>(){ return ref new T(); }
: Terrible, foolish, ERROR!!
public ref T RefExample<T>(){ return ref (new T[1])[0]; }
: Completely fine.
Clown language.
Said software, EmberGen, is written fully in Odin, and now apparently the industry standard for generating fire and smoke VFX. This is in large part thanks to the fact that, unlike it’s predecessors, it can render in a matter of minutes what used to take hours or days!!! Wow!
Honorable mention: Raylib is a similar framework that was originally envisioned as a friendlier competitor to SDL. Unfortunately, its only switch port is locked away tightly in the avaricious grasp of the publisher who commissioned it, and, based on our conversations, the framework’s creator does not seem to have the freedom to release it to licensed developers, despite his willingness to do so.