Claude-skill-registry godot-modernize-ui
Modernize Godot 3.x UI to 4.x best practices with theme extraction, responsive layouts, hiDPI support, and RichTextLabel BBCode patterns
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/godot-modernize-ui" ~/.claude/skills/majiayu000-claude-skill-registry-godot-modernize-ui && rm -rf "$T"
manifest:
skills/data/godot-modernize-ui/SKILL.mdsource content
Godot UI Modernization
Modernizes Godot 3.x UI scenes and scripts to Godot 4.x best practices.
When to Use
- Migrating Godot 3.x projects to 4.x with UI scenes
- UI doesn't scale properly on hiDPI displays
- Hardcoded styles scattered across Control nodes
- Manual positioning instead of responsive anchoring
- RichTextLabel using deprecated BBCode tags
- Creating reusable custom controls
Core Patterns
1. Theme Resource Extraction
Before (Godot 3.x):
# Hardcoded in each Control node $Button.modulate = Color.red $Button.get("custom_styles/normal").bg_color = Color.blue $Label.add_font_override("font", preload("res://fonts/big.tres"))
After (Godot 4.x):
# Centralized in Theme resource # Create: res://ui/themes/main_theme.tres # Assign to root Control or project settings
Theme.tres structure:
Theme ├── Button/styles/normal (StyleBoxFlat) ├── Button/colors/font_color = Color(1, 1, 1, 1) ├── Button/fonts/font = preload("res://fonts/button_font.tres") ├── Label/fonts/font = preload("res://fonts/label_font.tres") └── Panel/styles/panel (StyleBoxFlat)
2. Control Anchoring Standardization
Before:
# Manual positioning $Panel.position = Vector2(100, 100) $Panel.size = Vector2(400, 300)
After:
Control (root) ├── Layout Mode: Anchors ├── Anchor Left: 0.5, Anchor Top: 0.5 ├── Anchor Right: 0.5, Anchor Bottom: 0.5 ├── Grow Horizontal: Center ├── Grow Vertical: Center └── Custom Minimum Size: (400, 300)
Anchor Presets for Common Layouts:
| Layout | Anchors | Offsets | Grow |
|---|---|---|---|
| Fullscreen | 0,0 to 1,1 | 0 all sides | Both |
| Top Bar | 0,0 to 1,0 | Bottom: 60 | Horizontal |
| Bottom Bar | 0,1 to 1,1 | Top: 60 | Horizontal |
| Center Panel | 0.5,0.5 | Size defined | Both |
| Left Sidebar | 0,0 to 0,1 | Right: 250 | Vertical |
| Right Sidebar | 1,0 to 1,1 | Left: 250 | Vertical |
3. UI Scaling (hiDPI Support)
Project Settings:
Display > Window > Stretch ├── Mode: canvas_items (or viewport) ├── Aspect: expand └── Scale: 1.0 (auto-detected)
Script for Dynamic Scaling:
func _ready(): # Handle hiDPI on different displays var screen_dpi = DisplayServer.screen_get_dpi() if screen_dpi > 150: get_tree().root.content_scale_factor = 2.0
Responsive Sizing:
@export var base_width: float = 1920.0 @export var base_height: float = 1080.0 func _on_viewport_size_changed(): var viewport_size = get_viewport().get_visible_rect().size var scale_x = viewport_size.x / base_width var scale_y = viewport_size.y / base_height scale = Vector2(min(scale_x, scale_y), min(scale_x, scale_y))
4. RichTextLabel BBCode Patterns
Deprecated Godot 3.x:
[color=red]Warning[/color] [url=https://example.com]Link[/url] [img]res://icon.png[/img]
Godot 4.x BBCode:
[color=ff0000]Warning[/color] [color=#ff0000]Hex Color[/color] [url=https://example.com]Link[/url] [img=64x64]res://icon.png[/img] [font=res://fonts/custom.tres]Custom font[/font] [wave amp=50 freq=5]Animated text[/wave] [tornado radius=5 freq=2]Spinning text[/tornado] [shake rate=5 level=10]Shaking text[/shake] [fgcolor=00ff00 bgcolor=000000]Background[/fgcolor] [outline_color=ffffff]Outlined[/outline_color]
Script Integration:
@onready var rich_label: RichTextLabel = $RichTextLabel func append_colored_text(text: String, color: Color): rich_label.append_text("[color=%s]%s[/color]" % [color.to_html(), text]) func append_url(text: String, url: String): rich_label.append_text("[url=%s]%s[/url]" % [url, text]) func _on_meta_clicked(meta): OS.shell_open(str(meta))
5. Custom Control Creation
Custom Button with Built-in Theme:
@tool class_name ModernButton extends Button @export var button_style: ButtonStyle = ButtonStyle.PRIMARY: set(value): button_style = value _update_style() enum ButtonStyle { PRIMARY, SECONDARY, DANGER } func _ready(): _update_style() func _update_style(): match button_style: ButtonStyle.PRIMARY: add_theme_color_override("font_color", Color(1, 1, 1)) add_theme_stylebox_override("normal", preload("res://ui/styles/primary_normal.tres")) ButtonStyle.SECONDARY: add_theme_color_override("font_color", Color(0.2, 0.2, 0.2)) add_theme_stylebox_override("normal", preload("res://ui/styles/secondary_normal.tres")) ButtonStyle.DANGER: add_theme_color_override("font_color", Color(1, 1, 1)) add_theme_stylebox_override("normal", preload("res://ui/styles/danger_normal.tres"))
Responsive Design Patterns
Container Hierarchy
CanvasLayer (UI Layer) └── Control (Full Rect anchor, Fullscreen preset) ├── MarginContainer (padding) │ ├── VBoxContainer (vertical layout) │ │ ├── HBoxContainer (top bar) │ │ │ ├── Label (title) │ │ │ └── Button (close) │ │ └── ScrollContainer (scrollable content) │ │ └── VBoxContainer │ │ └── (dynamic content) │ └── HBoxContainer (bottom bar) │ └── Button (action) └── Panel (overlay/popup)
Safe Area Handling (Mobile)
func _ready(): # Apply safe area insets for notched devices var safe_area = DisplayServer.get_display_safe_area() var window_size = DisplayServer.window_get_size() $MarginContainer.add_theme_constant_override("margin_left", safe_area.position.x) $MarginContainer.add_theme_constant_override("margin_top", safe_area.position.y) $MarginContainer.add_theme_constant_override("margin_right", window_size.x - safe_area.end.x) $MarginContainer.add_theme_constant_override("margin_bottom", window_size.y - safe_area.end.y)
Common Mistakes
Using RectPosition/RectSize directly:
# ❌ Bad - breaks with different resolutions $Control.rect_position = Vector2(100, 100) $Control.rect_size = Vector2(200, 150) # ✅ Good - use anchors and layout containers # Set anchors in editor or use custom_minimum_size $Control.custom_minimum_size = Vector2(200, 150)
Hardcoding pixel values without scaling:
# ❌ Bad - too small on hiDPI var padding = 10 # ✅ Good - scale with content_scale_factor var padding = 10 * get_tree().root.content_scale_factor
Manual font size calculations:
# ❌ Bad - inconsistent across displays label.add_theme_font_size_override("font_size", 24) # ✅ Good - use theme with dynamic fonts # Set in Theme resource with multiple sizes
Forgetting to handle window resize:
# ✅ Good - subscribe to resize signal func _ready(): get_viewport().size_changed.connect(_on_viewport_size_changed) _on_viewport_size_changed() func _on_viewport_size_changed(): # Recalculate responsive layout pass
Quick Reference
| Task | Godot 3.x | Godot 4.x |
|---|---|---|
| Theme override | | |
| Font override | | |
| Color override | | |
| Constant override | | |
| Size override | | |
| Position | | |
| Size | | |
| Global position | | |
| BBCode color | | or |
| BBCode image | | |
| hiDPI setting | | |
Migration Checklist
- Extract all hardcoded styles to Theme resources
- Replace rect_* properties with position/size
- Update BBCode syntax (colors, images)
- Configure hiDPI in project settings
- Convert manual positioning to anchor/layout system
- Test on multiple resolutions and DPIs
- Add viewport resize handling
- Create custom controls for repeated patterns