0 votes

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 "currenthealth" and "maxhealth" as well as signals like "healthchanged" and "nohealth" 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 "currenthearts," a numeric value equal to the variable "currenthealth" 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)
Godot version 3.4.3.stable
in Engine by (37 points)

i admit , i didnt understand your whole situation but adressing the script directly like
onready var health-bar-variables = getnode("/root/Level1/CanvasLayer")
or finding the script with get
parent.getchild.getnode() should be enough

2 Answers

+1 vote
Best answer

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
by (281 points)
edited by

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

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

+2 votes

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.

by (1,981 points)

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 "currenthealth" within the "Stats" script that can have instanced values for different cases. For instance, maybe I want an enemy to have their currenthealth 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?

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

Welcome to Godot Engine Q&A, where you can ask questions and receive answers from other members of the community.

Please make sure to read Frequently asked questions and How to use this Q&A? before posting your first questions.
Social login is currently unavailable. If you've previously logged in with a Facebook or GitHub account, use the I forgot my password link in the login box to set a password for your account. If you still can't access your account, send an email to [email protected] with your username.