The COSMOSTRIDE Post-Mortem

Preface

As mentioned in an earlier blog post (turns out this is way overdue,) I decided to take part in Ludum Dare 54, and I had decided to go solo, just to see how much I would be able to do all by myself, having to handle all the assets, design, and programming.

The end result of this grind was COSMOSTRIDE, which is now available to play on itch.io, as well as my website.

This blog post has been in the works since shortly after I finished making the game, some time in November 2023. I kind of put it off until I couple days ago, when I decided to finish it up.... uhhhh.. Happy 2025!!!!! Better late than never!!

Theme announcement and brainstorming

For Ludum Dare 54 I wanted to go against a pattern that I noticed on my previous failed attempts at making a game for a jam. As I wrote on my last post on the subject:

For a lot of these jams, especially the failed attempts, I've noticed that I took more of an art-first approach, rather than one focusing on mechanics, or any real gameplay aspect that would be more substantial for a finished product than aesthetics.

For this reason, before I opened Godot, or Blender, or Krita, or Inkscape, or SunVox, I created a new Canvas on Obsidian to organize my thoughts. I wanted to put my ideas down into words that I could later reference, and when I was done with that, I could focus on a goal-oriented approach to implementing the game.

The theme was then announced... "Limited Space"

As is always the case, the theme was controversial, and a lot of people didn't like it. I didn't have any strong feelings towards it myself and started writing out what I thought others would take away from this prompt.

Since I wanted my game to "stick out" in a way, I wanted to avoid using the theme in a way that would be common.

In hindsight, the first thing I thought of, a "close-quarters single-screen action roguelike" was not actually as common of a type of entry, and I could have definitely gone with that.

A crop of an Obsidian Canvas showing initial thoughts on the theme to the left and the idea for the game I wanted to implement on the right

The idea

After about an hour of brainstorming, I decided to settle on something a little like a previous attempt at another Game Jam. This is one I didn't include on my last blog post, since I couldn't find the files for it then, but here it is:

A screenshot of an upgrade menu, it is a 5x5 grid of buttons which are empty, but some are filled in with sprites of modules

I don't remember at all which edition of Ludum Dare this was for, the theme, though, was Growth.

In this game you would be able to build your ship out of modules, and those modules would take up physical space in the world.

I had only implemented three different modules:

  • Health Modules that would increase your HP, they would individually have their own health, and enemy bullets would be able to pass through them and damage modules behind it as well.
  • The more expensive Shield Modules, which would recharge with time and protect modules behind it for as long as they still had power.
  • Weapon Modules which would fire full-auto plasma shots upon clicking the screen, however they could overheat if fired for too long, and enemy fire would be able to heat it up, too. If overheated, it would stop working temporarily to cool down.

Another screenshot of the prototype, this time in game, the player ship has twenty shield modules equipped and four guns, but no health at all. On the bottom, there are three bars, one says "0 Health," the second says "97 Shield," and the last one, "70 Energy"

The scope was broad and motivation was low, I was taking part in this jam while friends were also trying to make a game for the same event, but in the end none of us managed to submit a game.

I still thought the idea had merit, even if I didn't manage to carry through with it.

Now that I take a closer look at this project, I can think of a few ways I could have implemented COSMOSTRIDE that made better use of the jam theme of "Limited Space."

Theme usage

I decided I wanted to go for an on-rails space-shooter, just like StarFox. The module idea had merit, but I went with a safer version of it, where the modules are not physical objects in the world, but rather just objects that modify statistics.

In my original drafts for the implementation, I had a few wild ideas, such as Resident Evil 4 style inventory management for modules (which was not uncommon among other entries, given the theme,) and module damage much like the previous prototype, which would force the player to gauge risks when switching modules out or when choosing new ones.

Those were ideas that I still like, but my main goal for the jam wasn't to "wow," but rather to get anything done at all. I have many failed solo jam projects behind my back, and I didn't want this to be another one of those.

For this reason, I went with a very simple, and very loose usage of the theme. You have a very limited amount of module slots, make the most of them.

Three available slots, for six different types of modules, most of which have three different levels. Most of these modules stack, meaning that you can use the same type of module in more than one slot and get an amplified effect out of it.

Once I had this idea, I set it in stone and did not allow myself to change it.

From there, I made a task list, each task a goal, and I would have to tick all of them off to get the product I wanted.

A task list, featuring many different items organized hierarchically and a set of "Bonus and Optional" items beneath them

A slight bit of anxiety

My jam experience had a strange duality of taking it easy, and putting a lot of pressure on myself. I definitely did take it easy in making my scope very small and realistic within the time frame, and for this reason I put a lot of pressure on myself to actually finish the dang thing. I put myself in a very specific mindset: "If I don't do it now, I'm not doing it at all," forcing myself to get back in the fray with very few breaks.

Getting to work

With the idea set in stone, I was ready to go. I put on cool music, fired up Blender and made myself a cool combat spaceship model, low on the polys, but stylish nonetheless.

A ray-traced Blender render of the spaceship for COSMOSTRIDE

Yes, the ship was the very first thing I made, so much for not taking an "art-first approach" (!!)

Secondly, and probably most importantly, was getting the spaceship movement just right. So I fired up Godot and started working on it.

Initially, I made the spaceship a CharacterBody3D, since that is the node I'm most familiar with, and I'm used to making every single player controller extend this node.

extends CharacterBody3D

const SPEED = 20.0
const JUMP_VELOCITY = 4.5



var display_direction: Vector3 = Vector3.ZERO


func _physics_process(delta):
	var input_dir = Input.get_vector("move_left", "move_right", "move_descend", "move_ascend")
	if input_dir:
		velocity.x += input_dir.x * SPEED * delta
		velocity.y += input_dir.y * SPEED * delta
		velocity.x = clamp(velocity.x, -SPEED, SPEED)
		velocity.y = clamp(velocity.y, -SPEED, SPEED)

	velocity.x = lerp(velocity.x, 0.0, 0.05)
	velocity.y = lerp(velocity.y, 0.0, 0.05)
	
	display_direction.z = lerp(display_direction.z, -(input_dir.x) * 30.0, 0.05)
	display_direction.x = lerp(display_direction.x, (input_dir.y) * 30.0, 0.05)
	
	$ModelPosition.rotation_degrees.z = display_direction.z
	$ModelPosition.rotation_degrees.x = display_direction.x

	move_and_slide()

It is in fact a very simple controller, but it works for what I'm trying to do!

As it was a CharacterBody3D, I used invisible StaticBody3Ds to limit movement range to the screen space I wanted the player to be restricted to.

And so, this was my initial setup for the scene:

A screenshot of the "Scene" tab in Godot, showing the node hierarchy

As you may have noticed, I have a Node3D called PlayerRoot as the root of the scene, instead of the CharacterBody3D PlayerController, this is different from my usual approach, and it was done for several reasons:

  • It's an on-rails shooter I'm working on, so I wanted the whole scene to move, but I didn't want the PlayerController's local coordinates to be modified directly, so it was useful to keep a separate root node around.
  • To make it easier for myself to navigate the code, I decided I wanted the PlayerController to handle movement only, signal handling, data storage and other verbs (read: "aiming and shooting,") would be delegated to other nodes. This was a big plus and what made my code easy to read through even on the later hours of the event.
  • Child nodes inherit the position of their parents, and apply an offset of their own, I wanted PlayerController's siblings to move together, but not follow PlayerController, this is so the spaceship doesn't occupy the same space on-screen from having the camera follow it, and so that the PlayerBounds would actually have any effect at all. (It's hard for a wall to do its job when it's moving along with you!)

PlayerController has a child ModelPosition which is used to apply an offset to the transformation, which I used to tilt the spaceship in the direction the player moved their ship.

Before making the first commit, I had set "W" for move_descend, and "S" for move_ascend, but I quickly noticed that scheme didn't feel very good. The opposite felt more natural, and so I quickly changed them around. Nobody pointed this out when I released the game, so I believe I made the right call.

Enemies

You can't really have a space shooter without things to shoot, so I had to get right on to that. I fired up Blender once again and started making some things. I just put shapes together, I didn't want the enemy models to be as detailed as the player's, I wanted them to feel more like alien constructs, ones that don't even necessarily hint at anyone boarding them, as if they were unfeeling, automated murder machines targeting you.

An in-game screenshot showing the player model and three enemies shooting at it

Their projectiles, too, were simpler, just bright red energy balls, though that was more out of laziness than any kind of artistic intent. That was a bad call though, the lighting and color blending in with the enemies' made it very hard to tell where the bullets were, how close they were, and so dodging became harder.

I implemented two kinds of enemies, a quick, slim and vulnerable "Basic enemy," and a slower and tankier "Armored enemy." It is... quite boring, to be honest, but I was determined to keep my scope in check. Again, my main goal was to get anything done at all, even if I was not too happy with how it looks or feels.

Despite that, I found it really fun to glide around and shoot baddies, I made some very punchy hit and explosion sounds and spent a lot of time "testing," by which I mean I was having fun with the thing I was making.

Modules

Probably the biggest challenge for the game that I wanted to make was adding the Module system, the actual thing that will make the game (at least loosely) follow the theme, other than the somewhat claustrophobic nature of an on-rails space shooter.

I brainstormed a few ideas for modules and designed little icons for them in Inkscape. Starting with the simple, obvious ideas such as a shield module and a regeneration/repair module, then damage increase, fire rate upgrades.

Out of all the icons I came up with, only one ended up unused.

The first version of the module icons for COSMOSTRIDE

I made an on-rails space shooter, so it is almost upsetting that I didn't add a barrel roll, but if you take a look at the icons I designed, you can see I definitely intended to!

In hindsight it would be great if I did add a bullet deflecting barrel roll. It would be quite powerful, but the limited slots meant something else would have to be sacrificed.

The module that made it through, but that I didn't take too seriously or find very useful was the speed module, which increased how quickly you could dart from one side of the screen to another, but a few players noted that that was their favourite one, since it allowed for better and more reliable dodging. Better to not get hit at all than be able to tank hits.

Onto implementation, I had a lot of fun programming a module system, even though it is quite janky! Modules are represented as dictionaries with two fields: the name of the upgrade (SPEED, REGEN, SHOT_SPEED, etc.,) and its level. Most modules have three levels, the exception is the TWIN_FIRE module, which has only one. If I had implemented the Barrel Roll, I would've probably also added multiple levels to it, which would influence the cooldown time between each roll, can't just let you spam it, after all. In practice, there are around 9 levels to all of these levelled upgrades, since multiple upgrades of the same time can be dropped, this was intentional, and the stacking effects are pretty fun.

The modules were all added to an array, in order of what I thought was most basic, to what I thought was most powerful, the modules are then dropped randomly within a range of this list that is affected by the current game level:

An array named module_choices with a list of modules represented by dictionaries as described above

In theory, this is supposed to make it so the player becomes more powerful slowly as the game increases in intensity. Unfortunately, due to the nature of random chance, it is very possible that someone will get the best possible power-up for one level, and then get worse power-ups in subsequent ones, which kind of undoes the cool difficulty curve I had in my mind when making the game. This just brings to light how important it is to keep the RNG on a tight leash.

When I started work on COSMOSTRIDE, I hadn't yet used the full extent of features Godot 4 provides over Godot 3, and for the module menu, I used and abused GDScript's brand new Lambda functions to add the logic of swapping modules around the slots in the menu between each level.

Code for the player's module slot

Lambda functions are pretty great for adding logic to UI components! Rather than declaring some function somewhere else for something only a few buttons will do, I can inject the logic directly on the signal connection, and make use of any local variable inside the current function without the need to add it as an argument: i enumerates and iterates over each button and then adds a relation between the button and the underlying module data.

Difficulty and Progression

I've already had a level system implemented from the moment I added enemies to the game, and then with modules, for each level that passed, the player character would be (on average) more powerful. The enemies, then, had to put up a fight too, so I extended the enemy spawning script I'd implemented to change a few characteristics about enemies as levels progressed.

The first level would only spawn Basic enemies, the second would spawn them at a faster rate, the third would introduce Armored enemies and only spawn them, and from then on, both enemy types would be spawned at progressively faster rates, with a very punishing damage multiplier added to their bullets. Each level is time based, too, and the length increases by 5 seconds with each level, starting from 55.

I set up 13 levels using these properties, and then, after that they're generated programmatically, at which point, in theory, the difficulty plateaus, but nobody got that far to tell the story, so it is fine.

The music

I made every part of COSMOSTRIDE, including the music. That is something I don't have much experience with, but I have all the tools for. I used SunVox, a lightweight and powerful Tracker/DAW hybrid, to create the in-game music, "Wave"

I used a few samples I extracted from Bejeweled 2's "Beyond the Network" to create the music, and a couple of people noticed, which I found funny. Here's a fun fact for you: a lot of early PopCap music has music in tracker formats. You can open the music files in OpenMPT and give them a listen, extract their samples, and even fudge around with the patterns. That inspired a lot of curiosity and creativity in me to want to learn how to use tracker software, so I hope it can spark the same in you, too. :)

Playtesting

After doing a lot of development work for a long time, I finally got to a point I was satisfied enough to get a few playtesters on board before publishing it to the Ludum Dare site, so I posted it to my Discord server (RIP,) and after a little bit of anxious waiting, friends returned with positive feedback and shared their scores. That's exactly what I wanted to see, and it made me very happy. My mission was complete, and with a few hours left, I submitted my game to the Jam.

Once the submission window closed, it was time to rate and get rated, and I got plenty of feedback! As I said before, my objective was not to "wow" anyone, but rather get something fun up, and that definitely came to fruition. People liked my game, but since it was a very "safe" entry, there was not much of note to talk about in feedback. The aesthetic and the music were received very positively, but some noted the sound effects were pretty grating after a while. Some others pointed out that the game fits the theme very loosely, if at all, which I agree with.

The ratings I've gotten were pretty middling, from 2.1 to 3.6, with "Fun" being my best category.

Conclusion

I don't know how much it is because of how I strove to keep the scope manageable, but I see COSMOSTRIDE as a great success, I managed to keep my energy up with a mentality of "I NEED to get this done." There might have been points at which I wanted to give up, but I actually managed to not give up. It was pretty... different, for me, because I give up on things very often.

After the Jam was over, I pushed a few more updates to the game, first the Leaderboard, and then some QoL updates, including better looking color-coded icons for the modules, which I think look really good! The game is, as always, available on my site, and the source code is on GitHub.

Ludum Dare is great fun, when I'm motivated enough to take part. Unfortunately, since LD54, life has gotten in the way. Between uni, getting ill, and plenty other factors, I haven't been able to participate in any event in 2024, here's hoping that this year I can-- wait, what? The 2025 events are cancelled? Uh. Alright.

If you can, send Mike, creator of Ludum Dare, some support. Looks like, just like many of us, he's in a bad spot right now.

As for me, I'll try my best to join more jams and try to get more games done, but this is a promise I've made to myself far too many times already, and I'm really bad at keeping promises, it turns out.

If we're friends, you're more than welcome to try and drag me off to work on something with you, too! If I'm not busy or depressed, chances are I'll accept.