Sonolus Wiki

10. Note Drawing

In this chapter, we will implementing drawing logic of Note.

Visual Time

Let's calculate a note's visual time, which consists of minimum and maximum visual times, and refactor spawn time to use the minimum visual time:

export class Note extends Archetype {
    // ...

    visualTime = this.entityMemory({
        min: Number,
        max: Number,
    })

    // ...

    preprocess() {
        // ...

        this.visualTime.max = this.targetTime
        this.visualTime.min = this.visualTime.max - 1

        this.spawnTime = this.visualTime.min
    }

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

    visualTime = this.entityMemory({
        min: Number,
        max: Number,
    })

    // ...

    preprocess() {
        // ...

        this.visualTime.max = this.targetTime
        this.visualTime.min = this.visualTime.max - 1

        this.spawnTime = this.visualTime.min
    }

    // ...
}

Declaring

Just like Stage, we use the standard sprite for our note for simplicity:

export const skin = defineSkin({
    sprites: {
        // ...
        note: SkinSpriteName.NoteHeadCyan,
    },
})
export const skin = defineSkin({
    sprites: {
        // ...
        note: SkinSpriteName.NoteHeadCyan,
    },
})

Drawing

With both minimum and maximum visual time, as well as the current time, we can calculate note's y position. Because we transformed screen coordinate system to go from y = 0 (top of note spawn) to y = 1 (judgment line), this greatly simplifies our calculation:

export class Note extends Archetype {
    // ...

    updateParallel() {
        const y = Math.unlerp(this.visualTime.min, this.visualTime.max, time.now)
    }
}
export class Note extends Archetype {
    // ...

    updateParallel() {
        const y = Math.unlerp(this.visualTime.min, this.visualTime.max, time.now)
    }
}

With y position calculated, we can draw the note:

export class Note extends Archetype {
    // ...

    updateParallel() {
        // ...

        const layout = Rect.one.mul(note.radius).scale(1, -1).translate(0, y)

        skin.sprites.note.draw(layout, 0, 1)
    }
}
export class Note extends Archetype {
    // ...

    updateParallel() {
        // ...

        const layout = Rect.one.mul(note.radius).scale(1, -1).translate(0, y)

        skin.sprites.note.draw(layout, 0, 1)
    }
}

Z Fighting

While this is working already, there is a hidden issue we have not solved yet: z fighting.

It is referred to when multiple objects are rendered with the same z value, their ordering may not be consistent from frame to frame and can be flickering if they overlap.

To solve this, let's set the z order to be 1000 minus target time, so that earlier notes will always be on top of later notes.

This is also an unchanging property of the note, so we should calculate it once and store it in Entity Memory to reuse. However this time we are going to calculate it in initialize rather than preprocess, since it is not used for spawning logic so we can defer the calculation to improve level load time:

export class Note extends Archetype {
    // ...

    z = this.entityMemory(Number)

    // ...

    initialize() {
        this.z = 1000 - this.targetTime
    }

    updateParallel() {
        // ...

        skin.sprites.note.draw(layout, this.z, 1)
    }
}
export class Note extends Archetype {
    // ...

    z = this.entityMemory(Number)

    // ...

    initialize() {
        this.z = 1000 - this.targetTime
    }

    updateParallel() {
        // ...

        skin.sprites.note.draw(layout, this.z, 1)
    }
}