Skip to content

10 Note Drawing

In this chapter, we will implement the drawing logic for Note.

Visual Time

Let's calculate a note's visual interval, which contains its start and end times, and refactor the spawn time to use the start of that Interval. In guide/play/note.py, add the Interval import and declare visual_time immediately after target_time, before spawn_time. Then replace the existing visual and spawn-time assignments in preprocess with:

python
from sonolus.script.interval import Interval


class Note(PlayArchetype):
    # ...

    target_time: float = entity_data()
    visual_time: Interval = entity_data()
    spawn_time: float = entity_data()

    def preprocess(self):
        # ...

        self.visual_time.end = self.target_time
        self.visual_time.start = self.target_time - 1
        self.spawn_time = self.visual_time.start

The interval starts one second before the target time and ends at the target time.

Declaring

Just like Stage, we use a StandardSprite for our note. In guide/lib/skin.py, add note below judge_line:

python
class Skin:
    judge_line: StandardSprite.JUDGMENT_LINE
    note: StandardSprite.NOTE_HEAD_CYAN

Drawing

With both ends of the visual interval and the current time, we can calculate the note's y position. The time import already exists in guide/play/note.py. Add update_parallel with the initial position calculation:

python
class Note(PlayArchetype):
    # ...

    def update_parallel(self):
        y = self.visual_time.unlerp(time())

unlerp returns the position of a value within an interval: 0 at visual_time.start, 1 at visual_time.end, and a proportional value between them. Therefore, y moves from 0 at the note spawn position to 1 at the judgment line.

To draw the note, add these imports:

python
from sonolus.script.quad import Rect
from sonolus.script.vec import Vec2

from guide.lib.layout import Config
from guide.lib.skin import Skin

Then extend update_parallel with the layout and draw call:

python
class Note(PlayArchetype):
    # ...

    def update_parallel(self):
        # ...

        layout = Rect.from_center(Vec2(0, y), Vec2(2 * Config.note_radius, -2 * Config.note_radius))
        Skin.note.draw(layout, z=1)

Z Fighting

Although this already works, there is a hidden issue: z-fighting.

When multiple objects are rendered with the same z value, their ordering may not be consistent from frame to frame. They may flicker if they overlap.

Later values in a z-index break ties between objects whose earlier values are equal. We can use the first value for the note layer and the negative target time as a tie-breaker, so earlier notes will always be on top of later notes:

python
class Note(PlayArchetype):
    # ...

    def update_parallel(self):
        # ...

        Skin.note.draw(layout, z=(1, -self.target_time))

Reopen the development level. The note should travel from the top of the play area to the judgment line in one second.