Table of Contents
Introduction
Ready to take your Godot UI skills to the next level? We'll delve into the powerful world of control nodes and show you how to master them for creating dynamic and interactive user interfaces in Godot.
I briefly covered control nodes in the Godot Fundamentals series , but I felt we needed to dig a bit deeper to how we can better control the position and sizing of our UI.
Video
Control Layout Properties
In Godot, if we create a scene with a root Node of type Control
we can open up the Inspector panel to view it's properties.
Godot 4 has moved all of the anchor and sizing details under the Layout
category.
If we expand the Layout category we'll see the Anchors Preset
property, as well as the Container Sizing
Category.
It's important to note that the Transform
's size and position properties will be affected directly by the settings of our anchors and container sizing.
If we select the Anchor Presets dropdown, we'll see it has a preset for:
- Custom
- Full Rect
- Top Left
- Top Right
- Bottom Right
- Bottom Left
- Center Left
- Center Top
- Center Right
- Center Bottom
- Center
- Left Wide
- Top Wide
- Right Wide
- Bottom Wide
- V Center Wide
- H Center Wide
Anchor Points, Offsets, Grow Direction
To better understand what these presets are doing, we can select and explore the Custom preset. With our Custom preset selected, we can see new property categories in our Inspector.
Anchor Points:
Anchor Points
define the origin/bounds of our Control
, and span from 0 to 1 along the axis from left to right and top to bottom. 0 defines the beginning of the axis, where as 1 defines the end of the axis.
If we modify the Anchor Points
we can see how it adjusts the size and position of our Control
along those axis:
We can also better understand the Anchor Points
as more of anchored edges, as it's the edge we are pushing along the axis. For example setting our Left edge to .5 will push it to the halfway point of it's parent's bounds.
Anchor Offsets:
Anchor Offsets
behave similar to the Anchor Points
except they use pixels instead of a percentage to determine how much further from the Anchor Points
to offset the position and size of our Control
.
If we increase the offsets by 32px, you'll see that the edges are pushed out by 32px along those directions. Therefore for the Right and Bottom directions, we would want to subtract 32px if we wanted to create a margin.
Again take note of how the Transform's size and position properties are changing as we adjust the offsets as well.
Grow Direction
Grow Direction
controls the direction along the axis in which a control should grow if the minimum size is changed to be greater than its current size, as a control always has to be at least the minimum size.
We can visualize this by setting a minimum size of our Control
by setting it's custom_minimum_size property to 320
on the x and 64
on the y. Next set the Anchor points to be 0.5
on the Left, 0
on Top, 0.5
on the Right, and 1
on the Bottom. With these settings we can see that our control is anchored to the center, but the minimum size of our Control node pushes the edges in both directions along each axis.
If we select our Horizontal Grow Direction
and select it to the Left, we'll see the anchor stays in the center, but the Control
's position and size pushes out to the left of our anchor point.
We'll also notice that the Anchor Offsets
are respected with our Grow Direction
and continue to offset along the correct axis.
Anchor Presets
Now that we've taken a look at the 3 different components that make up our Anchor Presets
, we can look at how these presets are made up of these components.
Full Rect
The Full Rect
preset can be defined with Anchor Points
of Left
: 0, Top
: 0, Right
: 1, and Bottom
: 1. By default it will its Anchor Offsets
all set to 0px. As for the Grow Direction
they will both be defined to grow in Both directions. This will then anchor our edges along the full size of our parent container and has no additional margin/padding/offsets.
Top Left/Top Right/Bottom Left/Bottom Right
These presets act as a corner pin in the respective corner.
If we select the Top Left
preset and then immediately select the Custom
preset, we'll see that the settings of the Top Left
preset are retained and we can observe how components compose this preset.
Visualizer Tool Script
To better visualize the Anchor Offsets
and Grow Directions
I've created a utility tool script that you can apply to a control node and enable visualization options for each of these components.
You can find the script as a gist , or just copy it from the block below.
@tool
extends Control
## A simple Control Node Visualization tool.
##
## This script will allow user's to view information about the anchor offsets
## and growth directions of a control node by enabling the show_growth_direction
## and show_anchor_offsets flags in the Inspector.
##
## @tutorial: https://gamedevartisan.com/tutorials/understanding-godot-ui-control-nodes
@export_category("Visualize")
## This flag controls the view of the growth direction arrows.
@export var show_growth_direction: bool = false:
set(show):
show_growth_direction = show
_update_bounds()
## This flag controls the view of the anchor offsets for each edge.
@export var show_anchor_offsets: bool = false:
set(show):
show_anchor_offsets = show
_update_bounds()
## Class variable to track the full rect including the anchor's offsets.
var anchor_rect
## We connect signals to track changes as a tool script on ready.
func _ready() -> void:
minimum_size_changed.connect(_on_minimum_size_changed)
resized.connect(_on_resized)
size_flags_changed.connect(_on_size_flags_changed)
item_rect_changed.connect(_on_item_rect_changed)
## The drawing behavior for the debug information.
func _draw() -> void:
if show_anchor_offsets:
var x1 = anchor_rect.position.x
var x2 = anchor_rect.position.x + offset_left
var y1 = anchor_rect.position.y + offset_top
var y2 = anchor_rect.position.y + anchor_rect.size.y
#Left Offset
draw_line(Vector2(0, size.y / 2), Vector2(-offset_left, size.y / 2), Color.BLUE)
draw_string(get_theme_default_font(),Vector2(-offset_left / 2, size.y / 2), \
str(offset_left), HORIZONTAL_ALIGNMENT_FILL)
#Right Offset
draw_line(Vector2(size.x, size.y / 2), Vector2(size.x - offset_right, size.y / 2), Color.BLUE)
draw_string(get_theme_default_font(),Vector2(size.x - (offset_right / 2), size.y / 2), \
str(offset_right), HORIZONTAL_ALIGNMENT_FILL)
#Top Offset
draw_line(Vector2(size.x / 2, 0), Vector2(size.x / 2, -offset_top), Color.BLUE)
draw_string(get_theme_default_font(),Vector2(size.x / 2, -offset_top / 2), \
str(offset_top), HORIZONTAL_ALIGNMENT_FILL)
#Bottom Offset
draw_line(Vector2(size.x / 2, size.y), Vector2(size.x / 2, size.y -offset_bottom), Color.BLUE)
draw_string(get_theme_default_font(),Vector2(size.x / 2, size.y -(offset_bottom / 2)), \
str(offset_bottom), HORIZONTAL_ALIGNMENT_FILL)
if show_growth_direction:
#Draw Growth Directions
match(grow_horizontal):
GROW_DIRECTION_BEGIN:
_draw_arrow(Vector2(0, 0), Vector2(-96, 0), Color.GREEN)
GROW_DIRECTION_BOTH:
_draw_arrow(Vector2(0, 0), Vector2(-96, 0), Color.GREEN)
_draw_arrow(Vector2(size.x, 0), Vector2(size.x+96, 0), Color.GREEN)
GROW_DIRECTION_END:
_draw_arrow(Vector2(size.x, 0), Vector2(size.x+96, 0), Color.GREEN)
match(grow_vertical):
GROW_DIRECTION_BEGIN:
_draw_arrow(Vector2(0, 0), Vector2(0, -96), Color.GREEN)
GROW_DIRECTION_BOTH:
_draw_arrow(Vector2(0, 0), Vector2(0, -96), Color.GREEN)
_draw_arrow(Vector2(0, size.y), Vector2(0, size.y + 96), Color.GREEN)
GROW_DIRECTION_END:
_draw_arrow(Vector2(0, size.y), Vector2(0, size.y + 96), Color.GREEN)
## Updates changes to our full bounds, while also requesing to redraw.
func _update_bounds() -> void:
anchor_rect = Rect2(\
-offset_left, \
-offset_top, \
size.x - offset_right + offset_left, \
size.y - offset_bottom + offset_top\
)
queue_redraw()
func _on_size_flags_changed() -> void:
_update_bounds()
func _on_resized() -> void:
_update_bounds()
func _on_minimum_size_changed() -> void:
_update_bounds()
func _on_item_rect_changed() -> void:
_update_bounds()
## Draws an arrow from the start and end position with a given color.
func _draw_arrow(start: Vector2, end: Vector2, color: Color = Color.BLACK) -> void:
var tip_length = 16
var angle = PI / 8
var tip: Vector2 = (start - end).normalized() * tip_length
#Draw Arrow Line
draw_line(start, end, color)
#Draw Tip Lines
draw_line(end, end + tip.rotated(angle), color)
draw_line(end, end + tip.rotated(-angle), color)
Another great visualization tool is to add a ColorRect
node as a child of our Control
that fills the Full Rect
of our parent. By setting this within our layouts temporarily we can see the size and position of the Control
.
Anchor Move Mode
As we've been making adjustments within the editor by sliding the grow handles of our Control, we may notice that only the Anchor Offsets
are adjusted and the size and position modify based on those. If we were to toggle the Anchor Move Mode:
This setting changes the behavior of when we modify using those handles. While it is enabled, it will also modify the Anchor Points
as well as the Anchor Offsets
.
Container Nodes
A Container
Node is a type of Control
Node that controls the sizing and positioning of it's children, and also modifies it's own sizing based on it's children.
If we add a VBoxContainer
node as a child of our Control
, we can use this container to distribute it's children evenly in a vertical direction and control the alignment along the begin, center, or end of the container.
Set the VBoxContainer
's Anchor Preset
to Left Wide
and note that it is anchor to the Left edge of our parent Control, and spans the full height, but has a width of 0. If we add a child ColorRect
node to our VBoxContainer
, and set the color to #df00e2
and minimum size to 32x32, we should see that the parent VBoxContainer
will adjust it's size based on the children.
Container Alignment and Anchors
Now that we have a child element, we can visualize the Container
's alignment by selecting the Center
or End
options and see how the VBoxContainer
reposition's the child element based on the new alignment.
We can also see how the Container
s Anchor Presets
work by setting a the Anchor Points
to Left
: 0.5, Top
: 0, Right
: 1, Bottom
: 1. Then return to the parent Control
node and scale it's Preset to include the 32px Offset as before to create the margin.
Next we can further offset our VBoxContainer
by setting it's Anchor Offset
's Bottom to -32px and see how that is relative to the Parent's new bounds.
If we use an Anchor Preset
of V Center Wide
we can see how it sets the Anchor Points
of the Control
to the center and sets the Anchor Offsets
are set to the left and right exactly half each of the Container
's width.
Container Sizing Flags
This property tells the Child of a Container to expand or to shrink the bounds of the node along the axis.
NOTE: This property appears on all child
Control
nodes within aContainer
node.
flags SizeFlags:
● SIZE_SHRINK_BEGIN = 0
Tells the parent Container to align the node with its start, either the top or the left edge. It is mutually exclusive with SIZE_FILL and other shrink size flags, but can be used with SIZE_EXPAND in some containers. Use with size_flags_horizontal and size_flags_vertical.
Note: Setting this flag is equal to not having any size flags.
● SIZE_FILL = 1
Tells the parent Container to expand the bounds of this node to fill all the available space without pushing any other node. It is mutually exclusive with shrink size flags. Use with size_flags_horizontal and size_flags_vertical.
● SIZE_EXPAND = 2
Tells the parent Container to let this node take all the available space on the axis you flag. If multiple neighboring nodes are set to expand, they'll share the space based on their stretch ratio. See size_flags_stretch_ratio. Use with size_flags_horizontal and size_flags_vertical.
● SIZE_EXPAND_FILL = 3
Sets the node's size flags to both fill and expand. See SIZE_FILL and SIZE_EXPAND for more information.
● SIZE_SHRINK_CENTER = 4
Tells the parent Container to center the node in the available space. It is mutually exclusive with SIZE_FILL and other shrink size flags, but can be used with SIZE_EXPAND in some containers. Use with size_flags_horizontal and size_flags_vertical.
● SIZE_SHRINK_END = 8
Tells the parent Container to align the node with its end, either the bottom or the right edge. It is mutually exclusive with SIZE_FILL and other shrink size flags, but can be used with SIZE_EXPAND in some containers. Use with size_flags_horizontal and size_flags_vertical.
If we add an additional Control
or Container
node such as an HBoxContainer
as a child of our VBoxContainer
and set it's Vertical Sizing
flag to expand
it will take the remaining vertical height within the Container
and set it's height to that amount, seemingly pushing other elements to their minimum size.
If we set the ColorRect
's Vertical Sizing
flag to also expand
it will then be shared within the full heigh of the parent VBoxContainer
Building a Basic UI Scene
Let's take what we've learned and create something like a simple Inventory UI panel.
We can reuse our root Control
node that contains the 32px margin using the Anchor Offsets
. We'll also keep our ColorRect
filling the Full Rect
.
Let's split the Control
into 2 sections. We could do this 2 ways:
- We could create an
HBboxContainer
spanning the full width that we then create 2 children containers within. A practice that I've used many times, but has it's own limitations. - The method we'll take which will allow us to explore more with the
Anchor Positions
: MultipleControl
Node Parents.
We can add our First Control
node as a child of our root Control
node and we can call this InventoryControl
. Set the Layout mode
to Anchors and use the Custom
Preset. We'll set the Anchor Points
to Left
: 0, Top
: 0, Right
: 0.5, Bottom
: 1. Be sure to reset the default Anchor Offsets to 0px on each.
Next we'll add another Control
node that we'll call DescriptionControl
and we'll be setting it's Anchor Points
as a modified prest of Right Wide
by selecting it and then Custom
to further modify. We'll be sure the Anchor Points
are Left
: 0.5, Top
: 0, Right
: 1, Bottom
: 1.
Note that by selecting that preset, it both set our
Anchor Offsets
to 0 by default and set the grow direction to be left on the horizontal axis.
GridContainer
Inside our InventoryControl
node we'll be adding a GridContainer node as a child. This will act as our Inventory Item Container. Set the
Anchor Presetto the
Full Rect`.
We'll utilize the method we've mentioned before of splitting a container using the VBoxContainer
as an additional child of our InventoryControl
. We'll set the VBoxContainer
to also use the Full Rect
Anchor Preset
and reparent the GridContainer
to be a child of the newly created VBoxContainer
.
Next we'll take the GridContainer
and set the columns property to 4. We will also add some children ColorRect
nodes to act as placeholder for our item icons, with a minimum size of 32x32. Duplicate this so we have a total of 16 children within this Container. To distribute these items with additional separation, we can use the Theme Overrides Constants
for the H Separation and V Separation properties. We'll set those both to 8px to act as a gutter between elements. We'll also update the Container's Sizing
flags along the Horizontal
and Vertical
to Shrink Center and Expand along the Horizontal
axis.
Finally we'll add a Label
above the GridContainer
that will have the text
property set to "Inventory". Then set the Horizontal Alignment
to Center.
VBoxContainer
For our DescriptionControl
we'll separate content again using a VBoxContainer
as a child and set it's Anchor Preset
to Full Rect
. We'll be taking this element and restricting it's vertical size to leave room on the top and bottom for future elements. To do this we can customize the Anchor Preset
and adjust the Anchor Points
to leave a 20% padding on the top and bottom by setting the Anchor Points
to Left
: 0, Top
: 0.2, Right
: 0.5, Bottom
: 0.8.
Next add a Panel
node as a child of our VBoxContainer
and set it's Container Sizing
to Fill and Expand.
Inside our Panel
we will add a RichTextLabel
set to Full Rect
then modify the preset to add some Anchor Offsets
of Left
: 16px, Top
: 16px, Right
: -32px, Bottom
: -16px.
For the *text property we can set it to some placeholder text of "Item description, flavor text and whatever else looks good here.".
This gives us a simple but also great starting point for laying out our Inventory Panel UI.
Recap
Hopefully you now have a better understanding of how to control your layouts using the Control
's Anchor Points
, Anchor Offsets
, Grow Direction
and Container Sizing
flags. With these few components mastered, you'll be well on your way to making the flexible, dynamic layouts that bring your game's user interface to life!
If you want to discuss more about Godot Nodes, or just Game Development in general, be sure to join us over on our growing community Discord page !
I really appreciate all of the support I've been getting on the channel recently, and look forward to chatting with you over on discord !
Happy Coding.