Godot: Unable to Force Update 2D UI Controls before Next Frame

Created on 31 Jul 2018  路  20Comments  路  Source: godotengine/godot

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.

enhancement gui

Most helpful comment

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!

All 20 comments

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

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

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?

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.

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.

@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.

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?

glitch-2d-ui

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".

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.

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

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?

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.

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.

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!

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.

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.

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()

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

propagate_notification(NOTIFICATION_RESIZED)
worked for me

Was this page helpful?
0 / 5 - 0 ratings