强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 + 滑块/复选框

⚠️ 常见陷阱

  1. UI 节点的 positionglobal_position 不同,锚点影响定位
  2. Container 内的子节点不要手动设置 position,会被容器覆盖
  3. 主题继承是向上传递的,在根 Control 设置主题影响所有子节点
  4. 鼠标过滤模式影响事件传递,确保设对了 MOUSE_FILTER_STOP/PASS/IGNORE
  5. set_anchors_preset() 需要在 offset 设置之前调用,否则偏移会被覆盖

扩展阅读