Sub Class looking for Child node in Base Class

Godot Version

4.3

Question

I have a Player scene which inherits from a Base_Character scene

Inside my Base_character scene, I have a progress bar to indicate HP and a related HP variable with a setter function

class_name Base_Character

extends CharacterBody2D

var base_stats: Dictionary = {"STR": 1, "INT": 1, "WIS": 1, "END": 1, "GUI": 1, "AGI": 1}
@export var stats: Dictionary = base_stats

@export var max_hp: float:
	get():
		return (base_stats['END'] * 10) + (base_stats['WIS'] * 3) + (lvl * 15)

@export var hp: float:
	set(value):
		hp = value
		_update_hp_bar()

func _ready() -> void:
	stats = base_stats
	hp = max_hp

func _update_hp_bar():
	$ProgressBar.value = (hp / max_hp) * 100

In my Player scene, however, I have another progress bar which functions exactly like the HP bar but instead is used to store SP

The player scene has something like this

extends Base_Character

class_name Player

# Player stats
@export var max_sp: float:
	get():
		return (stats['AGI'] + stats['END']) * 3

@export var sp: float:
	set(value):
		sp = value
		print($ProgressBar2)
		_update_sp_bar()

func _ready() -> void:
	ally = 1
	super._ready()
	sp = max_sp

func _update_sp_bar():
	$ProgressBar2.value = (sp/max_sp) * 100

My scene tree looks something like this:
Base_Character
Screenshot From 2024-11-22 22-05-15

Player
Screenshot From 2024-11-22 22-05-54

When I run the Base_Character scene it runs just fine with no issues. However, when I try to run the Player scene I get the following error:

E 0:00:00:0427   player.gd:13 @ @sp_setter(): Node not found: "ProgressBar2" (relative to "Base Character").
  <C++ Error>    Method/function failed. Returning: nullptr
  <C++ Source>   scene/main/node.cpp:1792 @ get_node()
  <Stack Trace>  player.gd:13 @ @sp_setter()

E 0:00:00:0427   player.gd:31 @ _update_sp_bar(): Node not found: "ProgressBar2" (relative to "Base Character").
  <C++ Error>    Method/function failed. Returning: nullptr
  <C++ Source>   scene/main/node.cpp:1792 @ get_node()
  <Stack Trace>  player.gd:31 @ _update_sp_bar()
                 player.gd:14 @ @sp_setter()

I donā€™t know why it seems to be trying to find the SP bar inside the Base_Character scene. Please any help in understanding the issue and solutions will be greatly appreciated

Thatā€™s very odd. I just tried to replicate the issue in its simplest form and everything ran fine. I donā€™t see what is different about our structures, but maybe you will:

a.gd

class_name A
extends Node

func _ready() -> void:
	print("a", get_children());
	print($ASceneChild);

b.gd

class_name B
extends A

func _ready() -> void:
	print("b", get_children());
	print($BSceneChild);
	super();

a_scene.tscn

  • AScene (type A)
    • ASceneChild (type Node2D, but irrelevant)

b_scene.tscn

  • BScene (inherits ā€¦aScene.tscn. Type B)
    • BSceneChild (type Node2D, but irrelevant)
    • ASceneChild (highlighted in yellow, inherited and un-mutable)

Running a_scene outputs:

a[ASceneChild:<Node2D#30333207902>]
ASceneChild:<Node2D#30333207902>

Running b_scene outputs:

b[BSceneChild:<Node2D#30836524383>, ASceneChild:<Node2D#30819747166>]
BSceneChild:<Node2D#30836524383>
a[BSceneChild:<Node2D#30836524383>, ASceneChild:<Node2D#30819747166>]
ASceneChild:<Node2D#30819747166>

@duckbanditdan thanks for your response. This seems to be an accurate simplification and representation of my initial code.

However, comparing my initial code and trying again, it seems that my code doesnā€™t even approach the _ready function of ā€œB_Sceneā€ before erroring out.

I may not have mentioned this earlier, but the

print($ProgressBar2)

line in the sp setter function displays <Object#null> in the output console

From what I can observe the program seems to call _update_sp_bar() before even getting to the ready function. This causes it to try to assign a value to a null object.

So my issue remains: Why does this same syntax work for HP in Base_Character but fail for SP in Player? And why does the error tell me the program is trying to look for the node in Base_Character instead of Player?

If you need any further information Iā€™d be happy to provide.

I just tried commenting out the

$ProgressBar2.value = (sp/max_sp) * 100

line and it ran successfully (though as you can imagine this is not really a solution as I need to be able to actually update the progress bar)

However, this did allow me to observe something interesting. The script now reaches the _ready function and get_children() finally runs. This displays all the children including the $ProgressBar2. And on trying to update the sp value the setter function now accurately prints the address of the $ProgressBar2

So these two print lines

@export var sp: float:
	set(value):
		sp = value
		print($ProgressBar2)
		_update_sp_bar()

func _ready() -> void:
	print(get_children())
	ally = 1
	sp = max_sp
	super()

and the print(get_children()) line from the Base_Character _ready function now display


<Object#null>
[ProgressBar2:<ProgressBar#29225911574>, Sprite2D:<Sprite2D#29108471046>, CollisionShape2D:<CollisionShape2D#29125248263>, ProgressBar:<ProgressBar#29142025480>, Pointer:<Sprite2D#29209134356>, Gear:<Node#29276243224>, Inventory:<ItemList#29326574873>]
ProgressBar2:<ProgressBar#29225911574>
[ProgressBar2:<ProgressBar#29225911574>, Sprite2D:<Sprite2D#29108471046>, CollisionShape2D:<CollisionShape2D#29125248263>, ProgressBar:<ProgressBar#29142025480>, Pointer:<Sprite2D#29209134356>, Gear:<Node#29276243224>, Inventory:<ItemList#29326574873>]

So I think the workaround I will implement going forward is to run a null check on $ProgressBar2 in _update_sp_bar and pass during the initial setup. Something like this:

func _update_sp_bar():
	if $ProgressBar2:
		$ProgressBar2.value = (sp/max_sp) * 100

Though this works if anyone can explain the cause of the initial error it will be much appreciated. Especially why it worked for Base_Character and not for Player. And even more especially why it was looking for $ProgressBar2 in Base_Character instead of Player.

1 Like

Glad you found a workable solution.

I added these to my minimal reproduction:

# in A
@export var testa: int:
	set(value):
		testa = $ASceneChild.get_index();

# in B
@export var testb: int:
	set(value):
		testb = $BSceneChild.get_index();

a_scene still runs fine. b_scene runs into the same issue you have (or similar, "Cannot call method ā€˜get_indexā€™ on a null value).

I then tried removing the @export annotation from testb, and b_scene could run again without issues.

It looks like derived scenes call setters on their @exports before _ready(), while base class scenes do not. Iā€™ve no idea why this would be the case though.

It might be worth checking GitHub Ā· Where software is built to see if anything similar has been reported there. Or perhaps someone with a better understanding of the engines can come and tell us whatā€™s going on :joy: Honestly, Iā€™m more confused about why the base class doesnā€™t call the setter before _ready(), because the value of the export variable could well be needed before then. :confused:

1 Like

Alright then. Thanks for the help. I also slightly suspected that, though the why behind the behaviour still eludes me too. Glad to see itā€™s not cause Iā€™m just bad at least :sweat_smile:.

1 Like