Sonolus Wiki

14. Input Judgment

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

Input Archetype

You may have already noticed, despite our notes being fully functional, Sonolus doesn't treat them like so: you can skip the entire level right away, and at the result screen it shows you 0 notes for all the statistics.

In order for an entity to be considered as a playable note by Sonolus, its archetype must have input.

export class Note extends Archetype {
    hasInput = true

    // ...
}
export class Note extends Archetype {
    hasInput = true

    // ...
}

With that set:

  • Sonolus will know and count all the entities with an archetype that has input.
  • Only when all input entities are despawned, will player allow to skip rest of the level.
  • Input entity gets access to Entity Input block, which code can use to tell Sonolus how the player did.
  • Sonolus will automatically calculate statistics like score, combo, Perfect count, etc, based on input results.
  • Related UI are also updated and animated when a new input result comes in.
  • Input results that specified buckets will also get judgment graphs at the result screen.

Input Result

To tell Sonolus how the player did on a note, we simply modify this.result.

For this.result.judgment, we can manually assign Judgment.Miss, Judgment.Perfect, Judgment.Great, or Judgment.Good. However, the better way is to simply use input.judge helper function.

For this.result.accuracy, we should assign the timing difference in seconds.

export class Note extends Archetype {
    // ...

    initialize() {
        // ...

        this.result.accuracy = windows.good.max
    }

    // ...
    touch() {
        // ...

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

            this.result.judgment = input.judge(touch.startTime, this.targetTime, windows)
            this.result.accuracy = touch.startTime - this.targetTime

            // ...
        }
    }

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

    initialize() {
        // ...

        this.result.accuracy = windows.good.max
    }

    // ...
    touch() {
        // ...

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

            this.result.judgment = input.judge(touch.startTime, this.targetTime, windows)
            this.result.accuracy = touch.startTime - this.targetTime

            // ...
        }
    }

    // ...
}

Judgment and Combo UI

It's also important to have judgment and combo UI to give player instant feedback while playing.

We set them up in Initialization:

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

        ui.judgment.set({
            anchor: { x: 0, y: -0.4 },
            pivot: { x: 0.5, y: 0 },
            size: new Vec(0, 0.15).mul(ui.configuration.judgment.scale),
            rotation: 0,
            alpha: ui.configuration.judgment.alpha,
            horizontalAlign: HorizontalAlign.Center,
            background: false,
        })

        ui.combo.value.set({
            anchor: { x: screen.r * 0.7, y: 0 },
            pivot: { x: 0.5, y: 0 },
            size: new Vec(0, 0.2).mul(ui.configuration.combo.scale),
            rotation: 0,
            alpha: ui.configuration.combo.alpha,
            horizontalAlign: HorizontalAlign.Center,
            background: false,
        })
        ui.combo.text.set({
            anchor: { x: screen.r * 0.7, y: 0 },
            pivot: { x: 0.5, y: 1 },
            size: new Vec(0, 0.12).mul(ui.configuration.combo.scale),
            rotation: 0,
            alpha: ui.configuration.combo.alpha,
            horizontalAlign: HorizontalAlign.Center,
            background: false,
        })
    }

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

        ui.judgment.set({
            anchor: { x: 0, y: -0.4 },
            pivot: { x: 0.5, y: 0 },
            size: new Vec(0, 0.15).mul(ui.configuration.judgment.scale),
            rotation: 0,
            alpha: ui.configuration.judgment.alpha,
            horizontalAlign: HorizontalAlign.Center,
            background: false,
        })

        ui.combo.value.set({
            anchor: { x: screen.r * 0.7, y: 0 },
            pivot: { x: 0.5, y: 0 },
            size: new Vec(0, 0.2).mul(ui.configuration.combo.scale),
            rotation: 0,
            alpha: ui.configuration.combo.alpha,
            horizontalAlign: HorizontalAlign.Center,
            background: false,
        })
        ui.combo.text.set({
            anchor: { x: screen.r * 0.7, y: 0 },
            pivot: { x: 0.5, y: 1 },
            size: new Vec(0, 0.12).mul(ui.configuration.combo.scale),
            rotation: 0,
            alpha: ui.configuration.combo.alpha,
            horizontalAlign: HorizontalAlign.Center,
            background: false,
        })
    }

    // ...
}

Lastly, let's also give our judgment and combo UI some animations to make them more lively:

export const ui: EngineConfigurationUI = {
    // ...
    judgmentAnimation: {
        // ...
        alpha: {
            from: 1,
            to: 0,
            duration: 0.2,
            ease: 'outCubic',
        },
    },
    comboAnimation: {
        scale: {
            from: 1.2,
            to: 1,
            duration: 0.2,
            ease: 'inCubic',
        },
        // ...
    },
    // ...
}
export const ui = {
    // ...
    judgmentAnimation: {
        // ...
        alpha: {
            from: 1,
            to: 0,
            duration: 0.2,
            ease: 'outCubic',
        },
    },
    comboAnimation: {
        scale: {
            from: 1.2,
            to: 1,
            duration: 0.2,
            ease: 'inCubic',
        },
        // ...
    },
    // ...
}