Game Dev Artisan Logo
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.
Tutorials

Simplifying Godot UI Development for All Levels

Author Avatar
2024-02-28
19 min read

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.

Control Node Layout Category Inspector Panel

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 Presets Dropdown

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.

Anchor Points

If we modify the Anchor Points we can see how it adjusts the size and position of our Control along those axis:

Modifying Anchor Points

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.

Anchor Offsets 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.

Modifying Anchor Offsets 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.

Grow Directions

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. Grow Direction Example 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. Grow Direction Left 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.

Full Rect Preset

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.

Top Left 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. Control Visualizer Tool

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: 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 Example

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 Containers 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.

Container Example Parented

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.

Container Example Child Offset

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 a Container 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.

Vertical Fill Expand Container Sizing

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:

  1. 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.
  2. The method we'll take which will allow us to explore more with the Anchor Positions: Multiple Control 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.

Anchor Defaults for the Right Wide Preset

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 theFull 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.

Inventory Control Tree

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.".

Description Control Tree

This gives us a simple but also great starting point for laying out our Inventory Panel UI.

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.

Want to support our work?Support us via Ko-fi