Godot 4 GDScript 教程 / UI 系统(Control/主题)
UI 系统(Control/主题)
概述
Godot 4 的 UI 系统基于 Control 节点层次结构,通过容器(Container)实现自适应布局,通过主题(Theme)统一视觉风格。UI 节点使用锚点(Anchor)和偏移(Offset)定位,支持像素完美渲染。
| 核心节点 | 说明 |
|---|---|
Control | 所有 UI 节点的基类 |
Container | 自动排列子控件 |
Panel | 背景面板 |
Label | 文本标签 |
Button | 按钮 |
RichTextLabel | 富文本 |
TextureRect | 纹理显示 |
NinePatchRect | 九宫格拉伸 |
ProgressBar | 进度条 |
LineEdit | 单行输入框 |
TextEdit | 多行文本编辑 |
Control 节点基础
锚点与偏移
extends Control
func _ready():
# 锚点:0.0~1.0,相对于父节点的比例
# 左上角锚点
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.5
anchor_bottom = 0.5
# 偏移:像素值
offset_left = 10
offset_top = 10
offset_right = -10
offset_bottom = -10
# 快捷设置全屏
set_anchors_preset(Control.PRESET_FULL_RECT)
| 锚点预设 | 说明 |
|---|---|
PRESET_FULL_RECT | 全屏填充 |
PRESET_CENTER | 居中 |
PRESET_CENTER_TOP | 顶部居中 |
PRESET_CENTER_BOTTOM | 底部居中 |
PRESET_LEFT_WIDE | 左侧全高 |
PRESET_RIGHT_WIDE | 右侧全高 |
PRESET_TOP_WIDE | 顶部全宽 |
PRESET_BOTTOM_WIDE | 底部全宽 |
鼠标过滤
func _ready():
# 鼠标过滤模式
mouse_filter = Control.MOUSE_FILTER_STOP # 拦截鼠标事件
mouse_filter = Control.MOUSE_FILTER_PASS # 传递给父节点
mouse_filter = Control.MOUSE_FILTER_IGNORE # 完全忽略
容器布局
容器自动排列子控件,是实现自适应 UI 的核心。
常用容器
| 容器 | 排列方式 | 适用场景 |
|---|---|---|
MarginContainer | 四周留白 | 整体布局 |
HBoxContainer | 水平排列 | 工具栏、按钮行 |
VBoxContainer | 垂直排列 | 菜单列表 |
GridContainer | 网格排列 | 物品格子 |
FlowContainer | 自动换行 | 标签云 |
SplitContainer | 可拖拽分栏 | 编辑器布局 |
ScrollContainer | 可滚动 | 长列表 |
TabContainer | 标签页 | 设置面板 |
物品格子布局
extends GridContainer
@export var slot_scene: PackedScene
@export var columns: int = 6
func _ready():
columns = columns
# 创建 24 个物品格子
for i in range(24):
var slot = slot_scene.instantiate()
slot.slot_index = i
add_child(slot)
垂直菜单
extends VBoxContainer
func _ready():
# 添加间距
add_theme_constant_override("separation", 10)
var buttons = ["新游戏", "继续游戏", "设置", "退出"]
for text in buttons:
var btn = Button.new()
btn.text = text
btn.custom_minimum_size = Vector2(200, 50)
btn.pressed.connect(_on_button_pressed.bind(text))
add_child(btn)
func _on_button_pressed(text: String):
match text:
"新游戏": get_tree().change_scene_to_file("res://scenes/game.tscn")
"退出": get_tree().quit()
主题 Theme 系统
主题统一控制 UI 节点的视觉样式。
代码创建主题
extends Control
func _ready():
var theme = Theme.new()
# 设置 Button 默认样式
var btn_style = StyleBoxFlat.new()
btn_style.bg_color = Color(0.2, 0.4, 0.8)
btn_style.border_width_bottom = 2
btn_style.border_color = Color(0.1, 0.2, 0.6)
btn_style.corner_radius_top_left = 8
btn_style.corner_radius_top_right = 8
btn_style.corner_radius_bottom_left = 8
btn_style.corner_radius_bottom_right = 8
btn_style.content_margin_left = 20
btn_style.content_margin_right = 20
btn_style.content_margin_top = 10
btn_style.content_margin_bottom = 10
theme.set_stylebox("normal", "Button", btn_style)
# 悬停样式
var hover_style = btn_style.duplicate()
hover_style.bg_color = Color(0.3, 0.5, 0.9)
theme.set_stylebox("hover", "Button", hover_style)
# 按下样式
var pressed_style = btn_style.duplicate()
pressed_style.bg_color = Color(0.15, 0.3, 0.7)
theme.set_stylebox("pressed", "Button", pressed_style)
# 字体颜色
theme.set_color("font_color", "Button", Color.WHITE)
theme.set_font_size("font_size", "Button", 18)
# 应用主题
self.theme = theme
主题变体(Theme Type Variation)
extends Control
func _ready():
var theme = Theme.new()
# 创建 "PrimaryButton" 变体,继承 Button
theme.set_type_variation("PrimaryButton", "Button")
var primary_style = StyleBoxFlat.new()
primary_style.bg_color = Color(0.1, 0.7, 0.3)
primary_style.corner_radius_top_left = 12
primary_style.corner_radius_top_right = 12
primary_style.corner_radius_bottom_left = 12
primary_style.corner_radius_bottom_right = 12
theme.set_stylebox("normal", "PrimaryButton", primary_style)
self.theme = theme
# 在编辑器中设置节点的 theme_type_variation = "PrimaryButton"
💡 提示:主题变体让你创建 Button 的多种视觉风格(如主要按钮、次要按钮、危险按钮),无需为每个按钮单独设置样式。
UI 信号交互
extends Control
@onready var label: Label = $Label
@onready var slider: HSlider = $HSlider
@onready var line_edit: LineEdit = $LineEdit
func _ready():
# 按钮
$Button.pressed.connect(_on_button_pressed)
# 滑块
slider.value_changed.connect(_on_slider_changed)
# 输入框
line_edit.text_submitted.connect(_on_text_submitted)
line_edit.text_changed.connect(_on_text_changed)
func _on_button_pressed():
label.text = "按钮被点击!"
func _on_slider_changed(value: float):
label.text = "音量: %d%%" % int(value)
func _on_text_submitted(text: String):
print("提交: ", text)
func _on_text_changed(new_text: String):
print("输入中: ", new_text)
弹窗与对话框
确认弹窗
extends Control
@onready var dialog: ConfirmationDialog = $ConfirmationDialog
func _ready():
dialog.dialog_text = "确定要退出游戏吗?"
dialog.confirmed.connect(_on_confirmed)
dialog.canceled.connect(_on_canceled)
func show_dialog():
dialog.popup_centered(Vector2(300, 150))
func _on_confirmed():
get_tree().quit()
func _on_canceled():
print("取消退出")
自定义弹窗动画
extends PanelContainer
signal confirmed
signal canceled
func popup_center(size: Vector2 = Vector2(400, 250)):
custom_minimum_size = size
visible = true
# 从缩放 0 弹出到 1
modulate.a = 0
scale = Vector2(0.8, 0.8)
var tween = create_tween().set_parallel(true)
tween.tween_property(self, "modulate:a", 1.0, 0.2)
tween.tween_property(self, "scale", Vector2.ONE, 0.2).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_BACK)
func _on_confirm_pressed():
confirmed.emit()
close()
func _on_cancel_pressed():
canceled.emit()
close()
func close():
var tween = create_tween()
tween.tween_property(self, "modulate:a", 0.0, 0.15)
tween.tween_callback(func(): visible = false)
富文本 RichTextLabel
extends RichTextLabel
func _ready():
bbcode_enabled = true # 启用 BBCode
# 动态添加内容
append_text("[b]粗体[/b] [i]斜体[/i] [color=red]红色[/color]\n")
append_text("[font_size=24]大字[/font_size]\n")
append_text("[url=https://godotengine.org]链接[/url]\n")
append_text("[img]res://icon.svg[/img]\n")
# 交互式链接
meta_clicked.connect(_on_meta_clicked)
func _on_meta_clicked(meta):
if meta is String and meta.begins_with("http"):
OS.shell_open(meta)
| BBCode 标签 | 说明 |
|---|---|
[b] | 粗体 |
[i] | 斜体 |
[u] | 下划线 |
[s] | 删除线 |
[color=X] | 颜色 |
[font_size=X] | 字号 |
[url=X] | 链接 |
[img] | 图片 |
[center] | 居中 |
[right] | 右对齐 |
[fill] | 两端对齐 |
[indent] | 缩进 |
UI 动画
extends Control
@onready var panel: PanelContainer = $PanelContainer
# 淡入效果
func fade_in():
panel.modulate.a = 0
panel.visible = true
var tween = create_tween()
tween.tween_property(panel, "modulate:a", 1.0, 0.3)
# 滑入效果
func slide_in_from_right():
var target_pos = panel.position
panel.position.x = get_viewport_rect().size.x
panel.visible = true
var tween = create_tween()
tween.tween_property(panel, "position:x", target_pos.x, 0.4).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_CUBIC)
# 进度条动画
func animate_progress(bar: ProgressBar, target: float, duration: float = 0.5):
var tween = create_tween()
tween.tween_property(bar, "value", target, duration).set_ease(Tween.EASE_OUT)
游戏 HUD 设计
extends CanvasLayer
@onready var hp_bar: ProgressBar = $MarginContainer/VBoxContainer/HPBar
@onready var mp_bar: ProgressBar = $MarginContainer/VBoxContainer/MPBar
@onready var score_label: Label = $ScoreLabel
@onready var minimap: SubViewportContainer = $MinimapContainer
var current_score: int = 0
func update_hp(current: float, maximum: float):
hp_bar.max_value = maximum
var tween = create_tween()
tween.tween_property(hp_bar, "value", current, 0.3)
func update_mp(current: float, maximum: float):
mp_bar.max_value = maximum
var tween = create_tween()
tween.tween_property(mp_bar, "value", current, 0.3)
func add_score(amount: int):
current_score += amount
score_label.text = "分数: %d" % current_score
# 得分弹跳动画
var tween = create_tween()
tween.tween_property(score_label, "scale", Vector2(1.3, 1.3), 0.1)
tween.tween_property(score_label, "scale", Vector2.ONE, 0.1)
💡 提示:HUD 应放在 CanvasLayer 上,确保不受游戏世界相机影响。
游戏开发场景
| 场景 | 推荐方案 |
|---|---|
| 主菜单 | VBoxContainer + 主题 |
| 背包系统 | GridContainer + 拖放信号 |
| 对话系统 | RichTextLabel + BBCode |
| 血条/能量条 | ProgressBar + Tween 动画 |
| 小地图 | SubViewportContainer |
| 设置面板 | TabContainer + 滑块/复选框 |
⚠️ 常见陷阱
- UI 节点的
position和global_position不同,锚点影响定位 - Container 内的子节点不要手动设置 position,会被容器覆盖
- 主题继承是向上传递的,在根 Control 设置主题影响所有子节点
- 鼠标过滤模式影响事件传递,确保设对了
MOUSE_FILTER_STOP/PASS/IGNORE set_anchors_preset()需要在offset设置之前调用,否则偏移会被覆盖