Godot Game Ui | Skills Pool
Godot Game Ui Expert skill for building game user interfaces in Godot Engine. Use when creating game UI, Control nodes, Godot themes, HUD, game menus, inventory UI, health bars, dialog boxes, pause menus, settings screens, responsive game UI, StyleBox, CanvasLayer, or any Godot UI component. Triggers include 'Godot UI', 'game UI', 'game menu', 'HUD', 'inventory', 'dialog system', 'pause menu', 'settings screen', 'Control nodes', 'theme editor'.
npx skills add aiguy611/cc-tools
스타 0
업데이트 2026. 4. 13.
직업
Quick Start
Basic UI Scene Structure CanvasLayer (layer=10)
└── Control (full rect anchor)
├── MarginContainer
│ └── VBoxContainer
│ ├── Label (title)
│ ├── HBoxContainer (buttons)
│ └── Panel (content)
└── ColorRect (background)
Minimal HUD Setup ## hud.gd - Basic HUD controller
extends CanvasLayer
@onready var health_bar: ProgressBar = $Control/HealthBar
@onready var score_label: Label = $Control/ScoreLabel
func update_health(current: int, max_health: int) -> void:
health_bar.value = float(current) / float(max_health) * 100
func update_score(score: int) -> void:
score_label.text = "Score: %d" % score
Control Node Reference
Base Controls Node Purpose Key Properties ControlBase UI node anchor_*, offset_*, size, custom_minimum_sizeContainerBase container Auto-arranges children PanelVisual background Uses StyleBox for appearance ColorRectSolid color fill colorTextureRectImage display texture, stretch_mode, expand_mode
Layout Containers Node Purpose Direction HBoxContainerHorizontal layout Left to right VBoxContainerVertical layout Top to bottom GridContainerGrid layout columns propertyFlowContainerWrap layout HFlow or VFlow CenterContainerCenter single child Both axes MarginContainerAdd margins All sides via theme PanelContainerPanel + margins Combines Panel + Margin AspectRatioContainerMaintain ratio ratio, stretch_modeSplitContainerResizable split HSplit or VSplit ScrollContainerScrollable area horizontal_scroll_mode, vertical_scroll_modeTabContainerTabbed pages Manages child visibility SubViewportContainerEmbed SubViewport For 3D in UI or effects
Interactive Controls Node Purpose Key Signals ButtonClickable button pressed, button_down, button_upTextureButtonImage button Same as Button LinkButtonHyperlink style Same as Button CheckBoxToggle checkbox toggled(pressed: bool)CheckButtonToggle switch Same as CheckBox OptionButtonDropdown select item_selected(index: int)MenuButtonDropdown menu PopupMenu access SpinBoxNumber input value_changed(value: float)SliderRange slider HSlider/VSlider, value_changed RangeBase for sliders value, min_value, max_value, step
Text Controls Node Purpose Key Features LabelStatic text text, horizontal_alignment, autowrap_modeRichTextLabelFormatted text BBCode, [b], [color], [url] LineEditSingle-line input text_changed, text_submittedTextEditMulti-line input Full text editor features CodeEditCode editor Syntax highlighting, line numbers
Progress & Bars Node Purpose Properties ProgressBarProgress indicator value, min_value, max_value, show_percentageTextureProgressBarTextured progress texture_under, texture_progress, fill_mode
Node Purpose Key Methods PopupBase popup popup(), popup_centered()PopupMenuContext menu add_item(), add_separator()PopupPanelPanel popup Popup with Panel background WindowFloating window title, always_on_topAcceptDialogOK dialog dialog_text, ok_button_textConfirmationDialogOK/Cancel cancel_button_textFileDialogFile picker file_mode, access, filters
Item Lists Node Purpose Key Features ItemListIcon/text list Selection modes, icons, multi-select TreeHierarchical list TreeItems, columns, editable cells GraphEditNode graph For visual scripting UIs
Layout System
Anchor Presets # Common anchor presets
Control.PRESET_TOP_LEFT # (0, 0) - (0, 0)
Control.PRESET_TOP_RIGHT # (1, 0) - (1, 0)
Control.PRESET_BOTTOM_LEFT # (0, 1) - (0, 1)
Control.PRESET_BOTTOM_RIGHT # (1, 1) - (1, 1)
Control.PRESET_CENTER # (0.5, 0.5) - (0.5, 0.5)
Control.PRESET_FULL_RECT # (0, 0) - (1, 1) Full screen
# Apply preset
control.set_anchors_preset(Control.PRESET_FULL_RECT)
Sizing Flags # Container child sizing
control.size_flags_horizontal = Control.SIZE_EXPAND_FILL
control.size_flags_vertical = Control.SIZE_SHRINK_CENTER
# Flag values
Control.SIZE_FILL # Fill available space
Control.SIZE_EXPAND # Expand proportionally
Control.SIZE_EXPAND_FILL # Both fill and expand
Control.SIZE_SHRINK_BEGIN # Shrink to start
Control.SIZE_SHRINK_CENTER # Shrink to center
Control.SIZE_SHRINK_END # Shrink to end
Minimum Size # Set minimum size (prevents shrinking below)
control.custom_minimum_size = Vector2(100, 50)
# For containers, call after adding children
container.reset_size()
Responsive Layout Example ## responsive_container.gd
extends Control
@export var mobile_breakpoint: int = 768
@onready var content: BoxContainer = $Content
func _ready() -> void:
get_viewport().size_changed.connect(_on_viewport_size_changed)
_update_layout()
func _on_viewport_size_changed() -> void:
_update_layout()
func _update_layout() -> void:
var viewport_width := get_viewport_rect().size.x
if viewport_width < mobile_breakpoint:
# Mobile: vertical layout
if content is HBoxContainer:
_convert_to_vbox()
else:
# Desktop: horizontal layout
if content is VBoxContainer:
_convert_to_hbox()
func _convert_to_vbox() -> void:
var children := content.get_children()
var new_container := VBoxContainer.new()
new_container.name = "Content"
for child in children:
content.remove_child(child)
new_container.add_child(child)
content.queue_free()
add_child(new_container)
content = new_container
func _convert_to_hbox() -> void:
# Similar logic for horizontal
pass
Theme System
Theme Resource Structure Theme Resource
├── Colors (Color properties)
├── Constants (int properties: margins, spacing)
├── Fonts (FontFile or SystemFont)
├── Font Sizes (int)
├── Icons (Texture2D)
└── Styles (StyleBox variations)
StyleBox Types Type Use Case Properties StyleBoxEmptyNo visual Margins only StyleBoxFlatSolid colors bg_color, border_*, corner_*StyleBoxTextureTextured 9-slice support, margins StyleBoxLineLines/borders color, thickness, vertical
Creating Theme in Code ## theme_generator.gd
extends Node
func create_game_theme() -> Theme:
var theme := Theme.new()
# Colors
theme.set_color("font_color", "Label", Color.WHITE)
theme.set_color("font_color", "Button", Color.WHITE)
theme.set_color("font_pressed_color", "Button", Color.YELLOW)
# Font sizes
theme.set_font_size("font_size", "Label", 16)
theme.set_font_size("font_size", "Button", 18)
# Constants
theme.set_constant("margin_left", "MarginContainer", 20)
theme.set_constant("margin_right", "MarginContainer", 20)
theme.set_constant("margin_top", "MarginContainer", 10)
theme.set_constant("margin_bottom", "MarginContainer", 10)
theme.set_constant("separation", "VBoxContainer", 10)
# Button styles
var button_normal := StyleBoxFlat.new()
button_normal.bg_color = Color(0.2, 0.2, 0.3, 1.0)
button_normal.set_corner_radius_all(8)
button_normal.set_content_margin_all(12)
var button_hover := button_normal.duplicate()
button_hover.bg_color = Color(0.3, 0.3, 0.4, 1.0)
var button_pressed := button_normal.duplicate()
button_pressed.bg_color = Color(0.15, 0.15, 0.25, 1.0)
theme.set_stylebox("normal", "Button", button_normal)
theme.set_stylebox("hover", "Button", button_hover)
theme.set_stylebox("pressed", "Button", button_pressed)
# Panel style
var panel_style := StyleBoxFlat.new()
panel_style.bg_color = Color(0.1, 0.1, 0.15, 0.9)
panel_style.set_corner_radius_all(12)
panel_style.set_border_width_all(2)
panel_style.border_color = Color(0.3, 0.3, 0.4, 1.0)
theme.set_stylebox("panel", "Panel", panel_style)
theme.set_stylebox("panel", "PanelContainer", panel_style)
return theme
Theme Overrides # Override specific properties on a node
label.add_theme_color_override("font_color", Color.RED)
label.add_theme_font_size_override("font_size", 24)
button.add_theme_stylebox_override("normal", custom_stylebox)
# Remove overrides
label.remove_theme_color_override("font_color")
Common UI Patterns
HUD System ## hud.gd - Complete HUD with health, stamina, and score
extends CanvasLayer
signal health_depleted
@onready var health_bar: TextureProgressBar = $Control/TopLeft/HealthBar
@onready var stamina_bar: TextureProgressBar = $Control/TopLeft/StaminaBar
@onready var score_label: Label = $Control/TopRight/ScoreLabel
@onready var coins_label: Label = $Control/TopRight/CoinsLabel
@onready var notification_label: Label = $Control/Center/NotificationLabel
var _notification_tween: Tween
func _ready() -> void:
notification_label.modulate.a = 0.0
func update_health(current: int, max_health: int) -> void:
var tween := create_tween()
tween.tween_property(health_bar, "value",
float(current) / float(max_health) * 100, 0.3)
if current <= 0:
health_depleted.emit()
func update_stamina(current: float, max_stamina: float) -> void:
stamina_bar.value = current / max_stamina * 100
func update_score(score: int) -> void:
score_label.text = "%06d" % score
func update_coins(coins: int) -> void:
coins_label.text = "x %d" % coins
func show_notification(message: String, duration: float = 2.0) -> void:
notification_label.text = message
if _notification_tween:
_notification_tween.kill()
_notification_tween = create_tween()
_notification_tween.tween_property(notification_label, "modulate:a", 1.0, 0.2)
_notification_tween.tween_interval(duration)
_notification_tween.tween_property(notification_label, "modulate:a", 0.0, 0.3)
Main Menu ## main_menu.gd - Main menu with transitions
extends Control
signal play_pressed
signal options_pressed
signal quit_pressed
@onready var buttons: VBoxContainer = $CenterContainer/VBoxContainer
@onready var title: Label = $Title
func _ready() -> void:
_animate_intro()
func _animate_intro() -> void:
title.modulate.a = 0.0
buttons.modulate.a = 0.0
buttons.position.y += 50
var tween := create_tween()
tween.tween_property(title, "modulate:a", 1.0, 0.5)
tween.tween_property(buttons, "modulate:a", 1.0, 0.3)
tween.parallel().tween_property(buttons, "position:y",
buttons.position.y - 50, 0.3).set_ease(Tween.EASE_OUT)
func _on_play_button_pressed() -> void:
play_pressed.emit()
func _on_options_button_pressed() -> void:
options_pressed.emit()
func _on_quit_button_pressed() -> void:
quit_pressed.emit()
get_tree().quit()
## pause_menu.gd - Pause menu with resume/settings/quit
extends CanvasLayer
signal resumed
signal settings_opened
signal quit_to_menu
@onready var panel: PanelContainer = $Control/PanelContainer
var _is_paused: bool = false
func _ready() -> void:
process_mode = Node.PROCESS_MODE_ALWAYS
visible = false
func _input(event: InputEvent) -> void:
if event.is_action_pressed("pause"):
if _is_paused:
_resume()
else:
_pause()
func _pause() -> void:
_is_paused = true
get_tree().paused = true
visible = true
_animate_in()
func _resume() -> void:
_is_paused = false
get_tree().paused = false
visible = false
resumed.emit()
func _animate_in() -> void:
panel.scale = Vector2(0.8, 0.8)
panel.modulate.a = 0.0
var tween := create_tween()
tween.set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_BACK)
tween.tween_property(panel, "scale", Vector2.ONE, 0.2)
tween.parallel().tween_property(panel, "modulate:a", 1.0, 0.15)
func _on_resume_pressed() -> void:
_resume()
func _on_settings_pressed() -> void:
settings_opened.emit()
func _on_quit_pressed() -> void:
get_tree().paused = false
quit_to_menu.emit()
## settings_menu.gd - Audio/Video/Controls settings
extends Control
signal settings_changed(settings: Dictionary)
signal back_pressed
@onready var master_slider: HSlider = $Panel/VBox/Audio/MasterSlider
@onready var music_slider: HSlider = $Panel/VBox/Audio/MusicSlider
@onready var sfx_slider: HSlider = $Panel/VBox/Audio/SFXSlider
@onready var fullscreen_check: CheckBox = $Panel/VBox/Video/FullscreenCheck
@onready var vsync_check: CheckBox = $Panel/VBox/Video/VsyncCheck
@onready var resolution_option: OptionButton = $Panel/VBox/Video/ResolutionOption
const RESOLUTIONS := [
Vector2i(1280, 720),
Vector2i(1920, 1080),
Vector2i(2560, 1440),
Vector2i(3840, 2160)
]
var _settings: Dictionary = {}
func _ready() -> void:
_setup_resolution_options()
_load_settings()
func _setup_resolution_options() -> void:
resolution_option.clear()
for res in RESOLUTIONS:
resolution_option.add_item("%dx%d" % [res.x, res.y])
func _load_settings() -> void:
# Load from ConfigFile or use defaults
var config := ConfigFile.new()
if config.load("user://settings.cfg") == OK:
master_slider.value = config.get_value("audio", "master", 1.0)
music_slider.value = config.get_value("audio", "music", 1.0)
sfx_slider.value = config.get_value("audio", "sfx", 1.0)
fullscreen_check.button_pressed = config.get_value("video", "fullscreen", false)
vsync_check.button_pressed = config.get_value("video", "vsync", true)
func _save_settings() -> void:
var config := ConfigFile.new()
config.set_value("audio", "master", master_slider.value)
config.set_value("audio", "music", music_slider.value)
config.set_value("audio", "sfx", sfx_slider.value)
config.set_value("video", "fullscreen", fullscreen_check.button_pressed)
config.set_value("video", "vsync", vsync_check.button_pressed)
config.save("user://settings.cfg")
func _on_master_slider_value_changed(value: float) -> void:
AudioServer.set_bus_volume_db(
AudioServer.get_bus_index("Master"),
linear_to_db(value)
)
func _on_fullscreen_check_toggled(pressed: bool) -> void:
if pressed:
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
else:
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
func _on_back_pressed() -> void:
_save_settings()
back_pressed.emit()
Inventory System ## inventory_ui.gd - Grid-based inventory UI
extends Control
signal item_selected(item: ItemResource)
signal item_used(item: ItemResource)
@export var slot_scene: PackedScene
@export var columns: int = 5
@export var slot_size: Vector2 = Vector2(64, 64)
@onready var grid: GridContainer = $Panel/ScrollContainer/GridContainer
@onready var tooltip: PanelContainer = $Tooltip
@onready var tooltip_name: Label = $Tooltip/VBox/Name
@onready var tooltip_desc: Label = $Tooltip/VBox/Description
var _slots: Array[Control] = []
var _selected_slot: int = -1
func _ready() -> void:
grid.columns = columns
tooltip.visible = false
func setup_inventory(inventory: Inventory) -> void:
_clear_slots()
for i in range(inventory.max_slots):
var slot := slot_scene.instantiate()
slot.slot_index = i
slot.mouse_entered.connect(_on_slot_hover.bind(i))
slot.mouse_exited.connect(_on_slot_unhover)
slot.gui_input.connect(_on_slot_input.bind(i))
grid.add_child(slot)
_slots.append(slot)
_refresh_from_inventory(inventory)
func _refresh_from_inventory(inventory: Inventory) -> void:
var items := inventory.get_all_items()
var slot_index := 0
for item_data in items:
if slot_index >= _slots.size():
break
_slots[slot_index].set_item(item_data.item, item_data.quantity)
slot_index += 1
# Clear remaining slots
for i in range(slot_index, _slots.size()):
_slots[i].clear()
func _on_slot_hover(slot_index: int) -> void:
var slot := _slots[slot_index]
if slot.has_item():
tooltip_name.text = slot.item.name
tooltip_desc.text = slot.item.description
tooltip.visible = true
tooltip.global_position = get_global_mouse_position() + Vector2(10, 10)
func _on_slot_unhover() -> void:
tooltip.visible = false
func _on_slot_input(event: InputEvent, slot_index: int) -> void:
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
_select_slot(slot_index)
elif event.button_index == MOUSE_BUTTON_RIGHT and event.pressed:
_use_item(slot_index)
func _select_slot(index: int) -> void:
if _selected_slot >= 0:
_slots[_selected_slot].set_selected(false)
_selected_slot = index
_slots[index].set_selected(true)
if _slots[index].has_item():
item_selected.emit(_slots[index].item)
func _use_item(index: int) -> void:
if _slots[index].has_item():
item_used.emit(_slots[index].item)
func _clear_slots() -> void:
for slot in _slots:
slot.queue_free()
_slots.clear()
Dialog System UI ## dialog_box.gd - RPG-style dialog box with typewriter effect
extends CanvasLayer
signal dialog_finished
signal choice_made(choice_index: int)
@onready var panel: PanelContainer = $Control/Panel
@onready var portrait: TextureRect = $Control/Panel/HBox/Portrait
@onready var name_label: Label = $Control/Panel/HBox/VBox/NameLabel
@onready var text_label: RichTextLabel = $Control/Panel/HBox/VBox/TextLabel
@onready var choices_container: VBoxContainer = $Control/Panel/ChoicesContainer
@onready var continue_indicator: TextureRect = $Control/Panel/ContinueIndicator
@export var text_speed: float = 0.03
@export var choice_button_scene: PackedScene
var _is_typing: bool = false
var _can_advance: bool = false
var _current_text: String = ""
func _ready() -> void:
process_mode = Node.PROCESS_MODE_ALWAYS
visible = false
continue_indicator.visible = false
func show_dialog(speaker: String, text: String, speaker_portrait: Texture2D = null) -> void:
visible = true
get_tree().paused = true
name_label.text = speaker
portrait.texture = speaker_portrait
portrait.visible = speaker_portrait != null
_current_text = text
text_label.text = ""
_type_text()
func _type_text() -> void:
_is_typing = true
_can_advance = false
continue_indicator.visible = false
for i in range(_current_text.length()):
text_label.text = _current_text.substr(0, i + 1)
await get_tree().create_timer(text_speed).timeout
if not _is_typing: # Skip was requested
text_label.text = _current_text
break
_is_typing = false
_can_advance = true
continue_indicator.visible = true
_animate_continue_indicator()
func _animate_continue_indicator() -> void:
var tween := create_tween().set_loops()
tween.tween_property(continue_indicator, "modulate:a", 0.3, 0.5)
tween.tween_property(continue_indicator, "modulate:a", 1.0, 0.5)
func show_choices(choices: Array[String]) -> void:
_can_advance = false
continue_indicator.visible = false
for child in choices_container.get_children():
child.queue_free()
for i in range(choices.size()):
var button: Button = choice_button_scene.instantiate()
button.text = choices[i]
button.pressed.connect(_on_choice_pressed.bind(i))
choices_container.add_child(button)
choices_container.visible = true
func _on_choice_pressed(index: int) -> void:
choices_container.visible = false
choice_made.emit(index)
func _input(event: InputEvent) -> void:
if not visible:
return
if event.is_action_pressed("ui_accept"):
if _is_typing:
_is_typing = false # Skip typing
elif _can_advance:
_advance_dialog()
func _advance_dialog() -> void:
_can_advance = false
continue_indicator.visible = false
dialog_finished.emit()
func hide_dialog() -> void:
visible = false
get_tree().paused = false
The following Godot MCP tools are particularly useful for UI development:
Tool UI Use Case mcp__godot__create_sceneCreate new UI scene with Control root mcp__godot__add_nodeAdd Control nodes to scene mcp__godot__get_project_infoCheck existing UI scenes mcp__godot__save_sceneSave UI scene changes mcp__godot__run_projectTest UI in game
Example: Creating HUD Scene with MCP // Step 1: Create the scene
mcp__godot__create_scene({
projectPath: "/path/to/project",
scenePath: "scenes/ui/hud.tscn",
rootNodeType: "CanvasLayer"
})
// Step 2: Add Control container
mcp__godot__add_node({
projectPath: "/path/to/project",
scenePath: "scenes/ui/hud.tscn",
nodeType: "Control",
nodeName: "HUDControl",
parentNodePath: "root",
properties: {
"anchor_left": 0,
"anchor_right": 1,
"anchor_top": 0,
"anchor_bottom": 1
}
})
// Step 3: Add health bar
mcp__godot__add_node({
projectPath: "/path/to/project",
scenePath: "scenes/ui/hud.tscn",
nodeType: "ProgressBar",
nodeName: "HealthBar",
parentNodePath: "root/HUDControl"
})
// Step 4: Save the scene
mcp__godot__save_scene({
projectPath: "/path/to/project",
scenePath: "scenes/ui/hud.tscn"
})
UI Animation
Tween-Based Animations ## ui_animator.gd - Common UI animation utilities
class_name UIAnimator
extends RefCounted
static func fade_in(control: Control, duration: float = 0.3) -> Tween:
control.modulate.a = 0.0
var tween := control.create_tween()
tween.tween_property(control, "modulate:a", 1.0, duration)
return tween
static func fade_out(control: Control, duration: float = 0.3) -> Tween:
var tween := control.create_tween()
tween.tween_property(control, "modulate:a", 0.0, duration)
return tween
static func slide_in_from_left(control: Control, duration: float = 0.3) -> Tween:
var target_x := control.position.x
control.position.x = -control.size.x
var tween := control.create_tween()
tween.set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_BACK)
tween.tween_property(control, "position:x", target_x, duration)
return tween
static func slide_in_from_right(control: Control, duration: float = 0.3) -> Tween:
var target_x := control.position.x
var viewport_width := control.get_viewport_rect().size.x
control.position.x = viewport_width
var tween := control.create_tween()
tween.set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_BACK)
tween.tween_property(control, "position:x", target_x, duration)
return tween
static func scale_pop(control: Control, duration: float = 0.2) -> Tween:
control.scale = Vector2.ZERO
control.pivot_offset = control.size / 2
var tween := control.create_tween()
tween.set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_BACK)
tween.tween_property(control, "scale", Vector2.ONE, duration)
return tween
static func shake(control: Control, intensity: float = 5.0, duration: float = 0.3) -> Tween:
var original_pos := control.position
var tween := control.create_tween()
var shake_count := int(duration / 0.05)
for i in range(shake_count):
var offset := Vector2(
randf_range(-intensity, intensity),
randf_range(-intensity, intensity)
)
tween.tween_property(control, "position", original_pos + offset, 0.05)
tween.tween_property(control, "position", original_pos, 0.05)
return tween
static func pulse(control: Control, scale_amount: float = 1.1, duration: float = 0.5) -> Tween:
control.pivot_offset = control.size / 2
var tween := control.create_tween().set_loops()
tween.tween_property(control, "scale", Vector2.ONE * scale_amount, duration / 2)
tween.tween_property(control, "scale", Vector2.ONE, duration / 2)
return tween
## animated_button.gd
extends Button
@export var hover_scale: float = 1.05
@export var press_scale: float = 0.95
@export var animation_duration: float = 0.1
func _ready() -> void:
pivot_offset = size / 2
mouse_entered.connect(_on_mouse_entered)
mouse_exited.connect(_on_mouse_exited)
button_down.connect(_on_button_down)
button_up.connect(_on_button_up)
func _on_mouse_entered() -> void:
var tween := create_tween()
tween.tween_property(self, "scale", Vector2.ONE * hover_scale, animation_duration)
func _on_mouse_exited() -> void:
var tween := create_tween()
tween.tween_property(self, "scale", Vector2.ONE, animation_duration)
func _on_button_down() -> void:
var tween := create_tween()
tween.tween_property(self, "scale", Vector2.ONE * press_scale, animation_duration / 2)
func _on_button_up() -> void:
var tween := create_tween()
tween.tween_property(self, "scale", Vector2.ONE * hover_scale, animation_duration / 2)
Best Practices
Use visible instead of removing nodes for show/hide
Set mouse_filter = IGNORE on decorative elements
Use CanvasGroup to batch draw calls for complex UIs
Avoid deep nesting - flatten hierarchies where possible
Use custom_minimum_size sparingly - prefer container sizing
Accessibility
Minimum touch targets : 44x44 pixels for interactive elements
High contrast : Ensure 4.5:1 ratio for text
Keyboard navigation : Test all menus with arrow keys
Focus styles : Make focus states clearly visible
Screen reader hints : Use tooltip_text for complex controls
Organization project/
├── scenes/
│ └── ui/
│ ├── components/ # Reusable UI pieces
│ │ ├── button.tscn
│ │ ├── slot.tscn
│ │ └── health_bar.tscn
│ ├── screens/ # Full-screen UIs
│ │ ├── main_menu.tscn
│ │ ├── pause_menu.tscn
│ │ └── settings.tscn
│ └── hud/ # In-game overlays
│ └── hud.tscn
├── scripts/
│ └── ui/
│ ├── components/
│ ├── screens/
│ └── hud/
└── themes/
├── game_theme.tres
└── menu_theme.tres
# Correct: Handle UI-specific input
func _gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
_on_clicked()
# For global UI actions (pause, inventory toggle)
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("toggle_inventory"):
_toggle_inventory()
Complete Templates
Minimal HUD Scene (.tscn) [gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://scripts/ui/hud.gd" id="1"]
[node name="HUD" type="CanvasLayer"]
layer = 10
script = ExtResource("1")
[node name="Control" type="Control" parent="."]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="TopLeft" type="MarginContainer" parent="Control"]
layout_mode = 1
anchors_preset = 0
offset_right = 200.0
offset_bottom = 80.0
[node name="VBox" type="VBoxContainer" parent="Control/TopLeft"]
layout_mode = 2
[node name="HealthBar" type="ProgressBar" parent="Control/TopLeft/VBox"]
layout_mode = 2
value = 100.0
[node name="StaminaBar" type="ProgressBar" parent="Control/TopLeft/VBox"]
layout_mode = 2
value = 100.0
[node name="TopRight" type="MarginContainer" parent="Control"]
layout_mode = 1
anchors_preset = 1
anchor_left = 1.0
anchor_right = 1.0
offset_left = -150.0
offset_bottom = 50.0
grow_horizontal = 0
[node name="ScoreLabel" type="Label" parent="Control/TopRight"]
layout_mode = 2
text = "000000"
horizontal_alignment = 2
Main Menu Template [gd_scene load_steps=3 format=3]
[ext_resource type="Script" path="res://scripts/ui/main_menu.gd" id="1"]
[ext_resource type="Theme" path="res://themes/menu_theme.tres" id="2"]
[node name="MainMenu" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
theme = ExtResource("2")
script = ExtResource("1")
[node name="Background" type="ColorRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
color = Color(0.1, 0.1, 0.15, 1)
[node name="Title" type="Label" parent="."]
layout_mode = 1
anchors_preset = 5
anchor_left = 0.5
anchor_right = 0.5
offset_left = -200.0
offset_top = 100.0
offset_right = 200.0
offset_bottom = 160.0
text = "GAME TITLE"
horizontal_alignment = 1
[node name="CenterContainer" type="CenterContainer" parent="."]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -100.0
offset_top = -75.0
offset_right = 100.0
offset_bottom = 75.0
[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer"]
layout_mode = 2
theme_override_constants/separation = 10
[node name="PlayButton" type="Button" parent="CenterContainer/VBoxContainer"]
layout_mode = 2
text = "Play"
[node name="OptionsButton" type="Button" parent="CenterContainer/VBoxContainer"]
layout_mode = 2
text = "Options"
[node name="QuitButton" type="Button" parent="CenterContainer/VBoxContainer"]
layout_mode = 2
text = "Quit"
[connection signal="pressed" from="CenterContainer/VBoxContainer/PlayButton" to="." method="_on_play_button_pressed"]
[connection signal="pressed" from="CenterContainer/VBoxContainer/OptionsButton" to="." method="_on_options_button_pressed"]
[connection signal="pressed" from="CenterContainer/VBoxContainer/QuitButton" to="." method="_on_quit_button_pressed"]
Resources
Official Documentation
Video Tutorials
GDQuest UI tutorials
Godot official UI demos
Skill version: 1.0.0 | Godot 4.5.x compatible
02
Table of Contents
게임 개발
Ideation Generate project ideas through creative constraints. Use when the user says 'I want to build something', 'give me a project idea', 'I'm bored', 'what should I make', 'inspire me', or any variant of 'I have tools but no direction'. Works for code, art, hardware, writing, tools, and anything that can be made.