11. Note Input
In this chapter, we will implementing input logic of Note.
Basic Input
Let's first do the very basic input: if player taps, note despawns.
In touch
, we can loop through touches to look for one that just started. If found, despawn and return.
To prevent note being drawn in updateParallel
on the frame when it's scheduled to despawn, we add a simple despawn check.
export class Note extends Archetype {
// ...
touch() {
for (const touch of touches) {
if (!touch.started) continue
this.despawn = true
return
}
}
updateParallel() {
if (this.despawn) return
// ...
}
}
export class Note extends Archetype {
// ...
touch() {
for (const touch of touches) {
if (!touch.started) continue
this.despawn = true
return
}
}
updateParallel() {
if (this.despawn) return
// ...
}
}
Judgment Windows
While it works now, it definitely isn't the way rhythm games work normally.
A note can only be tapped when the time is in note's judgment windows: if the time is too early, tapping won't trigger it; if the time too late, it's considered a Miss and the note will despawn by itself.
For our engine, let's say if you tap within 50 ms
of the target time you get a Perfect, 100 ms
for Great, 200 ms
for Good, and anything higher is considered not registered/Miss:
export const windows = {
perfect: {
min: -0.05,
max: 0.05,
},
great: {
min: -0.1,
max: 0.1,
},
good: {
min: -0.2,
max: 0.2,
},
}
export const windows = {
perfect: {
min: -0.05,
max: 0.05,
},
great: {
min: -0.1,
max: 0.1,
},
good: {
min: -0.2,
max: 0.2,
},
}
Input Offset
When player physically touches the screen, there is a delay until it registers in Sonolus and broadcasts it via touch
callback. This mostly comes from hardware delay and is unavoidable.
Input offset is what allows players to tell Sonolus to take that into account.
For example, player touches the screen at 00:01.00
, it takes some time and it reaches at 00:01.06
. If player calibrates their input correctly and gives you an input offset of 0.06
, engine can then subtract it from touch time, and correctly judge player based on their real touch time of 00:01.00
.
Input offset is already taken into account in time values of touches by Sonolus. However, other aspects where input is involved, engines still need to do it manually, and ensure a fair gameplay is given to all players.
Early Input
Let's first calculate the earliest time player can tap:
export class Note extends Archetype {
// ...
inputTime = this.entityMemory({
min: Number,
})
// ...
initialize() {
this.inputTime.min = this.targetTime + windows.good.min + input.offset
// ...
}
// ...
}
export class Note extends Archetype {
// ...
inputTime = this.entityMemory({
min: Number,
})
// ...
initialize() {
this.inputTime.min = this.targetTime + windows.good.min + input.offset
// ...
}
// ...
}
Then let's make touch
only run after minimum input time:
export class Note extends Archetype {
// ...
touch() {
if (time.now < this.inputTime.min) return
// ...
}
// ...
}
export class Note extends Archetype {
// ...
touch() {
if (time.now < this.inputTime.min) return
// ...
}
// ...
}
Late Input
Similar to early input, let's calculate the latest time player can tap:
export class Note extends Archetype {
// ...
inputTime = this.entityMemory({
// ...
max: Number,
})
// ...
initialize() {
// ...
this.inputTime.max = this.targetTime + windows.good.max + input.offset
// ...
}
// ...
}
export class Note extends Archetype {
// ...
inputTime = this.entityMemory({
// ...
max: Number,
})
// ...
initialize() {
// ...
this.inputTime.max = this.targetTime + windows.good.max + input.offset
// ...
}
// ...
}
Let's make note despawn automatically if time is already past maximum input time.
export class Note extends Archetype {
// ...
updateParallel() {
if (time.now > this.inputTime.max) this.despawn = true
// ...
}
}
export class Note extends Archetype {
// ...
updateParallel() {
if (time.now > this.inputTime.max) this.despawn = true
// ...
}
}