Godot UI
Created: 2020-07-12 21:43:58 -0700 Modified: 2020-07-12 21:47:11 -0700
Basics
Section titled Basics- 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 addingControl
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 anHBoxContainer
, 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 aLabel
orRichTextLabel
that you want to shrink for a tooltip, your tree should probably beVBoxContainer
→PanelContainer
→MarginContainer
→Label
. - 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 overrideDraw
(reference).
- You have to mark input as handled (reference), e.g. call
AcceptEvent()
from_GuiInput
and callGetViewport().SetInputAsHandled()
from_UnhandledInput
. Without this, I think you may see strange behavior where an event seems to be consumed twice. TextureProgressBar
has an “over” texture; it’s for some texture that should cover the entire lifebar. If you want to use it for a border, I think you would need to do more than just provide a solid, filled rectangle (e.g. you would need a proper border graphic or use a 9-patch rect).- Primer video on anchors
Making elements clickable (reference)
Section titled Making elements clickable (reference)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:
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:
If you still can’t figure out why a node isn’t receiving input, add this code to the node:
…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 toStop
, in which case try changing ancestors’ mouse filters toPass
- If you haven’t, it’s possible that a parent
- 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
- withDisable Input
off andObject Picking
onSubViewport
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:
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
toPass
- Set
Disable Input
onSubViewport
tofalse
…I get this output when I right-click:
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:
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 SubViewport
s handle inputs. If you put this code in the SubViewportContainer
:
…then after the SubViewportContainer
gets the input, it will pass it to the SubViewport
and you’ll see this:
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 aSubViewport
and can’t get the parent of those nodes to process input, then consider overriding_PropagateInputEvent
(or maybe callingPushInput
on theViewport
depending on what you need)
Another consideration for clickability
Section titled Another consideration for clickabilityAt one point, I ran into this situation with my game:
- I wanted a horizontal
ScrollContainer
to contain up to three elements. - The elements would be centered on the bottom of the screen.
- If the game window wasn’t wide enough room, you would scroll the container either via the scroll wheel or by clicking and dragging one of the elements.
The problem I had with this was that the ScrollContainer
was set to always take up the entirety of the screen, so when there were no elements, there would just be an invisible Control
consuming mouse inputs.
I tried a bunch of stupid things like redirecting input events before realizing two key things:
- The
ScrollContainer
simply shouldn’t be taking up the full screen if there aren’t visible elements within it. - The
ScrollContainer
should be in charge of the clicking and dragging. E.g. one of the problems I ran into when trying to have the elements handle it was that there was padding between the elements that the elements couldn’t know about and thus couldn’t have an event handler for.
Input event handling
Section titled Input event handlingThis 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
andMouseExited
to set that boolean properly - In
_Input
, make sure_isMouseOver == true
when you click something
Fonts
Section titled Fonts- 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.
Tweens
Section titled TweensTweens vs. AnimationPlayer
Section titled Tweens vs. AnimationPlayerTween 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.
Tweening a label’s font size
Section titled Tweening a label’s font sizeNotes:
- 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 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):
Maximum size of a container
Section titled Maximum size of a containerI 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 aViewport
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
Visibility
Section titled Visibility- A
CanvasItem
may have itsVisible
property set totrue
but still not actually be visible in the tree (reference). You have to useIsVisibleInTree()
for that. - To find out when a
CanvasItem
is shown or hidden even due to a parent being shown or hidden, use theVisibilityChanged
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 layoutTo achieve something like this:
…my scene tree looks like this:
- Panel
- GridContainer
- TextureRect
- CenterContainer
- Label
- GridContainer
The TextureRect has “Keep Aspect Centered”:
Center elements in a GridContainer
Section titled Center elements in a GridContainerThis 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”)
- CenterContainer
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 containersI kept running into issues like this:
This is a scene tree that looks like this:
- VBoxContainer
- CenterContainer
- HBoxContainer
- Item
- HBoxContainer
- CenterContainer
- HBoxContainer
- Item
- Item
- HBoxContainer
- CenterContainer
- HBoxContainer
- Item
- Item
- HBoxContainer
- CenterContainer
…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.
MarginContainer (reference)
Section titled MarginContainer (reference)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.
Positioning labels or other UI through code
Section titled Positioning labels or other UI through codeMaking a Label
and setting its text doesn’t immediately update its size (reference). Call get_combined_minimum_size()
instead of size
, and if that still doesn’t work (e.g. the CanvasItem
was only just shown in the scene tree), then wait for process_frame
, e.g. await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
(reference).
Notes:
- The reason why you have to get the combined minimum size is because
MinimumSize
can report a size a smaller thanCustomMinimumSize
, soCombinedMinimumSize
is the larger of the two, which is the actual minimum size. I don’t understand whatMinimumSize
actually represents (the documentation says it’s the “internal minimum size”). - Invisible nodes may not ever have their proper sizes reported, in which case you would have to make them visible first.
- There’s still an oddity on Windows sometimes where the size of something is reported as 0 even after waiting for the next frame. I didn’t have time to investigate, but if everything is working fine on macOS but not on Windows, then consider just calculating the size of that thing yourself. I don’t have anything to link to due to the lack of an investigation on this. Private Skeleseller code is here.
Coloring button backgrounds
Section titled Coloring button backgroundsGo to Theme Overrides → Styles → Normal → Make a StyleBoxFlat
To do this through GDScript (reference) (WARNING: this may cause Godot to freeze/crash on exit due to this issue (which should be fixed by this PR, but if not, consider keeping references to styleboxes from GDScript)):
Screen coordinates
Section titled Screen coordinatesWhen working with 2D scenes, the UI isn’t just automatically placed in screen coordinates. I believe this is what you’d use a CanvasLayer
for (reference). follow_viewport_enabled
should be off in that case.
Theme editor
Section titled Theme editorIf you want many components in a UI to use the same theme, make a theme in the inspector, then modify it using the “Theme” button at the bottom of the editor:
To use this:
- Start by clicking the ➕ button at the upper right and selecting something like “Button”.
- From there, you can customize all of the shared properties of a button. This still isn’t the most straightforward though.
- Click the ➕ button to the left of one of the style boxes to add a new one
- Select “New StyleBoxFlat”
- Click the StyleBoxFlat that you just made to change the inspector to that style box
You can apply a theme to a control node and it will be inherited by all children.
Also, you can apply a default theme across your whole project using Project settings → GUI → Theme → Custom (it’s literally just the name “Custom” for right now).
NinePatchRect with children
Section titled NinePatchRect with childrenIt’s best to make a PanelContainer
and override its style to have a StyleBoxFlat with your nine-patch texture:
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.
Troubleshooting
Section titled TroubleshootingTextureProgressBar has a height of 1px
Section titled TextureProgressBar has a height of 1pxThis 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).
Text problems
Section titled Text problemsBlurry text
Section titled Blurry textSee 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 artifactsThis is caused by the font having overlapping glyphs (reference). The official documentation even talks specifically about using MSDF and Google Fonts.
Misaligned glyph baseline
Section titled Misaligned glyph baselineIn this picture, see how the word “battle” has its characters misaligned vertically?
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: