Sonolus Wiki

20. Time Scale

In this chapter, we will add time scale to Note.

Time Scale

Time scale is a common feature in many rhythm games, and another name for it is soflan. We will be implementing time scale, and more specifically the time scale that works like slowing down/speeding up the entire world.

Because of time scale's prevalence, Sonolus has also provided convenient functions to work with time scale.

Time Scale Changes

Similar to BPM changes, there is also a special archetype name for time scale changes as well as special archetype data name for time scale value. Let's add a list of time scale changes to our test level:

export const data: LevelData = {
    // ...
    entities: [
        // ...

        ...chart.timeScales.map(({ beat, timeScale }) => ({
            archetype: EngineArchetypeName.TimeScaleChange,
            data: [
                {
                    name: EngineArchetypeDataName.Beat,
                    value: beat,
                },
                {
                    name: EngineArchetypeDataName.TimeScale,
                    value: timeScale,
                },
            ],
        })),

        // ...
    ],
}
export const data = {
    // ...
    entities: [
        // ...

        ...chart.timeScales.map(({ beat, timeScale }) => ({
            archetype: EngineArchetypeName.TimeScaleChange,
            data: [
                {
                    name: EngineArchetypeDataName.Beat,
                    value: beat,
                },
                {
                    name: EngineArchetypeDataName.TimeScale,
                    value: timeScale,
                },
            ],
        })),

        // ...
    ],
}

Note

To implement time scale for Note, we simply need to change all rendering related logics to use scaled time instead.

Notice that we are not changing input related logics, as time scale should remain a purely visual feature and should not affect how input is handled.

export class Note extends Archetype {
    // ...

    preprocess() {
        // ...

        this.visualTime.copyFrom(Range.l.add(timeScaleChanges.at(this.targetTime).scaledTime))

        // ...
    }

    // ...

    shouldSpawn() {
        return time.scaled >= this.spawnTime
    }

    // ...

    updateParallel() {
        // ...

        const y = Math.unlerp(this.visualTime.min, this.visualTime.max, time.scaled)

        // ...
    }
}
export class Note extends Archetype {
    // ...

    preprocess() {
        // ...

        this.visualTime.copyFrom(Range.l.add(timeScaleChanges.at(this.targetTime).scaledTime))

        // ...
    }

    // ...

    shouldSpawn() {
        return time.scaled >= this.spawnTime
    }

    // ...

    updateParallel() {
        // ...

        const y = Math.unlerp(this.visualTime.min, this.visualTime.max, time.scaled)

        // ...
    }
}