Godot 3 GDScript 教程 / UI 系统(Control 节点)
UI 系统(Control 节点)
Control 节点基础
在 Godot 中,所有 UI 元素都继承自 Control 节点。Control 提供了位置、大小、锚点、边距等通用 UI 属性。
常用 Control 节点一览
| 节点类型 | 用途 | 常见场景 |
|---|---|---|
Label | 文本显示 | 分数、生命值、提示文字 |
RichTextLabel | 富文本 | 对话框、物品描述 |
Button | 按钮 | 菜单按钮、技能按钮 |
TextureButton | 纹理按钮 | 图标按钮、手柄按钮提示 |
LineEdit | 单行输入 | 玩家名称、搜索框 |
TextEdit | 多行输入 | 控制台、聊天 |
TextureRect | 纹理显示 | 背景图、头像 |
ProgressBar | 进度条 | 血条、经验条 |
HSlider / VSlider | 滑块 | 音量控制、亮度 |
SpinBox | 数值输入 | 设置数值 |
CheckBox | 复选框 | 开关选项 |
OptionButton | 下拉菜单 | 分辨率选择 |
ItemList | 列表 | 背包物品 |
Tree | 树形结构 | 文件浏览器 |
TabContainer | 选项卡 | 设置面板 |
ScrollContainer | 滚动容器 | 长列表 |
ColorRect | 色块 | 背景遮罩 |
NinePatchRect | 九宫格缩放 | 对话框背景 |
VideoPlayer | 视频播放 | 过场动画 |
创建第一个 UI
CanvasLayer
└── Control (根节点)
├── Background (ColorRect)
├── Title (Label)
├── HealthBar (ProgressBar)
└── ScoreLabel (Label)
extends Control
func _ready() -> void:
$Title.text = "我的游戏"
$Title.align = Label.ALIGN_CENTER
$HealthBar.max_value = 100
$HealthBar.value = 75
$ScoreLabel.text = "Score: 0"
func update_health(new_health: int) -> void:
$HealthBar.value = new_health
func update_score(new_score: int) -> void:
$ScoreLabel.text = "Score: %d" % new_score
💡 提示:UI 节点应放在 CanvasLayer 下,使其不受游戏世界相机影响。
常用 UI 节点详解
Label(文本标签)
extends Label
func _ready() -> void:
# 基本设置
text = "Hello, Godot!"
align = Label.ALIGN_CENTER # 对齐: LEFT, CENTER, RIGHT
valign = Label.VALIGN_CENTER # 垂直对齐: TOP, CENTER, BOTTOM
autowrap = true # 自动换行
clip_text = true # 超出部分裁剪
uppercase = true # 全部大写
# 字体设置
var font = DynamicFont.new()
font.font_data = load("res://assets/fonts/GameFont.ttf")
font.size = 24
font.color = Color.white
add_override("font", font)
# 颜色
add_color_override("font_color", Color.yellow)
add_color_override("font_color_shadow", Color.black)
add_constant_override("shadow_offset_x", 2)
add_constant_override("shadow_offset_y", 2)
func set_text_with_animation(new_text: String) -> void:
# 打字机效果
visible_characters = 0
text = new_text
for i in text.length():
visible_characters = i + 1
yield(get_tree().create_timer(0.05), "timeout")
RichTextLabel(富文本)
extends RichTextLabel
func _ready() -> void:
bbcode_enabled = true # 启用 BBCode
# BBCode 格式化
bbcode_text = """
[color=red]红色文字[/color]
[color=#00ff00]绿色文字[/color]
[bold]粗体[/bold]
[i]斜体[/i]
[outline_size=2 outline_color=black]带描边的文字[/outline_size]
[shake]抖动效果[/shake]
[wave]波浪效果[/wave]
[center]居中文字[/center]
[image=res://icon.png][/image]
"""
# 追加文本
append_bbcode("[rainbow]彩虹文字[/rainbow]")
# 打字机效果
func typewriter_effect(text_content: String, speed: float = 0.05) -> void:
bbcode_text = text_content
visible_characters = 0
for i in get_total_character_count():
visible_characters = i + 1
yield(get_tree().create_timer(speed), "timeout")
Button(按钮)
extends Button
func _ready() -> void:
# 基本设置
text = "开始游戏"
disabled = false
toggle_mode = false # 是否为切换模式
flat = false # 是否为扁平样式
# 图标
icon = load("res://assets/ui/play_icon.png")
# 连接信号
connect("pressed", self, "_on_pressed")
connect("toggled", self, "_on_toggled") # toggle_mode 模式下
connect("button_down", self, "_on_button_down")
connect("button_up", self, "_on_button_up")
func _on_pressed() -> void:
print("按钮被点击!")
func _on_toggled(is_pressed: bool) -> void:
print("按钮状态: ", is_pressed)
func _on_button_down() -> void:
print("按下瞬间")
func _on_button_up() -> void:
print("释放瞬间")
LineEdit(文本输入)
extends LineEdit
func _ready() -> void:
placeholder_text = "输入玩家名称..."
max_length = 20
secret = false # 密码模式
editable = true
# 连接信号
connect("text_changed", self, "_on_text_changed")
connect("text_entered", self, "_on_text_entered")
connect("focus_entered", self, "_on_focus_entered")
connect("focus_exited", self, "_on_focus_exited")
func _on_text_changed(new_text: String) -> void:
print("文本变化: ", new_text)
func _on_text_entered(new_text: String) -> void:
print("回车确认: ", new_text)
# 提交名称
_submit_name(new_text)
func _submit_name(name: String) -> void:
if name.length() < 3:
$ErrorLabel.text = "名称至少需要3个字符"
return
GameManager.player_name = name
ProgressBar(进度条)
extends Control
onready var hp_bar = $HPBar
onready var mp_bar = $MPBar
onready var exp_bar = $EXPBar
func _ready() -> void:
_setup_bar(hp_bar, 100, Color.red)
_setup_bar(mp_bar, 50, Color.blue)
_setup_bar(exp_bar, 1000, Color.yellow)
func _setup_bar(bar: ProgressBar, max_val: int, color: Color) -> void:
bar.max_value = max_val
bar.value = max_val
bar.show_percentage = false
# 动画更新血条
func update_hp(new_hp: int, max_hp: int) -> void:
hp_bar.max_value = max_hp
$Tween.interpolate_property(
hp_bar, "value",
hp_bar.value, new_hp, 0.3,
Tween.TRANS_LINEAR
)
$Tween.start()
# 低血量警告
if float(new_hp) / max_hp < 0.3:
hp_bar.modulate = Color.red
_flash_warning()
else:
hp_bar.modulate = Color.white
func _flash_warning() -> void:
$Tween.interpolate_property(
hp_bar, "modulate",
Color.red, Color.white, 0.5
)
$Tween.start()
布局系统
MarginContainer(边距容器)
extends MarginContainer
func _ready() -> void:
# 设置边距(像素)
margin_left = 20
margin_top = 20
margin_right = -20
margin_bottom = -20
# 或使用 theme_override
add_constant_override("margin_left", 20)
add_constant_override("margin_top", 20)
add_constant_override("margin_right", 20)
add_constant_override("margin_bottom", 20)
HBoxContainer(水平布局)
HBoxContainer
├── Icon (TextureRect)
├── NameLabel (Label)
└── ValueLabel (Label)
效果: [图标] [名称] [数值]
extends HBoxContainer
func _ready() -> void:
# 间距
separation = 10
# 子节点对齐
alignment = BoxContainer.ALIGN_CENTER # BEGIN, CENTER, END
VBoxContainer(垂直布局)
VBoxContainer
├── Title (Label)
├── Button1 (Button)
├── Button2 (Button)
└── Button3 (Button)
效果:
标题
[按钮1]
[按钮2]
[按钮3]
GridContainer(网格布局)
extends GridContainer
func _ready() -> void:
columns = 4 # 每行4列
h_separation = 10
v_separation = 10
# 背包格子示例
func setup_inventory(items: Array) -> void:
# 清空现有格子
for child in get_children():
child.queue_free()
# 创建物品格子
for item in items:
var slot = preload("res://ui/InventorySlot.tscn").instance()
slot.set_item(item)
add_child(slot)
CenterContainer(居中容器)
extends CenterContainer
# 子节点自动居中
# 常用于主菜单、弹窗
其他容器
| 容器 | 功能 |
|---|---|
HBoxContainer | 水平排列 |
VBoxContainer | 垂直排列 |
GridContainer | 网格排列 |
CenterContainer | 居中对齐 |
MarginContainer | 添加边距 |
TabContainer | 选项卡切换 |
ScrollContainer | 可滚动区域 |
SplitContainer | 可拖拽分割 |
AspectRatioContainer | 保持宽高比 |
锚点(Anchors)与边距(Margins)
锚点和边距是 Godot UI 布局的核心概念。
锚点系统
锚点定义了 Control 节点相对于父节点的参考点。
锚点值范围: 0.0 ~ 1.0
父节点:
(0,0)───────────────────(1,0)
│ │
│ (0.5, 0.5) 中心 │
│ │
(0,1)───────────────────(1,1)
预设锚点
| 预设 | anchor_left | anchor_top | anchor_right | anchor_bottom |
|---|---|---|---|---|
| Full Rect | 0 | 0 | 1 | 1 |
| Center | 0.5 | 0.5 | 0.5 | 0.5 |
| Top Wide | 0 | 0 | 1 | 0 |
| Bottom Wide | 0 | 1 | 1 | 1 |
| Left Wide | 0 | 0 | 0 | 1 |
| Right Wide | 1 | 0 | 1 | 1 |
代码设置锚点
extends Control
func _ready() -> void:
# 设置锚点为全屏
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 1.0
anchor_bottom = 1.0
# 边距设为 0(填满父节点)
margin_left = 0
margin_top = 0
margin_right = 0
margin_bottom = 0
# 使用预设
func set_full_rect() -> void:
rect_size = get_viewport_rect().size
# 或使用 anchor 预设
anchor_left = 0
anchor_top = 0
anchor_right = 1
anchor_bottom = 1
margin_left = 0
margin_top = 0
margin_right = 0
margin_bottom = 0
# 居中显示
func center_in_parent(size: Vector2) -> void:
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
margin_left = -size.x / 2
margin_top = -size.y / 2
margin_right = size.x / 2
margin_bottom = size.y / 2
边距(Margins)
边距是相对于锚点的偏移量(像素)。
# 左上角固定位置
anchor_left = 0
anchor_top = 0
anchor_right = 0
anchor_bottom = 0
margin_left = 20 # 距左边缘 20px
margin_top = 20 # 距上边缘 20px
margin_right = 220 # 右边距(用于设置宽度)
margin_bottom = 120 # 下边距(用于设置高度)
# 右下角固定位置
anchor_left = 1
anchor_top = 1
anchor_right = 1
anchor_bottom = 1
margin_left = -120 # 负值 = 从右往左偏移
margin_top = -80
margin_right = -20
margin_bottom = -20
💡 提示:理解锚点和边距的关系是做好 UI 适配的关键。锚点决定参考点,边距决定偏移量。
主题(Theme)
主题用于统一 UI 的视觉风格。
创建主题
extends Control
func _ready() -> void:
var theme = Theme.new()
# 设置默认字体
var font = DynamicFont.new()
font.font_data = load("res://assets/fonts/GameFont.ttf")
font.size = 18
theme.default_font = font
# 设置 Button 样式
var normal_style = StyleBoxFlat.new()
normal_style.bg_color = Color(0.2, 0.2, 0.3)
normal_style.border_width_bottom = 2
normal_style.border_color = Color(0.4, 0.4, 0.5)
normal_style.corner_radius_top_left = 4
normal_style.corner_radius_top_right = 4
normal_style.corner_radius_bottom_left = 4
normal_style.corner_radius_bottom_right = 4
var hover_style = StyleBoxFlat.new()
hover_style.bg_color = Color(0.3, 0.3, 0.4)
var pressed_style = StyleBoxFlat.new()
pressed_style.bg_color = Color(0.15, 0.15, 0.2)
theme.set_stylebox("normal", "Button", normal_style)
theme.set_stylebox("hover", "Button", hover_style)
theme.set_stylebox("pressed", "Button", pressed_style)
theme.set_color("font_color", "Button", Color.white)
# 设置 Label 样式
theme.set_color("font_color", "Label", Color(0.9, 0.9, 0.9))
# 应用主题
$UI.set_theme(theme)
StyleBoxFlat 样式
# 创建一个漂亮的面板背景
func create_panel_style() -> StyleBoxFlat:
var style = StyleBoxFlat.new()
style.bg_color = Color(0.1, 0.1, 0.15, 0.9)
# 边框
style.border_width_left = 2
style.border_width_right = 2
style.border_width_top = 2
style.border_width_bottom = 2
style.border_color = Color(0.3, 0.5, 0.8)
# 圆角
style.corner_radius_top_left = 8
style.corner_radius_top_right = 8
style.corner_radius_bottom_left = 8
style.corner_radius_bottom_right = 8
# 内边距
style.content_margin_left = 16
style.content_margin_right = 16
style.content_margin_top = 8
style.content_margin_bottom = 8
# 阴影
style.shadow_color = Color(0, 0, 0, 0.3)
style.shadow_size = 4
style.shadow_offset = Vector2(2, 2)
return style
StyleBoxTexture(纹理样式)
func create_textured_style() -> StyleBoxTexture:
var style = StyleBoxTexture.new()
style.texture = preload("res://assets/ui/panel_bg.png")
style.margin_left = 16
style.margin_right = 16
style.margin_top = 16
style.margin_bottom = 16
return style
信号交互
UI 信号交互模式
extends Control
# 动态创建 UI
func _ready() -> void:
# 创建按钮
var button = Button.new()
button.text = "点击我"
button.rect_position = Vector2(100, 200)
button.rect_size = Vector2(200, 50)
button.connect("pressed", self, "_on_button_pressed")
add_child(button)
# 创建滑块
var slider = HSlider.new()
slider.min_value = 0
slider.max_value = 100
slider.value = 50
slider.rect_position = Vector2(100, 300)
slider.connect("value_changed", self, "_on_slider_changed")
add_child(slider)
func _on_button_pressed() -> void:
print("按钮被点击!")
func _on_slider_changed(value: float) -> void:
print("滑块值: ", value)
AudioServer.set_bus_volume_db(0, linear2db(value / 100))
菜单导航
extends Control
var buttons: Array = []
var current_index: int = 0
func _ready() -> void:
buttons = [$StartButton, $OptionsButton, $QuitButton]
_highlight_button(0)
# 连接所有按钮
$StartButton.connect("pressed", self, "_on_start")
$OptionsButton.connect("pressed", self, "_on_options")
$QuitButton.connect("pressed", self, "_on_quit")
func _input(event: InputEvent) -> void:
if event.is_action_pressed("ui_down"):
current_index = (current_index + 1) % buttons.size()
_highlight_button(current_index)
elif event.is_action_pressed("ui_up"):
current_index = (current_index - 1 + buttons.size()) % buttons.size()
_highlight_button(current_index)
elif event.is_action_pressed("ui_accept"):
buttons[current_index].emit_signal("pressed")
func _highlight_button(index: int) -> void:
for i in buttons.size():
buttons[i].modulate = Color.white if i == index else Color(0.6, 0.6, 0.6)
buttons[index].grab_focus()
func _on_start() -> void:
get_tree().change_scene("res://scenes/Game.tscn")
func _on_options() -> void:
$OptionsMenu.show()
func _on_quit() -> void:
get_tree().quit()
弹出对话框(Popup)
AcceptDialog(确认对话框)
extends Control
func show_message(title: String, message: String) -> void:
var dialog = AcceptDialog.new()
dialog.dialog_text = message
dialog.window_title = title
dialog.popup_exclusive = true
add_child(dialog)
dialog.popup_centered(Vector2(300, 150))
dialog.connect("popup_hide", dialog, "queue_free")
ConfirmationDialog(确认/取消对话框)
extends Control
func confirm_quit() -> void:
var dialog = ConfirmationDialog.new()
dialog.dialog_text = "确定要退出游戏吗?"
dialog.window_title = "退出"
add_child(dialog)
dialog.popup_centered()
dialog.connect("confirmed", self, "_on_quit_confirmed")
dialog.connect("popup_hide", dialog, "queue_free")
func _on_quit_confirmed() -> void:
get_tree().quit()
自定义弹窗
extends CanvasLayer
onready var panel = $Panel
onready var title_label = $Panel/Title
onready var content_label = $Panel/Content
signal popup_closed(result: bool)
func show_popup(title: String, content: String) -> void:
title_label.text = title
content_label.text = content
visible = true
# 动画效果
panel.rect_scale = Vector2.ZERO
$Tween.interpolate_property(
panel, "rect_scale",
Vector2.ZERO, Vector2.ONE, 0.2,
Tween.TRANS_BACK, Tween.EASE_OUT
)
$Tween.start()
func _on_confirm_pressed() -> void:
_close(true)
func _on_cancel_pressed() -> void:
_close(false)
func _close(result: bool) -> void:
$Tween.interpolate_property(
panel, "rect_scale",
Vector2.ONE, Vector2.ZERO, 0.15,
Tween.TRANS_BACK, Tween.EASE_IN
)
$Tween.start()
yield($Tween, "tween_all_completed")
visible = false
emit_signal("popup_closed", result)
自定义绘制(_draw)
基本绘制
extends Control
func _draw() -> void:
# 绘制线段
draw_line(Vector2(0, 0), Vector2(100, 100), Color.red, 2.0)
# 绘制矩形
draw_rect(Rect2(10, 10, 80, 40), Color.blue)
# 绘制填充矩形
draw_rect(Rect2(120, 10, 80, 40), Color.green, true)
# 绘制圆形
draw_circle(Vector2(250, 50), 30, Color.yellow)
# 绘制弧线
draw_arc(Vector2(350, 50), 30, 0, PI, 32, Color.cyan, 2.0)
# 绘制多边形
var points = PoolVector2Array([
Vector2(400, 10),
Vector2(450, 40),
Vector2(430, 80),
Vector2(370, 80),
Vector2(350, 40),
])
draw_colored_polygon(points, Color(1, 0.5, 0))
# 绘制纹理
var tex = preload("res://icon.png")
draw_texture(tex, Vector2(500, 10))
# 需要更新时调用
func _process(delta: float) -> void:
update() # 触发 _draw 重新调用
小地图绘制
extends Control
export var map_size: Vector2 = Vector2(200, 200)
export var world_size: Vector2 = Vector2(2000, 2000)
export var dot_size: float = 4.0
var player_pos: Vector2 = Vector2.ZERO
var enemy_positions: Array = []
func _draw() -> void:
# 背景
draw_rect(Rect2(Vector2.ZERO, map_size), Color(0, 0, 0, 0.5))
# 边框
draw_rect(Rect2(Vector2.ZERO, map_size), Color.white, false, 2.0)
# 敌人点
for pos in enemy_positions:
var map_pos = _world_to_map(pos)
draw_circle(map_pos, dot_size, Color.red)
# 玩家点
var player_map_pos = _world_to_map(player_pos)
draw_circle(player_map_pos, dot_size * 1.5, Color.green)
func _world_to_map(world_pos: Vector2) -> Vector2:
return Vector2(
world_pos.x / world_size.x * map_size.x,
world_pos.y / world_size.y * map_size.y
)
func update_map(p_pos: Vector2, e_positions: Array) -> void:
player_pos = p_pos
enemy_positions = e_positions
update() # 重新绘制
UI 适配策略
设计分辨率
# project.godot 中设置:
# display/window/size/width = 1920
# display/window/size/height = 1080
# display/window/stretch/mode = "2d"
# display/window/stretch/aspect = "keep"
# 不同适配模式:
# "disabled" - 不拉伸,保持原始像素
# "canvas_items" (2d) - 拉伸画布,保持像素比例
# "viewport" - 拉伸视口,像素完美但可能模糊
响应式布局策略
| 方案 | 适用场景 | 实现方式 |
|---|---|---|
| 锚点 + 容器 | 通用 UI | MarginContainer, VBox/HBox |
| 动态缩放 | 像素游戏 | 代码缩放 rect_scale |
| 多套 UI | 多平台 | 根据平台切换 UI 场景 |
自适应 UI 代码
extends Control
func _ready() -> void:
get_tree().get_root().connect("size_changed", self, "_on_resize")
_on_resize()
func _on_resize() -> void:
var viewport_size = get_viewport_rect().size
# 根据分辨率调整 UI
if viewport_size.x < 1280:
_apply_mobile_layout()
elif viewport_size.x < 1920:
_apply_hd_layout()
else:
_apply_fullhd_layout()
func _apply_mobile_layout() -> void:
$HUD.rect_scale = Vector2(1.5, 1.5)
$Menu.rect_scale = Vector2(1.3, 1.3)
func _apply_hd_layout() -> void:
$HUD.rect_scale = Vector2(1.2, 1.2)
$Menu.rect_scale = Vector2(1.1, 1.1)
func _apply_fullhd_layout() -> void:
$HUD.rect_scale = Vector2.ONE
$Menu.rect_scale = Vector2.ONE
安全区域(手机刘海屏)
extends Control
func _ready() -> void:
# 获取安全区域
var safe_area = OS.get_window_safe_area()
# 设置 UI 边距
$MarginContainer.margin_left = safe_area.position.x
$MarginContainer.margin_top = safe_area.position.y
$MarginContainer.margin_right = -(OS.window_size.x - safe_area.end.x)
$MarginContainer.margin_bottom = -(OS.window_size.y - safe_area.end.y)
完整 UI 示例:游戏 HUD
extends CanvasLayer
# 节点引用
onready var hp_bar = $Margin/VBox/HPBar
onready var mp_bar = $Margin/VBox/MPBar
onready var score_label = $Margin/ScoreLabel
onready var combo_label = $Margin/ComboLabel
onready var boss_bar = $Margin/BossBar
var combo_count: int = 0
var combo_timer: float = 0.0
func _ready() -> void:
# 初始隐藏
boss_bar.visible = false
combo_label.visible = false
func _process(delta: float) -> void:
# 连击计时器
if combo_timer > 0:
combo_timer -= delta
if combo_timer <= 0:
_reset_combo()
# 更新血条
func update_hp(current: int, maximum: int) -> void:
var ratio = float(current) / maximum
hp_bar.value = ratio * 100
# 低血量警告
if ratio < 0.3:
$Tween.interpolate_property(
hp_bar, "modulate",
Color.red, Color.white, 0.5
)
$Tween.start()
# 更新蓝条
func update_mp(current: int, maximum: int) -> void:
mp_bar.value = float(current) / maximum * 100
# 更新分数
func update_score(score: int) -> void:
score_label.text = "SCORE: %08d" % score
# 分数弹出动画
$Tween.interpolate_property(
score_label, "rect_scale",
Vector2(1.2, 1.2), Vector2.ONE, 0.2,
Tween.TRANS_BACK
)
$Tween.start()
# 连击系统
func add_combo() -> void:
combo_count += 1
combo_timer = 2.0 # 2秒内保持连击
combo_label.text = "%d COMBO!" % combo_count
combo_label.visible = true
# 连击动画
$Tween.interpolate_property(
combo_label, "rect_scale",
Vector2(1.5, 1.5), Vector2.ONE, 0.2,
Tween.TRANS_ELASTIC
)
$Tween.start()
func _reset_combo() -> void:
combo_count = 0
combo_label.visible = false
# Boss 血条
func show_boss_hp(boss_name: String, hp: int, max_hp: int) -> void:
boss_bar.visible = true
$Margin/BossBar/Name.text = boss_name
$Margin/BossBar/Bar.value = float(hp) / max_hp * 100
func hide_boss_hp() -> void:
boss_bar.visible = false
# 淡入淡出
func fade_in(duration: float = 0.5) -> void:
$FadeRect.visible = true
$Tween.interpolate_property(
$FadeRect, "color:a",
1.0, 0.0, duration
)
$Tween.start()
yield($Tween, "tween_all_completed")
$FadeRect.visible = false
func fade_out(duration: float = 0.5) -> void:
$FadeRect.visible = true
$Tween.interpolate_property(
$FadeRect, "color:a",
0.0, 1.0, duration
)
$Tween.start()
对应的场景结构:
CanvasLayer (HUD)
├── FadeRect (ColorRect) - 全屏黑色遮罩
└── Margin (MarginContainer)
├── VBox (VBoxContainer)
│ ├── HPBar (ProgressBar) - 生命条
│ └── MPBar (ProgressBar) - 魔法条
├── ScoreLabel (Label) - 分数
├── ComboLabel (Label) - 连击
└── BossBar (HBoxContainer)
├── Name (Label) - Boss 名字
└── Bar (ProgressBar) - Boss 血条