How to go about a simple rhythm game/timed inputs system?

Hi! I'm relatively new to working in GMS. I'm trying to create something similar to Mother 3's rhythm combo system, wherein you press a button in time with the beat of a song (here's a video of the combo system in action). I've gotten about as far as having an alarm print to the console at each beat, but I'm not sure how to move forward with tracking the inputs and making sure they're on beat (and adjusting the window around the beat for inputs to be made). Most of the tutorials I've seen for rhythm games are 4K ones that seem to check the position of an arrow sprite, but there's none of that here. I figured it might be possible to check the position of an invisible arrow, but I wonder if there's a more elegant solution that doesn't rely on that if possible. I've also searched around and seen someone say that GMS probably isn't a good engine to make something like this in--is that true? Thanks for your time!
 

Binsk

Member
First off, don't use alarms for this (as convenient as they are). If the game lags for some reason the music will be off with your alarm triggers because alarms are based off of your frame-rate and audio is based off of real time. For example, you might have a song with 120bpm. Let's say you have your alarm going off every 30 frames in a 60fps game (so twice a second). Should the game lag to 30fps for a beat, that alarm will take an entire second instead of half a second to execute and thus every beat afterwards will be off by half a second.

Use delta time. GameMaker has a built-in variable called "delta_time" which is the length of time since the last frame (in micro-seconds, so 1/1000000th of a second). Instead of alarms you could keep a timer that starts at 0 at audio start and increments by delta_time every frame to keep track of timing that way. In the above example you would simple execute a beat every 500000 micro seconds instead of every 30 frames. This way it is based on your system clock so lag won't throw off your game logic.

So, let's say you want a beat every 500000 micro seconds and you have a variable called `song_timer` that is incremented by delta_time every frame. How do you know when it has been 500000 micro seconds? Well, if you 'reset' the value every 500000 micro seconds it will be a new beat every time the value is 0. So something like this:
Code:
song_timer += delta_time; // Add number of micro-seconds between frames
song_timer %= 500000;     // Wrap around value 500000
Now, this won't work because the value will rarely be EXACTLY 0, you might be off by some number of micro-seconds. So when checking for user input, let's add an 'error' threshold where they only need to be within a certain range. Let's say 1/10th of a second, so 100000 microseconds. It might look like this:

Code:
song_time += delta_time;
song_timer %= 500000;

// Check for user input:
var threshold = 100000; // Number of micro-seconds we can be off by
if (keyboard_check_pressed(vk_up)){
    if (song_timer < threshold or (500000 - song_timer) < threshold){
        // They hit the beat on time!
    }
    else{
        // They were off beat
    }
}
Does that make sense?

For the actual visual representation, it should technically be separate from your actual input detection. They shouldn't effect each-other. However, the logic for drawing the sprite should be similar. In your video you have notes circling around the player and the bottom one gets hit on the beat. That is as easy as this:

Code:
var cx = 512; // Some point to circle around
var cy = 512;
var offset = 0; // Degrees offset from the beat (0 = bottom on beat, 180 = top on beat)
var distance = 128; // Number of pixels away from center

draw_sprite(sprite_index, image_index, cx + lengthdir_x(distance, 360 / 500000 * song_timer + offset), cy + lengthdir_y(distance, 360 / 500000 * song_timer + offset));
With an offset of 0 this will draw the sprite at the bottom of your center point (in the given video this would be the character) right on the beat. For the other notes you would just increment the offset some amount for each to get the swirling circle.

I have to be somewhere in 5 so I'm out, but if you have questions I'll come back and check afterwards.

EDIT:
In regards the comment about GameMaker's capability, they don't know what they are talking about. GameMaker is perfectly capable (and easily so) of doing something like this. I made my own rhythm game engine years back and it took less than a day to implement the underlying timing system.
 
Last edited:
So sorry for the late reply, but this was incredibly in-depth and exactly what I was looking for! Thanks for putting so much time into making such a high-quality reply. I only have one question, how would you go about detecting when a user misses a beat?
EDIT: Came up with a second question: How do you stop someone from hitting the beat multiple times within the given input window?
 
Last edited:

Binsk

Member
Well, as demonstrated in my above example you have that threshold that you are looking for user input. You know when you are in the threshold and you know when you are not.

If you want to check if a user didn't press something within the threshold, just make a variable to monitor the input. Have the variable set to 'false' at the beginning of the valid threshold and set it to true if the user presses the input within the threshold. The next time we are outside the valid threshold check if the variable is true or false and if false we know a note was missed. If true, the user hit the note.

This can also be used for your second question. If the user presses a button multiple times the variable is already set to 'true' so you know they already hit the beat and can ignore it or do whatever is required in that situation.

You might need a second variable or something to make sure you aren't constantly resetting your 'user_pressed_a_thing' variable to 'false' and only doing so when first entering the threshold but with the mechanics demonstrated you shouldn't need much more in ways of complexity.


Another method that might be simpler is that you have a variable that monitors the last time the user pressed an input. If it is longer than your beat measurement you know a beat was missed.

There are always multiple ways to skin a cat.
 

gnysek

Member
EDIT: Came up with a second question: How do you stop someone from hitting the beat multiple times within the given input window?
For example, if there's "n" time between current beat and next one, count every button hit for current beat if it was performed before < n/2, and as "too early" for n/2.
Question is, should you stop someone from ruining his performance, or just react to his failure?
 
Top