Skip to content

Godot UI

Created: 2020-07-12 21:43:58 -0700 Modified: 2020-07-12 21:47:11 -0700

  • If you’re seeing a Control node in world space, then it’s likely because you didn’t use a canvas layer (reference). Don’t make your UI a child of a Camera (reference).
  • Containers are intended to be used to size/position children.
    • Control nodes themselves sort of break this. I don’t think that adding Control nodes is generally a good solution. It’s probably better to position using more containers when possible. E.g. one time, I wanted a “Close” button at the upper right of a dialog, so I put the first item in the dialog into an HBoxContainer, that way the “Close” button could live in that container.
    • If you want a container to shrink to the size of its children, then you probably just need to wrap your container inside one that does shrink to its children, e.g. a VBoxContainer. E.g. if you’re making a Label or RichTextLabel that you want to shrink for a tooltip, your tree should probably be VBoxContainerPanelContainerMarginContainerLabel.
    • As an example, at one point, I wanted to make a GridContainer with highlightable rows, so I thought I would use a ColorRect in conjunction with the grid. But due to how container positioning works, it was actually much easier to override Draw (reference).
  • You have to mark input as handled (reference), e.g. call AcceptEvent() from _GuiInput and call GetViewport().SetInputAsHandled() from _UnhandledInput. Without this, I think you may see strange behavior where an event seems to be consumed twice.
  • Primer video on anchors

In general, if you find yourself trying to write code to detect if the mouse is within a rectangle yourself, then you’re likely doing things wrong.

E.g. I originally tried to use global_position and global_scale to figure out a Rect2 so that I could call “has_point” on it with event.position. However, this didn’t take the Camera2D’s bounds into account, so it would mess up whenever I moved the camera, and probably would have been a pain to maintain anyway.

Instead, the “right” way to do things is to add an Area2D and CollisionShape2D to your object, then use the Area2D’s input_event signal:

func _on_input_event(_viewport: Node, event: InputEvent, _shape_idx: int) -> void:
if event is InputEventMouseButton and event.pressed:
# do something
get_tree().set_input_as_handled()

Then, the issue I ran into was that my CanvasLayer was consuming clicks (reference). I had a Control under the CanvasLayer whose mouse_filter I needed to set to IGNORE. If you want to figure out which component is getting clicks, run your game from Godot, then click Debugger → Misc to see the last-clicked control: 500

If you still can’t figure out why a node isn’t receiving input, add this code to the node:

public override void _UnhandledInput(InputEvent @event)
{
GD.Print(@event.AsText());
}

…if it doesn’t print a bunch of Mouse motion at position messages while hovering over the component, then something must be consuming the events before it.

  • Use Debugger → Misc to figure out which component is being clicked.
  • Turn off visibility of nodes directly in the editor while the game is running. Eventually, your component should get the events, meaning you’ve found the offending node.
    • If you haven’t, it’s possible that a parent Control has a mouse filter set to Stop, in which case try changing ancestors’ mouse filters to Pass
  • Consult this page of the documentation for more information about how input handling works.

For example, I had this scene tree at one point:

  • VBoxContainer Game
    • SubViewportContainer Container - with Disable Input off and Object Picking on
      • SubViewport
        • Sprite2D

…the sprite was getting UnhandledInput for mouse events as expected, but Game wasn’t. It’s because SubViewportContainer was propagating the right-click event to the SubViewport, which I needed to stop with this code:

public override bool _PropagateInputEvent(InputEvent @event)
{
if (
@event is InputEventMouseButton inputEventMouseButton
&& inputEventMouseButton.Pressed
&& inputEventMouseButton.ButtonIndex == MouseButton.Right
)
{
return false;
}
return true;
}

However, understanding this particular scenario for mouse clicks is quite difficult. At least in Godot 4.3, if I have this setup:

  • Override _UnhandledInput in all four nodes to print the event
  • Set mouse filter on Container to Pass
  • Set Disable Input on SubViewport to false

…I get this output when I right-click:

[UNHANDLED] Right-click in Sprite2D
[UNHANDLED] Right-click in SubViewport

Note that it matters where you right-click. E.g. you may have black bars around your game due to the aspect ratio and window size. In that case, the output you get when you click can change based on where the mouse was.

Anyway, it’s strange to me that Game doesn’t get to process the click as unhandled input. My thought process is this: the click isn’t handled in a child node, so I would expect it to propagate up the scene tree. It’s also strange that SubViewportContainer doesn’t print anything.

If I take that same setup and override SubViewportContainer’s _PropagateInputEvent so that right-click isn’t propagated, I get this output:

[UNHANDLED] Right-click in SubViewportContainer
[UNHANDLED] Right-click in Game

In other words, now the SubViewport doesn’t get the input at all (since it’s not propagated), and the Game gets a chance to handle the input. This seems to be the same behavior you can achieve by setting Container’s mouse filter is Ignore (although that will affect all mouse events, not just right-click).

I can’t explain this though. 😢 When right-click events are propagated to the SubViewport, why doesn’t SubViewportContainer or Game get the clicks as unhandled input?

I think it may just be how SubViewports handle inputs. If you put this code in the SubViewportContainer:

public override void _UnhandledInput(InputEvent @event)
{
if (
@event is InputEventMouseButton inputEventMouseButton
&& inputEventMouseButton.Pressed
&& inputEventMouseButton.ButtonIndex == MouseButton.Right
)
{
GD.Print("[UNHANDLED] Right-click in SubViewportContainer");
}
((Viewport)GetChild(0)).PushInput(@event);
}

…then after the SubViewportContainer gets the input, it will pass it to the SubViewport and you’ll see this:

[UNHANDLED] Right-click in SubViewportContainer
[UNHANDLED] Right-click in Sprite2D
[UNHANDLED] Right-click in SubViewport

The important part is that the Game still doesn’t trigger its unhandled input at all, meaning the SubViewport must be consuming the event. I don’t see code for this in the Godot engine itself.

Either way, here are my conclusions:

  • I don’t fully understand why mouse clicks don’t propagate up through unhandled input. Something must be marking it as handled even in my minimal repros which have no such calls to mark it handled. I filed a bug on this.
  • None of this unusual behavior would happen if you turn Object Picking off. However, then you wouldn’t be able to click anything with a collider (e.g. an item or NPC in your game).
  • If you use a SubViewportContainer and a SubViewport and can’t get the parent of those nodes to process input, then consider overriding _PropagateInputEvent (or maybe calling PushInput on the Viewport depending on what you need)

This page of documentation has the order that input handling works. It’s important to note that clicks happening from colliders are considered part of the final step, physics picking.

If you want to change the order that something happens in, then you need to use a different event entirely. For example, suppose you have an object that requires picking, but you can’t wait until the physics-picking step. In that case, you can “promote” it to an input event:

  • Keep track of a bool _isMouseOver
  • Connect to MouseEntered and MouseExited to set that boolean properly
  • In _Input, make sure _isMouseOver == true when you click something
  • Add your TTF font to res://assets/fonts (not required, but a good convention)
  • Choose “New FontFile” and drag your file onto it
  • Alternatively, you can set it everywhere via Project Settings → GUI → Theme → Custom Font.

Tween code can get ugly pretty quickly. It may be better to use an AnimationPlayer, especially because then you can play the animations directly in Godot rather than having to build/run/test your tweens repeatedly.

Callable tweenFontSize = Callable.From<int>(TweenFontSize);
sizeTween.SetLoops(0);
sizeTween.TweenMethod(tweenFontSize, 19, 25, 1);
sizeTween.TweenMethod(tweenFontSize, 25, 19, 1);
private void TweenFontSize(int value)
{
_label.AddThemeFontSizeOverride("font_size", value);
}

Notes:

  • I tried tweening a Label’s scale, but it sort of jittered the position.
  • The font size is an integer, so tweening it may still look sort of jittery.
    • I got around that by making the font huge (~300px) and then setting the scale to ~0.06 (for an effective font size of 18px). However, this caused several problems (all fixable):
      • When I updated the pivot via code, the label would move down and right. I fixed this by making sure I had the correct pivot set in the editor.
      • When I increased the label size, the text would move depending on its anchor. I fixed this by making the size of the label large enough to accommodate the biggest font size that would be applied.
      • The outline and shadow needed to be scaled up, too. However, whenever you update the outline, you may also need to change the MSDF Pixel Range of the font (in the Import settings) since it’s always supposed to be at least 2x the largest outline used (reference).

I ran into a situation where I wanted to set a maximum size of 0px so that I could animate a container growing. However, the container itself had a minimum size of 27 px because of a button with a label in it. I didn’t want to have to hide every child with a minimum size, so I concocted this solution:

  • Make a SubViewportContainer with a Viewport underneath it
  • Add your container to the Viewport and set its anchor to “Full Rect”
  • Set the minimum size of the SubViewportContainer to the maximum size that you wanted originally (you can do this through code)
  • If the SubViewportContainer has a sibling, then make sure its container sizing has “Expand” turned off
  • A CanvasItem may have its Visible property set to true but still not actually be visible in the tree (reference). You have to use IsVisibleInTree() for that.
  • To find out when a CanvasItem is shown or hidden even due to a parent being shown or hidden, use the VisibilityChanged signal.

Spritesheet sprite in a TextureRect

Section titled Spritesheet sprite in a TextureRect
  • To use a single sprite from a spritesheet as a TextureRect, use an AtlasTexture.
    • Make a new TextureRect
    • In the Inspector, make a new AtlasTexture
    • Expand the AtlasTexture so that you can see the “Atlas” property
    • Drag a spritesheet onto the “Atlas”
    • Set the region below it

  • If you end up using the AtlasTexture a lot, you can save it as a file and share it between controls.
  • Note: I had found a much more complicated way of doing this with a ViewportTexture in the TextureRect, then a sibling Viewport in the scene with a Sprite child of that Viewport, but then the Sprite size had to be doubled for some reason and I couldn’t figure out why.

Centering an AtlasTexture sprite in a layout

Section titled Centering an AtlasTexture sprite in a layout

To achieve something like this:

…my scene tree looks like this:

  • Panel
    • GridContainer
      • TextureRect
      • CenterContainer
        • Label

The TextureRect has “Keep Aspect Centered”:

Center elements in a GridContainer

Section titled Center elements in a GridContainer

This is sort of like “justify-content: space-between” in CSS:

How I accomplished this was with this scene tree:

  • Container
    • CenterContainer
      • Label (“Row 1”)
    • GridContainer (or HBoxContainer if you want)
      • Label (“Row”)
      • Label (“Two”)
      • Label (“Here”)

The GridContainer has 3 columns and has the “fill” and “expand” size flags for “Horizontal” (the scripting constant for this is SIZE_EXPAND_FILL). Each child label has the same horizontal size flags and also “Align: Center”.

To get closer to “justify-content: space-between”, I think you’d just set the middle label to SIZE_EXPAND_FILL, not the left/right labels.

Overlapping problems with containers

Section titled Overlapping problems with containers

I kept running into issues like this:

This is a scene tree that looks like this:

  • VBoxContainer
    • CenterContainer
      • HBoxContainer
        • Item
    • CenterContainer
      • HBoxContainer
        • Item
        • Item
    • CenterContainer
      • HBoxContainer
        • Item
        • Item

…where “Item” was itself another scene whose root node was a Panel:

My issue ended up being that I didn’t set a minimum size on the Panel in Item. Without that, a CenterContainer can’t know how big the Panel should be, so it sets the size to (0, 0).

Alternatively, you could be running into this issue where dynamically laying out a container’s children isn’t straightforward.

A MarginContainer is like the “padding” style in HTML; it puts extra space inside the container. This is not to be confused with the “Margin” properties on all Control nodes! MarginContainer’s properties are here:

Here’s a MarginContainer with a Panel as a child:

As you can see, the panel is “pulled away” from each edge by 25 pixels.

It’s best to make a PanelContainer and override its style to have a StyleBoxFlat with your nine-patch texture: 800

As shown, I’m using only a specific region within UI.png which is 48x48. Because there are 9 tiles inside that 48x48 region, I set the texture margins to 16x16 (since 48*48 == 9*16*16).

If the borders of your graphic are stretching, then it means that you didn’t set the texture margins correctly.

TextureProgressBar has a height of 1px

Section titled TextureProgressBar has a height of 1px

This is probably because you need to check this the “Nine Patch Stretch” box. If not for that, then your container sizing or containers may be wrong (or just the size of the component is wrong if it’s not in a container).

See this; you generally just need to set the stretch mode to canvas_items in the project settings. However, that mode isn’t recommended for pixel art due to how it scales.

If you can’t do that, then turn on theme/default_font_multichannel_signed_distance_field in the project settings and make sure each label/button/whatever’s filter setting is “Linear”, not “Nearest”. This isn’t perfect though. It’s just really, really hard to get font settings to look good in a pixel-art game. Perhaps consider using a bitmap font.

If the blurry text only appears inside a tooltip, then I think that’s just a bug.

A font has “holes” or weird artifacts

Section titled A font has “holes” or weird artifacts

Pasted image 20240902154049.png

This is caused by the font having overlapping glyphs (reference). The official documentation even talks specifically about using MSDF and Google Fonts.

In this picture, see how the word “battle” has its characters misaligned vertically? 600

This is caused by a combination of the hinting property, which snaps glyphs to integral coordinates (reference), and the MSDF size (if MSDF is enabled), which is a feature used to pre-generate textures for different font sizes. It should not be caused by gui/theme/default_font_subpixel_positioning, which only affects horizontal positioning. It’s much clearer if you select a font in Godot, click “Import” at the upper left, then click “Advanced…” and just try changing some settings:

Pasted image 20240725092909.png

Pasted image 20240725092921.png