Search code examples
multithreadingasynchronousthread-safetygodot4

While loop stop main loop


i make loop, but it stop main loop. Help plz

extends Area2D


@onready var sprite = $Sprite
@onready var audio = $Audio
@onready var body = $Body
var rock = false


func disable_stone(player, stone_thread):
    stone_thread.start(await disable(player, stone_thread))

func disable(player, stone_thread):
    if !rock:
        print("super")
        rock = true
        await get_tree().create_timer(0.5).timeout
        body.colbox.disabled = true
        sprite.modulate.a8 = 100
        audio.play()
        await get_tree().create_timer(2).timeout
        while !(player in get_overlapping_areas()): pass
        body.colbox.disabled = false
        sprite.modulate.a8 = 255
        rock = false
    stone_thread.wait_to_finish()

I spawn thread. I dont know how fix that. I trying all thats i know.


Solution

  • Summary of how signals and await work

    Signals are dispatched on the main thread, and executed synchronously.

    When you await as signal※, the method returns (and thus stops executing) an object that represents the position inside the code it was.

    Then when the awaited signal is emitted, Godot will take that object, try to find the position in the code it came from, and executes from there... Again: synchronously.

    In consequence, when you await, the execution of the code will end up resuming in the main thread.

    By the way, you know some signals signal will pass some arguments, well, when you await a signal, it returns the value of the first argument passed.

    ※: yes, you await signals. When you call create_timer, you get an SceneTreeTimer, and then you await the timeout signal from it. You could await any other signal.

    I also see you await the disable method, which makes that method a co-routine... And you are giving the result to the Thread? I don't think that is what you meant.


    The root of your issue

    The crux of your problem seems to be this line:

    while !(player in get_overlapping_areas()): pass
    # REST OF YOUR CODE HERE
    

    You are making a loop waiting for the player to no longer be overlapping the Area2D. Except the list from get_overlapping_areas is updated on the physics frame. And if the main thread is here in this loop, it will never get to execute the physics frame. Which makes this loop an infinite loop.

    Thus what we will be trying to solve is how to wait for the player to no longer be overlapping the Area2D.

    And don't use a secondary Thread, nor a co-routine, for this.


    Solution checking every physics frame

    An orthodox approach to solve this problem would be to check in _physics_frame if the player is still being overlapped by the Area2D. By the way, instead of checking player in get_overlapping_areas(), check overlaps_area(player).

    Since you do not care about the check right away, we need a second bool flag (aside of rock). I'll call it waiting_exit, so you can do this:

    func _physics_process(_delta:float) -> void:
        if waiting_exit and not overlaps_area(player):
            waiting_exit = false
    
            # REST OF YOUR CODE HERE
    

    It might be worth noting that get_overlapping_areas() (and overlaps_area) might give you slightly outdated results (if I recall correctly, they are updated after _physics_process).


    Solution connecting a method to the area_exited signal

    You have a signals for when an object is no longer overlapping the Area2D: body_exited (if the object is a physics body or similar), or area_exited (if the object is another area).

    Since you are checking with get_overlapping_areas, I'm assuming that player is an Area2D, and thus you need area_exited.

    So you could connect a method to the area_exited signal (see Using signals), and in that method you place the second part of your code:

    func _on_area_exited(area:Area2D) -> void:
        if waiting_exit and player == area:
            waiting_exit = false
    
            # REST OF YOUR CODE HERE
    

    Solution connecting an anonymous method to the area_exited signal

    While I can't recommend this approach (too much effort), it is a solution to connect an anonymous method to the area_exited signal:

    # Declare a continuation variable
    # Initialize it with an empty callable, so we can reference it
    var continuation := Callable()
    
    # Set the continuation to an anonymous method
    continuation = func(area:Area2D) -> void:
        # Make sure the area leaving is the player
        if area != player:
            return
    
        # REST OF YOUR CODE HERE
    
        # Safely disconnect
        if continuation.is_valid() and area_exited.is_connected(continuation):
           area_exited.disconnect(continuation)
    
           # Release the reference to this anonymous method
           continuation = Callable()
    
    # Connect the signal to the continuation
    area_exited.connect(continuation)
    

    And yes, you could reuse the anonymous functions. However, at that point using a traditional named function (such as in the prior solution) is better.


    Solution awaiting the area_exited signal

    It might be tempting to await the area_exited signal, so I'm going to mention it here. However, I can't recommend this either (not thread safe).

    I believe would be like this:

    while player != await area_exited:
        pass
    
    # REST OF YOUR CODE HERE
    

    The issue is that might not work correctly when using multi-threaded physics (as your code would not be constantly awaiting signals).


    Solution checking every physics frame (but with await)

    And finally, if you are going to solve this with await, I would suggest to await the physics frame instead:

    while overlaps_area(player):
        await get_tree().physics_frame
    
    # REST OF YOUR CODE HERE
    

    There should be no issues with this approach (edit: it might take an extra frame compared to having the signal always connected, but that is a non-issue in this case), and it is the minimal change from what you have.