
Rhythm synchronization in music games
- Transfer

I recently started work at Unity on the beatbox music game Boots-Cut . In the process of prototyping the basic mechanics of the game, I found that it is quite difficult to correctly synchronize notes with music. There are quite a few articles on the Internet on this topic. Therefore, in my article I will try to give the most important tips on developing a music game (especially in Unity).
It turned out that the following three aspects are most important:
- Use
AudioSettings.dspTime
insteadTime.timeSinceLevelLoad
to track the position in a song. - You must always use the position in the song to update the movements.
- Do not update notes in each frame according to the time difference, interpolate them.
We will take this into account and get to work!
Main class
You need to create a class
SongManager
to track the position in the song, create notes and other song management features.Position tracking
In all musical games, you need to track the position in the song in order to know which note should be created. Below are the fields required to track the position in a song:
//текущая позиция в песне (в секундах)
float songPosition;
//текущая позиция в песне (в ударах)
float songPosInBeats;
//длительность удара
float secPerBeat;
//сколько времени (в секундах) прошло после начала песни
float dsptimesong;
We initialize these fields in the function
Start()
:void Start()
{
//вычисление количества секунд в одном ударе
//объявление bpm выполняется ниже
secPerBeat = 60f / bpm;
//запись времени начала песни
dsptimesong = (float) AudioSettings.dspTime;
//начало песни
GetComponent().Play();
}
For convenience, we will convert
bpm
to secPerBeat
. Later it secPerBeat
will be used to calculate the position in the song in beats, which is very important for creating notes. In addition, we record the song's start time at
dsptimesong
. We use AudioSettings.dspTime
instead Time.timeSinceLevelLoad
, because it is Time.timeSinceLevelLoad
updated only in each frame, and is AudioSettings.dspTime
updated more often, since this is an audio timer. To maintain the tempo of a song, you need to use an audio timer. In this way, we can avoid the delay caused by the time difference between frame updates and audio updates. The function
Update()
calculates the position in the song using AudioSettings.dspTime
:void Update()
{
//вычисление позиции в секундах
songPosition = (float) (AudioSettings.dspTime - dsptimesong);
//вычисление позиции в ударах
songPosInBeats = songPosition / secPerBeat;
}
We calculate the position in seconds by subtracting the current
AudioSettings.dspTime
start time of the song ( dsptimesong
) from the current time . We got a position in seconds, but in the world of music, notes are recorded in beats. Therefore, it is better to convert the position in seconds to the position in beats. Dividing songPosition
by secPerBeat
(second / (second / hit)), we get the position in beats. Look at the picture: The

position of the notes in the beats: 1, 2, 2.5, 3, 3.5, 4.5, and the duration of the beat is 0.5 s. Therefore, if 1.75 s (
songPosition == 1.75
) elapsed after the start of the song , then we know that we are in position 1.75 ( songPosition
) / 0.5 ( secPerBeat
) = 3.5 beats, and it is necessary to create a note for beat 3.5.Song info
Let's move on to the fields in which we recorded information about the song:
//количество ударов в минуту
float bpm;
//сохранение всех позиций нот в ударах
float[] notes;
//индекс ноты, которую нужно создать следующей
int nextIndex = 0;
For simplicity, I demonstrate a song with only one track of notes ( three tracks were made in Guitar Hero Mobile , and only one in Taikono Tatsujin ).
bpm
Is the number of beats per minute. As we saw, for convenience they are converted to secPerBeat
. notes
- This is an array in which all notes positions in beats are stored. For example, for the notes shown in the figure, the array notes
will contain {1f, 2f, 2.5f, 3f, 3.5f, 4.5f}
: 
And, finally,
nextIndex
this is the integer needed to traverse the array. It is initialized to 0 because the next note to be created will be the first note in the song. When creating a note, the counter is nextIndex
incremented by one.Creating notes
We determine whether a note should be created in a function
Update()
. However, you must first determine how many strokes will be shown in advance. For example, for the following track: the

current position in beats is 1, but beat 3 has already been created. This means that 3 hits are shown in advance.
Add below the
songPosInBeats = songPosition / secPerBeat;
following lines:if (nextIndex < notes.Length && notes[nextIndex] < songPosInBeats + beatsShownInAdvance)
{
Instantiate( /* префаб ноты */ );
//инициализация полей ноты
nextIndex++;
}
First you need to check if there are any notes left in the song (
nextIndex < notes.Length
). If there are still notes, then we check to see if the song hit the beat where the next note ( notes[nextIndex] < songPosInBeats + beatsShownInAdvance
) should be created . If reached, create a note and increase nextIndex
to track the next note to be created.Movement of notes
Finally, let's talk about how to move the created notes according to the tempo of the song. This is quite simple, if you recall the item “Do not update notes in each frame according to the time difference, interpolate them.”
Always update movement by position in a song because:
- The audio timer has a time difference with the frame timer
- Beats can be exactly in the middle of two frames (which leads to a time difference)
So how do you move the notes? By interpolation!
To simplify, I cut out all the code in the class
MusicNote
and leave only the function Update()
in which we move each note://функция обновления нот
void Update()
{
transform.position = Vector2.Lerp(
SpawnPos,
RemovePos,
(BeatsShownInAdvance - (beatOfThisNote - songPosInBeats)) / BeatsShownInAdvance
);
}
In the diagram below, this is clearly visible:

Conclusion
I talked about the basics of programming a music game. Following these principles, you can create games with synchronization. In games with several tracks, you can create nested arrays
notes
, deleting notes is performed by checking the position relative to the deletion line, notes of long duration are implemented by tracking the initial and final beat, etc. Thank you for reading the article, I hope it will be useful. My own music game Boots-Cuts will be ready next year, stay tuned.