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. - 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)
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.
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: