Create a SpriteStack component
This guide explains how to create a visual sub-component by extending SpriteStack. We build GiftRow, the row that displays a claimed gift (name on the left, code on the right). It builds on the composition guide: you already have a scene with a Composition that draws the header and frame.
Principle
A sub-component is a class that extends SpriteStack and groups related sprites together: a background, one or more texts, an icon -- together they form a single reusable visual building block.
All positions inside a SpriteStack are relative to the stack's origin, defined by super(viewport, x, y). The component does not know its absolute position on screen; it only reasons in local coordinates.
The Composition creates and orchestrates sub-components. It pushes them into its own stack via push_sprite for lifecycle management (automatic dispose), but each sub-component remains an independent SpriteStack with its own internal state.
SpriteStack API
The key methods for building a SpriteStack's content:
add_background
@background = add_background('mystery_gift/gift_row')
add_backgroundloads a sprite from the interface cache and adds it at position (0, 0) of the stack.
add_sprite
@icon = add_sprite(120, 6, 'mystery_gift/icon')
add_sprite(x, y, filename)adds a sprite at position (stack.x + x, stack.y + y).- Coordinates are relative to the stack's origin.
add_text
@label = add_text(8, 4, 80, 16, 'Hello', color: 10)
add_text(x, y, w, h, text, color: N)adds text with a palette color.- The
alignparameter (3rd positional argument after the text) controls alignment: 0 = left (default), 1 = center, 2 = right. - Warning: this method applies the FOY offset (see below).
push_sprite
custom_sprite = Sprite.new(@viewport)
push_sprite(custom_sprite)
push_sprite(sprite)adds an externally created sprite to the stack, without modifying its position.- Useful for integrating a sub-component SpriteStack into a parent's lifecycle.
set_rect_div
@background.set_rect_div(0, 0, 2, 1)
set_rect_div(col, row, ncols, nrows)slices the bitmap into a grid and displays a single cell.- The arguments are: column, row, total number of columns, total number of rows.
- Used for spritesheets: one image contains multiple visual states, only one is displayed at a time.
The FOY trap
add_text subtracts 2 pixels from the Y position passed as parameter. FOY stands for Font Offset Y. In practice, a call like add_text(10, 20, ...) places the text at y = 18, not y = 20. This 2-pixel offset is systematic and invisible in the code. For pixel-perfect placement, account for it: if text should sit at y = 20, pass y = 22.
The visible= trap
Setting visible = true on a SpriteStack propagates to all children, including ones you intentionally hid. If some children must stay hidden, override visible=:
# Set visibility of the component
# @param value [Boolean]
def visible=(value)
super(value)
@hidden_sprite.visible = false
end
super(value)propagates visibility to all children of the stack.- Then
@hidden_sprite.visible = falseforces the sprite to stay hidden. - Our GiftRow does not need this override, but it is a common pitfall worth knowing.
The GiftRow component
Here is the complete GiftRow component. It displays a row with the gift name on the left and the code on the right.
Create scripts/20 MysteryGift/002 UI/000 GiftRow.rb:
module UI
module MysteryGift
# A single row displaying a claimed gift (name left, code right)
class GiftRow < SpriteStack
# Padding inside the row
ROW_PADDING = 8
# Create a new GiftRow
# @param viewport [Viewport]
# @param x [Integer] x position
# @param y [Integer] y position
def initialize(viewport, x, y)
super(viewport, x, y, default_cache: :interface)
create_background
create_name_text
create_code_text
self.selected = false
end
# Tell if the mouse is hovering this row
# @return [Boolean]
def hovered?
return @background.simple_mouse_in?
end
# Set the selected state of the row
# @param value [Boolean]
def selected=(value)
@selected = value
@background.set_rect_div(value ? 1 : 0, 0, 2, 1)
end
# Update the displayed gift data
# @param gift [Hash, nil] the gift data (with :name and :code) or nil to hide
def data=(gift)
if gift
@name_text.text = gift[:name] || '???'
@code_text.text = gift[:code] || ''
self.visible = true
else
self.visible = false
end
end
private
# Create the row background sprite
def create_background
@background = add_sprite(0, 0, 'mystery_gift/gift_row')
@background.set_rect_div(0, 0, 2, 1)
end
# Create the gift name text (left-aligned)
def create_name_text
text_width = (GIFT_ROW_WIDTH - ROW_PADDING * 2) / 2
@name_text = add_text(ROW_PADDING, 4, text_width, 16, '---', color: 10)
end
# Create the gift code text (right-aligned)
def create_code_text
text_width = (GIFT_ROW_WIDTH - ROW_PADDING * 2) / 2
right_x = GIFT_ROW_WIDTH - ROW_PADDING - text_width
@code_text = add_text(right_x, 4, text_width, 16, '', 2, nil, color: 9)
end
end
end
end
- The constructor receives a viewport and a position;
super(viewport, x, y, default_cache: :interface)sets the stack's origin and the default sprite cache. create_backgroundusesadd_sprite(0, 0, ...)instead ofadd_backgroundso thatset_rect_divcan be called afterward. The sprite is thegift_row2×1 spritesheet (column 0 = normal, column 1 = selected), set up in the setup guide.selected=toggles the background appearance between the two spritesheet columns. The constructor callsself.selected = falseto initialize the visual state.hovered?delegates hit-testing to the background sprite viasimple_mouse_in?-- the component owns its own hover detection (used by the mouse guide).data=is the public API for updating the displayed content. It receives either a hash with:nameand:code, ornilto hide the row. The scene never touches the texts directly; it goes through this method (used from the PFM guide on).create_name_textcreates left-aligned text (default alignment) with color 10 (white);create_code_textcreates right-aligned text (alignment2) with color 9 (grey). Thenilbetween alignment andcolor:is the font_id (default font).
Integration in the Composition
The Composition creates a fixed number of GiftRow slots and pushes them into its own stack. Update scripts/20 MysteryGift/002 UI/999 Composition.rb:
module UI
module MysteryGift
# Visual orchestrator for the Mystery Gift UI
class Composition < SpriteStack
# Create the composition
# @param viewport [Viewport]
def initialize(viewport)
super(viewport, 0, 0, default_cache: :interface)
create_header
create_frame
create_gift_rows
end
# Update the composition each frame
def update; end
# Tell if all animations are done
# @return [Boolean]
def done?
return true
end
private
# Create the header bar and title
def create_header
@header = add_sprite(0, HEADER_Y, 'mystery_gift/header')
@header.set_z(2)
@title = add_text(0, 0, 320, 14, ext_text(TEXT_FILE_ID, TEXT_TITLE), 1, nil, color: 10)
@title.z = 3
end
# Create the main content frame
def create_frame
@frame = add_sprite(FRAME_X, FRAME_Y, 'mystery_gift/frame')
end
# Create the gift row slots
def create_gift_rows
@rows = Array.new(VISIBLE_ROWS) do |index|
row = GiftRow.new(@viewport, GIFT_ROW_X, GIFT_ROW_Y + index * GIFT_ROW_PITCH)
push_sprite(row)
end
end
end
end
end
Array.new(VISIBLE_ROWS)creates a fixed number of rows, positioned vertically with regular spacing (GIFT_ROW_PITCH). Each row is created with computed coordinates -- the sub-component does not know its position in the list.push_sprite(row)adds each GiftRow to the Composition's stack for lifecycle management: when the Composition is disposed, the GiftRows are disposed too.- For now the rows show their placeholder text (
---). In the next guide we add the data layer and fill the rows with the player's claimed gifts; the keyboard and mouse guides then add selection and hover, which is why GiftRow already exposesselected=andhovered?.
Try it
Reopen the scene:
GamePlay.open_mystery_gift
You should now see six empty rows (showing ---) inside the frame, under the title.
Conclusion
- A sub-component extends SpriteStack and groups related sprites together (background, texts, icons).
- All internal positions are relative to the stack's origin set in
super(viewport, x, y). - Use
set_rect_div(col, row, ncols, nrows)for spritesheets: one image holds multiple visual states. add_textapplies a FOY offset of 2 pixels -- account for it in pixel-perfect layouts.visible=propagates to all children -- override it if some must stay hidden.- The sub-component owns its hit-testing (
simple_mouse_in?) and exposes a public API (data=,selected=) so the Composition can drive it. - The Composition orchestrates sub-components: it creates them, stores them, and (later) exposes finder methods for the scene.