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):
// ...
const toMs = ({ min, max }: Range) => new Range(Math.round(min * 1000), Math.round(max * 1000))
export const bucketWindows = {
perfect: toMs(windows.perfect),
great: toMs(windows.great),
good: toMs(windows.good),
}
// ...
const toMs = ({ min, max }) => new Range(Math.round(min * 1000), Math.round(max * 1000))
export const bucketWindows = {
perfect: toMs(windows.perfect),
great: toMs(windows.great),
good: toMs(windows.good),
}
export class Note extends Archetype {
// ...
globalPreprocess() {
buckets.note.set(bucketWindows)
}
// ...
}
export class Note extends Archetype {
// ...
globalPreprocess() {
buckets.note.set(bucketWindows)
}
// ...
}
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
// ...
}
}
// ...
}