godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.17k stars 98 forks source link

Make coroutines cancelable #8838

Open chris-profico opened 10 months ago

chris-profico commented 10 months ago

Describe the project you are working on

Mobile test application using features from a game that I was developing on another game engine

Describe the problem or limitation you are having in your project

The current handling of coroutines in Godot 4 does not allow for easy and controlled cancellation of currently running coroutines. This limitation makes handling asynchronous tasks less flexible, especially in scenarios where it is necessary to specifically interrupt certain asynchronous operations

Describe the feature / enhancement and how it helps to overcome the problem or limitation

I propose to introduce an optional async syntax for cancelable coroutines, thus allowing a clear distinction between standard methods and coroutines, and making coroutine cancellation possible. Additionally, adding the cancel_coroutine("coroutine_name") and cancel_all_coroutines() methods would provide increased flexibility for handling coroutine cancellation

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

Using async would be optional but necessary for canceling coroutines:


# Declaring a cancelable coroutine
func async my_cancelable_coroutine():
    print("wait one second")
    await get_tree().create_timer(1).timeout
    print("done")
# Canceling a specific coroutine by name
cancel_coroutine("my_cancelable_coroutine")

# Canceling all coroutines in this node
cancel_all_coroutines()

If this enhancement will not be used often, can it be worked around with a few lines of script?

To cancel all coroutines in a script, I use a cancel_all_coroutines_now variable. This works, but does not stop a specific coroutine and makes the code heavier. The proposed functionality would provide an integrated and cleaner solution.

Cancellation system for all coroutines

var cancel_all_coroutines_now := false

func my_awaiter(t :float) -> void:
    var end_time := Time.get_ticks_msec() + t * 1000
    while Time.get_ticks_msec() < end_time:
        if cancel_all_coroutines_now: return
        await get_tree().process_frame

func cancel_all_coroutines():
    cancel_all_coroutines_now = true
    await get_tree().process_frame
    cancel_all_coroutines_now = false

Example of a situation where you need to check if the coroutine should be canceled

func async_example():
    if cancel_all_coroutines_now: return # Checking at the start of each coroutine
    print("code here")

    for i in range(40):
        if cancel_all_coroutines_now: return # Check at the start of a timed loop
        print("code here")
        await get_tree().process_frame

    if cancel_all_coroutines_now: return # Checking after a timed loop
    print("code here")

    await my_awaiter(1)
    if cancel_all_coroutines_now: return # Checking after calling my_awaiter coroutine
    print("code here")

    await async_other_coroutine()
    if cancel_all_coroutines_now: return # Checking after calling a coroutine
    print("code here")

Is there a reason why this should be core and not an add-on in the asset library?

This functionality is essential and common in engines like Unity. Its integration into Godot would ease the transition for developers coming from other platforms and enrich the engine's native capabilities

dalexeev commented 10 months ago

Related:

RadiantUwU commented 7 months ago

Please note that currently coroutines dont need any async keyword, they're declared by the single fact that it uses await in the function body. (#3469)

RadiantUwU commented 7 months ago

Hello, while implementing this, i see no clear way to implement a way to cancel all coroutines.