Sonolus Wiki

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
        // ...
    }
}