Building a Game Engine with Snake — A Retrospective
The Idea
I have always been interested in learning how games work under the hood. If you have only played with popular game engines such as Unreal Engine, Unity, or Godot, you may not fully understand how these programs work under the hood. This is of course by design, you want to be able to jump in and make games after all. I decided that I wanted to know what makes these tools actually work under the hood. How do I make a game without a game engine? How can I make one from "scratch"? These questions peaked my curiosity and inspired me to see if I can find a way to make a simple game of snake without a game engine. What started off being a simple month long experiment expanded into something much larger. I want to share my experience from this project and share what I have learned.
The Beginning
To help me get going, I discovered a couple different tools: Raylib and SDL2. These tools are essentially low level frameworks that handle Windowing, Rendering and Input, among other things. When decided where to start, I decided that Raylib had more game development related tools out of the box, so I started with this one. My objective was simple. Build a game of snake.
While Raylib includes a lot of definitions and helpers for useful things like vectors, colors and key mappings, it doesn't have a game loop set up out of the box. This is where I had to begin. This is where I learned that there is a fairly standard order of events that happen in a typical game loop. At the beginning of the loop we completely clear the screen of any pixels so that we are starting from a blank canvas. Next we capture any player input, this then gets passed to what is essentially the update function, this updates the position of game entities such as the position of the player based on the input of the player. Then, we draw pixels to the screen with the updated positions of the game entities. This is what makes up a very basic game loop. Later as I expanded on the project, I added a couple more steps to this, but I will get to this later.
After the game loop was loosely defined, the games state machine started to take shape. After all, I needed a main menu screen, a gameplay state and a game over screen. When a certain action was performed, the games state needed to be switched to a new state and the state needed to be reset as if it was brand new. The snake needed to return to it's starting size, points needed to be reset to zero and the apple needed to have a new location. I started implementing the functionality of the game within this structure all in a single main.cpp file. The entire game was 357 lines long.
Once the game was done, I was pretty happy with what I had. It wasn't the cleanest code, and it felt really brittle when adding features to it, but I recognised some patterns within the code and I was interested in seeing if I could refactor it into reusable chunks. That way, if I wanted to make a new game, I could reuse some of the code. This is when I started transitioning from building a game, into building a game engine.
The Game Engine
This process was extensive, and the problems I continued to face became more and more complicated. I wanted an easy way of printing out logs, also with different colors so that they are easier to read and identify their Importance. This had me building my own logger which eventually became a logging library. Eventually, logging became difficult, especially when I was debugging in the update function every frame. The log I wanted was buried in the terminal and I couldn't see what I was debugging. At first, I thought that maybe adding the ability to log to a file would be a good idea because then I could just search the file afterwards and see the log, and if I add a couple different file formats, it could be easier to manage. While this worked, I quickly found that my file had over 20,000 entries within just a couple minutes. This wasn't ideal. This lead to me making a Debug HUD with a tree like structure that prints out updated values to the screen and would spit out the latest values to a file on exit, in case of a crash. This lead to me adding a DrawDebug step to my game loop. I'm going to do a post in the future around the Logging and Debug sections of the engine as I have a lot to talk about, but this is just a couple examples of tools I had to build out from scratch to solve real problems I was having during development.
Additionally, I built a Dependency Injector so I could inject specific implementations into the game from the entry point so that it is modular. The idea for this came from the use of Raylib, which I abstracted into a facade so that I could use my own definitions of simple structures like vectors, colors and keybindings and convert them into the raylib definitions in the concrete class while still using the underlying low-level Raylib functionality. This means the game itself doesn't actually know anything about Raylib. This is setting myself up so that I can replace that functionality with my own implementation once I feel ready to dive a layer deeper and understand how it's working under the hood.
Slowly, the project started to take a very different structure and expanded in size dramatically. Different modules started to take shape, a coding standard formed, and features like a simple Entity Component System, State Machines and Game Entities started to be defined. The refactor took some time before the game was actually up and working properly again, but at the end of it, I didn't just have a snake game. I had my own custom, modular game engine with a system architecture that made sense to me.
The Takeaways
Excluding all of the "Engine" files, the game now resides in just 23 C++ files, including header files. No additional game functionality was added. It's simply boiler plate from creating classes and function definitions. This highlights a very interesting trade off when programming and an important lesson I learned during the development process of this project: It can be significantly faster to write programs without abstractions or structure but you sacrifice readability, reusability and maintainability. If I were to set out to just create a game, the initial completed state in a single main.cpp file would have been totally fine, provided the game was simple enough that it wasn't too difficult to extend. However, with added complexity, having clean, well architected code will speed you up in the long run. It is definitely possible to over engineer when refactoring though, as I have done with the final version of the snake game with the engine, so check in often to make sure you are only building out what you need. Did I need a full game engine to make a game of snake? No, but my intentions were beyond the game of snake, I was thinking about the game I was planning to make after the game of snake and the things I wanted to learn while refactoring. This brings me to another learning from this project: building a project quickly and getting it working first before worrying about abstractions and making it neat it a far better way of working than doing it "right" the first time. There are 2 main benefits to finishing the functionality first before refactoring:
1. You have working code and you aren't spending too much time perfecting something that doesn't even work yet
2. You have a clearer picture of what abstractions you need after the functionality is working than you do while you are building it. This can allow you to assess whether you are over engineering the project or adding meaningful abstractions.
There were many times while working on this project where I developed something I didn't end up needing because I wasn't really sure what I was building. It wasn't until I returned to the game code to find that creating clean abstractions was easy and made sense. Eventually, I ended up with something I'm pretty happy with, and I have a few ideas of games I can build next to add features to the engine, and I'm excited to dive deeper into rendering and some of the other lower-level functionality that I got Raylib to assist me with.
Conclusion
Overall, I think the experience was invaluable, and I encourage you to occasionally leap to a lower level of programming to get a better understanding of how things work. There are so many aspects of programming that come out of the box these days with existing solutions, and programming often feels like you are building with Legos and stitching existing solutions together rather than solving low-level problems and understanding how things work under the hood.
Have you ever tried building a game engine? What was your experience? I'd love to hear more. Feel free to leave a comment below or share your story with me by sending me an email at mail@fearlessgamedev.com