We live in a great time to be developers. With such a huge amount of great AAA-grade Engines available to everyone, making simple games can be as easy as drag-and-drop. There seems to be no reason at all to write an engine anymore these days. And with the common sentiment, "Write Games, not Engines", why should you?
This Article is primarily aimed at solo-developers and small teams. I assume some familiarity with Object-Oriented Programming.
I want to give you some insight into approaching Engine development and will use a simple fictive Engine to illustrate this.
Why write an Engine?
The short answer is: Don't, if you can avoid it.
Life is to short to write an engine for each game (Taken from the Book 3D Graphics Programming by Sergei Savchenko)
The current selection of excellent Engines such as Unity, Unreal or CryEngine are as flexible as one could hope and can be used to make pretty much any game. For more specialised tasks, there are of course more specialised solutions like Adventure Game Studio or RPG Maker, just to name a few. Not even the cost of commercial grade Engines is an argument anymore.
There are only a few niche reasons left to write your own engine:
- You want to learn how an engine works
- You need certain functionalities that aren't available or the available solutions are unstable
- You believe you can do it better / faster
- You want to stay in control of development
All of these are perfectly valid reasons and if you are reading this, you probably belong to one of those camps. My goal is not to go into a lengthy "Which engine should I use?" or "Should I write an Engine?" debate here and will jump right into it. So, let us begin.
How to fail writing an Engine
Wait. First I tell you not write one, next I explain how to fail? Great Introduction...
Anyways, there are alot of things to concider before even writing a single line of code. The first and biggest problem everyone who starts writing a game engine has can be boiled down to this:
I want to see some gameplay as fast as possible!
However, the faster you realise that it will take alot of time until you actually see something interesting happen, the better off you will be writing your engine.
Forcing your code to show some form of graphics or gameplay as fast as you can, just to have some visual confirmation of "progress", is your biggest enemy at this point. Take. Your. Time!
Don't even think about starting with the graphics. You've probably read alot of OpenGL / DirectX tutorials and books and know how to render a simple triangle or sprite. You might think a short code snippet of Rendering a little mesh on screen is a good place to start. It is not.
Yes, your initial progress will be amazing. Heck, you could be running around a little level in First-Person in just a day copy-pasting code snippets from various tutorials and Stack Overflow. But I guarantee you, you will delete every single line of that code 2 days later. Even worse, you might even get discouraged from writing an Engine, as it isn't motivating to see less and less.
The second big problem developers face while writing Engines is feature creep. Everyone would love to write the holy grail of Engines. Everyone wants that perfect Engine that can do everything. First Person Shooters, Tactical RPGs, you name it. But the simple fact remains, we can't. Yet. Just look at the big names. Not even Unity can truly cater to every Game genre perfectly.
Don't even think about writing an Engine that can do more than one genre on your first go. Don't!
Where to actually begin when writing an Engine
Writing an Engine is like engineering a real Engine for a car. The steps are actually pretty obvious, assuming you know what Game (or Car) you are working on. Here they are:
- Pinpoint exactly what your engine needs to be capable of AND what your engine doesn't need to be capable of.
- Organize the needs into Systems your engine will require.
- Design your perfect Architecture that ties all these Systems together.
- Repeat Steps 1. - 3. as often as possible.
Iff (= if and only if) you spend enough time and effort on steps 1. - 4. and the Game Design doesn't suddenly change from a Horror Game to a Slot Machine (read: Silent Hill), coding will be a very pleasent endeavour. Coding will still be far from easy, but perfectly manageable, even by Solo-Developers.
This is the reason this Article is mainly about Steps 1. - 4. Think of Step 5. as "Filling in the Blanks. 50.000 LOC of Blanks".
The most crucial part of all of this is Step 3. We will focus most our efforts here!
Step 1. Pinpoint the Needs and Need Nots
All of these Steps may seem rather trivial at first. But they really aren't. You might think Step 1 of the process of developing a First Person Shooter Engine can be boiled down to this:
I need to load a Level, the Players gun, some Enemies with AI. Done, on to Step 2.
If it were only this easy. The best way to go about Step 1 is to go through the entire Game Click-by-Click, Action-by-Action from Clicking the Icon on your Desktop, to hitting the Exit Key after rolling the Credits. Make a List, a large List of what you need. Make a List of what you definitely don't need.
This will probably go down like this:
I start the game and it goes directly to the Main Menu. Will the Menu use a Static Image? A Cut Scene? How do I control the Main Menu, Mouse? Keyboard? What kind of GUI Elements do I need for the Main Menu? Buttons, Forms, Scrollbars? What about Music?
And those are just macro-conciderations. Go in as detailed as possible. Deciding that you need Buttons is fine and good, but also concider what a button can do.
I want the buttons to have 4 States, Up, Hover, Down, Disabled. Will I need Sound for the Buttons? What about Special Effects? Ar they animated in the Idle State?
If your List of Needs and Need Nots only contains about 10 Items by the end of the Main Menu, you did something wrong.
At this stage, what you are doing is simulating the Engine in your brain and writing down what needs to be done. Step 1 will become clearer each iteration, don't worry about missing anything the first time.
Step 2. Organize the needs into Systems
So, you have your lists of things you need and don't need. It is time to organize them. Obviously, GUI related things like Buttons will go into some sort of GUI System. Rendering related items go into the Graphics System / Engine.
Again, as with Step 1, deciding on what goes where will be more obvious on your second iteration, after Step 3. For the first pass, Group them logically as in the example above.
The best Reference on "what goes where" and "what does what" is without a doubt the Book Game Engine Architecture by Jason Gregory.
Start grouping the functionality. Start thinking of ways of combining them. You don't need
Camera->rotateYaw(float yaw) and
Camera->rotatePitch(float pitch) if you can combine them into
Camera->rotate(float yaw, float pitch). Keep it simple. Too much functionality (remember, feature creep) will hurt you later on.
Think about what functionality needs to be publicly exposed and what functionality only needs to reside inside the System itself. For example, your Renderer needs to sort all transparent Sprites before drawing. The function to sort these sprites however, does not need to be exposed. You know you need to sort transparent Sprites before drawing, you don't need some external System to tell you this.
Step 3. The Architecture (Or, the actual Article)
We might as well have started the Article here. This is the interesting and important part.
One of the simplest possible Architectures your engine can have is putting each System into a Class and having the Main Game Loop call their subroutines. It might look something like this:
isRunning = GameLogic->doLogic();
Seems perfectly reasonable at first. You have all your basics covered, Input -> processing Input -> Output.
And indeed, this will suffice for a simple Game. But it will be a pain to maintain. The reason for this should be obvious: Dependencies.
Each System must communicate with other Systems in some way. We don't have any means to do that in our Game Loop above. Therefore the example clearly indicates, that each System must have some Reference of the other Systems in order to do anything meaningful. Our GUI and Game Logic must know something about our Input. Our Renderer must know something about our Game Logic in order to display anything meaningful.
This will lead to this architectural marvel:
If it smells like Spaghetti, it is Spaghetti. Definitely not what we want. Yes, it is easy and quick to code. Yes, we will have acceptable results. But maintainable, it is not. Change a little piece of code somewhere and it might have devestating effects on all other systems without us knowing.
Further, there will always be code that many Systems need Access to. Both the GUI and the Renderer need to do Draw calls or at least have access to some sort of Interface to handle this for us. Yes, we could just give each System the power to call OpenGL / DirectX functions directly, but we will end up with alot of redundancies.
We could solve this by collecting all draw functions inside the Renderer System and call those from the GUI system. But then the Rendering System will have specific functions for the GUI. These have no place in the Renderer and is therefore contrary to Step 1 and 2. Decisions, Decisions.
Thus the first thing we should concider is dividing our Engine into Layers.
Lasagna is better than Spaghetti. At least programming wise. Sticking to our Renderer Example, what we want is to call OpenGL / DirectX functions without calling them directly in the System. This smells like a Wrapper. And for the most part it is. We collect all the draw functionality inside another Class. These classes are even more basic than our Systems. Let's call these new classes the Framework.
The idea behind this is to abstract away alot of the low level API calls and form them into something tailored to our game. We don't want to set the Vertex Buffer, set the Index Buffer, set the Textures, enable this, disable that just to do a simple draw call in our Renderer System. Let's put all that low level stuff in our Framework. And I'll just call this part of the Framework "Draw". Why? Well, all it does is set everything up for drawing and then draw it. It doesn't care what it draws, where it draws, why it draws. That is left to the Renderer System.
This might seem like a weird thing, we want speed in our engine right? More Abstraction Layers = Less Speed.
And you would be right, if it were the 90's. But we need the maintainability and can live with the barely noticable speed loss for most parts.
How then should our Draw Framework be designed? Simply put, like our own little API. SFML is a great example of this.
Important things to keep in mind:
- Keep it well documented. What functions do we have? When can they be called? How are the called?
- Keep it simple. Easy Functions like drawMesh(Mesh* oMesh) or loadShader(String sPath) will make you happy in the long run.
- Keep it functional. Don't be too specific. instead of
drawButtonSprite, have a
drawSpritefunction and let the Caller handel the rest.
What do we gain? Alot:
- We only need to setup our Framework once and can use it in every System we need (GUI, Renderer....)
- We can easily change the underlying API's if we chose, without rewriting every System. Switch from OpenGL to DirectX? No Problem, just rewrite the Framework Class.
- It keeps the Code in our Systems clean and tight.
- Having a well documented Interface means, one person can work on the Framework, while one person works in the System Layer.
We will probably end up with something like this:
My rule of thumb of what goes into the Framework is rather simple. If I need to call an external Library (OpenGL, OpenAL, SFML...) or have Data Structures / Algorithms every System needs, I should do that in the Framework.
We now have our first Layer of Lasagna done. But we still have this huge ball of Spaghetti above it. Let's tackle that next.
The Big Problem however remains. Our Systems are still all interconnected. We don't want that. There are a multitude of ways of dealing with this issue. Events, Messages, Abstract Classes with Function Pointers (How esoteric)...
Let's stick to Messages. This is a simple concept that is still very popular in GUI programming. It is also well suited as an easy example for our Engine.
It works like a Postal Service. Company A sends a message to company B and requests something be done. These companies need no physical connection. Company A simply assumes that company B will do it at some point. But, for now, company A doesn't really care when or how company B does it. It just needs doing. Heck, company B might even decide to reroute the message to company C and D and let them handle it.
We can go one step further, company A doesn't even need to send it to someone specific. Company A simply posts the letter and anyone who feels responsible will process it. This way company C and D can directly process the request.
Obviously, the companies equal our Systems. Let's take a look at a simple example:
- Framework notifies Input System that "A" was pressed
- Input translates that Keystroke "A" means "Open Inventory" and sends a Message containing "Open Inventory"
- GUI handles the Message and opens the Inventory Window
- Game Logic handles the Message and pauses the Game
Input doesn't even care what is being done to it's Message. GUI doesn't care that Game Logic also processes the same Message. Were they all coupled, Input would need to call a function in the GUI System and a function in Game Logic. But it doesn't need to anymore. We were able to successfully decouple this using Messages.
How does a Message look like? It should at least have some Type. For example, opening the inventory could be some enum called
OPEN_INVENTORY. This suffices for simple Messages like that. More advanced Messages that need to include data will need some way to store that data. There are a multitude of ways to accomplish this. The easiest to implement is using a simple map structure.
But how do we send Messages? Via a Message Bus of course!
Isn't it beautiful? No more Spaghetti, just good ol' plain Lasagna. I deliberately put our Game Logic on the other side of the Message Bus. As you can see, it has no Connection to the Framework layer. This is important to avoid any temptation to "just call that one function". Trust me, you will want to sooner or later, but it would break our design. We have enough Systems dealing with the Framework, no need to do that in our Game Logic.
The Message Bus is a simple Class with References to every System. If it has a Message in queue, the Message Bus posts it to every System via a simple
handleMessage(Msg msg) call. In return, every System has a reference to the Message Bus in order to post Messages. This can obviously be stored internally or passed in as a function argument.
All our Systems must therefore inherit or be of the following form:
void handleMessage(Msg *msg);
//// Usage: msgBus->postMessage(msg);
(Yes, Yes, raw Pointers...)
Suddenly, our Game Loop changes to simply letting the Message Bus send around Messages. We will still need to periodically update each System via some form of
update() call. But communication will be handled differently.
However, like with our Frameworks, using Messages creates overhead. This will slow down the engine a bit, let's not kid ourselves. But we don't care! We want a clean and simple Design. A clean and simple Architecture!
And the coolest part? We get amazing things for free!
Every Message is pretty much a function call. And every Message gets send pretty much everywhere! What if we have a System that simply prints every Message that is send into some Output window? What if this System can also send Messages we type into that window?
Yes, we have just given birth to a Console. And all it took us is a few lines of code. My mind was blown way back when I first saw this in action. It isn't even tied into anything, it just exists.
A console is obviously very helpful while developing the game and we can simply take it out in Release, if we don't want the Player to have that sort of access.
In-Game Cinematics, Replays & Debugging
What if we fake Messages? What if we create a new System that simply sends Messages at a certain Time? Imagine it sending something like
MOVE_CAMERA, followed by
And Voila, we have In-Game Cinematics.
What if we simply record the Input Messages that were sent during the Game and save them to a file?
And Voila, we have Replays.
What if we just record everything the Player does, and when the game crashes, have them send those data files to us?
And Voila, we have an exact copy of the players actions that lead to the crash.
Multi-Threading? Yes, Multi-Threading. We decoupled all our Systems. This means, they can process their Messages whenever they want, however they want and most importantly, wherever they want. We can have our Message Bus decide on what thread each System should process a Message -> Multi-Threading
Frame Rate Fixing
We have too many Messages to process this Frame? No Problem, let's just keep them in the Message Bus Queue and send them the next Frame. This will give us the opportunity to ensure that our Game runs at a smooth 60 FPS. Gamers won't notice the AI taking a few Frames longer to "think". They will however notice Frame Rate drops.
Messages are cool.
It is important that we meticulously document each Message and it's parameters. Treat it like an API. If you do this right, every developer can work on different Systems without breaking anything. Even if a System should be offline or under Construction, the Game will still run and can be tested. No Audio System? That's fine, we still have Visuals. No Renderer, that's fine, we can use the Console...
But Messages aren't perfect. Sadly.
Sometimes, we DO want to know the outcome of a Message. Sometimes we DO need them to be processed immediately. We need to find viable options. A solution to this is to have a Speedway. Apart from a simple
postMessage function, we can implement a
postImmediateMessage function that get's processed immediately. Handling return Messages is far easier. Those get send to our
handleMessage function sooner or later. We just need to remember this when posting a Message.
Immediate Messages obviously break Multi-Threading and Frame Rate Fixing if done in excess. It is thus vital to restrict yourself to limit their usage.
But the biggest Problem with this System is latency. It isn't the fastest Architecture. If you are working on a First Person Shooter with twitch like response times, this might be a deal breaker.
Back to Designing our Architecture
We have decided to use Systems and a Message Bus. We know exactly how we want to structure our Engine.
It is time for Step 4 of our Design process. Iteration. Some functions might not fit inside any System, we must find a solution. Some functions need to be called extensively and would clog up the Message Bus, we must find a solution.
This takes time. But it is worth it in the long run.
It is finally time to Code!
Step 4. Where to Start Coding?
Before you start coding, read the Book/Articles Game Programming Patterns by Robert Nystrom.
Other than that, I have sketched a little roadmap you could follow. It is by far not the best way, but it is productive.
- If you are going with a Message Bus type of Engine, concider coding the Console and the Message Bus first. Once those are implemented, you can fake the existence of any System that has not yet been coded. You will have constant control over the entire engine at every stage of developement.
- Concider moving on to the GUI next, as well as the needed Draw functionality inside the Framework. A solid GUI paired with the Console will allow you to fake all other Systems even easier. Testing will be a breeze.
- Next should be the Framework, at least it's interface. Functionality can follow later.
- Finally, move on to the other Systems, including Gameplay.
You will notice, actually rendering anything related to Gameplay might be the last thing you do. And that is a good thing! It will feel so much more rewarding and keep you motivated to finish the final touches of your Engine.
Your Game Designer might shoot you during this process however. Testing Gameplay through Console commands is about as fun as playing Counter Strike via IRC.
Take your time to find a solid Architecture and stick with it! That is the advice I hope you take away from this Article. If you do this, you will be able to construct a perfectly fine and maintainable Engine at the end of the day. Or century.
Personally, I enjoy writing Engines more than doing all that Gameplay stuff. If you have any questions, feel free to contact me via Twitter @Spellwrath. I'm currently finishing up another Engine using the methods I have described in this Article.
You can find Part 2 here.