Calling an instanced variable from one scene's script to another

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

TLDR; is there a line of code that can allow me to call a variable from an instance in a different scene (avoiding global if possible)?

I have a scene called “Stats.” This scene is just a single blank node with an attached script, also called “Stats,” which houses variables such as “current_health” and “max_health” as well as signals like “health_changed” and “no_health” and functions like “set_health.” The idea with this is that if I have multiple kinds of scenes in my game needing common stats with different values at different times (like enemy health and player health), I can have 1 scene + script that manages all the base stats and just slap that scene into these other scenes as a child instance, and adjust a slider or two in the Inspector menus. I hope this makes sense. Please let me know if not.

I have a scene called “Player” which is a KinematicBody2D node with an attached script also called “Player” and a bunch of child nodes under it which aren’t relevant here.

I have a scene called “HealthUI.” This scene is a control node + attached script also called “HealthUI” and a child label node which I want to fill up with text reading “HP = XXXX” (where the X’s are the actual numeric health value of the player). The idea with this scene is to have a simple HUD/UI scene available for pasting into any room I want it in. This is a very ugly and basic way of displaying health, but it’s just to prototype for the time being.

I have a scene called “World” which is just my basic first room that I’ve made with the “Player” and enemies to run around in. “HealthUI” and “Player” are children scenes within this “World” scene.

I want the “XXXX” value inside the “HealthUI” scene to be a variable called “current_hearts,” a numeric value equal to the variable “current_health” in the “Stats” instance within the “Player” scene’s script. I want this to work regardless of whatever scene I put “HealthUI” into. I know about global variables and all that, but, if possible, I would just like to be able to be consistent with how my Stats variables are used and called thruout the game whether I am referring to Player stats, Enemy Stats, or Hell, maybe there’s a door I gotta beat down at some point and that has stats.

So, same as the TLDR above, is there a line of code that can allow me to call a variable from an instance in a different scene?

What I have right now:
Stats (parts relevant to health):

# Variables
export(int) var max_health = 1

onready var current_health = max_health setget set_health


# Functions
signal no_health
signal health_changed(value)


func set_health(value):
	current_health = value
	emit_signal("health_changed", current_health)
	if current_health <= 0:
		emit_signal("no_health") 

Player (parts relevant to health):

func _ready():
	animationTree.active = true
	swordHitbox.knockback_vector = Vector2.DOWN
	stats.connect("no_health", self, "queue_free")  


func _on_Hurtbox_area_entered(area):
	stats.current_health -= area.damage
	
	tween.interpolate_property(sprite, "modulate", 
		Color(1,0,0), Color(1,1,1),
		Tween.TRANS_QUAD, Tween.EASE_IN)
	tween.start()
	
	hurtbox.start_invincibility(1)
	hurtbox.create_hit_effect() 


func _on_Stats_no_health():
	queue_free()

HealthUI:

# Variables
onready var label = $Label 
var player = preload("res://Player/Player.tscn")
var instance = player.instance()
var stats = instance.stats
var current_hearts = stats.current_health setget set_current_hearts
var max_hearts = stats.max_health setget set_max_hearts


# Functions
func _ready():
	add_child(instance)
	self.max_hearts = stats.max_health
	self.current_hearts = stats.current_health
	stats.connect("health_changed", self, "set_hearts")


func set_current_hearts(value):
	current_hearts = clamp(value, 0, max_hearts)
	if label != null:
		label.text = "HP = " + str(current_hearts)


func set_max_hearts(value):
	max_hearts = max(value, 1)

i admit , i didnt understand your whole situation but adressing the script directly like
onready var health-bar-variables = get_node(“/root/Level1/CanvasLayer”)
or finding the script with get_parent._get_child._get_node() should be enough

horsecar123 | 2022-03-07 00:34

:bust_in_silhouette: Reply From: Gluon

I dont really understand why you are trying to avoid a global script to be honest but in any case the only way I think this could be done is with a global script. If you make the node stats global you will be able to call it from a player no matter when you have instanced that player.

First off, thank you so much for your quick answer! I did not expect to have such a great response right off the bat.

The reason why I don’t want “Stats” as a global script is so I can have variables like “current_health” within the “Stats” script that can have instanced values for different cases. For instance, maybe I want an enemy to have their current_health at 2HP, but I want my player’s current health to be at 3HP. If “Stats” is a global script, wouldn’t that make it so variables within couldn’t be instanced?

PIZZA_ALERT | 2022-03-06 18:54

Just make more variables. One dedicated to a player and one to an enemy.

Fenisto | 2022-06-01 23:30

:bust_in_silhouette: Reply From: ichster

IMO I would toss the idea of an abstracted “Stats” Scene, just because stats are easiest to track internally. An abstracted Scene would either just be a glorified wrapper around some ints or floats, or would be so specialized to the parent scene that would use them that they would be useless for any other type of parent scene. I tried rigging something of this sort up in the attached project, but ended up coming to the aforementioned conclusion:

# Stats.gd
extends Node

#----------
# API
#-----------
# Set value of a stat
func set_stat(stat, value) -> bool:
	var success : bool = true
	# Check if stat exists
	if (not stat_list.has(stat)):
		print_debug("Unknown stat. stat name: {0}  stat value: {1}".format({0:stat, 1:value}))
		success = false
	else:
		# Handle stats with a default setter behavior
		if (typeof(value) == typeof(stat_list[stat])):
			stat_list[stat] = value
		else:
			print_debug("stat type mismatch. stat name: {0}".format({0:stat}))
			success = false
	return success
	
# Get value of a stat
func get_stat(stat) -> bool:
	var value = null
	if (not stat_list.has(stat)):
		print_debug("Unknown stat. stat name: {0}".format({0:stat}))
		value = null
	else:
		# Handle stats with a default getter behavior
		value = stat_list[stat]
	return value
	
# Add a new stat category
func add_stat(stat, value) -> bool:
	var success : bool = true
	if (stat_list.has(stat)):
		print_debug("Stat already in use. stat name: {0}".format({0:stat}))
		success = false
	else:
		stat_list[stat] = value
	return success
	
#-----------------
# Private
#-----------
export(Dictionary) var stat_list := {}

Ok, so gdscript is not good at abstracting children scenes. Since there is no real polymorphism or whatever, gdscript requires the parent to manually expose itself to the child every time the child is present. This leads to technical upkeep when children and parent scenes content changes, and usually some extra wrapper code on both the children and parent to get them to work nicely together. You will find yourself copying and pasting a whole bunch to make children and parent variations work to the standard.
Its much better to envision scenes as standalone units, dependent on nothing, sitting there grimly for their API methods and signals to be called. Then, if some hoity-toity scene somewhere wants the standalone scene to do something, it can make it do so via a group! Keep in mind that while groups are usually accessed from the SceneTree down (as thats the built in behavior), you can certainley write a function that filters groups from an arbitary Parent down; let me know if you want me to write a function that does something like that:

# Player.gd
extends KinematicBody2D

# Health variables
export(int) var max_health = 5
onready var current_health = max_health

# Child ref
onready var hurtbox = $Hurtbox
onready var sprite = $Sprite
onready var tween = $Tween
onready var animationTree = $AnimationTree
onready var swordHitbox = $SwordHitbox

func _ready():
	# Initialize UI (note that HealthUI might not be ready for this call yet, but hey, Player don't care. It is not couple to nothin' ye ha)
	get_tree().call_group_flags(SceneTree.GROUP_CALL_DEFAULT, "health_ui", "set_max_hearts", max_health)
	get_tree().call_group_flags(SceneTree.GROUP_CALL_DEFAULT, "health_ui", "set_current_hearts", current_health)
	# Do some other stuff idk
	animationTree.active = true
	swordHitbox.knockback_vector = Vector2.DOWN
	
func _input(event:InputEvent):
	if event is InputEventKey and event.is_pressed():
		# Decrement health on left arrow press
		if event.get_scancode() == KEY_LEFT:
			set_health(current_health - 1)
		# Increment health on right arrow press
		if event.get_scancode() == KEY_RIGHT:
			set_health(current_health + 1)

# Handle damage enemy interaction
func _on_Hurtbox_area_entered(area):
	# Update health
	set_health(current_health - area.damage)
	# Do some visual stuff I guess
	tween.interpolate_property(sprite, "modulate", 
		Color(1,0,0), Color(1,1,1),
		Tween.TRANS_QUAD, Tween.EASE_IN)
	tween.start()
	hurtbox.start_invincibility(1)
	hurtbox.create_hit_effect() 

# Handle health change
func set_health(value):
	# Update health
	current_health = clamp(value, 0, max_health)
	# Update UIs
	get_tree().call_group_flags(SceneTree.GROUP_CALL_REALTIME, "health_ui", "set_current_hearts", current_health)
	# Check if dead
	if current_health <= 0:
		queue_free()

HealthUI.gd:

# HealthUI.gd
extends Label
# Variables
var max_hearts = 5
var current_hearts = current_hearts

# Functions
func _ready():
	# In case the group was not in set in the inspector, add this to needed groups ehre
	if not is_in_group("health_ui"):
		add_to_group("health_ui")

func set_max_hearts(value):
	max_hearts = clamp(value, 0, max_hearts)

func set_current_hearts(value):
	current_hearts = clamp(value, 0, max_hearts)
	text = "HP = " + str(current_hearts)

EDIT: Gathering children of a node by group

# Gathers all descendants of nodes if they are in the provided group. If search 
# depth is left as -1 then it will ignore depth constraints and look at every child.
# Ideally don't change current_depth and result. This function is recursive and
# those parameters are used to help destroy the stack.
static func gather_child_nodes_by_group(node, group_name:String, search_depth=-1, current_depth=1, result=Array()) -> Array:
	if (node.get_child_count() > 0) and ((current_depth == search_depth) or search_depth == -1):
		for child_node in node.get_children():
			if child_node.has_method("is_in_group") and child_node.is_in_group(group_name):
				result.append(child_node)
			gather_child_nodes_by_group(child_node, group_name, search_depth, current_depth+1, result)
	return result

Very informative! I hope the children will be okay after being exposed to by their parents. I will check out groups in the meantime.

PIZZA_ALERT | 2022-03-07 03:37

Software engineers love making their terms dicey af. “master”, “slave”, “child” are all fair game.

ichster | 2022-03-07 03:53