Skip to content

Skins & Sprites

Skins define sprites (images) that can be drawn in Sonolus. An engine typically has a single skin used by all modes.

Declaration

Skins are declared with the @skin decorator. Standard Sonolus sprites are declared by using a value from StandardSprite as the type hint. Custom sprites may also be defined using the Sprite type hint and the sprite function.

from sonolus.script.sprite import skin, StandardSprite, sprite, Sprite


@skin
class Skin:
    tap_note: StandardSprite.NOTE_HEAD_CYAN

    custom_sprite: Sprite = sprite("name_of_custom_sprite")

Render Mode

The skin also defines the render mode for sprites. This is done by defining a render_mode attribute in a skin class to a value from RenderMode.

from sonolus.script.sprite import skin, RenderMode


@skin
class Skin:
    render_mode = RenderMode.LIGHTWEIGHT
    ...

The three render modes available are:

  • RenderMode.LIGHTWEIGHT: Less taxing and well suited for engines implementing realistic 3D-like graphs, but is less accurate for some engines.
  • RenderMode.STANDARD: Slower, but more accurate for some engines and works better with some special cases such as drawing sprites in a triangular shape.
  • RenderMode.DEFAULT: Use either LIGHTWEIGHT or STANDARD based on the user's preferences. This is the default mode, and is suitable for engines where STANDARD is preferred, but are still playable in LIGHTWEIGHT when more performance is desired.

Drawing a Sprite

To draw a sprite, you can use the draw method of the sprite. This method accepts a Quad object that defines the position of the sprite on the screen, as well as an optional z-index to control the rendering order of the sprite and an alpha (transparency) value:

from sonolus.script.sprite import Sprite
from sonolus.script.quad import Quad
from sonolus.script.vec import Vec2

my_sprite: Sprite = ...
my_quad = Quad(
    tl=Vec2(-0.5, 0.5),
    tr=Vec2(0.5, 0.5),
    bl=Vec2(-0.5, -0.5),
    br=Vec2(0.5, -0.5),
)
my_sprite.draw(my_quad, z=123.4, a=1.0)

Z-index (z) is important to set correctly, as it ensures that sprites overlap in the correct order. It's especially important for two sprites that may overlap to have different z-index values, or they may conflict and render incorrectly (z-fighting).

Alpha (a) is a value between 0.0 (fully transparent) and 1.0 (fully opaque). It controls the transparency of the sprite when drawn. If not provided, it defaults to 1.0.

Checking Sprite Availability

Some skins may not have some sprites available, especially custom sprites. To check if a sprite is available, you can use the is_available property:

from sonolus.script.sprite import Sprite

my_sprite: Sprite = ...

if my_sprite.is_available:
    # The sprite is available, you can use it.
    ...
else:
    # Do something else, such as using a fallback.
    ...

Layers

To manage z-indexes, most engines will probably want to have some concept of a "layer" to categorize which sprites should be drawn on top of others. Additionally, some offset computed based on position is useful to ensure that two sprites on the same layer always have distinct z-indexes to prevent z-fighting.

For example, the pydori engine uses the following code to define layers:

LAYER_STAGE = 0
LAYER_LANE = 1
LAYER_JUDGE_LINE = 2

LAYER_CONNECTOR = 10

LAYER_PREVIEW_COVER = 20
LAYER_MEASURE_LINE = 21
LAYER_SIM_LINE = 22
LAYER_TIME_LINE = 23
LAYER_BPM_CHANGE_LINE = 24
LAYER_TIMESCALE_CHANGE_LINE = 25

LAYER_NOTE_HEAD = 30
LAYER_NOTE = 31
LAYER_ARROW = 32


def get_z(layer: int, lane: float = 0, y: float = 0) -> float:
    """Calculate z-index based on layer, lane, and y-coordinate.

    Lane and y are used to prevent z-fighting between sprites in the same layer.
    """
    return layer * 10000 + lane * 100 + y