Finishing the SHMUP


This was my first real project in Godot, and I wanted to get some thoughts out. It's by no means perfect, and I have pages of ideas and sketches of things I could add, but I feel like doing so will also involve rewiring a bunch of early implementation, and at that point, it might make sense to chalk it up to experience and move on to the next thing. 

The game was initially inspired by the hacking sections of Nier: Automata. I'm not particularly in love with SHMUPs or Bullet Hells, but it seemed like a sweet spot for a game that is simple enough to keep the scope small, but has enough going on that working out the implementation was interesting and fun. And for the purpose of learning, I definitely recommend building something similar. 

I learned a lot about :

  • Scene structure, and resuing/ instantiating scenes as objects
  • Composition and State Chart patterns
  • Signals to communicate between instances
  • Collisions, movement and physics interactions
  • Implementing simple visual effects and animations
  • Basic UI
  • Managing a lot of instances to make sure they don't impact performance

Each of those contained a ton of either lessons or code / scenes I think I will reuse going forward. It's been a bit of a whirlwind over the last couple weeks but I'll try to speak to each one a bit.

Scene Structure / Composition

I've used other creative software with parent/child structures before, so that part wasn't new. In the realm of programming, though, the ability to drop a few nodes into the tree, add a script to the root node, then save it out as a reusable object was a game changer in how I thought about building things. 

For example, this is how I built the double shot bullet type that travels in a wave pattern. Rather than working out some function to describe the animation in code, I made 2 bullets (reused scenes) a child of a dummy node used as a pivot point. Then an animation player to rotate them around the pivot, and a "bullet parent" to make the whole setup behave like any other bullet. 

The beauty of this setup is that the bullets it contains still have their logic for collisions and dealing damage, but I can create patterns or animations for how they travel in a more intuitive way. I used a similar structure for how my enemies and player guns were built, which ultimately comes down to a composition pattern, combining and reusing common components across multiple objects. 

Signals

I'm not sure I'm using signals correctly yet but I do see how they can be powerful for decoupling objects, while allowing them to communicate.  For example, when tracking kill streaks, I need to know when an enemy died from player damage, and add it to the counter. To do this, I have a KillStreakManager as an autoload that handles tallying, resetting and also relaying the count and milestone messages to the UI. Any time a Node is added to the scene tree, I check if it belongs to the enemy group, and if it does, I connect its "died_by_player_damage" signal to the manager. With some checks before making the connections, that means both the enemy logic and the manager logic work perfectly fine if the other is moved in the scene, or even if they don't exist. 

I will say that signals created some confusion for me when it came to reporting to the UI. Relaying from enemies to the KillStreakManager, and having both that and the Player object relaying directly to the UI created some issues where they could get out of sync. In the future, I'd like to explore a Signal Bus pattern, with a central manager for signals (where it makes sense) which should simplify some of that, along with maybe a central autoload to track and report player stats. I have a lot of calls to find player and enemy objects, which aren't awful for performance because of groups, but could be simplified to make them easier to track mentally, and keep a single source of truth. 

Collisions

I really appreciate how simple collision detection is to implement in Godot. Having built some rudimentary collision detection basically from scratch in Processing, I know it's not always easy. This also ties into the signals, giving you options for how or when you want to handle the result of a collision. 

The big lesson that's standing out in my memory regarding collisions is call_deferred(). In this game, when collisions happen, I'm spawning particle effects and sometimes explosion areas, which themselves have collision shapes. Spawning them directly at the time of the collision throws an error “Can’t change this state while flushing queries” (IIRC). That's because you're still in the physics process at that point, and trying to add new physics objects into the scene while processing physics. call_deferred() solves this, by basically queuing a function call, until it is safe to do it. In this example case, that means calling it after the current physics process cycle is finished. 

Managing Instances/ Performance

While I don't think obsessing about performance at the prototype stage is a good idea, there are definitely times where you may create a lot of future headache if you don't at least consider it early on. This game spawns a ton of instances in the form of bullets, various visual effects and enemy waves. A lot of heavy lifting is done by the VisibleOnScreenNotifier2D node, which checks if the object is visible in your current viewport, and emits a signal when entering or leaving visibility. For the bullets, this is most of what I need to keep the number of spawned instances low, by having them queue_free() when they leave the screen. 

Early on, I still had some objects or their parents that weren't freeing themselves appropriately, causing slowdown as the game ran longer, which is where the Remote scene tree view was valuable:

While running the game, you can click over to the Remote tab to see the live Scene Tree in the running game, along with all the instantiated nodes, and even your Autoloads. You can click on any of them and see exactly what their variables are set to, and more importantly this was how I was able to track down invisible node objects that weren't getting freed properly, and address them. 

I also made heavy use of the direct selection tools running the game through the editor:

Switching from game input to 2D, and switching to list mode, shows a list of all nodes at the position you click. Paired with the Remote Scene Tree, if I saw something behaving strangely, I could pause, click on it, and check its values to see why it might be misbehaving. This is probably not the most efficient way of debugging, but it helped me track down inconsistencies in a way that felt intuitive for me. 

UI

The UI was the last thing I worked on, and I definitely have a lot to learn about Control nodes and UI layout in Godot. In the interest of time, I skipped some layout shenanigans by making the tutorial popup a single image in Affinity Designer. As a vector, it's very easy to edit and quick to export, so it worked well for this case where there weren't dynamic elements, like the total kills on the Game Over screen for example. 

In terms of lessons, I think my main takeaway is that I really want to make sure the information the UI is displaying is calculated and stored somewhere else, and just reported to the UI. Early on, I had the Secondary charge meter some calculations, because it has 5 charge levels but each charge takes 2 kills. That ended up creating a case where a division was rounding to an integer and showing different values than what the player actually had. Ideally, the values the UI is showing should just be pulled from a common source to prevent stuff like this from happening. 


Next Steps

Like I said earlier in the post, I think I'm ok with leaving this project where it's at. It's playable enough to get the point across, and I have a better sense of how to structure and approach things going forward. 

I'm making a list of things I'd like to explore next in Godot, and I think something that is more heavy on stats and data management, as well as more reliant on UI would be good to explore, as this project was light on those aspects. I think around 2 weeks is a good timebox for projects in the learning phase, so I'm still sticking to concepts that I think lend themselves to a smaller scope. Maybe a Card game or an Incremental of some kind fits that bill. 

If you made it this far, thanks for reading and I hope there was something insighful or helpful in here for you. 

Files

SHMUPy Prototype v5.1.zip 17 MB
3 days ago

Get SHMUPy Prototype

Leave a comment

Log in with itch.io to leave a comment.