Godot 4 中的控件布局
大多数出问题的 Godot UI 都源于三个错误:手动放置容器的子控件、锚定控件时未重置偏移量、以及保持拉伸设置为默认值。请使用容器进行布局,只锚定每个 UI 的根节点,并让最小尺寸驱动一切。
容器管理其子控件
任何 Container(VBoxContainer、HBoxContainer、GridContainer、MarginContainer、PanelContainer、CenterContainer、ScrollContainer、FlowContainer)的子控件都无法保留自己的 position、size 或锚点:容器会在下一次排序时覆盖它们。通过以下方式控制子控件的空间分配:
size_flags_horizontal/size_flags_vertical:Control.SIZE_FILL(默认)、SIZE_EXPAND_FILL用于占据额外空间、SIZE_SHRINK_CENTER/SIZE_SHRINK_END用于保持小尺寸并对齐。size_flags_stretch_ratio:在可扩展的同级控件之间分配额外空间的比例。custom_minimum_size:子控件接受的最小尺寸。容器永远不会将子控件缩小到其组合最小尺寸以下,这就是文本和按钮避免被裁剪的方式。
根据布局选择容器:行或列(VBoxContainer、HBoxContainer)、网格(GridContainer 配合 columns)、边距(MarginContainer,通过主题常量 margin_left、margin_top、margin_right、margin_bottom 设置)、背景(PanelContainer,样式盒 panel)、长内容(ScrollContainer 配合一个具有 SIZE_EXPAND_FILL 的子控件)、自动换行项(HFlowContainer)。
仅锚定根节点
锚点将控件相对于其父控件的矩形定位,用于最外层的 UI 节点。在代码中,使用 set_anchors_and_offsets_preset()。set_anchors_preset() 仅移动锚点并保留旧偏移量,因此代码中创建的控件会保持尺寸 (0, 0)。
预设:PRESET_FULL_RECT、PRESET_TOP_LEFT、PRESET_TOP_RIGHT、PRESET_BOTTOM_LEFT、PRESET_BOTTOM_RIGHT、PRESET_CENTER、PRESET_TOP_WIDE、PRESET_BOTTOM_WIDE、PRESET_LEFT_WIDE、PRESET_RIGHT_WIDE、PRESET_HCENTER_WIDE、PRESET_VCENTER_WIDE,以及中心边缘预设如 PRESET_CENTER_TOP。
基于 CanvasLayer 的 HUD
位于 Node2D 下的 Control 在世界空间中绘制,并随 Camera2D 移动。将 HUD 放在 CanvasLayer 下,使其保持在屏幕上;layer 用于排序多个画布层(更高的数值绘制在上层)。
extends CanvasLayer
var score_label: Label
func _ready() -> void:
var margin := MarginContainer.new()
margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
for side in ["margin_left", "margin_top", "margin_right", "margin_bottom"]:
margin.add_theme_constant_override(side, 16)
add_child(margin)
var column := VBoxContainer.new()
margin.add_child(column)
var top_bar := HBoxContainer.new()
column.add_child(top_bar)
score_label = Label.new()
score_label.text = "Score: 0"
score_label.add_theme_font_size_override("font_size", 24)
score_label.add_theme_color_override("font_color", Color.GOLD)
top_bar.add_child(score_label)
var spacer := Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
spacer.mouse_filter = Control.MOUSE_FILTER_IGNORE
top_bar.add_child(spacer)
var pause_button := Button.new()
pause_button.text = "Pause"
pause_button.custom_minimum_size = Vector2(96, 40)
top_bar.add_child(pause_button)
func set_score(value: int) -> void:
score_label.text = "Score: %d" % value
在编辑器中构建,相同的树结构是:CanvasLayer > MarginContainer (Layout: Full Rect) > VBoxContainer > HBoxContainer > Label, Control (Expand), Button。
不裁剪的文本
- 设置了
autowrap_mode的Label需要从某处获得宽度。在默认标志的HBoxContainer内部,它会折叠为 1 像素宽并变得非常高。为其赋予SIZE_EXPAND_FILL或设置custom_minimum_size.x。 - 对于固定空间中的单行文本,设置
text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS(或clip_text = true)。 - 优先使用
add_theme_font_size_override("font_size", n)而非缩放控件:scale不会改变容器预留的尺寸。
extends VBoxContainer
func add_message(text: String) -> void:
var label := Label.new()
label.text = text
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
add_child(label)
func add_title(text: String) -> void:
var label := Label.new()
label.text = text
label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
label.custom_minimum_size = Vector2(0, 32)
add_child(label)
不会穿透到游戏的点击
mouse_filter 决定控件是否消耗鼠标输入。默认值不同:普通 Control、ColorRect 和 Panel 使用 MOUSE_FILTER_STOP,容器使用 MOUSE_FILTER_PASS,Label 使用 MOUSE_FILTER_IGNORE。设置为 STOP 的全屏 Control 或 ColorRect 会吞噬所有点击,导致游戏中的 _unhandled_input() 永远无法接收到。在覆盖层、间隔器和装饰矩形上设置 mouse_filter = Control.MOUSE_FILTER_IGNORE。
主题覆盖(Godot 4 名称)
| Godot 3 | Godot 4 |
|---|---|
add_color_override("font_color", c) | add_theme_color_override("font_color", c) |
add_font_override("font", f) | add_theme_font_override("font", f) |
| (字号存在于 DynamicFont 中) | add_theme_font_size_override("font_size", 24) |
add_stylebox_override("normal", sb) | add_theme_stylebox_override("normal", sb) |
add_constant_override("separation", 8) | add_theme_constant_override("separation", 8) |
add_icon_override("icon", tex) | add_theme_icon_override("icon", tex) |
.tscn 中的 custom_colors/font_color | theme_override_colors/font_color |
使用对应的 remove_theme_*_override(name) 移除覆盖。为了一致的外观,建议将单个 Theme 资源分配给根控件,而不是覆盖每个节点;子节点会继承它。
分辨率与拉伸
在项目设置 > 显示 > 窗口中设置(它们存储在 project.godot 中):
display/window/size/viewport_width和viewport_height:你设计所基于的基本分辨率(默认为 1152 x 648)。display/window/stretch/mode:canvas_items适用于大多数游戏(UI 和 2D 平滑缩放),viewport适用于像素艺术,disabled适用于工具。Godot 3 的2d模式在 Godot 4 中是canvas_items。该设置的默认值为disabled;Godot 4.7 或更高版本创建的项目默认使用canvas_items。display/window/stretch/aspect:expand让锚定 UI 利用更宽或更高屏幕上的额外空间;keep添加黑边。默认为keep;Godot 4.7 或更高版本创建的项目默认使用expand。display/window/stretch/scale_mode:integer用于清晰的像素艺术。
在运行时,相同的设置位于窗口上:get_window().content_scale_mode = Window.CONTENT_SCALE_MODE_CANVAS_ITEMS 和 get_window().content_scale_aspect = Window.CONTENT_SCALE_ASPECT_EXPAND。
UI 被裁剪或错位时的检查清单
- 该控件是否是具有手动设置的
position或size的容器子控件?请使用尺寸标志和custom_minimum_size。 - 根节点是否仅使用了
set_anchors_preset()进行锚定?请使用set_anchors_and_offsets_preset()。 - 自动换行的
Label是否具有宽度? - HUD 是否位于
CanvasLayer下而非Node2D下? - 当窗口尺寸变化时,
display/window/stretch/mode是否仍为disabled? - 是否存在一个不可见的、具有
MOUSE_FILTER_STOP的全屏控件位于顶层?