-
-
Notifications
You must be signed in to change notification settings - Fork 21.4k
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
Destroying an object running a coroutine (yield) should interrupt this coroutine properly #24311
Comments
This is a frustrating issue. Yield is recommended in almost every tutorial I've read, without mention of the problem with not being able to interrupt it. If you free an object during a yield, it crashes. The various workarounds are complex and convoluted. I'd love to see yields automatically handle object deletion. No one is assigned to this - so no chance this will be worked on? |
@alwaysusa GDScript is being rewritten for Godot 4.0, see #39093. |
Thanks for the info! I'm guessing 4.0 is a ways off. I'll have to break up my current use of yields into timers and signals instead. Thanks again :) |
Actually, you can keep use yields in this case, this is just a warning. If the instance is gone after resuming yield, there is just no code that will be executed after. I use yields a lot, but I have to recompile the engine with this warning disabled as it seems useless. |
Agree, the developers say 'be sure to eliminate all errors during production' but there's a bunch of stuff like this that prints out that you can't easily eliminate |
As long as you use yield only on builtin nodes without nested coroutines this is indeed fine, but once free an object/node that has running nested coroutines it seems to corrupt memory: #47703 I think these issues are related so I bring it up here. I really would love to use coroutines more often, they are such an elegant way of expressing sequences of actions. |
We use coroutines a lot. I mean A LOT. And there are zero problems with them. The issue you mentioned is generally about |
Well in my latest jam project I also used coroutines alot(baiscally every animation and enemy) and had the game crashing seemingly random and fixed it by making sure each coroutines is stopped before destroying the owning object. I think when running the mentioned code in #47703 long enough without asan it would crash in the same way. Might give it a try. Ignoring asan errors is not a good Idea I think. I used timer nodes though instead of extends Node
func _ready():
coro()
queue_free()
print("No Crash/Memory corruption")
func coro():
while true:
yield(get_tree().create_timer(1.0), "timeout")
print("Please don't free me") I just mentioned my issue here because I think solving this issue should also solve mine and vice versa. They are related but not the same. |
I have the same problem. My question is, when an instance is "deleted", the coroutines is still stored in memory waiting? |
Had to spend hours today rewriting my existing code which was heavily coroutine based because of this issue. I agree that it's frustrating because a lot of the documentation and helpful tips show use of yield. This issue also seems to present more when the CPU has more load due to race conditions. This means that while Godot may be fast for prototyping, later in the project you are pretty much guaranteed to have a bad time if you use yield a lot. I think since this seems unlikely to be fixed, we should get the word out that yield is basically bad GDScript form and that you should avoid it at all costs, especially in larger projects. |
This kinda pattern were you tie the coroutine to the owning node should solves most issues. extends Reference
class_name TempTimer
class FrameTimer_ extends Node:
signal timeout
var frames := 1
func _ready() -> void:
get_tree().connect("idle_frame", self, "_on_timeout")
func _on_timeout() -> void:
frames -= 1
if frames == 0:
emit_signal("timeout")
queue_free()
static func idle_frame(node:Node, frames:int = 1):
var t := FrameTimer_.new()
t.frames = frames
node.add_child(t)
return t Useage: func some_func():
yield(TempTimer.idle_frame(self), "timeout") Though I agree with the sentiment of this issue, just giving a work around for people. |
This is a very nasty caveat considering how many times it's mentioned in the docs as a best practice. I think resumes on freed objects should behave more intuitively. The developer would typically expect from a coroutine whose owner was freed to stop, similarly to how a node stops being processed when it's removed from a tree or freed. In general, yields are awesome for keeping complex sequences of steps. I find myself using this pattern a lot: $shoot.play()
$animation_player.play('shoot')
var recoil = rand_range(-0.25, -1)
yield(get_tree().create_timer(0.25), 'timeout')
$reload.play("recoil", -1, 1 / recoil)
yield(get_tree().create_timer(0.15 * reload_speed), 'timeout')
$reload2.play();
yield(get_tree().create_timer(0.15 * recoil), 'timeout')
# etc etc However once the node is freed (e. g. user has switched weapons), problems arise. One can argue that I could use AnimationPlayer here, but:
|
3.5 has the new Tween system backported as SceneTreeTween, so you may no longer have to use Last time I heard, |
Afraid not. Issue persists with await in extends Node
func _ready():
queue_free()
await get_tree().create_timer(1).timeout
|
@samsface 's answer inspired me to write this util, using Godot native Timer this time: EDIT: check out the latest version more below: #24311 (comment) extends Reference
class_name TempTimer
static func create_timer_under(node: Node, duration: float):
var temp_timer = Timer.new()
node.add_child(temp_timer)
temp_timer.start(duration)
return temp_timer
# Usage
# Instead of:
# yield(get_tree().create_timer(seconds), "timeout")
# write:
# yield(TempTimer.create_timer_under(node, seconds), "timeout")
# where node is some node with lifetime in sync with when we want to keep running the coroutine
# (typically the object affected by coroutine, or an object with same lifetime) I tested it and it also fixed the issue. In my case the caller |
@hsandt unfortunately your fix will leak timers as you have no free call. Maybe you could create a timer subclass that is always oneshot and auto queue frees itself? |
Afaik this issue is not back ported to 3.x, so a workaround may be needed there for now |
OK, I got a new version (with a weird workaround because of another bug - #21461 (comment)) # Adapted from https://github.com/godotengine/godot/issues/24311#issuecomment-1059531870
extends Timer
class_name TempTimer
func _ready() -> void:
# Temporary timers are always one-shot
one_shot = true
# Free itself on timeout, if parent node was not already freed
var _err = connect("timeout", self, "_on_timeout")
func _on_timeout() -> void:
queue_free()
static func create_temp_timer_under(node: Node, duration: float):
# The 2 lines below are equivalent to `var temp_timer = TempTimer.new()`
# but we need this workaround to avoid:
# > Parser Error: Using own name in class file is not allowed (creates a cyclic reference)
# See https://github.com/godotengine/godot/issues/21461#issuecomment-466565674
# We could also move this static function in a different script
var Self = load("res://Scripts/Utils/TempTimer.gd")
var temp_timer = Self.new()
node.add_child(temp_timer)
temp_timer.start(duration)
return temp_timer
# USAGE
#
# Instead of:
# yield(get_tree().create_timer(seconds), "timeout")
# write:
# yield(TempTimer.create_temp_timer_under(node, seconds), "timeout")
# where node is some node with lifetime in sync with when we want to keep running the coroutine
# (typically the object affected by coroutine, or an object with same lifetime) |
And a two files version to avoid the hacky workaround: # TempTimer.gd
# Alternative to Timer to use in contexts where the Timer is used by a coroutine
# that works on certain nodes, but the nodes may be freed in the middle of the coroutine
# (due to game restart, etc.)
#
# This is a temporary timer that you should place with static function
# `TimerUtils.create_temp_timer_under` so it is placed under some representative node
# affected by the coroutine, and freed when the node is freed, thus avoiding
# an error due to coroutine trying to access the freed node:
# > resume: Resumed function 'my_coroutine()' after yield, but class instance is gone
#
# This simulates what SceneTreeTimer does (auto-free on timeout) while having
# a proper timer node placed under a parent so it gets freed with it.
#
# Adapted from https://github.com/godotengine/godot/issues/24311#issuecomment-1059531870
#
# USAGE
# See TimerUtils.gd
extends Timer
class_name TempTimer
func _ready() -> void:
# Temporary timers are always one-shot
one_shot = true
# Free itself on timeout, if parent node was not already freed
var _err = connect("timeout", self, "_on_timeout")
func _on_timeout() -> void:
queue_free() # TimerUtils.gd
class_name TimerUtils
# This function creates a TempTimer under a node so it supports node deletion
# during a coroutine (see TempTimer.gd).
#
# USAGE
# Instead of:
# yield(get_tree().create_timer(seconds), "timeout")
# write:
# yield(TimerUtils.create_temp_timer_under(node, seconds), "timeout")
# where node is some node with lifetime in sync with when we want to keep running the coroutine
# (typically the object affected by coroutine, or an object with same lifetime)
static func create_temp_timer_under(node: Node, duration: float):
var temp_timer = TempTimer.new()
node.add_child(temp_timer)
temp_timer.start(duration)
return temp_timer |
Godot 3.0.6
This is one of the things that make me avoid
yield
often: basically, I like to useyield
to lay down a series of asynchronous tasks in a normal function, which is very easy to follow and maintain. The only problem is, there is no clean way to interrupt such a task. By this, I mean I would expect the function to be stopped as if resuming it and immediatelyreturn
ing. So no "magic" cleaning, you would still have to putyield
in proper places, but at least that would be something workable.Currently, If the object running that coroutine gets destroyed (scene change, plugin disabled, enemy death, etc), it will throw the following error in the debugger:
YieldDestroyedObject.zip
I thought about putting an
if stop
after eachyield
, but that makes the code really ugly and still doesn't handle interruption properly. So I keep going back to_process
to handle this, which is frustrating and time consuming.Is that due to how
yield
was implemented? Is there a way to handle this?The text was updated successfully, but these errors were encountered: