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)
}
}