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

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 设置之前调用,否则偏移会被覆盖

扩展阅读