Douglas ’ Blog

What I learned from building Asteroids for the third time

~ Mar 18, 2024 ~


Making games was my only motivation to enter the programming career. After getting sidetracked with web development, I got bored of the web and decided to dive in on game development.

Around two years ago, I watched the CS50 for game dev. The course used Lua and Love2D, and then, after watching, I tried to make an ‘Asteroids’ clone. I was able to create a very crude version of ‘Asteroids’.

In general, it was easy to work with Lua and Love2D. But there were some drawbacks. Love2D did not have a good abstraction for input. Fortunately, the course provided a better abstraction. Also, the Love2D API is weird compared to Raylib. For example, you need to set the color before drawing a shape instead of passing the color as an argument for the draw function. Also, the color is three separate arguments with a range between 0 and 1. The lack of types makes refactoring unfeasible without an enormous amount of frustration.

You can see the gameplay below.

Would I recommend Lua and Love2D for creating games? No. Raylib is a way better option.

I also tried to convert to an ECS architecture, but it was difficult and not worth the complexity. I understood that ECS is not a silver bullet.


A while after, I decided to learn Godot. Then, I recreated the game but with some improvements. I added fire particles for the spaceship, particles for the asteroid explosion, and a screen shake when you destroy an asteroid.

Since Godot is an engine, you can drag and drop to lay out the images and texts. It’s also easy to create particles. There are lots of tutorials. One of them was about screen shake. I just copied the code and didn’t bother to understand the code.

Would I recommend making games in Godot? Yes. The tradeoff should be obvious: faster development but a lack of control. Also, you need to learn the engine, not the fundamentals. Would I use it for a future game? No, because I want to create my abstractions, but Godot forces me to use the Node system abstraction.

You can play the web version here.

You can see the gameplay below. Also, notice the bug at the beginning.


Finally, I made one with Raylib. I decided to make a ‘shippable’ game. Something that you could say is a polished game.

There were a few things that I learned from previous iterations. But mostly, I learned from this last.

Sine and Cosine

Knowing how to use sine and cosine for rotation and direction is essential for making the spaceship turn and go forward.

Calculus Integration

Knowing integration was essential for making the spaceship move like it was in space. It also helped me understand how acceleration, velocity, and position work together. In Godot, I used the built-in physics, but making my own wasn’t difficult. As a bonus, I have more fine-grained tuning over the spaceship movement.

Timers should always be a float type and decrease by delta time.

That way, you can express the timer in seconds, which is easier to calculate and is frame rate independent. Also, delta time is crucial for ensuring that various systems, such as animation, timers, and physics, remain independent of frame rate.

Events are everywhere

In this simple game, there are a lot of events and events that trigger events. For example, when energy reaches zero, it triggers the ‘hit stop’ state. Then, when the timer ends, it triggers the ‘game over’ state. It gives time to the player to see the collision with the meteor. Masahiro Sakurai has a video on this.

Save and load game data is super easy.

Sometimes, when working with other languages, I forget that text files are not the only way to store data in a file. To save the high score, I store the bytes in a file. To read is the inverse, you cast to the type that you need. If the data is a struct, the process would be the same. Therefore, you eliminate the overhead of reading the file and parsing the data.

Explosion particles were simple to make

Particles are essential in any game, and creating explosion particles is much easier in Raylib than in Godot. Just generate a random Vector2 in every direction and adjust the fading color based on the particle timer. The fire particles I created for the spaceship propulsion in Godot were simpler to implement. I couldn’t replicate the same effect in Raylib. An alternative approach would be to use sprites for the fire animation.

The slow-motion effect is simple to make.

Just multiply the slow-motion factor by anything that needs to slow down. In Godot, you change one variable to take effect on the whole game.

No memory management.

Everything is stack-allocated except for Raylib functions that might allocate memory internally. After closing the game, the OS takes care of deallocating the memory. For me, C nudges you in this direction by not providing dynamic arrays. Godot, on the other hand, compels you to despawn and spawn objects. In the Godot version, I destroy and spawn new meteor objects, but in the Raylib version, I reuse the same data.

Compiler flags are helpful.

Two of them made it easy to find bugs, the -fstack-protector-all and -Wshadow flags. The first gives a stack trace of the out-of-bounds index access in an array. The second one warns about redeclaring the same variable name within the same scope, particularly when dealing with nested loops.

Several interconnected data are hard to decouple and organize into files.

For example, the spaceship’s collision with meteors is on the main file, and several data are updated there, like score, slow-motion timer, and camera shake. I didn’t think too hard about this but maybe use a central message queue. A possible downside is that the data is processed only on the next frame unless you run in a separate thread. It might also be the wrong and unnecessary abstraction.

Gradient noise is awesome!

Randomness is a crucial aspect of game development, yet it alone is insufficient. We require a form of ‘controlled’ randomness to achieve desired outcomes. Perlin noise provides a more organic sense of randomness. I utilized stb_perlin.h and translated Godot’s code to C to make the camera shake effect. Additionally, it’s worth noting that cameras remain significant even in stationary games.

Layout UI elements by hand are time-consuming.

A possible solution is to create a built-in editor.

State machines govern the game state.

Games operate on state machines, and I must research more to discover how to simplify the code using state machines and make the code easier to reason about.

Possible improvements

Bundle assets with the binary.

Single binary, easily portable on a pen drive, with the probable downside of lacking mod support.

Save player initials and high score.

Just like the old games!

Release a Windows and Web version.

Utilize Raylib’s multiplatform support.

Vendorize Raylib and Raymath headers.

The best practice in game development is to include your libraries in your source code. I used this approach for the stb_perlin header file. The benefit of this approach is that it guarantees a fixed version and makes upgrades explicit, simplifying the process for others to run the code. Different from the JavaScript world, there is no need for package managers.

Find a better way to organize assets.

Should I separate by entity or by type of asset?


During development, there were some fortunate events that I believe improved the game design.

When I added the planet texture, I positioned it in the center of the screen, but to my surprise, it was like this.

screeshot of asteroids with a big planet on the background

I forgot that the texture was big, but it has a better appeal than having a little planet far away like this.

screeshot of asteroids with a little planet on the background

To test if the score was rendering correctly, I increased the score when the player moved forward. Then I remembered that Sakurai talked about risk-reward. Staying in place gives almost no reward because each meteor destroyed is worth 10 points, but moving adds 70 points per frame! The bigger the risk, the bigger the reward! I found a way to somewhat cheese it. Can you guess how?

Check out the code here and the gameplay below.

Conclusion

Game development is challenging but a fun experience, distinct from web development, which typically presents the same problems every time. The advantages include not needing meetings, dailies, deadlines, or pressure, as well as avoiding general workplace boredom.

As a mental exercise, read through the source code of each implementation and try to imagine the difficulty of adding a second player. I believe this is an interesting way to gauge the complexity of working with the codebase.