space-game001/UI.md
2026-05-30 21:27:25 +03:00

16 KiB
Raw Blame History

UI System

UI layouts are defined in JSON files and loaded at runtime by UiManager. Each file has a single "root" node that is the top-level container.

The coordinate system has the origin at the bottom-left of the screen. Y increases upward. The virtual canvas size is defined by Environment::projectionWidth × Environment::projectionHeight.

{
    "root": { ... }
}

Common Node Properties

These properties are available on every node type.

Property Type Default Description
name string "" Unique name used to find the node from C++ code
x float 0 Horizontal offset from the parent's origin (or gravity-adjusted position)
y float 0 Vertical offset
width float | "match_parent" 0 Width in virtual pixels. "match_parent" fills the parent
height float | "match_parent" 0 Height in virtual pixels
horizontal_gravity "left" | "center" | "right" "left" Positions the node horizontally inside a FrameLayout parent
vertical_gravity "bottom" | "center" | "top" "bottom" Positions the node vertically inside a FrameLayout parent
visible bool true Whether the node (and all its children) are rendered and interactive. Can be toggled at runtime via setNodeVisible

Containers

FrameLayout

Children are positioned using absolute x/y offsets and/or horizontal_gravity / vertical_gravity.

{
    "type": "FrameLayout",
    "name": "hud_root",
    "width": "match_parent",
    "height": "match_parent",
    "children": [ ... ]
}
Property Type Default Description
children array [] Child nodes

LinearLayout

Children are stacked automatically in a row or column. Gravity and align properties control the layout of the block and its children.

{
    "type": "LinearLayout",
    "orientation": "vertical",
    "vertical_align": "center",
    "horizontal_align": "center",
    "spacing": 10,
    "width": 400,
    "height": 600,
    "children": [ ... ]
}
Property Type Default Description
orientation "vertical" | "horizontal" "vertical" Direction children are stacked
spacing float 0 Gap in pixels between consecutive children
vertical_align "top" | "center" | "bottom" "top" Vertical alignment of the child block inside this layout. For vertical orientation, controls how the whole stack is aligned; for horizontal orientation, controls each child's cross-axis alignment
horizontal_align "left" | "center" | "right" "left" Horizontal alignment of the child block. For horizontal orientation, controls how the whole row is aligned; for vertical orientation, controls each child's cross-axis alignment
children array [] Child nodes, laid out in order

Widgets

Button

An image-only clickable button. Swaps textures on hover/press.

{
    "type": "Button",
    "name": "closeButton",
    "width": 90,
    "height": 90,
    "x": 580,
    "y": 240,
    "horizontal_gravity": "center",
    "vertical_gravity": "center",
    "textures": {
        "normal":   "resources/w/ui/img/Close001_State=Default.png",
        "hover":    "resources/w/ui/img/Close001_State=Selected.png",
        "pressed":  "resources/w/ui/img/Close001_State=Tap.png",
        "disabled": "resources/w/ui/img/Close001_State=Disabled.png"
    },
    "border": 4,
    "clickZoneWidth": 80,
    "clickZoneHeight": 80
}
Property Type Default Description
textures.normal string Texture path shown in the default state (required)
textures.hover string Texture path shown when the mouse hovers
textures.pressed string Texture path shown while pressed
textures.disabled string Texture path shown when the button is disabled
border float 0 Inset (pixels) applied to the hit-test zone on all sides
clickZoneWidth float 0 Explicit hit-test width; 0 uses the widget width
clickZoneHeight float 0 Explicit hit-test height; 0 uses the widget height

C++ callbacks:

uiManager.setButtonCallback("closeButton", [](const std::string&) { /* click */ });
uiManager.setButtonPressCallback("closeButton", [](const std::string&) { /* press */ });

TextButton

A button that renders a text label on top of an optional background texture.

{
    "type": "TextButton",
    "name": "item1name",
    "width": 270,
    "height": 60,
    "text": "Main Quest",
    "fontSize": 32,
    "fontPath": "resources/fonts/DroidSans.ttf",
    "textCentered": false,
    "topAligned": false,
    "textPaddingX": 12,
    "textPaddingY": -8,
    "wrap": true,
    "color": [1.0, 1.0, 1.0, 1.0],
    "textures": {
        "normal":  "resources/w/red.png",
        "hover":   "resources/w/red.png",
        "pressed": "resources/w/red.png"
    },
    "border": 0,
    "clickZoneWidth": 0,
    "clickZoneHeight": 0
}
Property Type Default Description
text string "" Label text. Supports Cyrillic and any codepoint in resources/symbols.txt
fontSize int 32 Font size in pixels
fontPath string "resources/fonts/DroidSans.ttf" Path to the TTF font file
textCentered bool true Horizontally centers the text within the widget. When false, text starts at textPaddingX from the left edge
topAligned bool false When true, the first text line is placed near the top of the widget; when false, the text is vertically centered
textPaddingX float 12 Left padding when textCentered is false; also used to compute the wrapping width
textPaddingY float 0 Vertical offset applied to the text baseline
wrap bool false Wraps text that exceeds width - textPaddingX * 2 pixels
color [R, G, B, A] [1,1,1,1] Text color, each channel 0..1
textures.* string Background textures (all optional — button can be text-only)
border float 0 Hit-test inset
clickZoneWidth / clickZoneHeight float 0 Explicit hit-test size; 0 uses the widget size

C++ callbacks:

uiManager.setTextButtonCallback("item1name", [](const std::string&) { /* click */ });
uiManager.setTextButtonPressCallback("item1name", [](const std::string&) { /* press */ });

// Programmatic updates
uiManager.setTextButtonText("item1name", "New Quest Name");
uiManager.setTextButtonColor("item1name", {1.f, 0.f, 0.f, 1.f});

TextView

A non-interactive text display widget.

{
    "type": "TextView",
    "name": "quest_description",
    "x": 170,
    "y": 390,
    "width": 1000,
    "height": 300,
    "text": "Long description here.",
    "fontSize": 32,
    "fontPath": "resources/fonts/DroidSans.ttf",
    "textCentered": false,
    "topAligned": true,
    "wrap": true,
    "paddingX": 0,
    "paddingY": 4,
    "maxLines": 10,
    "color": [1.0, 1.0, 0.0, 1.0]
}
Property Type Default Description
text string "" Display text. Supports Cyrillic and any codepoint in resources/symbols.txt
fontSize int 32 Font size in pixels
fontPath string "resources/fonts/DroidSans.ttf" Path to the TTF font file
textCentered bool true Horizontally centers the text when true; left-aligns from paddingX when false
topAligned bool false When true, the first line is placed near the top edge; when false, text is vertically centered
wrap bool false Wraps text at width - paddingX * 2 pixels
paddingX float 0 Left/right padding used for alignment and wrap width
paddingY float 0 Vertical inset applied when topAligned is true
maxLines int 0 Maximum number of lines to display; 0 means unlimited. Truncated text gets ...
color [R, G, B, A] [1,1,1,1] Text color

Legacy note: If none of wrap, topAligned, paddingX, paddingY, or maxLines are set, the text is drawn centered on (x + width/2, y + height/2) for backward compatibility.

C++ updates:

uiManager.setText("quest_description", "New text here.");
uiManager.setTextColor("quest_description", {1.f, 1.f, 0.f, 1.f});

TextField

An interactive single-line text input field. Receives keyboard input when focused.

{
    "type": "TextField",
    "name": "playerName",
    "x": 100,
    "y": 300,
    "width": 400,
    "height": 50,
    "placeholder": "Enter name...",
    "fontSize": 28,
    "fontPath": "resources/fonts/DroidSans.ttf",
    "maxLength": 64,
    "color":             [1.0, 1.0, 1.0, 1.0],
    "placeholderColor":  [0.5, 0.5, 0.5, 1.0],
    "backgroundColor":   [0.2, 0.2, 0.2, 1.0],
    "borderColor":       [0.5, 0.5, 0.5, 1.0]
}
Property Type Default Description
placeholder string "" Text shown when the field is empty
fontSize int 32 Font size in pixels
fontPath string "resources/fonts/DroidSans.ttf" Path to the TTF font file
maxLength int 256 Maximum number of characters
color [R, G, B, A] [1,1,1,1] Input text color
placeholderColor [R, G, B, A] [0.5,0.5,0.5,1] Placeholder text color
backgroundColor [R, G, B, A] [0.2,0.2,0.2,1] Field background color
borderColor [R, G, B, A] [0.5,0.5,0.5,1] Border color

C++ callbacks and queries:

uiManager.setTextFieldCallback("playerName", [](const std::string& name, const std::string& value) {
    // called on every keystroke
});
std::string current = uiManager.getTextFieldValue("playerName");

Slider

A draggable slider that returns a normalized value in the range [0, 1].

{
    "type": "Slider",
    "name": "volumeSlider",
    "x": 100,
    "y": 200,
    "width": 40,
    "height": 300,
    "orientation": "vertical",
    "value": 0.75,
    "textures": {
        "track": "resources/w/ui/img/slider_track.png",
        "knob":  "resources/w/ui/img/slider_knob.png"
    }
}
Property Type Default Description
textures.track string Texture for the slider track
textures.knob string Texture for the draggable knob
orientation "vertical" | "horizontal" "vertical" Drag direction
value float 0 Initial normalized value [0, 1]

C++ callbacks:

uiManager.setSliderCallback("volumeSlider", [](const std::string& name, float value) {
    // value is 0..1
});
uiManager.setSliderValue("volumeSlider", 0.5f);

StaticImage

A non-interactive image. Supports optional fade-in and pulse-scale animations.

{
    "type": "StaticImage",
    "name": "background",
    "width": 1266,
    "height": 585,
    "horizontal_gravity": "center",
    "vertical_gravity": "center",
    "texture": "resources/w/ui/img/journal/QuestJournal003.png",
    "fadeIn": {
        "durationMs": 600
    },
    "pulse": {
        "minScale": 0.92,
        "maxScale": 1.08,
        "periodMs": 1500
    }
}
Property Type Default Description
texture string Path to the PNG texture
fadeIn.durationMs float If present, the image fades in over this many milliseconds each time the UI is shown
pulse.minScale float 0.9 Minimum scale during the pulse cycle
pulse.maxScale float 1.1 Maximum scale during the pulse cycle
pulse.periodMs float 1000 Duration of one full pulse cycle in milliseconds

C++ pop-in animation (scales the node from 0 → 1, ease-out quad):

uiManager.startPopIn("background", 300.0f);  // duration in milliseconds

Typically called immediately after making a node visible. The node is automatically removed from the animation list when the scale reaches 1.


Animations

Animations can be defined on Button and TextButton nodes and started from C++ code. Each animation is a named sequence of steps.

{
    "type": "Button",
    "name": "myButton",
    "width": 100,
    "height": 100,
    "textures": { "normal": "resources/w/btn.png" },
    "animations": {
        "bounce": {
            "repeat": false,
            "steps": [
                { "type": "move",  "to": [0, 20],   "duration": 0.15, "easing": "easeout" },
                { "type": "move",  "to": [0, 0],    "duration": 0.15, "easing": "easein"  },
                { "type": "wait",                    "duration": 0.1                        }
            ]
        },
        "pulse": {
            "repeat": true,
            "steps": [
                { "type": "scale", "to": [1.1, 1.1], "duration": 0.4, "easing": "easeout" },
                { "type": "scale", "to": [1.0, 1.0], "duration": 0.4, "easing": "easein"  }
            ]
        }
    }
}

Animation sequence properties

Property Type Default Description
repeat bool false Whether the sequence loops after the last step
steps array Ordered list of animation steps

Step properties

Property Type Description
type "move" | "scale" | "wait" Step kind
to [x, y] Target offset (move) or scale factors (scale)
duration float (seconds) Duration of the step. 0 applies the target instantly
easing "linear" | "easein" | "easeout" Interpolation curve (default "linear")

C++ control:

uiManager.startAnimationOnNode("myButton", "bounce");
uiManager.stopAnimationOnNode("myButton", "bounce");
uiManager.setAnimationCallback("myButton", "bounce", []() {
    // called when the non-repeating sequence finishes
});

C++ API Quick Reference

Loading and navigation

uiManager.loadFromFile("resources/w/ui/screen.json", renderer);
uiManager.pushMenuFromFile("resources/w/ui/popup.json", renderer);  // push on stack
uiManager.popMenu();       // restore previous UI
uiManager.clearMenuStack();

Finding nodes

auto node   = uiManager.findNode("myNode");
auto btn    = uiManager.findButton("myButton");
auto tbtn   = uiManager.findTextButton("item1name");
auto tv     = uiManager.findTextView("quest_description");
auto img    = uiManager.findStaticImage("background");
auto slider = uiManager.findSlider("volumeSlider");
auto tf     = uiManager.findTextField("playerName");

Visibility

uiManager.setNodeVisible("hint5", false);
bool visible = uiManager.getNodeVisible("hint5");

Pop-in animation

Scales a node from 0 to 1 using an ease-out curve. Useful for chat bubble reveals and similar "appear" effects.

uiManager.startPopIn("messageBubble", 300.0f);  // node name, duration ms

Set the node's scaleX/scaleY to 0 and call setNodeVisible before calling startPopIn to avoid a one-frame flash at full size.

Dynamic node repositioning

node->localY (and localX) can be modified directly on a node pointer, then a layout recalculation applied:

auto node = uiManager.findNode("messageBubble");
node->localY = 350.0f;          // new bottom-Y (for vertical_gravity: bottom nodes)
uiManager.updateAllLayouts();   // recomputes screenRect and rebuilds meshes

This is how the phone chat manager repositions bubbles as new messages arrive.

Per-frame update

uiManager.update(deltaMs);   // advance animations and fade-ins
uiManager.draw(renderer);    // render everything

Dialogue → UI integration (phone chat bubbles)

Dialogue nodes in JSON can carry a "bubbleSlot" field naming a StaticImage UI node. When the dialogue runtime presents that line, it fires the onBubbleSlotReady callback with the slot name, which the game uses to reveal the corresponding bubble image.

{
    "id": "line_1",
    "type": "Line",
    "speaker": "Айпери",
    "text": "...",
    "next": "line_2",
    "bubbleSlot": "message01in"
}

Lines without "bubbleSlot" (or with an empty value) do not trigger any UI change — useful for internal monologue lines that have no corresponding chat image.

C++ wiring:

dialogueSystem.setOnBubbleSlotReady([](const std::string& slotName) {
    // slotName == "message01in" etc.
    menuManager.revealPhoneChatBubble(slotName);
});