第11章 UI 系统
11.1 Control 节点基础
Control 是所有 UI 节点的基类。
锚点系统示意图
父容器(如屏幕 1280x720)
┌─────────────────────────────────────┐
│(0,0) (1,0)│
│ 左上角 右上角 │
│ │
│ (0.5, 0.5) │
│ 中心 │
│ │
│ 左下角 右下角 │
│(0,1) (1,1)│
└─────────────────────────────────────┘
常见锚点配置:
左上角固定 居中固定 全屏拉伸
┌────┐ ┌────┐ ┌────────────┐
│按钮│ │ │ │ │
└────┘ │按钮│ │ 按钮 │
│ │ │ │
└────┘ └────────────┘
(0,0,0,0) (.5,.5,.5,.5) (0,0,1,1)
gdscript
# 锚点(Anchor):决定 Control 相对于父容器的定位参考点
# 范围:0.0 ~ 1.0
# (0, 0) = 左上角, (1, 1) = 右下角, (0.5, 0.5) = 中心
# 常见锚点预设
# 左上角:anchor_left=0, anchor_top=0, anchor_right=0, anchor_bottom=0
# 右下角:anchor_left=1, anchor_top=1, anchor_right=1, anchor_bottom=1
# 全屏拉伸:anchor_left=0, anchor_top=0, anchor_right=1, anchor_bottom=1
# 居中:anchor_left=0.5, anchor_top=0.5, anchor_right=0.5, anchor_bottom=0.5
# 代码设置锚点
func set_anchor_fullscreen() -> void:
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 0
offset_top = 0
offset_right = 0
offset_bottom = 0
11.2 常用控件
Label 文本
gdscript
extends Label
func _ready() -> void:
text = "Hello, Godot!"
horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
vertical_alignment = VERTICAL_ALIGNMENT_CENTER
# 设置字体大小(通过 Theme)
add_theme_font_size_override("font_size", 24)
# 自动换行
autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
Button 按钮
gdscript
extends Button
func _ready() -> void:
text = "开始游戏"
# 连接信号
pressed.connect(_on_pressed)
button_down.connect(_on_button_down) # 按下
button_up.connect(_on_button_up) # 松开
toggled.connect(_on_toggled) # 切换模式
func _on_pressed() -> void:
print("按钮被点击!")
# 切换模式
func _on_toggled(is_pressed: bool) -> void:
toggle_mode = true # 在检查器中启用
print("切换状态:", is_pressed)
ProgressBar 进度条
gdscript
extends ProgressBar
# ⚠️ Godot 4.x 中 ProgressBar 没有 show_percentage 属性
# 如需显示百分比文字,用子节点 Label 实现
func _ready() -> void:
min_value = 0
max_value = 100
value = 100
func update_health(current: int, maximum: int) -> void:
max_value = maximum
value = current
# 颜色变化
var ratio := float(current) / maximum
if ratio < 0.3:
modulate = Color.RED
elif ratio < 0.6:
modulate = Color.YELLOW
else:
modulate = Color.GREEN
TextureRect 图片
gdscript
extends TextureRect
func _ready() -> void:
texture = preload("res://assets/ui/frame.png")
stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
expand_mode = TextureRect.EXPAND_IGNORE_SIZE
11.3 CanvasLayer(UI 层分离)
gdscript
# CanvasLayer 让 UI 独立于游戏世界
# UI 不受相机移动影响
# 场景结构:
# Player (CharacterBody2D)
# Sprite2D
# CollisionShape2D
# Camera2D
# UILayer (CanvasLayer) ← 这里的 UI 跟随玩家但不受相机影响
# HealthBar (ProgressBar)
# AmmoLabel (Label)
# 设置
# CanvasLayer → Layer: 10(更高的层显示在上面)
# CanvasLayer → Follow Viewport Enabled: false(不跟随相机)
11.4 布局容器
gdscript
# VBoxContainer:垂直排列
# HBoxContainer:水平排列
# GridContainer:网格排列
# MarginContainer:边距
# CenterContainer:居中
# 代码控制容器子节点的对齐
extends VBoxContainer
func _ready() -> void:
alignment = BoxContainer.ALIGNMENT_CENTER # 居中对齐
# 属性面板中的 Separation 控制间距
11.5 UI 动画(Tween)
gdscript
# 血条平滑变化
extends ProgressBar
var target_value: float = 100.0
func set_health(new_health: float) -> void:
target_value = new_health
var tween := create_tween()
tween.tween_property(self, "value", target_value, 0.3).set_ease(Tween.EASE_OUT)
# 数字滚动效果
func animate_score(new_score: int) -> void:
var tween := create_tween()
tween.tween_method(_update_score_display, current_score, new_score, 0.5)
func _update_score_display(value: int) -> void:
$ScoreLabel.text = "分数:%d" % value
# UI 元素弹出动画
func popup_animation() -> void:
scale = Vector2.ZERO
var tween := create_tween()
tween.tween_property(self, "scale", Vector2.ONE, 0.3) \
.set_ease(Tween.EASE_OUT) \
.set_trans(Tween.TRANS_ELASTIC)
11.6 主菜单与暂停菜单
gdscript
# 暂停菜单
extends Control
func _ready() -> void:
process_mode = Node.PROCESS_MODE_ALWAYS # 暂停时也能处理输入
visible = false
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("pause"):
toggle_pause()
func toggle_pause() -> void:
get_tree().paused = not get_tree().paused
visible = get_tree().paused
func _on_resume_pressed() -> void:
toggle_pause()
func _on_quit_pressed() -> void:
get_tree().paused = false
get_tree().change_scene_to_file("res://scenes/ui/main_menu.tscn")
11.7 UI 焦点系统(手柄/键盘导航)
gdscript
# 设置焦点邻居(控制导航方向)
func setup_focus_navigation() -> void:
$Button1.focus_neighbor_right = $Button2.get_path()
$Button2.focus_neighbor_left = $Button1.get_path()
$Button2.focus_neighbor_right = $Button3.get_path()
$Button3.focus_neighbor_left = $Button2.get_path()
# 设置初始焦点
$Button1.grab_focus()
# focus_mode 设置
# FOCUS_NONE = 0 不能获取焦点
# FOCUS_CLICK = 1 只能通过点击获取
# FOCUS_ALL = 2 可以通过键盘/手柄获取
11.8 多分辨率适配
gdscript
# 项目设置 → 窗口 → 拉伸
# Mode: canvas_items # 推荐:缩放画布内容
# Aspect: keep # 保持宽高比,可能有黑边
# Aspect: expand # 扩展,显示更多内容
# 代码中获取实际显示信息
func _ready() -> void:
var viewport_size := get_viewport().get_visible_rect().size
var window_size := DisplayServer.window_get_size()
var scale_factor := viewport_size.x / 1280.0 # 基于设计分辨率
11.9 Theme 主题
gdscript
# 创建全局主题
var theme := Theme.new()
# 设置字体
var font := load("res://assets/fonts/main_font.tres")
theme.set_default_font(font)
theme.set_default_font_size(16)
# 设置按钮样式
var normal_style := StyleBoxFlat.new()
normal_style.bg_color = Color(0.2, 0.2, 0.3)
normal_style.corner_radius_top_left = 8
theme.set_stylebox("normal", "Button", normal_style)
var hover_style := StyleBoxFlat.new()
hover_style.bg_color = Color(0.3, 0.3, 0.5)
theme.set_stylebox("hover", "Button", hover_style)
# 应用主题
get_tree().root.theme = theme
实操练习
练习:完整 HUD
gdscript
extends CanvasLayer
@onready var health_bar: ProgressBar = %HealthBar
@onready var score_label: Label = %ScoreLabel
@onready var combo_label: Label = %ComboLabel
func _ready() -> void:
GameManager.score_changed.connect(_on_score_changed)
GameManager.combo_changed.connect(_on_combo_changed)
func update_health(current: int, maximum: int) -> void:
var tween := create_tween()
tween.tween_property(health_bar, "value", float(current) / maximum * 100, 0.3)
func _on_score_changed(new_score: int) -> void:
var tween := create_tween()
tween.tween_method(
func(v): score_label.text = "分数:%d" % v,
int(score_label.text.replace("分数:", "")),
new_score,
0.5
)
func _on_combo_changed(combo: int) -> void:
combo_label.text = "%d 连击!" % combo
combo_label.visible = combo > 1
# 弹出动画
combo_label.scale = Vector2(1.5, 1.5)
var tween := create_tween()
tween.tween_property(combo_label, "scale", Vector2.ONE, 0.2).set_trans(Tween.TRANS_ELASTIC)