Using yield in a loop only resumes the first iteration of the loop

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By Kazuren

I am making a card game and I have an end_turn() function that is called when a button is pressed. It loops through every card in my Hand node and removes it as child

func end_turn() -> void:
	for card in hand.get_cards():
		hand.remove_card(card)

The hand.remove_card(card) function emits a signal called card_removed(card) that I have connected to in _ready() of my main scene

func _ready() -> void:
	hand.connect("card_removed", self, "_on_Hand_card_removed")

I want that function to add the card to the tree again, animate it to a “graveyard” field and finally when its done animating, remove it from the tree again and add it to the graveyard but it only works for the first card that got removed when I press the end turn button.

func _on_Hand_card_removed(card: Card) -> void:
	add_child(card)
	card.tween("position", card.get_global_position(), graveyard.position, 1)
	yield(card.tween_node, "tween_all_completed")
	remove_child(card)
	graveyard.add_card(card)

What am I missing?

:bust_in_silhouette: Reply From: Inces

Or does it work for all cards that got removed simultanously, so You can only see the first one ? Because that is what should happen with this code. Yield only stops one function, not whole scenetree. If You want to animate cards one after another, You have to use less corroutines. That is always the problem with yield(), it forces unellegant sollutions :).

Generally, You have to use yield inside iteration, so whole end_turn()function becomes yielded, not just on_hand_card_removed
optionally, If You need to use this card removed signal, design it in a way You pass an array of all cards removed during this turn, and iterate them there using yield

I’m sorry if I didn’t elaborate clearly what I wanted my end result to be. I want the cards to animate simultaneously and not one after the other.
So right now the card_removed signal is being emitted once for every card being removed so the _on_Hand_card_removed could potentially be called multiple times in 1 frame.
I want for each _on_Hand_card_removed(card) function that got called to stop execution until the card that got removed is done animating like so:
yield(card.tween_node, "tween_all_completed")

I would have expected the below code to do the same, doesn’t have to be with signals.
So my expectation would be that for each card I would animate it and then for each card when it’s done animating to add it to the graveyard.

func end_turn() -> void:
    for card in hand.get_cards():
	    hand.remove_card(card)
	    add_child(card)
	    card.tween("position", card.get_global_position(), graveyard.position, 1)
    for card in hand.get_cards():
	    yield(card.tween_node, "tween_all_completed")
	    remove_child(card)
	    graveyard.add_card(card)

I understand that doing the below would animate the cards one after the other and resume correctly but it’s not what I want.

  func end_turn() -> void:
	    for card in hand.get_cards():
		    hand.remove_card(card)
		    add_child(card)
		    card.tween("position", card.get_global_position(), graveyard.position, 1)
		    yield(card.tween_node, "tween_all_completed")
		    remove_child(card)
		    graveyard.add_card(card)

Kazuren | 2022-02-10 17:21

I see :slight_smile:
Did you make any debugging attempts ? Like one print() inf for loop to ensure all removed cards are actually iterated and another two before and after yield() to check if function isn’t frozen in yielded state ?
I also notice You use a lot of custom corroutines. I can imagine some problem in get_cards(), and You did remember to start() tween in your custom tween function ?

Inces | 2022-02-10 20:34

hand.get_cards() just returns an array of nodes similar to if I did hand.get_node("cards").get_children()
I did start the tween yes, all the cards animate normally.
All the cards are iterated through but only one card resumes after the yield so yes the function is frozen in a yielded state but I can’t understand why as seen below:

func _on_Hand_card_removed(card: Card) -> void:
	add_child(card)
	card.tween("position", card.get_global_position(), graveyard.position, 1)
	print(card)
	yield(card.tween_node, "tween_all_completed")
	print("Resumed after tween:")
	print(card)
	remove_child(card)
	graveyard.add_card(card)

Which gives this output for 5 cards removed:

[Node2D:7099]
[Node2D:7078]
[Node2D:7057]
[Node2D:7036]
[Node2D:7015]
Resumed after tween:
[Node2D:7099]

Kazuren | 2022-02-10 21:37

I think I get it. Does card.tween mean each card has its own tween node ? Or does .tween property lead to reference of only one tween node for all the cards ? Investigate it, try printing card.tween to check if one or more IDs are printed. I have strong suspicion there is only one referenced tween.
If there is only one tween node, it will only return one yielded state, that will be resumed after first card reaches yield line, so other yielding cards will miss it. That is how yield works, frozen function is only a value until unfrozen. Maybe You can use other tween signals ? You just need every card to trigger one signal. Or let every card have its own tween.

Inces | 2022-02-11 06:21

I realized the problem was a side-effect from the hand.remove_card(card) that caused all the other cards in the hand to tween to their new position in hand which would mean all the cards except the first one that got removed had ongoing tweens that were never completed. I added an answer myself, you can check in more detail there but thanks for the troubleshooting suggestions, they were what made me realize where the root of the problem was.

Kazuren | 2022-02-11 11:04

:bust_in_silhouette: Reply From: Kazuren

I realized that the problem was something else entirely and it had almost nothing to do with yield behaving unexpectedly, it was doing exactly what I asked it to do.
The problem was that the tween_all_completed signal was only being emitted once by the first card and that was because whenever a card gets added/removed from the hand, all the other cards in the hand will tween to their new positions however if I remove the card from the scene tree while the tween was ongoing it would essentially call stop_all() on the tween node automatically and I would have to resume_all() myself later.
That means that the card had tweens that were never completed and never resumed which would explain why the tween_all_completed signal was never emitted for all the cards except the first one, because the cards had to tween to their new position in hand after I removed the first one.
I fixed it simply by doing card.tween_node.remove_all() after I remove it from the hand and then doing the tween I want.
I thought that the problem had something to do with yield because the first card worked fine so I only showed this part of the code but I was wrong and I apologize for not giving the whole picture. I fixed it thanks to some troubleshooting suggestions @Inces gave me which made me have this realization.