gamedev-skills/game-ui-ux
> based responsive layout, resolution/aspect scaling and safe areas, keyboard/gamepad focus navigation, a screen/menu state stack, and event-driven (not polled) HUD updates. Engine- neutral patterns that pair with the detected engine's UI skill. Use when the user mentions HUD, health bar, main menu, pause menu, settings screen, UI layout, anchors, UI scaling, aspect ratio, safe area, controller/keyboard menu navigation, or wiring UI to game state.
npx skills add https://github.com/gamedev-skills/awesome-gamedev-agent-skills --skill game-ui-ux
Build HUDs and menus that stay correct on a phone, an ultrawide monitor, and a TV across a
gamepad and a mouse. This skill owns the engine-neutral UI architecture — responsive layout,
scaling, focus navigation, screen flow, and how UI talks to game state — and defers the
concrete widget API to the engine UI skill.
shop screen, or any overlay, and you want it to scale and navigate correctly.
can't be used with a controller, or is wired to game state by per-frame polling.
When *not* to use: for the engine's concrete UI nodes/components and styling, use
godot-ui-control or Unity UI (UGUI/UI Toolkit). For *visual* punch (button pop, damage
numbers, shake) use game-feel. For branching conversation UI use dialogue-systems. For
translating UI strings, that is localization (see references/ and input-systems for
rebinding screens). For card/board layout specifics, the card-game genre composes this skill.
edges/corners/center and let containers (rows, columns, grids) flow children. Absolute
(x, y) positions break at the first new resolution.
(most games), plus a policy for extra width/height on other aspect ratios (letterbox,
expand, or anchor HUD corners outward).
and TV overscan don't clip it.
define focus order/neighbors, and show a clear focus highlight. Mouse and focus must coexist.
handed to the top screen. This makes overlays and "back" trivial.
health_changed,score_changed, etc. and updates only when they fire — it does not read game state every
frame.
mouse and navigate by gamepad only, and confirm focus, scaling, and safe-area insets. Report
what you actually observed at which resolutions.
# Godot 4.x. Anchor a HUD label to the TOP-LEFT; let a container flow a row of hearts.
func _ready() -> void:
$Score.set_anchors_preset(Control.PRESET_TOP_LEFT) # sticks to the corner at any size
# An HBoxContainer auto-lays-out children left-to-right; never position hearts by hand.
for i in lives:
$Hearts.add_child(make_heart()) # HBoxContainer spaces them for you
# Unity 6 uGUI: set RectTransform anchors to the corner; use a HorizontalLayoutGroup.
# RIGHT: anchors + layout groups. WRONG: rect.anchoredPosition = new Vector2(640, 360) (1080p-only).
# Godot 4.x — Project Settings > Display > Window > Stretch:
# Mode = "canvas_items", Aspect = "expand", reference size e.g. 1920x1080.
# UI scales to the window; "expand" reveals extra space you anchor HUD corners into.
# Unity 6 — Canvas > CanvasScaler:
# UI Scale Mode = "Scale With Screen Size", Reference Resolution = 1920x1080,
# Match = 0.5 (blend width/height) — pick 1.0 if your HUD is height-critical.
# Godot 4.x. Inset a margin container to the OS-reported safe rect (phones, TVs).
func _apply_safe_area() -> void:
var safe: Rect2i = DisplayServer.get_display_safe_area()
var win := DisplayServer.window_get_size()
$Margin.add_theme_constant_override("margin_left", safe.position.x)
$Margin.add_theme_constant_override("margin_top", safe.position.y)
$Margin.add_theme_constant_override("margin_right", win.x - safe.end.x)
$Margin.add_theme_constant_override("margin_bottom", win.y - safe.end.y)
# Unity 6: read Screen.safeArea (Rect in pixels) and set a panel's anchorMin/anchorMax to
# safeArea.position / (position+size) normalized by Screen.width/height.
# Godot 4.x. Give each screen a default focus and wire neighbors so a stick/d-pad walks it.
func _on_screen_shown() -> void:
$PlayButton.grab_focus() # always focus SOMETHING on open
$PlayButton.focus_neighbor_bottom = $SettingsButton.get_path()
$SettingsButton.focus_neighbor_top = $PlayButton.get_path()
# Unity 6: EventSystem.SetSelectedGameObject(playButton) on enable; set each Selectable's
# Navigation (Explicit or Automatic). RIGHT: a control is focused on open. WRONG: nothing
# selected → the gamepad does nothing and the player is stuck.
# RIGHT: HUD reacts to a signal; it updates only when health actually changes.
func _ready() -> void:
player.health_changed.connect(_on_health_changed) # emitted by gameplay
func _on_health_changed(current: int, max: int) -> void:
$HealthBar.value = float(current) / max
# WRONG: func _process(dt): $HealthBar.value = player.hp / player.max_hp # polls every frame,
# couples UI to the player's internals, and runs work even when nothing changed.
everywhere else. Anchor to edges/center and flow with containers.
Decide expand vs letterbox and anchor HUD to corners that move outward.
on a menu with nothing selected. Always focus one control and define navigation.
_process/Update. Couples UI to internals and wastes work. Pushupdates via signals/events.
UI and offer a text-size option.
isPaused, inSettings, …) becomes unmanageable. Use ascreen stack with push/pop.
strings and let containers size to content (see references/).
screen-stack pattern, diegetic vs non-diegetic UI, accessibility (text size, contrast,
colorblind-safe state), and localization-ready layout, read references/layout-and-flow.md.
godot-ui-control, Unity UI (UGUI/UI Toolkit) — the concrete widgets, themes, and styling.game-feel — button pops, transitions, and HUD juice that ride on top of this layout.dialogue-systems — conversation/choice UI that lives inside this UI shell.input-systems — device switching, rebinding screens, and accessible controls.rpg, card-game, tower-defense, visual-novel — UI-heavy genres that compose this skill.Take gamedev-skills/game-ui-ux from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.