Sonolus Wiki

08. BPM and Beat

In this chapter, we will refactor Note to use BPM and beat in data rather than time.

BPM and Beat

Currently Note uses time in seconds, however that is not how rhythm game charts are commonly done. Rather, each note has a beat number, and its time is calculate using the beat number and a list of BPMs (Beats Per Minute) of the BGM.

While engines can store the BPMs and perform conversion themselves, Sonolus provides convenient functions to do it. Let's take a look at how to integrate our engine to use this feature.

BPM Changes

We first need to tell Sonolus about all the BPM changes.

This is done in the level data by giving a list of entities using the special archetype name for BPM change, with special data name for beat as well as the BPM value:

export const data: LevelData = {
    // ...
    entities: [
        // ...
        {
            archetype: EngineArchetypeName.BpmChange,
            data: [
                {
                    name: EngineArchetypeDataName.Beat,
                    value: 0,
                },
                {
                    name: EngineArchetypeDataName.Bpm,
                    value: 120,
                },
            ],
        },
        // ...
    ],
}
export const data = {
    // ...
    entities: [
        // ...
        {
            archetype: EngineArchetypeName.BpmChange,
            data: [
                {
                    name: EngineArchetypeDataName.Beat,
                    value: 0,
                },
                {
                    name: EngineArchetypeDataName.Bpm,
                    value: 120,
                },
            ],
        },
        // ...
    ],
}

This entity tells Sonolus that, at beat 0, BPM now changes to 120.

Note that we do not need to implement the BPM change archetype. In general, any entity with archetype name that is not implemented will simply be ignored, so we can safely use them to store metadata in our levels.

Refactoring

With BPM changes set, we can refactor Note to use beat in data rather than time:

export class Note extends Archetype {
    import = this.defineImport({
        beat: { name: EngineArchetypeDataName.Beat, type: Number },
    })

    // ...
}
export class Note extends Archetype {
    import = this.defineImport({
        beat: { name: EngineArchetypeDataName.Beat, type: Number },
    })

    // ...
}

Note that we do not have to use the special data name for beat, however we do so simply for consistency.