Update: I made a GitHub repo!
As I was bouncing around the mindset that made me catalogue all my projects past and present, I was watching streams from Casey Muratori from Handmade Hero. In particular, one on his views about Impostor Syndrome. I always find Casey’s opinions fairly challenging but he suggested a good experiment to combat Impostor Syndrome in game development: implement Asteroids from scratch.
I don’t feel like I have Impostor Syndrome in game development, but I think the idea was useful to my own dilemma: Why can’t I make/finish a game? I had some guesses, but it would be good to expose them experimentally. Implementing Asteroids in Python was a good way to try that experiment.
Asteroids is pretty simple game. You have a ship with some simple controls, some simple physics, simple interactions, and the graphics and sound can be very simple. It’s a known quantity. There’s some meat to the project, but it’s not a rabbit hole like, say, reimplementing Angband might be, or a doddle like implementing “Guess the number”.
Of course Asteroids can appear so simple that you can fool yourself into thinking you can nail it, no problems. I thought at first it was possibly too easy a project. But, like Casey said, you need to actually do the experiment to get the results.
I also made a few rules for myself:
- Just implement for now, don’t implement for the future.
- No bells and whistles.
- Establish each tiny bit of the game as a goal.
- Don’t care about faithfulness to the original.
I’m prone to brainstorming, so I allowed myself a
design.txt file where I could store my ideas, but focus on Getting Asteroids Programmed.
Despite my keenness to get back into C++ programming after a long hiatus, I decided to stick to Python. I didn’t want to mix this experiment with relearning parts of C++. I’m very fluent with Python, and have experience in pygame from previous projects.
I launched Emacs, made a directory in my projects directory and started a git repository with
asteroids.py. No fancy directory structure, no partitioning into engine and components. Just a big ol’
design.txt I listed the things I definitely needed to declare Job Done:
What's needed: - Ship - Momentum - Lives - Shooting - Asteroids
In a nutshell Asteroids is about a ship shooting asteroids and trying not to die by colliding with asteroids. Hence my list. There’s an implicit “and make physics work in a toroidal geometry” goal there, but I didn’t want to get too prescriptive.
I identified extra “stretch” goals:
- Main menu - Score - Enemy Spaceship - Sound - Speed limiting - Single entity multiply visible
Obviously menus are nice to have, as is recording a score. I had vague memories of a rogue enemy spaceship, so that was a thing to maybe think about. Sound is also important, but a potential content trap to fall into.
Speed limiting came about thinking about the physics. Your ship has a thruster and you maintain momentum. If you keep thrusting, do you keep accelerating as a reckless disciple of Newton? I thought it might be worthwhile to speed limit the ship so you don’t get weird artifacts like phasing through asteroids at high speeds, or weird arithmetic edge cases.
“Single entity multiply visible” was that I remembered on some implementations, if an asteroid was, say, poking over the top edge, you could see the same asteroid at the top and bottom. It’s a little bit of polish, but not too crazy if I go after it.
I’m currently at the stage where I have completed all the main goals and the “sound” stretch goal. You can scoot around and shoot asteroids. They explode into smaller ones. Hurray!
I implemented sound effects with BFXR. Lasers go pew pew. Asteroids explode with chiptune noise. There’s a different explosion for the ship. There’s a rumble for the thrusters.
Currently if you crash too many times you are unceremoniously dumped from the game. There’s no UI, so meh.
Pygame was pretty great to work with. About my only concern for a bigger game would be about managing sound channels. But graphics, sound and collisions all worked pretty easily.
The git log looks like this:
d4ec748 Initial code 4fb50c2 Major refactor b901a64 Change Asteroid to Debris 1c1aaca Add audio effects 8cd21a5 Trivial change 95e957a Add effects 5a95355 Spawn bullets and event system 7526ec3 Register user events normally 913e240 Add fade to bullets ad2848f Add cache etc to gitignore b7b689d Fix missing debris line 9ca6372 Make debris beautiful ece2703 Add code for collisions and explosions 5f6297c Add mask collision 2a2f12f Add nicer ship image 9c96e20 Add ship collision/dying de5699e Add ship explode sound dd9c5c0 master Add lives and (basic) game over
The second commit isn’t me going back on my rules for the experiment. The single file of code was getting unwieldy, so I moved classes into their own files and made some very, very light object-orientation.
Early on I had to rename the Asteroid class to Debris because it collided with the game name. If I do some more polish, I might have to change it again because I want debris from exploded ships to scatter when you die.
It was pretty neat to have the asteroids tumbling around the screen for the first time. The wraparound screen had hardly any issues. By god I was relieved when I moved away from locating things by their pixels to giving them a natural coordinate system (axes were -1 to 1, and (0,0) was the dead centre of the screen). Physics was easy to implement once I was in my natural coordinate system.
Each asteroid was procedurally generated. This sounds fancy, but really each asteroid was a regular 16-gon, with a few edges randomly pulled closer to the middle to look chunky.
Initially the debris was all line art, like the ship. I had some issues with collisions, so I filled them in with a brown rock colour. It’s not like the original, but it fixed a problem quickly.
I wrote some code so I could spawn asteroids from a keypress. This is the same code that spawns the initial configuration of asteroids, and the scatter of smaller asteroids when you destroy one. This code nicely tries to avoid starting a ship inside an asteroid, which is very useful, but now my mini-asteroid-scattering looks dumb. But it works!
A neat-but-not-complicated inclusion was pixel-perfect collisions. I could create the mask from the graphics and it was easy to handle all the collisions. It did mean that a bullet could juuuust miss an asteroid, which was an amusing irritation when trying to test explosions.
The ship was pretty easy to make. Getting the feel of the momentum right took some fiddling. I did try implementing drag but I couldn’t get it to feel right quickly enough, so I abandoned that.
Launching bullets was pretty easy - just spawn a bullet at the right radius away from the ship, with the right velocity. As an indulgence, my bullets start out white-hot and cool to red as they travel. I did have to remember to implement bullet life time, otherwise they just keep going and going…
The ship, like debris, has an image it shows on screen. Originally I had a base image that per-frame I’d rotate into the right angle. This would muddy the edges a little, but it worked. I “fixed” that by overdrawing my anti-aliased lines.
Later I fixed that problem in a way they totally would not do in the original. At load time I created 360 images, one for each angle of rotation. For each of these I drew the lines fresh, so I had nice anti-aliased lines and no muddiness. 360 times 32x32 pixels with alpha channel… About 12 Mb of pre-drawn images. Times two if you had the thrusters on. The upside was that at frame update time, I just pointed to the correct image and I was done.
Because each asteroid was procedurally generated, they each had 12 Mb of pre-drawn images for the large asteroids. Honestly, this is way overkill. I could potentially draw each frame with raster calls, but I didn’t want to.
I could maybe even reduce the number of angles of rotation - you probably won’t notice the difference between 37° and 38°, so I could easily halve the memory requirements at the risk of making the code more complex. You could probably get away with 32+31+32+31=126 different images, where that magic number is just which part of the 32x32 perimeter is closest to the peak of the ship. But you’d only do that if it mattered and currently my 60 FPS says “don’t worry”.
Furthermore, I have 32Gb of memory. I can fill the screen with asteroids and not notice the impact on my system memory graph. Who cares about memory management? (Somewhere far away on a craggy peak, Jon Blow’s head snaps to the south-west… He senses trouble.)
Super stretch goals
When I wasn’t programming for this, I daydreamed a little. I thought about putting the final result on itch.io, but maybe Atari’s lawyers would stomp on me. The $3 total I might make isn’t worth that.
I also came up with some neat game modes where I took an element of the game and messed with it. If I care enough, I’ll implement the ones I can. For example:
- Physicsteroids where asteroids bounce off each other rather than passing through cleanly.
- Wasteroids where you have infinite lives, no guns, but you have to impact an asteroid with sufficient force to fracture it.
- Gravity Asteroids where every entity exhibits gravitational force, and your missiles are special gravitron missiles that warp space-time.
- Voxeloids You have a beam weapon and carve off voxel chunks from the asteroids.
Some of these are trivial additions. Others like the voxel one… much more trouble.
What did we learn?
I think the experiment was a success. I could make Asteroids, or at least a rough approximation of it. I am not necessarily doomed to lugging around husks of projects forever.
Working iteratively without too much investment in future architecture is a way I can and should work. If I could make an improvement quickly, I could do it.
Adding just trivial amounts of sound helps a game enormously. It definitely helps you as the developer invest in what you have. Same goes for little graphical tweaks.
I had to be careful not to accidentally fall into traps of anti-productivity. Using pygame 1.9.6 or 2.0 was a dilemma until I just chose one and ran with it. Similarly attempting to source the original sound effects would have been a wasted effort.
I might push on a little more to get some UI going in the game, but otherwise will finish the project soon. I could add more and more to it, but why? One important part of this project was to reach the end and declare a finish. And move onto a real project.