Game Programming on an ESP32: Motivation (Part 0)
I’ve been looking for a side project to work on outside of my day job to distract me from the state of the world. My primary interest is game programming but I also enjoy working with embedded systems. My current job is at a game company but most of my jobs before that were working with microcontrollers. While I eventually decided to change gears and join the game industry, I still enjoy playing with microcontrollers.
Why not both?
I have an Odroid Go hanging around that might be fun to play around with. It’s core is an ESP32 which is a very popular microcontroller that has common MCU functionality (SPI, I2C, GPIO, timers, etc) but also has WiFi and Bluetooth which makes it appealing to those building IoT applications.
The Odroid Go bundles the ESP32 with a bunch of peripherals to turn it into a portable game machine that is reminiscent of a Gameboy Color: LCD, speaker, D-pad, two main buttons, four auxiliary buttons, a battery, and an SD card reader.
The primary reason people buy the Odroid Go is to run emulators for old 8-bit systems, so if this thing can emulate old games then it should be able to run a native game created specifically for it.
The display is only 320x240 so we are very limited in how much we can have on screen at one time. We’ll have to think carefully about what sort of game to make and what sort of assets to use.
The display only supports 16 bits of color for every pixel: 5 bits for red, 6 bits for green, and 5 bits for blue. This scheme is commonly called RGB565 for obvious reasons. Green gets one more bit than red and blue because the human eye is better at picking up gradations of green than blue or red.
16 bits of color means we only have access to 65K colors. Compare that to the typical 24 bits of color (8 bits for each color) which provides 16 million colors.
Without a GPU we won’t be able to use an API like OpenGL. The common way to do a 2D game these days is to use the same GPU you’d use for a 3D game but to simply draw quads and then map texture bitmaps to them. Without a GPU we will instead need to rasterize every pixel directly with the CPU which is slower but simpler.
With a screen resolution of 320x240 and 16-bit color, the total framebuffer size is 153,600 bytes. Meaning at least thirty times per second we need to send 153,600 bytes to the display. It may end up being an issue and we’ll instead have to get clever about how we draw the screen. We may choose to do indexed color into a palette, for example, so for every pixel we store one byte which acts as an index into a palette of 256 colors.
The ESP32 has 520KB of built in RAM and the Odroid Go adds an extra 4MB of external RAM. But not all of that memory is available to us because it’s in use by the ESP32 SDK (more on that later). After disabling all extraneous features that I could and then entering my main function, the ESP32 reports 4,494,848 bytes free for our use. If we need more memory later we’ll revisit ways of trimming the fat.
The CPU is configurable at three speeds: 80MHz, 160MHz, and 240MHz. Even the max of 240MHz is a far cry from the 3GHz+ machines that we’re used to working with. We’re going to start at 80MHz and see how far that gets us. If we want to be able to run this game on battery then we need to keep the power consumption low. Lowering the clock is a good way to do that.
There are ways to use debuggers with embedded devices (JTAG) but unfortunately the Odroid Go does not expose the necessary pins so we can’t step through the code in a debugger like we would normally be able to do. That means that debugging will be difficult and we’ll need to make judicious use of on-screen debugging (by using colors and text) and printing information to the debug console (which thankfully is easily accessible over USB UART).
Why try making a game for this middling device with all of the aforementioned limitations instead of just creating one on your desktop PC? Two reasons:
Limitations breed creativity
When you’re working with a system with a specific set of hardware, each with its own limitations, it forces you to think about how best to take advantage of those limitations. It’s the closest we can come to how it must have felt like making a game for something like the Super Nintendo (but still much easier than that).
Low-level is fun
Writing a game from scratch on a normal desktop would require us to tackle typical low-level game engine concepts like rendering, physics, and collision detection. But doing those things on an embedded device means we also need to deal with low-level computer concepts like writing an LCD driver.
When it comes to going low-level and rolling your own code, you have to draw the line somewhere. If we were trying to make a game without libraries on a desktop, that line would probably be the operating system or a cross-platform API like SDL. For our purposes, I’m drawing the line at writing things like SPI drivers and bootloaders. That’s more pain than fun.
So we’re going to use the ESP-IDF which is basically an SDK for the ESP32. We can think of it as providing some of the utilities that an operating system would normally provide, but the ESP32 is not running an operating system. Technically it uses FreeRTOS which is a real-time operating system, but it’s not a true operating system. It’s just a scheduler. We likely won’t be interacting with it much but the ESP-IDF uses it at its core.
The ESP-IDF will give us an API for the ESP32’s peripherals like SPI, I2C, and UART, as well as the C Runtime Library so that when we call something like printf, it actually sends the bytes out over UART to be displayed in the serial monitor. It also handles all of the startup code that is required to get the machine ready before it calls the entry point into our game.
These posts will be a sort of development log where I cover things that I’ve done that I find interesting, or where I want to explain things that were especially painful to figure out. I have no plan and will likely make many mistakes. It’s just for fun.