Child nodes are null after calling new() on parent

###Godot Version
v4.2.2

Question

Hi all,
I’m trying to write a create method for one of my scene (CharacterPortrait), to call it from another scene (CharacterListScreen) and add multiple instances of it to a grid container , but while the scene instance seems to be created, the child nodes, in this case TextureRect “portrait_texture” stays at null and the code crashes when trying to access it.

here is in order, the script where I wrote the create() method, the tree of its linked scene, and the script and tree of the scene where I’m trying to call this create() method

character_portrait.gd

extends Control

class_name CharacterPortrait

@onready var banned_indicator = $GlobalContainer/InnerMarginContainer/BannedIndicator
@onready var portrait_texture = $GlobalContainer/InnerMarginContainer/PortraitTexture

const PORTRAITS_FOLDER_PATH = "res://Assets/CharacterPortraits/"

static var char:Character
static var p_name:String
var is_banned:bool
# Called when the node enters the scene tree for the first time.
func _ready():
	print("character portrait is ready")
	is_banned = false

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
	if is_banned:
		banned_indicator.visible = true

static func create(c_name:String) -> CharacterPortrait:
	var instance:CharacterPortrait = CharacterPortrait.new()
	var dir_access:DirAccess = DirAccess.open(PORTRAITS_FOLDER_PATH)
	for file_path in dir_access.get_files():
		if file_path.contains(c_name) and !file_path.contains(".import"):
			print("Found texture for character: ",c_name)
			var img = Image.new()
			var itex = ImageTexture.new()
			img.load(PORTRAITS_FOLDER_PATH+file_path)
			itex.create_from_image(img)
			instance.portrait_texture.texture = itex
			instance.p_name = c_name
	return instance

func set_character(_character:Character):
	char = _character
	p_name = _character.character_name

CharacterPortrait.tscn

image

character_list_screen.gd

extends Control
@onready var grid_container:GridContainer = $ColorRect/GridContainer


# Called when the node enters the scene tree for the first time.
func _ready():
	for _name in CharacterManager.get_character_names_list():
		var portrait:CharacterPortrait = CharacterPortrait.create(_name)
		grid_container.add_child(portrait)


# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
	pass

character_list_screen.tscn
image

instance.portrait_texture.texture = itex fails, saying portrait_texture is null

Are you trying to make a character portrait selection screen? or did I misinterpret the code?
If you’re going for that, I don’t think that’s the best way to load a picture. It’s better to have all the images preloaded. However if you have to do it dynamically, sure, you can call load instead of preload.

This is how you’d do it with preload:

const CHARACTER_PICTURE_1 = preload("res://Images/Characters/char_pic_1.png")
const CHARACTER_PICTURE_2 = preload("res://Images/Characters/char_pic_2.png")

Then you can use that variable to assign the picture. I’d recommend you create a TextureRect, then assign the pictures and then add them to the GridContainer.

var tex 

tex = TextureRect.new()
tex.texture = CHARACTER_PICTURE_1
grid_container.add_child(tex)

tex = TextureRect.new()
tex.texture = CHARACTER_PICTURE_2
grid_container.add_child(tex)

Or do it dynamically based on an array with load()

for _name in CharacterManager.get_character_names_list():
    tex = TextureRect.new()
    tex.texture = load (PORTRAITS_FOLDER_PATH + _name + '.png')
    grid_container.add_child(tex)
1 Like

CharacterPortrait is not a scene, think of it like a Node+Script or Class.

  1. Scene is .tscn file, it holds Nodes as tree, Scripts and changed properties of those nodes
  2. @onready vars, like _ready() function become valid after this class/scene will be added into SceneTree (),
    @onready variables will be null while scene not be added with add_child(scene)
  3. static var is kind of data that same for all class instances, you need usual var for per-instance data

Correct way to do that you want is create variables for name and texture_path
In _ready you must create texture from texture_path and assign it to portrait_texture.texture

Do not use preload("CharacterPortrait.tscn") in this script or you will get cyclil loading, correct way is to use load

1 Like

Hi and thanks for your answer,

Could you elaborate a bit ? I tried writing the following to _ready() in CharacterPortrait but the issue stays the same

    portrait_texture.texture = load (placeholder_portrait_texture)
	is_banned = false
	print("character portrait is ready")

code in CharacterListScreen

func _ready():
	for _name in CharacterManager.get_character_names_list():
		var portrait:CharacterPortrait = CharacterPortrait.new()
		grid_container.add_child(portrait)
		portrait.create(_name)

This happens right when calling add_child(), I changed the order and updated the create() method following you comment so it does not seem related.
With “placeholder_portrait_texture” being the path to one of my png files.

I get

E 0:00:01:0244   character_portrait.gd:6 @ _ready(): Node not found: "GlobalContainer/InnerMarginContainer/PortraitTexture" (relative to "/root/CharacterListScreen/ColorRect/GridContainer/@Control@2").
  <C++ Error>    Method/function failed. Returning: nullptr
  <C++ Source>   scene/main/node.cpp:1651 @ get_node()
  <Stack Trace>  character_portrait.gd:6 @ _ready()
                 character_list_screen.gd:9 @ _ready()

pointing to:
@onready var portrait_texture = $GlobalContainer/InnerMarginContainer/PortraitTexture