Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unable to Force Update 2D UI Controls before Next Frame #20623

Open
naturally-intelligent opened this issue Jul 30, 2018 · 46 comments
Open

Unable to Force Update 2D UI Controls before Next Frame #20623

naturally-intelligent opened this issue Jul 30, 2018 · 46 comments

Comments

@naturally-intelligent
Copy link

naturally-intelligent commented Jul 30, 2018

Godot version:
3.0.5

OS/device including version:
All

Issue description:
Other game engines have a way to force components to update after new data is set. Godot seems unable to do this.

For example: setting text in a wordwrapped Label and finding the new vertical size, adding children to VBoxContainer. Then, using that new size to position the control (for example, moving a VBoxContainer based on it's size).

If we have to wait for the next frame to find out what new sizes/etc our controls are, then we have one frame of visual glitchiness to contend with. And that can be multiplied for however many times you need to be changing controls. On some systems the framerate is so fast I can't see the glitches, but on most devices I've used they are very apparent. I've had to resort to hacks, like taking parts of the background, and pasting them again to hide the glitches, keeping an hidden clone and switching back and forth, but this shouldn't need to be done and eventually becomes cumbersome.

All of this can be fixed by have a means to force a cascading update.

@bojidar-bg
Copy link
Contributor

Try call_deferred instead of waiting for the next frame; that's when controls are updated as well.

@naturally-intelligent
Copy link
Author

call_deferred doesn't solve the problem, it just pushes the problem ahead one frame

@naturally-intelligent
Copy link
Author

Starting to get concerned, implementing hack after hack after hack trying to work with Controls, and now they are starting to diverge in behaviour across machines.

I dug a little bit into the C++ code to try to find where the UI is updated. My thought is to expose a C++ call to GDScript that would force update the UI (minimum_sizes?) without rendering the next frame. And maybe this could be done via GDNative? Anyone weigh in on the feasibility or point to where in the C++ code the UI is calculated?

@mischiefaaron
Copy link

mischiefaaron commented Aug 17, 2018

Can you make a software demo of the issue? We can record the result and try to catch that frame or two on video to really demonstrate it.

I agree that it's an issue too because even at 60fps I've noticed my text having to recenter and it looks very unclean when any label text is replaced and the words bounce around as they do.

I've found my own hack to get around it, but I'd rather not mess with the code in my project and it's been a while since I've touched that code. I think a demo of the issue might really help all of us either way though as we'll have a minimal version of the issue to test with and find solutions for.

@juddrgledhill
Copy link

How does one actually wait a frame to continue working? I have code that looks like this (for instance), how would I delay one frame given the code below...?

func _paint_kapow(message, position, time = _kapow_time_out, rot = _kapow_rotation):

	if OS.get_ticks_msec() >= _next_kapow_time:
		var kapow_label = _kapow_template.duplicate()
		var kapow_tween = Tween.new()
		
		kapow_label.name = 'klabel_'
		kapow_tween.name = 'ktween_'
		
		_kapow_holder.add_child(kapow_label)
		_kapow_holder.add_child(kapow_tween)
		
		kapow_label.get_node('label').text = message
		
		kapow_label.rect_pivot_offset = kapow_label.rect_size/2 #scale from the middle
		kapow_label.rect_global_position = position - kapow_label.rect_size/2
		
		kapow_label.show()
		
		kapow_tween.interpolate_property(kapow_label, 'rect_scale', _kapow_start_scale, _kapow_end_scale, time, Tween.TRANS_LINEAR, Tween.EASE_IN)
		kapow_tween.interpolate_property(kapow_label, 'modulate', _kapow_start_color, _kapow_end_color, time, Tween.TRANS_LINEAR, Tween.EASE_IN)
		kapow_tween.start()
		
		_next_kapow_time = OS.get_ticks_msec() + _kapow_delay_msec
		
		yield(kapow_tween, "tween_completed")
		kapow_label.queue_free()
		kapow_tween.queue_free()
		emit_signal('KAPOW_COMPLETE')

Since I am scaling and just waiting for the tween, I am not sure how I would know where to split this function (kapow_start and kapow_finish?). Just confusing... Also feels like it would make the code completely unwieldy to do something like this. In my case, I am showing a message onscreen that scales and fades out, but needs to be positioned exactly (and centered).

Since this code cannot get the correct size, the label is always in the wrong spot.

@naturally-intelligent
Copy link
Author

naturally-intelligent commented Sep 24, 2018

@juddrgledhill It's not possible AFAIK. I've tried many ways. Didn't look too deeply into your code. However what I've been doing recently is given up trying to make Godot update with accurate information, but rather, to always add in Controls with minimum sizes as parents of nodes that I need to animate their size. So for example if I want a dialog bubble to grow, I add an already-grown empty Control node as the parent for the dialog bubble to grow inside of (custom_minimum_size). That way there is no glitching. It isn't ideal because it's not exactly what I want but is better than glitchiness. Some hack like that might help you as well.

@agameraaron I would like to make a sample project to demonstrate this, haven't had time, but it's on my bucket list. It's pretty simple though, just add a container/button to a VBox and get the size before and after the draw call. Unless you do something with the incorrect size, there won't be a glitch. So, moving the VBox before+after the draw call based on the incorrect size will cause it to glitch.

@naturally-intelligent
Copy link
Author

Built a sample project. Which led me experiment with using "get_minimum_size" instead of "get_size". For static movement, it does seem that "get_minimum_size" correctly reports the size before next frame. So, that's a bit of good news! Not sure if it helps determining text box sizes, but that is another issue.

glitch-2d-ui.zip

Animated containers still pose a problem. Perhaps I'm just doing them wrong? Can we tween the minimum_size instead of rect_size?

@naturally-intelligent
Copy link
Author

glitch-2d-ui

@juddrgledhill
Copy link

Since you specify the minimum size, that makes sense. The actual size is computed, which, I believe, is what you need that single frame for. I was asking online and someone suggested this modification to the approach.

kapow_label.text = message
yield(get_tree(), "idle_frame")

Set the text, then yield to an idle frame.

Not sure if this is literally doing what I expect, but I could no longer see the issue physically. So I call that a "win".

@naturally-intelligent
Copy link
Author

naturally-intelligent commented Sep 29, 2018

Good find! That does solve the first column issue of the glitch-2d-ui project attached above:

func update_vbox1():
	yield(get_tree(), "idle_frame")
	var size_now = vbox1.get_size()
	if size_now.y > vbox1_size.y:
		vbox1.set_position(Vector2(vbox1_position.x, vbox1_position.y - (size_now.y - vbox1_size.y)))

Very good progress. However the animated button is still glitchy, once a solution to that is found (whether existing or new) I think this issue could be closed.

@xphere
Copy link

xphere commented Sep 29, 2018

If you hide() and show() the label, its bounded rect is recalculated again. At least in 3.0... Still works in 3.1?

@pikmeir
Copy link

pikmeir commented Dec 22, 2018

It'd be much better if there were a built-in way of forcing an update, instead of having to do hack workarounds. hide() and show() could work temporarily though but won't there be flicker doing that?

@rokasv
Copy link

rokasv commented Mar 21, 2019

I have also encountered the issue:
4tw
The text is updated on selecting an object to show its name. You can see name of the previous object flash for exactly one frame.

Please allow forcing a redraw.

@ReneHabermann
Copy link

ReneHabermann commented Apr 7, 2019

I stumbled across the same thing. I'd like to create a dynamically layouting container in the sense, that i specifiy a maximum width (depending on the current screen size) and the container takes child elements, adds them in a horizontal list and the breaks to a new line as soon as my maximum width is reached. I found no way to do this, as i am missing an explicit layout() function. The initial size of the child elements or container is not correct, as it will be changed by layouting.
The only way to do this now is to do the whole ui calculation up front, to know before layouting when a line break would be neccesary. Just allowing to manually call a layout function (that is probably already somewhere below), that layouts all elements in the subtree would be perfect.

@nate-trojian
Copy link

I was able to mitigate this issue myself by using propagate_notification(NOTIFICATION_VISIBILITY_CHANGED).

This forces a call to the Controls _notification method, and gets processed here

This amounts to the same thing as calling hide() then show() but without the potential to flicker, since it is already visible.

Hope this helps someone!

@Ramh5
Copy link

Ramh5 commented Oct 15, 2020

I was able to mitigate this issue myself by using propagate_notification(NOTIFICATION_VISIBILITY_CHANGED).

That does not seem to change anything for me. I am trying to calculate how many lines will my cells use in a custom table I made with Vbox, Hbox and labels font.get_wordwrap_string_size("string", width). I need to know if some rows will be multiline so I can display the proper number of rows so the table is a certain size. The entire problem lies in the fact that unless I use yield(get_tree(), "idle_frame") the rect_size of the cells don't update. None of the other work around worked for me. I don't think yielding a frame is an acceptable workaround either because there will be a glitch frame.

@Calinou
Copy link
Member

Calinou commented Oct 15, 2020

I don't think yielding a frame is an acceptable workaround either because there will be a glitch frame.

I suppose you could hide the control while it's being updated. It might be better than displaying a glitched version during one frame.

@Ramh5
Copy link

Ramh5 commented Oct 15, 2020

For some reason if I do that it does not work.

func update_table():
	hide()
	yield(get_tree(), "idle_frame")
	show()
	_line_count()

While that works as intended by just removing the hide():

func update_table():
	yield(get_tree(), "idle_frame")
	show()
	_line_count()

@Calinou
Copy link
Member

Calinou commented Oct 15, 2020

@Ramh5 Try setting the Control's modulate to Color(0, 0, 0, 0) instead. This will hide it visually without making it invisible in the scene tree.

If that still doesn't work, set the opacity to a very low value (Color(0, 0, 0, 0.001).

@SuzukaDev
Copy link

propagate_notification(NOTIFICATION_RESIZED)
worked for me

@GTcreyon
Copy link
Contributor

GTcreyon commented Sep 3, 2021

This is still an issue in 3.3.2. I want to position a panel based on the margins of a HBoxContainer while resizing both, but the update happens a frame too late. I can't seem to find NOTIFICATION_RESIZED either.

Edit: I found it, but it didn't help. :P

@ondesic
Copy link

ondesic commented Sep 18, 2021

3.3.3 has the same problem. I have to hide the label, change the text (which of course we want to update the size) then Show() the label.

@nate-trojian
Copy link

Some out of context code that may help some people with solving this issue:
This is for a speech bubble that grows and shrinks character by character after a delay. It contains a Label for the text and a NinePatchRect as the background that needs to scale to fit

func _on_Timer_timeout() -> void:
    # Content is being added and it autoscales to fit
    if adding:
        curr_text += full_text[text_index]
        text_index += 1
        # Need the newline buffer for the tail of the bubble
        label.text = curr_text + '\n'
        # Finished adding text
        if text_index == len(full_text):
            adding = false
            # Delay for readability
            timer.start(1)
            return
        timer.start(timer_time)
        return
    # Content is being removed and needs notification to scale
    elif len(curr_text) > 0:
        text_index -= 1
        curr_text.erase(text_index, 1)
        # Need the newline buffer for the tail of the bubble
        label.text = curr_text + '\n'
        # Propagate notification so that it scales
        propagate_notification(NOTIFICATION_VISIBILITY_CHANGED)
        timer.start(timer_time)
        return
    # Loop has finished
    showing = false
    hide()
    emit_signal("done_speaking")

@wareya
Copy link
Contributor

wareya commented Feb 10, 2022

@Ramh5 Try setting the Control's modulate to Color(0, 0, 0, 0) instead. This will hide it visually without making it invisible in the scene tree.

This is not an effective workaround; such invisible controls will still catch inputs and there's no way (at least in 3.x) to mark a control and all of its children as being inaccessible to inputs without recursively iterating over all of them. This is a really, really, really bad design pattern.

In my case:

I'm using the fully-transparent-modulation workaround in one of my games to make sure that a scroll container doesn't update a frame late when being opened, and I do indeed need to procedurally change the input flags of its children to keep it from causing problems. I need to add elements to a scroll container's child while they're all hidden and then also forcibly scroll the scroll container immediately after setting it to visible but before it renders. Some aspect of this does not work. I'm not convinced that it's a bug. The real problem is that there isn't a "ok, just forcibly update all the state you normally update when rendering" escape hatch on controls, not that there is such state to begin with.

And I can't set it to visible-but-transparent, wait a frame, scroll it, then set it to non-transparent. It needs to all happen the exact frame that the corresponding input happens. If it doesn't, I'm probably going to get bugs where people can bring up the scroll field and also do something else that's incompatible in the same frame (like load a saved game), breaking the UI. Having to use complicated workarounds like this makes UIs more fragile and hard to reason about.

propagate_notification(NOTIFICATION_RESIZED) does not work for what I'm trying to do. Maybe it works to forcibly update some aspects of some controls, but not all of them.

@naturally-intelligent
Copy link
Author

This issue is too old now, for old versions of Godot.

If it still persists for someone, then reopen against a specific version

@rokasv
Copy link

rokasv commented Feb 28, 2023

This issue is too old now, for old versions of Godot.

If it still persists for someone, then reopen against a specific version

I am confused, this issue is present in the currently live stable version of the engine (3.5.1).
There is a poster above confirming it for 3.3.3.
How can it bee too old?

@naturally-intelligent
Copy link
Author

It's a fair question. Last comment is 1 year old, seems like enough workarounds have been found to prevent it from being a showstopper issue for anyone. Unlikely anyone is ever going to work on it for v3.

I haven't tested it against 3.5 or 4. And I'm not sure if v4 has any features to force update.

If not, maybe it could be made into a feature request for v4.x?

@GTcreyon
Copy link
Contributor

If it's any help, my original use for this was in ignorance of the utility of Godot's container system. I have yet to find another use for it since then.

@rokasv
Copy link

rokasv commented Feb 28, 2023

Unlikely to be fixed is better than guaranteed to not be fixed in my eyes.
I could see it making sense when 4.0+ is out and 3.0+ is equivalent to what 2.0+ is now.
But closing this seems to serve no purpose.

@nccurry
Copy link

nccurry commented Mar 30, 2023

This is still an issue in Godot 4.0.1

@Calinou Calinou reopened this Mar 30, 2023
@Calinou Calinou removed the archived label Mar 30, 2023
@YuriSizov
Copy link
Contributor

Well, it's not an issue, it's a design limitation. It can't be solved as a bug, as it needs a proposal that would explain how this would work in practice and how this needs to be implemented. Responses to this ticket are a good starting point, but without a qualified technical explanation, a change in core is unlikely to happen.

@KoBeWi
Copy link
Member

KoBeWi commented Apr 14, 2023

Doesn't update_minimum_size() force size re-calculation or I'm missing something? 🤔

EDIT:
No it doesn't. I just had the same exact problem. The solution is, as always, await get_tree().process_frame.
I think size update should not rely on drawing.

@naturally-intelligent
Copy link
Author

Doesn't update_minimum_size() force size re-calculation or I'm missing something? thinking

EDIT: No it doesn't. I just had the same exact problem. The solution is, as always, await get_tree().process_frame. I think size update should not rely on drawing.

The inherent problem with waiting for the next frame, though, is the potential visual glitch on that first frame.

TBH tho I just haven't had a problem with this issue for over a year, workarounds are easy in my cases, as I could hide any problematic nodes until the next frame.

@plsholdmybeer
Copy link

plsholdmybeer commented Feb 9, 2024

what worked for me: "force redraw" by yield(idle_frame), but surround that with node.modulate.alpha=0 (then 1 after the yield/await)
✌️

@KoBeWi
Copy link
Member

KoBeWi commented Feb 9, 2024

After infinite problems with Control sizing I also resorted to modulate.a = 0, but unfortunately it makes the Control invisible for one frame and it's noticeable.

@luckyabsoluter
Copy link

Same issue on v4.2.1.stable.official [b09f793f5].

(node as Control).size is not updated where you use any method.
(node as Control).get_minimum_size() is updated but it's not work properly(or not fix the issue) with Expand of Control(Expand or HBoxContainer or Arbitrary Autowarp Mode Label).

@luckyabsoluter
Copy link

luckyabsoluter commented Apr 16, 2024

I confirmed (node as Control).get_minimum_size() is not work properly even if you set custom_minimum_size of parent or parent of parent, etc of Label.
Only await get_tree().process_frame fix this issue.

@luckyabsoluter
Copy link

  1. Call (box as BoxContainer).notification(NOTIFICATION_SORT_CHILDREN) to update Expand Information. (label is a child of box)
    (Now, you can get small minimum size.)
  2. Call (label as Label).update_minimum_size() to update label.size.x. You already can get small minimum size because label.size.x increased, but cache prevent decreasing label.size.y.
  3. Update label.size.y to low enough. (label as Label).size.y = 0
  4. Congratulations! Now, you can get new size.
(box as BoxContainer).notification(NOTIFICATION_SORT_CHILDREN)
(label as Label).update_minimum_size()
(label as Label).size.y = 0
print((label as Label).size)

Remember, (control as Control).get_minimum_size() will not work properly. But you can take (label as Label).get_minimum_size().y after (label as Label).size.x increased.

@luckyabsoluter
Copy link

I will share my code. Hopefully, advance humanity.

static func update_minimum_size_control_root(control: Control) :
	var previous_parent: Node
	var parent: Node = control
	while parent is Control :
		previous_parent = parent
		parent = parent.get_parent()
	
	update_minimum_size_sub_control(previous_parent)

static func update_minimum_size_sub_control(control: Control) :
	if control is Container :
		control.notification(Container.NOTIFICATION_SORT_CHILDREN)
		control.update_minimum_size()
		#control.size.y = 0
	for child: Node in control.get_children() :
		if child is Control :
			update_minimum_size_sub_control(child)
	control.update_minimum_size()

@luckyabsoluter
Copy link

NOTIFICATION_SORT_CHILDREN is defined in container.h, but the code that actually sorts the children is defined in individual containers, such as box_container.cpp or margin_container.cpp.

enum {
NOTIFICATION_PRE_SORT_CHILDREN = 50,
NOTIFICATION_SORT_CHILDREN = 51,
};

void BoxContainer::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_SORT_CHILDREN: {

void MarginContainer::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_SORT_CHILDREN: {

@Poobslag
Copy link

Poobslag commented Jun 11, 2024

In Godot 3.5.3, yielding for a frame is sufficient about 80% of the time but sometimes two frames is necessary, so I'm using the following code to keep a label centered:

var old_center_x := rect_position.x + rect_size.x * 0.5
yield(get_tree(), "idle_frame")
rect_position.x = old_center_x - rect_size.x * 0.5
yield(get_tree(), "idle_frame")
rect_position.x = old_center_x - rect_size.x * 0.5

@GTcreyon
Copy link
Contributor

I'm using the following code to keep a label centered:

Is there a reason you need to use code for this? Generally you should be able to center a label using built-in layout settings.

@Poobslag
Copy link

Poobslag commented Jun 19, 2024

I'm using the following code to keep a label centered:

Is there a reason you need to use code for this? Generally you should be able to center a label using built-in layout settings.

Which built-in layout settings specifically?

extends Control

func _ready() -> void:
	var label := Label.new()
	label.name = "Label"
	label.anchor_bottom = 0.5
	label.anchor_top = 0.5
	label.anchor_left = 0.5
	label.anchor_right = 0.5
	add_child(label)
	
	var timer := Timer.new()
	timer.connect("timeout", self, "_on_Timer_timeout")
	add_child(timer)
	timer.start(1.0)


func _on_Timer_timeout() -> void:
	$Label.text += "%"

Here is a program which adds a label, centers it with anchors, and gradually makes it wider and wider. It does not stay centered as its text changes. How would you modify this program to keep the label centered, using built-in layout settings?

The only approach I've come up with is similar to the post you replied to, manually calculating the rectangle's new bounds based on its contents and then adjusting its position.

@KoBeWi
Copy link
Member

KoBeWi commented Jun 19, 2024

Try using set_anchors_and_offsets_preset().

@wareya
Copy link
Contributor

wareya commented Jun 19, 2024

They're on Godot 3.5.2. set_anchors_and_offsets_preset doesn't exist in Godot 3.

@KoBeWi
Copy link
Member

KoBeWi commented Jun 19, 2024

In Godot 3 it's called set_anchors_and_margins_preset().

@adamscott adamscott self-assigned this Aug 11, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests