Animating UI nodes

Animating UI is a pretty ubiquitous element of any game. Despite this, it’s usually a bit painful to implement in my opinion.

I recently had a go at implementing a script for animating nodes, specifically showing/hiding them. I use tweening to animate the position, scale and modulate alpha properties. It works well on its own, but I’m having trouble doing exactly what I want to do while keeping the tree structure neat and flexible.

The main issue is position. Initially, I landed on keeping all animated UI elements children of Control nodes, since these can be set to full rect with position at (0, 0). Since most Container nodes will control their children’s positions, it’s a pain to mix in my own tweening values. The container might move the children around based on a resized viewport, etc.

What I am trying to achieve is purely visual - so some sort of “visual_offset” property on Control nodes would solve this issue. I suppose you could argue that this could be achieved with a shader, but I fear this would kill the performance. My project is primarily targeting mobile devices.

How have you worked around this in your projects?

2 Likes

My friend, when you figure this one out, please let me know.

Neat and flexible



:sob:

2 Likes

This looks good to me. There’s a lot here, but you’re making good use of containers. I was born in UI darkness (Unity UGUI), so maybe my view is skewed.

Edit: Back to the topic at hand; I came across this reddit post which handles this by using two copies of the UI elements; One is invisible and used purely for layout. The other follows the layout copy as it moves.
https://www.reddit.com/r/godot/comments/x00qc4/turn_order_ui_trick_to_animate_children_inside/

I’ve had this idea too, but I wonder how one would solve the auto-sizing nature of containers (ie calculating their own size based on children sizes and position).

1 Like

I was just doing this today on my UI.

In my case I was sliding UI targets off the screen. (Sometimes completely off if they are being cleared or to a position nearly off the screen if the targets are just being toggled out of view)

The entire targets list also tweens onto the screen from the right, and off the screen when completed. New items also flash, and the target icon (a checkbox circle) turns into a ticked circles with star particles when a target is completed. If they are toggled off the screen in the hidden position when completed the single completed target slides back on the screen, the completed animations are done, and it slides back off again. There are some other animations for flashing and for targets failed etc too.

Anyway, this was all done as you described with tweening modulation or positions but the key thing I wanted to share was this.

	var offset = get_size().x
	var current_x_position = position.x

For any particular control node, like for instance a label, or a node in a vbox etc, to slide off the screen to the right, I just store its current position (for sliding back in the future) and tween it’s position.x by its current size to the right. This takes it off the screen, whatever size it’s currently set to.

As for the tree, yes UI trees can get pretty complicated, but only when you expand them all out.

Heres mine for now:

I get annoyed with the TopRightHUD because it has two components in it that needed to be in the top right of the screen in a VBox, breaking my functional convention over layout convention.

The mission targets container looks like this:

And each of the lists themselves look like this (all of the same format):

And targets are instantiated scenes that look like this:

Now, yes, if you expand everything it looks complicated. But taking each element on its own, they are all straight forward enough are they not?

And I am animating the targets, the lists, the labels, the panels, the icons, everything really at different points.

I would not use a dummy node for positioning like that reddit post suggested. There is no need as far as I can tell. But containers do still use position and global_position. So once they have positioned themselves on your screen somewhere, you just need to note in a variable the original position, then tween their position to your hearts content, have it spin around the screen and scale in and out, do whatever you want with it. Eventually you can return it to its original position because you recorded it somewhere.

The only thing you have to be wary of, is recording positions before a container has resized itself. If this happens you can just make it visible but modulate a = 0, await a process frame, then record the position, set the position off the screen, modulate a = 1 again, and tween it back on the screen.

If you have ever worked on website layouts, the css for that can become a complicated mess too, except there you don’t have collapsable trees to maintain sanity.

For @nick_sig example tree, it does look messy, I am sure that could be cleaned up a bit. The bells all seem to have the same structures, and could be generalised bell scenes, or at least in their own margin containers so can be collapsed so you just see beginning_bell, interval_bell etc.

I love the control nodes and think what they do is amazing! If you have a particular animation problem perhaps I can help with that? I am aware some people have felt confused or perplexed by the UI system on Godot, but I am not sure how it could be done any other way, and leave us users still in total control of them.

Anyway good luck. UI’s are fun!

3 Likes

You can DRY up that Settings screen fairly easily.

As an example, here is a control I use fairly often in my latest project:

class_name StatBox
extends HBoxContainer

#region docs
## Displays statistics for a pair of values
#endregion docs 

@onready var text_ctl: Label = $TextCtl
@onready var value_ctl: Label = $ValueCtl

@export var text: String : set = _set_text
@export var value: String : set = _set_value

#region var_methods 
func _set_text(new_val: String) -> void:
	text = new_val
	text_ctl.text = new_val
	

func _set_value(new_val: String) -> void: 
	value = new_val
	value_ctl.text = new_val 
	

#endregion var_methods 

Usage:

@onready var health_stats: StatBox = %HealthStats

var format_value = func (value) -> String: return str(int(value))

health_stats.text = "Health" 
health_stats.value = format_value.call(combat_data.health)

The basic rule of DRY is if you find yourself repeating the same pattern multiple times you should consider making a separate piece of code for it.

2 Likes

Good post. This is the main nugget that I worry about, and I might have another go keeping this solution in mind, thanks.

I think it would be neat to have a “visual_offset” property that would be inherited by all children. Maybe even a “self_visual_offset”, and it could live down by the “modulate” and “self_modulate” properties in the inspector. This would be the definite solution.

Maybe it’s possible to discuss this as a proper feature request in the Godot repo.

1 Like

It’s one of the most popular proposals actually, and there’s been a PR for it for quite a while now! https://github.com/godotengine/godot/pull/87081

Unfortunately it’s in bikeshedding hell. The Godot maintainers are not convinced that having render offsets be a part of all Control nodes, and it’s not clear what it would take to have it merged.

The good news is, if you’re an advanced user, you can compile the engine yourself with that PR included.

3 Likes

Ah, that’s great! We need to make this happen.

I might just build it myself at some point, if I can’t find a solution I’m happy with in the next few months. Hopefully this or the TransformContainer PR gets merged soon.

1 Like

Actually those proposals look really good! I like the ideas.

I think I prefer the idea of a dedicated animation container rather than it being added to every control node. However this is only a superficial slight preference. I think new users might get even more confused if there was even more added to the control nodes.

Overall though, I think I have been converted.

I will leave others more proficient than me to judge all the overhead and input and inheritance issues. But I support the idea! Although I take on board the possible blockers that have been mentioned.

(It was a great read though! So much work put into this already - I was very impressed and humbled at the same time)

This reminds me of my issue where, when the virtual keyboard on a mobile device pops up, the entire viewport briefly resizes, distorting the size and shape of the controls (assuming you are using scaling, which you essentially must). There’s no way to “freeze” the viewport resolution in this situation and, on Android anyhow, the Android app setting that should ignore resizing doesn’t work. It could be fixed if there were a way to tell the viewport to maintain its current size and not automatically scale.

Similarly, here, could it be as simple as adding a toggle to containers to maintain their own size and stop auto-adjusting based on their children sizes and positions?