Yield returns value instead of waiting for signal

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

My problem is how to have a child node ask for data from a parent node and receive/retrieve it within the same function, so that it can be returned. A seemingly simple task but it’s been breaking my brain for the past two days.

I want to note I have avoided to call parent node functions or variables using get_parent(), because I understood this make your node structure brittle and unstable. (“Call down, signal up.”)

So in (this simplified version of) my game, I have two nodes: ‘TestParent’ and ‘TestChild’. I store the game data in a dictionary in TestParent. The gameplay takes place in TestChild, which needs access to the data in TestParent. TestChild has a button that calls the function retrieve() in order to get and print the value for key2from the dictionary in TestParent.

TestParent.gd:

extends Node2D

var child_scene = load("res://TestChild.tscn")
var child_instance

var dict = {
	"key1" : 1,
	"key2" : 2,
	"key3" : 3
}

func _ready():
	child_instance = child_scene.instance()
	add_child(child_instance)
	child_instance.connect("record", self, "record")
	child_instance.connect("get", self, "get")

func record(location, data):
	dict[location] = data

func get(location):
	var data = dict[location]
#   yield(get_tree().create_timer(1.0), "timeout") # (Simulate processing delay.)
	child_instance.store(location, data)

TestChild.gd:

extends Node2D

var temp_memory = {}

signal record(location, data)

func record(location, data):
	emit_signal("record", location, data)

signal get(location)

func retrieve(location):
	emit_signal("get", location)
	return temp_memory[location]

func store(location, data):
	temp_memory[location] = data

func _on_Button_pressed():
	print(retrieve("key2"))

This version works, but only because Godot manages to store the data in temp_memory[location] before the function retrieve() in TestChild.gd runs the line return temp_memory[location]. However, this is very fragile, because if this process takes a bit longer, temp_memory[location] will be returned as null or as an outdated value. This can be simulated by adding the commented out line in get().

So to prevent this instability, I wanted to add a command that would tell TestChild to wait with returning the value until the value has been stored in temp_memory. I thought yield() in combination with a signal would work for this. See the set-up below for my changes.

TestChild.gd (with yield() and signal)

extends Node2D

var temp_memory = {}

signal record(location, data)

func record(location, data):
	emit_signal("record", location, data)

signal get(location)

func retrieve(location):
	emit_signal("get", location)
	yield(self, "data_stored") #Added
	return temp_memory[location]

signal data_stored() #Added

func store(location, data):
	temp_memory[location] = data
	emit_signal("data_stored") #Added

func _on_Button_pressed():
	print(retrieve("key2"))

The problem with this script, however, is that instead of waiting for a signal, yield() actually returns data to _on_Button_pressed(). When printed, this data appears like this, with the number constantly changing (I suppose it’s the delta or number of frames?):

[GDScriptFunctionState:1264]

What do I not understand about yield() that I should know? I thought yield() would make a function pause and wait until a signal was received/observed. However, instead of waiting, the function returns even quicker. What would be a better way to do this?

Ugh, I hate that topic myself, but I have a feeling this will enlighten You enough :
https://www.youtube.com/watch?v=XJU0P6M5IKg
Basically yield returns frozen state of function, for other function to retrieve. So it is exactly the function You needed for Your idea, but didn’t find yet :).

Inces | 2021-12-09 20:31

Thanks for your help. I did watch that video before, but it still left me with a lot of questions. I will watch it again, however!

dr-pop | 2021-12-12 22:08

:bust_in_silhouette: Reply From: timothybrentwood

I want to note I have avoided to call parent node functions or variables using get_parent(), because I understood this make your node structure brittle and unstable. (“Call down, signal up.”)

This is true, however, I think in your case your nodes are already tightly coupled since the child can’t function properly without data from the parent. So decoupling them, although wise in theory, doesn’t exactly apply here. You would probably need to reconsider the structure of your objects in order to avoid this issue. If you want a child to work with multiple different parents, you should create a pseudo-interface (since gdscript doesn’t officially support interfaces) for the parent nodes which would guarantee that your child node works regardless of which parent it is operating under.

However, this is very fragile, because if this process takes a bit longer, temp_memory[location] will be returned as null or as an outdated value.

It seems like you have multiple hands in the cookie jar. What you should probably do is create a locking system around your data so you know that the data you get is not stale and safe for the entity to modify. It really depends on how complicated your data is in how you want to go about implementing this but here is a simple example:

# parent.gd
var locked_for_entity = null
var data = "some data"

func try_to_acquire_lock(entity) -> bool:
	var lock_acquired = false
	if locked_for_entity == entity:
		lock_acquired = true
	elif locked_for_entity:
		pass
	else:
		locked_for_entity = entity
		lock_acquired = true
	
	return lock_acquired
	
func release_lock(entity):
	if locked_for_entity == entity:
		locked_for_entity = null
		
func get_data(entity):
	if locked_for_entity == entity:
		return data
	return null
	
func update_data(entity, new_data):
	if locked_for_entity == entity:
		data = new_data
		
# child.gd
var my_data

func retrieve():
	if not get_parent().try_to_acquire_lock(self):
        call_deferred("retrieve") # try again next frame
		return
	call_deferred("release_parent_lock")
	return get_data(self)
	
func store(new_data):
	if not get_parent().try_to_acquire_lock(self):
        call_deferred("store", [new_data]) # try again next frame
		return
	get_parent().update_data(self, new_data)
	call_deferred("release_parent_lock")
	
func release_parent_lock():
	get_parent().release_lock(self)

Note that yield() is going away with Godot 4.0 in favor of await(). This change could potentially fix or at least partially fix your issue.

timothybrentwood | 2021-12-09 21:29

Thanks, this is very useful. You are right that the scenes are so closely tied together that my attempt at decoupling them is maybe redundant. Using call_deferred() to retry in the next frame (together with the locking/safeguard system) is a good way to make sure the right data is loaded.

dr-pop | 2021-12-12 22:05