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

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 血条

扩展阅读