View All Posts
Want to keep up to date with the latest posts and videos? Subscribe to the newsletter
HELP SUPPORT MY WORK: If you're feeling flush then please stop by Patreon Or you can make a one off donation via ko-fi

Learn how to create a classic Arduino ESP32 Asteroids game with laser projection, including a tour of the system hardware and a look at its firmware and game engine. Watch a demonstration and comparison of fonts, as well as an overview of the audio output. Dive into the schematics and try out the game for yourself!

Related Content

[0:05] Hey everyone!
[0:06] It’s retro games time.
[0:08] I’ve created a version of the classic arcade game asteroids on the ESP32 using my laser projector.
[0:16] So, Asteroids was originally released in 1979

[0:19] and used vector graphics instead of raster graphics for its display.
[0:23] This matches quite nicely what we can do with a laser projector.
[0:28] Let’s have a quick tour of the system.
[0:30] I’ll run through the hardware first

[0:32] and then we’ll go through the firmware that’s running on the ESP32.
[0:37] I’ve got a very basic game pad that I have 3D printed.
[0:41] We have some buttons i am currently using only using two of these buttons.
[0:46] One for firing and one for thrusting.
[0:48] And we have a rotary encoder.
[0:51] I’m using this to control the direction of the ship.
[0:54] This provides a very crude direction control as the encoder I’m using

[0:58] only has 20 pulses per revolution. Each step maps to about 18 degrees.
[1:04] I’ll do a follow-up video after this one where I look at both this mechanical rotary encoder

[1:10] and a more advanced magnetic-based one that will give me a much higher resolution.
[1:15] The wiring of the controls is pretty simple.
[1:18] We use the built-in pull-up resistors of the GPIO pins for the buttons.
[1:23] So these just need a common ground and a wire for each button.
[1:28] Due to the way the circuit board is configured on the rotary encoder

[1:32] with some built-in pull-down resistors to ground
[1:34] we need both 3.3 volts and ground wired up to it along with two signal wires

[1:40] going back to the esp32.
[1:44] Also outside the game controller box, we have the laser diode and mirror assembly.
[1:50] Ideally, I would have this inside the case but I ran out of space and my 3d

[1:55] printer isn’t quite big enough for a larger box.
[1:59] I’ve talked about this in the previous video where we created a laser show.
[2:04] The two mirrors direct the laser beam in the X and Y direction and are controlled by galvo motors.
[2:12] I have slowed this down so you can see the mirrors moving along with the projected beam.
[2:17] You should be able to see that the bottom mirror controls the X direction

[2:22] and the top mirror controls the Y direction.
[2:26] To control the laser diode we have a simple MOSFET circuit

[2:30] that lets the ESP32 switch the diode on and off.
[2:36] Let’s have a peek inside my games console.
[2:41] In the top of the case, we have the sound board and speaker.
[2:45] I am currently using a MAX98357 breakout board for this.
[2:50] This takes an I2S signal and amplifies it with enough power to drive a 4 or 8-ohm speaker.
[2:58] If I do a new version of the laser show PCB I’ll integrate the amplifier into the board

[3:04] to simplify construction and cut down on the number of hook-up wires that I’ve currently got.
[3:11] In the bottom of the gain console case, we have the power supply.
[3:16] This is the supply that came with the laser galvo kit

[3:19] and provides us with +15 volts ground and -15 volts.
[3:24] The power supply feeds the two galvo driver boards which are used to drive the mirror motors.
[3:31] I’ve daisy chained power off these boards.
[3:34] One wire goes to the audio board and the other wire goes to the brains of

[3:38] the system - the ESP32 module on a custom pcb.
[3:45] On this PCB we have an SPI digital to analog converter.
[3:50] The signal from this is fed into some op-amps that generate the differential signals

[3:55] that the galvo drivers require.
[3:58] We need two of these differential signals.
[4:00] One for the X-direction and one for the Y-direction.
[4:05] Having a look at one of these signals on the oscilloscope

[4:08] you can see both the positive trace and the mirrored negative trace.
[4:14] Potentially you could use the ESP32’s built-in digital to analog converters

[4:20] instead of the external SPI one.
[4:23] These would still work for this project but would give you a slightly lower resolution

[4:28] which may make some of the animations less smooth particularly the slowly

[4:32] moving asteroids which may appear to jump slightly with each frame.
[4:38] We also have the MOSFET for turning the laser diode on and off.
[4:42] Looking at this on the oscilloscope you can see that we are switching the laser

[4:45] on and off as we move positions of the mirrors.
[4:50] Back on the PCB I’ve broken out the remaining GPIO pins.
[4:54] Some of these are used to send I2S signals to the audio board

[4:58] and some of these are used for our game controller.
[5:02] So, that’s our hardware pretty much covered.
[5:05] Let’s jump into the firmware and see how that works.
[5:12] The main heartbeat of our game is the game loop.
[5:16] This game loop is triggered by a timer that runs 60 times a second.
[5:21] Every time the code is triggered it steps the game engine forward by 1/60th of a second.
[5:26] It then renders the new game state into a render buffer.
[5:30] I’ll go through rendering later in the video.
[5:33] First I want to run through the game engine.
[5:36] To do the heavy lifting of the game engine I’m using a library called Box2D.
[5:42] This is a 2D physics engine that is lightweight enough to run on the ESP32.
[5:47] It handles all the movement and collision detection logic that is required for the game.
[5:53] To initialize our physics world we just need to give it a vector defining gravity.
[5:58] Now, since our game is in space gravity is zero so this is pretty simple.
[6:04] Once we have our game world we can add objects into the world.
[6:08] We give them various physical attributes such as their shape, density, friction and restitution.
[6:16] Every time we step the game forward we ask Box2D

[6:19] to run a small physics simulation over the time step.
[6:23] The physics simulation updates the position rotation and speed of all the objects in the game

[6:28] and notifies us when objects have collided.
[6:32] This notification lets us detect bullets

[6:34] hitting asteroids and asteroids being hit by the user’s ship.
[6:39] The notification is fired during the simulation step so we can’t modify anything in the world

[6:44] without causing errors and problems.
[6:47] So, we keep track of what has happened for processing later.
[6:50] We keep a record of bullets and asteroids that have collided

[6:53] and we also set a flag if the player ship has collided with an asteroid.
[6:59] Once the world has finished its simulation step we have a few housekeeping jobs to do.
[7:05] We need to wrap any objects that have moved out of our game area

[7:08] so they appear on the other side of the world.
[7:11] And we need to process any of the collisions that were detected during the physics simulation.
[7:17] First, we process the user’s bullets.
[7:20] We keep track of how old each bullet is so we can remove old bullets that haven’t hit anything

[7:26] and remove any bullets that hit an asteroid.
[7:30] For the asteroids that were hit by bullets, we have several things to do.
[7:35] Depending on the generation of the asteroid we play a different explosion.
[7:40] If it’s a first-generation asteroid then it’s a big asteroid so we have a big explosion sound.
[7:47] When an asteroid in the first or second generation is destroyed we add two child asteroids.
[7:53] We send these off in opposite directions perpendicular to the original asteroid’s direction

[7:59] and we also apply a scale factor to make the child asteroids smaller than their parents.
[8:06] The destroyed asteroids are then removed from the game and we clean up our temporary state.
[8:11] Once we’ve done this housekeeping job we update a very simple state machine

[8:16] that tracks what stage of the game we are in.
[8:19] We can be in one of three states:
[8:21] We are either in the start game state. Here we’re waiting for the player to hit fire.
[8:26] Or we’re actually playing the game.
[8:28] Or we’re in the game over state.
[8:31] I’ve kept my state machine fairly simple for this implementation.
[8:35] In our start state, we just poll the fire button waiting for it to be pressed.
[8:40] As soon as the user hits the fire button we move into the game playing state.
[8:44] The same is true for our game over state.
[8:47] The only real difference between these two states is the text that is shown to the player.
[8:52] The playing game state is a lot more complicated

[8:56] if the player has recently been killed then we respawn the player when fire is pressed.
[9:02] We have a small cooldown timer to prevent the user from respawning

[9:05] immediately if they are hammering the fire button.
[9:09] We then have a check to see if the player’s ship was hit by an asteroid.
[9:13] If the ship was hit we play a sound effect and we decrease the number of lives the player has.
[9:18] If the player has no more lives left then we switch to the game over state.
[9:22] Otherwise, we allow them to respawn.
[9:26] Finally, we handle any input from the controls.
[9:30] We update the ship’s direction and we apply any thrust required to the ship.
[9:35] We play the thrust sound and we ask the physics engine to apply

[9:39] an impulse to the physics object in the direction the ship is pointing.
[9:43] We also set a flag on the ship so that it knows it is thrusting.
[9:48] This flag triggers the ship to change its shape slightly

[9:51] so it includes some flames coming out of the back of the ship.
[9:55] For the fire button we have another cooldown timer on this.
[9:59] This prevents the user firing too many bullets at once

[10:03] finally we have a check to see if all the asteroids have been destroyed.
[10:07] If there are no asteroids left we tell the game to increase the difficulty,

[10:11] reset the player’s ship to the center, and add new asteroids.
[10:15] There’s a few more details in the game engine code.
[10:18] Hopefully, it should be understandable and it’s all on Github so feel free to

[10:22] take a look and experiment.
[10:25] Let’s move on to the rendering side of things.
[10:28] As with the laser show code, we’re using SPI to send samples out.
[10:34] This lets us send samples out quickly enough

[10:36] to take advantage of persistence of vision and to synchronize with the laser.
[10:42] We set up the SPI device and then we kick off a timer to push the samples out.
[10:48] I’m using a task that is pinned to core 0 to run the timer

[10:51] and I’ve pinned the game engine to core 1 so we should get the maximum use of the CPUs.
[10:57] Each time the timer triggers we step through the drawing instructions in the current frame.
[11:02] These instructions tell us what values to send for the X and Y channels of the DAC.
[11:07] They also tell us if the laser should be on or off.
[11:10] There’s an additional part of the instruction that tells us how long to hold at each position.
[11:16] This allows for inertia in the laser galvo and lets the laser settle on the required position.
[11:23] Once we’ve rendered a complete buffer we ask the render buffer to swap to a new render buffer.
[11:29] The render buffer checks to see if a new buffer is ready.
[11:33] If it is then it swaps to the new one and sets a flag

[11:36] indicating the old buffer needs to be filled with data.
[11:40] This code decouples the rendering from the game engine completely.
[11:43] We can be continuously sending out samples and refreshing the displayed image

[11:47] while the game is stepping forward and doing its other processing.
[11:53] I’ve included a couple of additional renderers.
[11:56] One for using the internal DAC and another one that renders to the HelTec OLED display.
[12:03] If you have a device with an OLED display or some other display you should be able to

[12:07] use this renderer on your device and play the game even if you don’t have a laser projector.
[12:15] Back in the game loop, we call render_if_needed on the render buffer.
[12:19] This checks to see if a redraw has been requested and if it has then we

[12:23] run through the objects in the game drawing each one in turn into the render buffer.
[12:28] To try and optimize the drawing process we try and minimize the number of blank moves

[12:33] by drawing the objects nearest the current drawing position.
[12:38] In this clip of the video, I’ve turned on the laser all the time

[12:42] so we can see the laser moving between each object.
[12:46] To draw each object we need the position and rotation of the object in the physical world.
[12:51] We get these from the physics engine

[12:53] and then transform each point so it appears in the correct location at the correct position.
[13:00] Each point of the game object is then added as a drawing instruction to the render buffer.
[13:05] Its instruction is scaled to suit the renderer and the hold time is calculated

[13:09] this calculation is a bit of a finger in the air formula that I’ve worked out by a trial and error
[13:16] With all the game objects drawn we just need to draw the score and any in-game text.
[13:22] For this, we need a font.
[13:24] Initially, i looked at using Hershey fonts.
[13:26] These are a single stroke font that is used by a lot of people for laser engraving and CNC work.
[13:33] Now I’ve decoded some quite strange file formats in my time and i think

[13:38] this is probably the weirdest I’ve come across.
[13:41] Each character in the font file consists of a number - which is not used - and a number of

[13:46] vertices, with the first vertice specifying the left and right positions of the font.
[13:52] The following vertices encode the drawing positions with a specialist escape sequence

[13:56] of space followed by ‘R’ indicating the pen should be lifted for the next move.
[14:02] Each coordinate is encoded as an offset from the letter ‘R’.
[14:07] This animation shows a simple example of rendering a Hershey font character.
[14:14] The output from the Hershey font is very nice, but unfortunately, the number of

[14:19] vertices means that our refresh rate drops to the point that flicker is very noticeable.
[14:25] So to fix this I’ve designed my own very minimal font that uses as few vertices as possible.
[14:31] It’s a bit square and blocky

[14:33] but it doesn’t have many vertices so it does not impact the refresh rate too much.
[14:38] Here’s my simple font definition.
[14:41] I’ve only defined the characters that I need for my game.
[14:44] You can see here zero is simply a box,

[14:46] a one is a straight line, and so on for the other characters I need.
[14:52] Drawing text is simply a case of stepping through each character in the text

[14:56] and rendering the line segments that make up each character.
[15:00] I’ve taken a capture of the Hershey font so you can see how it compares to my very simple font.
[15:09] So, here’s the hershey font.
[15:11] And here’s the version running with my much simplified font.
[15:15] So, it looks actually pretty good and I think it’s in keeping with the game style and play.
[15:22] The final part of the code is the audio output.
[15:25] Now, we’ve looked at loading WAV files from SPIFFS before.
[15:29] And we have a very simple sound effects class that loads up all the required sound effects.
[15:35] We feed these into an I2S output device that has multiple voices for output.
[15:41] When we send out samples on this device we mix all the current WAV files together and then use a tanh

[15:47] function to provide some soft clipping before scaling the value to the full range.
[15:52] This lets us play multiple sounds simultaneously at quite a loud volume.
[15:57] It will distort the audio quite a bit but we’re dealing with game sound effects

[16:01] so we’re not aiming for high fidelity.
[16:04] So, that’s a whistle-stop tour of the firmware for the game.
[16:10] It’s quite a basic version of Asteroids.
[16:12] I have not included any of the flying saucers or any of the rules around gaining extra lives.
[16:20] But it’s a pretty fun game to play and I think you’d enjoy it if you try it out.
[16:26] All the codes on GitHub as always.
[16:28] Feel free to clone the repo and try it out for yourself.
[16:32] The schematics are also available you can either build it on a PCB or on breadboard

[16:38] and you can try out using the internal DAC instead of an external SPI DAC.
[16:44] So, that’s it for this video.
[16:46] Thanks for watching.
[16:47] I hope you found it enjoyable and interesting.
[16:50] It was certainly fun building the games console

[16:53] and I’ll be doing some more projects in the future which should be equally interesting.
[16:58] So please hit the subscribe button and I’ll see you in the next video.

HELP SUPPORT MY WORK: If you're feeling flush then please stop by Patreon Or you can make a one off donation via ko-fi
Want to keep up to date with the latest posts and videos? Subscribe to the newsletter
Blog Logo

Chris Greening

> Image


A collection of slightly mad projects, instructive/educational videos, and generally interesting stuff. Building projects around the Arduino and ESP32 platforms - we'll be exploring AI, Computer Vision, Audio, 3D Printing - it may get a bit eclectic...

View All Posts