Yoav
Yoav

Reputation: 305

Godot - best practice to instantiate nodes with inherited classes

I'm building a 2D hex based game using Godot Script. There will be multiple hex types, each with its own behaviour. My implementation plan is:

Create a single 'hex_tile' scene which will provide a sprite and a collision2D nodes

enter image description here

A 'HexTile' class which implements shared game logic

class_name HexTile extends Area2D

@onready var hex_sprite = $HexSprite
# Hex cube coordinates
@export var coordinate_x : int = 0
@export var coordinate_z : int = 0
@export var coordinate_y : int = 0

func _input_event(viewport: Node, event: InputEvent, shape_idx: int) -> void:
    if event.is_action_pressed("mouse_left_click"):
        print_my_coordinates()

func set_hex_position(_position: Vector2):
    position = _position

func set_hex_cube_coordinates(row_index: int, column_index: int):
    coordinate_x = row_index
    coordinate_z = column_index
    coordinate_y = - coordinate_x - coordinate_z
    
func set_texture(texture_path: String):
    hex_sprite.Texture = load("res://" + texture_path)

func print_my_coordinates():
    print("Cube coordinates: " + str(coordinate_x) 
    + ", " + str(coordinate_y) + "; " + str(coordinate_z))

A specific class for each type of hex. This class will inherit from the generic 'HexTile' class

class_name HexTileForest extends HexTile

    func print_my_coordinates():
        print("I am a forest tile")
        super.print_my_coordinates()

A level scene which is empty. During initiation, I instantiate each tile and assign to it the appropriate script and texture depending on its type.

class_name Level extends Node2D
    
    func _ready():
        # Initialise hex tile via code
        var hex_tile_scene: PackedScene = load("res://hex_tile.tscn")
        var new_hex_tile = hex_tile_scene.instantiate()
        add_child(new_hex_tile)
        new_hex_tile.set_script(load("res://hex_tile_forest.gd"))
        new_hex_tile.set_texture("hex_tile_forest.png")
        new_hex_tile.set_hex_position(Vector2(100, 200))
        new_hex_tile.set_hex_cube_coordinates(0,1)

I'm getting this error in hex_tile.set_texture:
Invalid set index 'Texture' (on base: 'Nil') with value of type 'CompressedTexture2D'.

If I understand it right, this indicates that the tile instance is not set, therefore base is 'nil'. Here is a similar issue: https://www.reddit.com/r/godot/comments/ei7if7/invalid_set_index_texture_on_base_null_instance/

There are some interesting things to see in the inspector in run time:

The script is assigned as expected: enter image description here

hex_sprite is empty (probably why I have an issue): enter image description here

I figured this has to do with the order of initialization. I tried assigning the script to the hex instance before it's added as a child, with the same result.

Edit:
Following up on advice below I moved the code from level._ready() to level._init(). I say the same result as before. It occurred to me that perhaps variable hex_sprite is only available for use in _ready() because of the keyword:

@onready var hex_sprite = $HexSprite

There's no @oninit keyword, so I'll leave this as is. Perhaps it's be useful to try other methods of accessing a (sprite)node and updating it's content.

I injected print statements in _ready() and _init() functions for all three classes to try and understand the order in which they happen:

level is initialized
forest tile is initialized
hex tile is
initialized forest tile is ready
hex tile is ready
Level is ready

Finally, I broke off the code in Level between _ready() and _init():

class_name Level extends Node2D

func _ready():
    var new_hex_tile = get_child(0)
    new_hex_tile.set_texture("hex_tile_forest.png")
    new_hex_tile.set_hex_position(Vector2(100, 200))
    new_hex_tile.set_hex_coordinates(0,1)

func _init():
    var hex_tile_scene: PackedScene = load("res://hex_tile.tscn")
    var new_hex_tile = hex_tile_scene.instantiate()
    new_hex_tile.set_script(load("res://hex_tile_forest.gd"))
    add_child(new_hex_tile)

This works but left me confused. Obviously, _init() runs ahead of _ready(). Why is the code working when split? In both cases, I was accessing hex_sprite from _ready(). Hopefully it's not a race condition.

Upvotes: 2

Views: 713

Answers (1)

Ivy O'Neal-Odom
Ivy O'Neal-Odom

Reputation: 111

EDIT

After some back and forth debugging with OP, the simplest solution is to switch the add_script and add_child lines. If that isn't an option for whatever reason, the old answer still holds value:

OLD ANSWER

First, a quick note that doesn't appear to be related to your current issue: in your set_texture function, hex_sprite.Texture should probably be hex_sprite.texture ("texture" instead of "Texture").

On to your actual question, I think the issue you're having isn't an initialization order problem, though that was absolutely my first thought as well. I believe the actual issue is that, when one calls set_script, the node does not run _ready(), nor does it appear to run @onready statements. Instead, per the documentation, it just runs _init(). If you take your @onready assignments and all the code in your _ready() function and move them into _init(), your code should work.

Hope this helps! Let me know if this doesn't solve the problem and I can take a closer look.

Also, it might be a good idea to suggest an improvement to the documentation of set_script, since this is definitely confusing behavior that could be cleared up with a note in the docs.

Upvotes: 2

Related Questions