Layout de controles en Godot 4
La mayoría de las UI rotas en Godot provienen de tres errores: posicionar hijos de un contenedor manualmente, anclar un control sin reiniciar sus offsets, y dejar la configuración de estiramiento en sus valores predeterminados. Diseña con contenedores, ancla solo la raíz de cada UI, y deja que los tamaños mínimos guíen todo lo demás.
Los contenedores son dueños de sus hijos
Un hijo de cualquier Container (VBoxContainer, HBoxContainer, GridContainer, MarginContainer, PanelContainer, CenterContainer, ScrollContainer, FlowContainer) no puede conservar su propio position, size ni anclajes: el contenedor los sobrescribe en el siguiente ordenamiento. Controla la porción de espacio de un hijo con:
size_flags_horizontal/size_flags_vertical:Control.SIZE_FILL(por defecto),SIZE_EXPAND_FILLpara ocupar espacio sobrante,SIZE_SHRINK_CENTER/SIZE_SHRINK_ENDpara mantenerse pequeño y alineado.size_flags_stretch_ratio: proporción del espacio sobrante entre hermanos que se expanden.custom_minimum_size: el tamaño mínimo que el hijo acepta. Los contenedores nunca reducen un hijo por debajo de su tamaño mínimo combinado, que es como el texto y los botones evitan cortarse.
Elige el contenedor según el layout: filas o columnas (VBoxContainer, HBoxContainer), una cuadrícula (GridContainer con columns), márgenes (MarginContainer, márgenes establecidos por las constantes de tema margin_left, margin_top, margin_right, margin_bottom), un fondo (PanelContainer, style box panel), contenido largo (ScrollContainer con un hijo que tenga SIZE_EXPAND_FILL), elementos que envuelven (HFlowContainer).
Anclajes solo para la raíz
Los anclajes colocan un control en relación con el rectángulo de su padre y son lo que usas para el nodo de UI más externo. Desde código, usa set_anchors_and_offsets_preset(). set_anchors_preset() solo mueve los anclajes y mantiene los offsets antiguos, por lo que un control creado en código se queda con tamaño (0, 0).
Presets: 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, y los presets de borde central como PRESET_CENTER_TOP.
HUD en un CanvasLayer
Un Control debajo de un Node2D se dibuja en el espacio del mundo y se mueve con la Camera2D. Pon el HUD debajo de un CanvasLayer para que permanezca en pantalla; layer ordena varias capas del canvas (cuanto mayor, más arriba se dibuja).
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
Construido en el editor, el mismo árbol es: CanvasLayer > MarginContainer (Layout: Full Rect) > VBoxContainer > HBoxContainer > Label, Control (Expand), Button.
Texto que no se recorta
- Una
Labelconautowrap_modeestablecido necesita un ancho de algún lado. Dentro de unHBoxContainercon flags predeterminados, se colapsa a 1 píxel de ancho y se vuelve muy alta. DaleSIZE_EXPAND_FILLo uncustom_minimum_size.x. - Para texto de una línea en un espacio fijo, establece
text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS(oclip_text = true). - Prefiere
add_theme_font_size_override("font_size", n)en lugar de escalar un control:scaleno cambia el tamaño que los contenedores reservan.
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)
Clics que no llegan al juego
mouse_filter decide si un control consume la entrada del ratón. Los valores predeterminados difieren: Control, ColorRect y Panel usan MOUSE_FILTER_STOP, los contenedores usan MOUSE_FILTER_PASS, Label usa MOUSE_FILTER_IGNORE. Un Control o ColorRect a pantalla completa con STOP se traga cada clic, por lo que _unhandled_input() en el juego nunca lo ve. Establece mouse_filter = Control.MOUSE_FILTER_IGNORE en superposiciones, espaciadores y rectángulos decorativos.
Anulaciones de tema (nombres 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) |
| (el tamaño de fuente vivía en 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) |
custom_colors/font_color en .tscn | theme_override_colors/font_color |
Elimina una anulación con el correspondiente remove_theme_*_override(nombre). Para una apariencia consistente, asigna un recurso Theme al control raíz en lugar de anular cada nodo; los hijos lo heredan.
Resolución y estiramiento
Establece esto en Proyecto > Configuración del proyecto > Pantalla > Ventana (se almacenan en project.godot):
display/window/size/viewport_widthyviewport_height: la resolución base para la que diseñas (por defecto 1152 x 648).display/window/stretch/mode:canvas_itemspara la mayoría de los juegos (la UI y el 2D se escalan suavemente),viewportpara arte pixelado,disabledpara herramientas. El modo2dde Godot 3 escanvas_itemsen Godot 4. El valor predeterminado esdisabled; los proyectos creados en Godot 4.7 o posteriores comienzan concanvas_items.display/window/stretch/aspect:expandpermite que la UI anclada use espacio extra en pantallas más anchas o altas;keepagrega barras negras. El valor predeterminado eskeep; los proyectos creados en Godot 4.7 o posteriores comienzan conexpand.display/window/stretch/scale_mode:integerpara arte pixelado nítido.
En tiempo de ejecución, la misma configuración está en la ventana: get_window().content_scale_mode = Window.CONTENT_SCALE_MODE_CANVAS_ITEMS y get_window().content_scale_aspect = Window.CONTENT_SCALE_ASPECT_EXPAND.
Lista de verificación cuando la UI está cortada o mal colocada
- ¿El control es hijo de un contenedor pero tiene
positionosizeestablecidos manualmente? Usa flags de tamaño ycustom_minimum_size. - ¿La raíz se ancló solo con
set_anchors_preset()? Usaset_anchors_and_offsets_preset(). - ¿Una
Labelcon ajuste de texto tiene un ancho? - ¿El HUD está debajo de un
CanvasLayeren lugar de unNode2D? - ¿
display/window/stretch/modesigue endisabledmientras el tamaño de la ventana cambia? - ¿Hay un control invisible a pantalla completa con
MOUSE_FILTER_STOPencima de todo?