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

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_leftanchor_topanchor_rightanchor_bottom
Full Rect0011
Center0.50.50.50.5
Top Wide0010
Bottom Wide0111
Left Wide0001
Right Wide1011

代码设置锚点

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" - 拉伸视口,像素完美但可能模糊

响应式布局策略

方案适用场景实现方式
锚点 + 容器通用 UIMarginContainer, 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 血条

扩展阅读