Sonolus Wiki

15. Input Bucket

In this chapter, we will add input bucket to Note.

Input Buckets

Input buckets are categories of notes which notes can feed judgment distance values into, and at the result screen a probability graph will be shown for each bucket.

While it's not necessary, it is very useful for players to calibrate their input offset and improve on their accuracy.

Setup Buckets

Our engine only has one type of note, so one bucket will do.

At the result screen each bucket will be represented by a graphic composed of skin sprites, so we should make it as close to the in game visual of the note as possible. Let's also use milliseconds as the unit.

export const buckets = defineBuckets({
    note: {
        sprites: [
            {
                id: skin.sprites.note.id,
                x: 0,
                y: 0,
                w: 2,
                h: 2,
                rotation: 0,
            },
        ],
        unit: Text.MillisecondUnit,
    },
})
export const buckets = defineBuckets({
    note: {
        sprites: [
            {
                id: skin.sprites.note.id,
                x: 0,
                y: 0,
                w: 2,
                h: 2,
                rotation: 0,
            },
        ],
        unit: Text.MillisecondUnit,
    },
})

Setup Bucket Windows

Next, we need to set the bucket windows.

Intuitively, we would write the code in Note's preprocess. However there is an issue: preprocess will be called for every note entity, but bucket windows only need to be set once.

We could move our code to Initialization's preprocess, but that violates single responsibility principal.

How should we structure this code cleanly?

Global Preprocess Pattern

For any preprocess code that we only want to run once globally, let's write them in a new globalPreprocess class method.

For Note, that would be setting up the bucket windows (remember to convert windows to millisecond):

export class Note extends Archetype {
    // ...

    globalPreprocess() {
        const toMs = (window: JudgmentWindow) => ({
            min: window.min * 1000,
            max: window.max * 1000,
        })

        buckets.note.set({
            perfect: toMs(windows.perfect),
            great: toMs(windows.great),
            good: toMs(windows.good),
        })
    }

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

    globalPreprocess() {
        const toMs = (window) => ({
            min: window.min * 1000,
            max: window.max * 1000,
        })

        buckets.note.set({
            perfect: toMs(windows.perfect),
            great: toMs(windows.great),
            good: toMs(windows.good),
        })
    }

    // ...
}

Currently, globalPreprocess isn't called anywhere. Let's make Initialization call it:

export class Initialization extends Archetype {
    preprocess() {
        // ...

        archetypes.Note.globalPreprocess()
    }

    // ...
}
export class Initialization extends Archetype {
    preprocess() {
        // ...

        archetypes.Note.globalPreprocess()
    }

    // ...
}

Now Note's globalPreprocess will be called once globally, because there is only one Initialization.

However, for every new archetype that needs globalPreprocess, we will have to remember to add to Initialization. Instead, let's iterate over archetypes and call every one that has a globalPreprocess method:

export class Initialization extends Archetype {
    preprocess() {
        // ...

        for (const archetype of Object.values(archetypes)) {
            if (!('globalPreprocess' in archetype)) continue

            archetype.globalPreprocess()
        }
    }

    // ...
}
export class Initialization extends Archetype {
    preprocess() {
        // ...

        for (const archetype of Object.values(archetypes)) {
            if (!('globalPreprocess' in archetype)) continue

            archetype.globalPreprocess()
        }
    }

    // ...
}

That's the global preprocess pattern implemented.

Input Result

Lastly, let's set input result's bucket index and value:

export class Note extends Archetype {
    // ...
    touch() {
        // ...

        for (const touch of touches) {
            // ...

            this.result.bucket.index = buckets.note.index
            this.result.bucket.value = this.result.accuracy * 1000

            // ...
        }
    }

    // ...
}
export class Note extends Archetype {
    // ...
    touch() {
        // ...

        for (const touch of touches) {
            // ...

            this.result.bucket.index = buckets.note.index
            this.result.bucket.value = this.result.accuracy * 1000

            // ...
        }
    }

    // ...
}