Godot Version
4.2.1
Question
I’m building a 2D hex based game using Godot Script. There will be multiple hex types, each with its own behaviour. I was able to make this work with some advice and experimenting, but I still don’t understand why.
Fair warning, this is going to be a long one
My original implementation was:
Create a single ‘hex_tile’ scene which will provide a sprite and a collision2D nodes
A ‘HexTile’ class which implements shared game logic. I did not attach this class to the ‘hex_tile’ scene.
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()
An empty level. 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’.
This means that the tile instance is not set properly, the ‘hex_sprite’ call returns null.
There are some interesting things to see in the inspector in run time:
The script is assigned as expected:
‘hex_sprite’ is null as expected.
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.
Next step
Following up on advice I moved the initialisation code from level._ready() to level._init(). I see 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 for now.
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() is executed 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.
Finally
I updated class HexTileI removed the declaration:
class_name HexTile extends Area2D
# This is removed: @onready var hex_sprite = $HexSprite
var hex_sprite
# Hex cube coordinates
var coordinate_x : int = 0
var coordinate_z : int = 0
var coordinate_y : int = 0
.
.
.All functions remain the same
func _init():
hex_sprite = $HexSprite
And updated level class, all initialisation is done in _init():
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)
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)
I’d really appreciate an explanation on why it happening under the hood or a reference to an article that covers this in details.