godotgdscriptgodot4

TextureProgressBar's progress_texture doesn't advance when its value is changed


I am working in Godot 4.0 and having issues with the TextureProgressBar. In order to recreate this issue, make a new project, create a new scene as a Node2D, and add the children Timer and CanvasLayer. Then give the CanvasLayer its own child, a TextureProgressBar. Add a gdscript file to the Node2D and connect the Timer's timeout() signal to it (I changed the name of the timer to StartTimer, the CanvasLayer to Loading Screen, and the TextureProgressBar to Loading Bar, which is why the func call name is different in the provided code). Then replace the code in the script file with this:

extends Node2D

func _ready():
    get_node("Loading Screen/Loading Bar").position = Vector2(200, 360)
    get_node("Loading Screen/Loading Bar").set_value(1)

func start_world_creation():
    start_progress_bar()

func start_progress_bar():
    for x in 100:
        for y in 100:
            for time_waster in range(20000):
                time_waster += 300
        get_node("Loading Screen/Loading Bar").value += 1
        print(get_node("Loading Screen/Loading Bar").value)

func _process(delta):
    pass


func _on_start_timer_timeout():
    start_world_creation()

Finally, assign the images below to their respective places for the TextureProgressBar: Under Texture Progress Texture Over Texture

For some reason, the TextureProgressBar does not update until its value reaches 100. Why is this, and how can I fix it? According to the documentation, it should update with every integer increase in value, since the TextureProgressBar's step value is set to 1, and max_value is 100.


Solution

  • So the signal of the Timer is emitted and this code will run before the next frame:

    func _on_start_timer_timeout():
        start_world_creation()
    

    Which is calling this code which will finish before the next frame:

    func start_world_creation():
        start_progress_bar()
    

    Which is calling this code which will finish before the next frame:

    func start_progress_bar():
        for x in 100:
            for y in 100:
                for time_waster in range(20000):
                    time_waster += 300
            get_node("Loading Screen/Loading Bar").value += 1
            print(get_node("Loading Screen/Loading Bar").value)
    

    So when Godot finally gets around to the next frame, the value of the TextureProgressBar is at 100.0, which is also the default max_value, so you see it full.

    Hopefully you can see the issue: The time_waster et.al. is keeping the thread busy, so Godot cannot do other stuff. In this situation using set_deferred or call_deferred won't help you.


    The easier solution is to await a frame:

    func start_progress_bar():
        for x in 100:
            for y in 100:
                for time_waster in range(20000):
                    time_waster += 300
            get_node("Loading Screen/Loading Bar").value += 1
            print(get_node("Loading Screen/Loading Bar").value)
            await get_tree().process_frame
    

    When the execution reaches await, Godot will save where it was, and continue after the specified signal (get_tree().process_frame in this case) is emitted.


    Another thing you could do is run the code in another Thread. In this case you can use set_deferred or call_deferred to manipulate the UI.


    If the time_waster et.al. code is an stand-in for some asynchronous operation, you might also want to look for an appropriate signal (or make one) so you can await until the asynchronous operation is completed.


    WARNING: Using await has drawbacks: It cannot be cancelled or disconnected.

    Thus:

    If you run into that, refactor the code by connecting a Callable continuation (which can be an anonymous method a.k.a. "lambda") to the signal... When the object is freed, Godot will disconnect it.

    If you need to check if a Node is about to be freed by queue_free, you can check is_queued_for_deletion. If you need to check if you have a reference to a Node that was freed, use is_instance_valid. If you need to do something before an Object is freed, have it override _notification and react to NOTIFICATION_PREDELETE.