A Rhythm-Based Tower Defense Game

<= Back

As February rolled in, I decided to pursue a project I've wanted to do for a long time: making a rhythm-based tower defense game. I figured that the most difficult part of this project would be actually implementing the rhythm mechanics, so I tried to get that out of the way first.

I guess I'll take this as an opportunity to state that I work with the Godot Game Engine. It's awesome, and it's just been getting better and better with every update. For the uninitiated, it's a free, open-source game engine. Some of the features include separate rendering pipelines for 2D and 3D projects, a unique workflow where there's really no distinction between scenes and game objects, a scripting layer with a built-in language and API designed specifically for game developers (with the ability to extend to other languages, if you're more comfortable using C# or something else), and a super-lightweight and compact editor program. I love it, I can't get enough of it, and I'm excited to use it on future projects.

Anyway, the reason that I mentioned Godot is that I've never implemented rhythm mechanics in the engine before. Fortunately, I've implemented rhythm mechanics in the Unity game engine before, so it was just a matter of translating the code from that project into this project. I guess this would be a good opportunity to describe how it works in the engine.

Developing flexible rhythm mechanics

This part of the article may get a little technical, but I'll try to keep it as accessible as I can. I divide rhythm mechanics into two different categories:

  1. Events that happen rhythmically in-sync with the game's music

    In guitar hero, the screen scrolls by a fixed unit on every beat of the song. In Dance Dance Revolution, the falling arrows move by a fixed amount of space on every beat of the song. In Crypt of the NecroDancer, the enemies move one space on every beat of the song. These are the sorts of events that happen in-sync with the games music. They occur in response to the beat of the song.

  2. User input that needs to happen in-sync with the game's music

    In guitar hero, when the player hits the 'strum' key, the game checks if it was pressed on-beat or off-beat with the music. If it's on-beat, the note is played. If it's off-beat, they miss. In Crypt of the NecroDancer, when the player tries to move in any direction, the game checks if the arrow key was pressed on-beat or off-beat with the music. If it's on-beat, they move in that direction (or attack in that direction, depending on context). If it's off-beat, nothing happens. In every rhythm game, there are events that happen as a result of user input.

For this project, I wanted to develop a system that would be able to handle both of these types of events consistently. In the end, I came up with a pretty compact solution. Every rhythm-based mechanic in the game revolves around a single object, which I call the BeatKeeper. The BeatKeeper is a "singleton" which, if you're a game developer, is either one of the most powerful tools in your toolbox or a curse-word which should never be used under any circumstances. I guess that's not entirely accurate, because I'm in neither of those camps. I side with Mr. Natural on this one:

Always pick the right tool for the job. In programming, I think the right tool for the job is the one that you can understand the best. Singletons feel really fluent to me, and I can easily wrap my head around them when I'm working on a large project like a game. For me, a singleton was the right tool for this job. However, I can understand why it might not be the right tool for other developers.

So what is a singleton, anyway? As the name implies, it is an object in a piece of software that may only be instantiated one time. If it was implemented properly, there should only be one of your singleton active when the program runs. Additionally, singletons are accessible by any other object in your program. There are many benefits to using singletons, but I'll try to explain the reasons why I chose to make the BeatKeeper a singleton for this project.

  1. A single source of the truth

    Since the BeatKeeper is a singleton, there is only one available when the game is running. The main benefit of this is that, if another object wants to know what measure of the song the game is currently playing, we know that the answer is coming from one place. If there were multiple BeatKeepers, there's a chance that they might disagree with one another about which measure of the music the game is playing. Rhythm games need to be precise, so a single source of information on such matters is imperative.

  2. Accessibility

    Potentially, I want any other game object to be able to make use of rhythm mechanics, if I want them to. Since the BeatKeeper is a singleton, any other object in the game can access it. This means any other object in the game can ask the BeatKeeper what measure of the music the game is playing (among other things) at any time.

Okay, I won't bore you with talk of singletons anymore. Here's how the BeatKeeper works: before I start playing the game's music, I provide the BeatKeeper with the tempo of the song in Beats Per Minute (BPM). Then, the BeatKeeper calculates a variable called the 'crotchet' in the rhythm-game world. The crotchet is simply 60 / BPM. You can think of the crotchet as the length of a single beat in seconds. In a song that plays at 120 BPM, the crotchet would be 0.5, which means that each beat is half a second.

After the crotchet has been calculated, I need to calculate one more thing: the time (in seconds) of the next beat in the song (assuming the first beat is at the beginning of the song). I simply set this variable equal to the crotchet. Now, I can start playing the music. Once the music is playing, I can start making calculations on every frame (for those who don't know, a 'frame' in game development is a moment when new information is rendered to your computer screen). Every frame, the BeatKeeper will check how far along (in seconds) we are into the song. If that time is greater than or equal to the time we have stored in the "next beat time" variable, then we increment the current beat number by one, increment the "next beat time" by the crotchet length, and raise an event called "on_beat".

If you're still following, that's great! If not, just leave it at this: basically, on every beat of the currently playing song, the BeatKeeper will raise an event called "on_beat". This event is how I handle both types of rhythm mechanics I described earlier in the article. Basically, other objects in the game register themselves as listeners for that event whenever they join the game, and whenever "on_beat" is raised by the BeatKeeper, they respond to it accordingly... And that's pretty much it!

With that foundation set up and running, I started developing the actual gameplay. See you in the next article!

<= Back