A bullet hell game made in 48h during the GMTK Game Jam 2023, where you don't actually pilot your ship, but summon waves of enemies and try to survive.
Link to the game : https://tourmi.itch.io/drunk-space-odyssey
We ( sapheu and I) ended up participating in the GMTK Game Jam this year, and pulled out with a game I feel like is decently interesting in the end. The theme was "Roles Reversed", and we believe the game fits the theme well. They say writing a post-mortem for game jam games is a good idea, and I needed an excuse to update this blog, so this will do! I wish to go over the different design decisions we ended up making for the game, and also talk about how I implemented some of the features, code-wise. Finally, the stuff we ran out of time for or could have done differently.
Technical details
I imagine this sort of stuff will interest some people, so here's some details!
- Engine : Godot 4.1
- Internal resolution : 320x180
- Main art program : Aseprite
- Music program : FL Studio
- Sound effects made with : Audacity and jsfxr
Spawning enemies
After our initial brainstorm, and landing on the bullet hell idea, we originally thought about having to spawn enemies and try to kill the "player". It would not have been the most original idea, considering how (in hindsight) so many games in the jam did it as well, but we started there. It's only when I started implementing the "player" AI that I realized it'd be much more fun to have to keep a balance between too many and too few enemies, and so, the enemy ship turned into the actual player ship.
Suddenly, we didn't need to make a resource system, since the player is discouraged from spawning too many enemies naturally, unless they want to overwhelm their ship and lose. However, we didn't have any ideas about how we'd go about forcing the player to spawn enemies in the first place. After all, you'll survive for an infinite amount of time if you just never spawn anything. We kept working on the game despite this, figuring we'd find a solution to this as we went. In the end, I realized that a simple fuel system, where the only way to get fuel was from pickups dropped by enemies was a good-enough motivation to force the player to actually play the game!
The AI
The enemy AI is very simple, it just blindly follows a pre-defined path and shoots once its cooldown expires. Nothing more to it. Where the meat of the AI is situated is with the player's ship.
Considering that we're dealing with a bullet hell game, and that the player does not control their ship, I had to get somewhat clever with the AI. A simple "move away from the nearest bullet" just would not work. At first, I added a circle around the player and just had it move away from the bullets in the circle, scaled by the distance from the bullet. It worked well, but trouble came in when I tried making the behaviour more complicated. I wanted the AI to avoid walls, pick-up powerups, and so on, and just doing all of these one after another without any way to adjust the behaviours easily would have gotten me stuck later on. I ended up inspiring myself with how flocking/boid AIs are usually implemented, except with my own custom objectives.
var calc := Vector2()
calc += _get_random_direction() * RANDOM_DIR_INFLUENCE
calc += _get_avoid_bullets_direction() * AVOID_BULLETS_INFLUENCE
calc += _get_avoid_enemies_direction() * AVOID_ENEMIES_INFLUENCE
calc += _get_avoid_walls_direction() * AVOID_WALLS_INFLUENCE
calc += _get_direction_to_powerup() * POWERUP_INFLUENCE
calc += _get_down_bias() * DOWN_BIAS_INFLUENCE
calc += _get_center_bias() * CENTER_BIAS_INFLUENCE
wanted_direction = calc.normalized()
Above is the code that drives the player AI, with some stuff removed for clarity, such as direction smoothing to avoid jerky motion. But as you can see, it is actually very simple. Each behaviour we want from the ship is calculated in a different function that returns a Vector that points towards the wanted direction. We then take that vector, and multiply it by some constant to mark its importance. In our case, we want the AI to dodge bullets above all else, so we give it a bigger value than all of the other behaviours. The rest follows. This way of doing AI worked quite well for me, and saved a ton of time in the long run. Adjusting it was as simple as modifying a value. This concept could have been extended to other actions (using a bomb, for instance), but I didn't feel like that was worth it within the limited time of the jam, and so the bomb usage was coded manually
Time time time
We didn't have time to do everything we wanted to, just like anyone else during a jam. A few issues came up last minute we couldn't fix either, that with enough time, we could have polished up. For instance, the balance of the game was pretty wonky, and allowed to only spawn the easiest enemies without ever spawning the harder ones. The idea was to encourage the player to spawn both types, by making the harder types drop more interesting items more often, but the easiest enemies dropped enough fuel on their own for the player to survive quite some time. This could have been fixed by making drops rarer as the difficulty increases, requiring to kill better enemies, but of course, lack of time.
Another issue, and this one is kinda major, is that the gameplay isn't super engaging. After all, all you do is click on buttons, and watch the result. To fix this, we would have needs an extra-layer of interactivity somewhere in the game. Maybe by clicking the playfield, we could attract the player to an area? Or maybe allow drawing the wave path after clicking on the enemy button? There's really tons of ideas we could have applied to make the game more interactive, but again, lack of time.
Outside of these issues, there's some more minor things I would have liked to put into the game that I just didn't consider due to knowing they'd take too long for the jam. I would have liked to display more information to the player about the enemies that they can spawn, or to have made the tutorial interactive instead of a text-based tutorial (though it's a miracle we managed to include a tutorial in the first place). Some AI-based enemy ships could have been nice as well (a player doppelganger?). The most important part of any game jam is scoping, and knowing in advance what you have time for, and what you don't. I believe we did a great job at scoping everything, and ended with a very polished game in the end.
In the end
This was a fun project, and a whole bunch of interesting games resulted from the GMTK 2023 game jam. The jam is still in its rating period, so we don't know how well we did in the grand scope of things, but I can safely say I'm proud of what we made, and that the anxiety I had at the start of not being able to make anything was pretty much founded on nothing. Being able to make a functioning game in 48 hours is a feat in itself, which I'm glad we accomplished.
Once the rating period ends, we might update the game to fix some small issues (balancing, animations not playing on some pickups), but outside of that, it is as complete as it will be. While our concept was very nice, I don't think such an idea has much room to grow into full-fledged video game, at least, not with how our game stands at the moment.
If I remember to, I will update this article with our final ranking once they come out.