Instiantiating a custom class as a scene

Godot Version

Godot 4.6 Stable

Question

I created a custom class called Arrow

class_name Arrow extends CharacterBody2D

@onready var sprite = $Sprite2D

var SPEED : int = 300
var SHOTS : int = 0
var PRESET : PackedScene

func _init(_speed : int, _shots : int, _preset : PackedScene) -> void:
	SPEED = _speed
	SHOTS = _shots
	PRESET = _preset
	
func _physics_process(delta: float) -> void:
	
	var collision: KinematicCollision2D = get_last_slide_collision()
	if collision : 
		var collider := collision.get_collider()
		
		#if arrows collide w/ each other
		if collider is Arrow: 
			queue_free()
			print("arrow collision!")
			
		if collider is StaticBody2D:
			velocity = Vector2.ZERO
			
	move_and_slide()


func spawn(start_pos: Vector2, direction : Vector2) -> void:
	sprite.scale.x *= sign(direction.x)
	global_position = start_pos
	velocity = direction * SPEED

I then have a child class of it called Normal Arrow

extends Arrow
class_name Funny_Arrow

func _init(_shots : int) -> void:
	super(100, _shots, preload("res://Scenes/funny_arrow.tscn"))

In my Bow script, I create an inventory of Normal Arrow and a Funny Arrow (which has the same structure as Normal Arrow but a couple of tweaks)

extends Node2D


var amount_of_arrows : int
var arrow_inventory : Arrow_Inventory
var arrow_inventory_index : int = 0
var selected_arrow : Arrow

func _ready() -> void:
	
	arrow_inventory = Arrow_Inventory.new([Normal_Arrow.new(5), Funny_Arrow.new(5)])
	
	SignalManager.change_arrow_count.emit(amount_of_arrows)
	
	selected_arrow = arrow_inventory.select_arrow(arrow_inventory_index)
	#print(selected_arrow is Arrow)

func _process(_delta: float) -> void:
	if Input.is_action_just_pressed("Switch_Arrows"):
		arrow_inventory_index = (arrow_inventory_index + 1) % arrow_inventory.get_size()
		selected_arrow = arrow_inventory.select_arrow(arrow_inventory_index)
	pass

func shoot(arrow_spawn_pos, direction) -> void:
	if $Timer.is_stopped():
		var arrow = selected_arrow.PRESET.instantiate()
		print(arrow is Arrow)
		get_tree().current_scene.add_child(arrow)
		arrow.position = arrow_spawn_pos
		arrow.spawn(arrow.position, Vector2(direction,0))
		SignalManager.change_arrow_count.emit(amount_of_arrows)
		$Timer.start(.5)

Here is my Arrow Inventory script; it’s just essentially an array.

extends Node
class_name Arrow_Inventory

var ARROW_INVENTORY : Array

func _init(_arrow_inventory : Array) -> void:
	ARROW_INVENTORY = _arrow_inventory
	pass


func decrease_shot(index : int) -> void:
	ARROW_INVENTORY[index].SHOTS -= 1
	
func select_arrow(index : int) -> Arrow:
	return ARROW_INVENTORY[index]
	
func get_size() -> int:
	return len(ARROW_INVENTORY)

Whenever I run the game and try to shoot, I get this error: Invalid call. Nonexistent function ‘spawn’ in base ‘CharacterBody2D’.

For this line in Bow: arrow.spawn(arrow.position, Vector2(direction,0))

I read somewhere that instantiating a scene calls _init() with zero parameters and therefore, the custom class is moot. When I put lines to check the class of the Arrow made it returns “CharacterBody2D.”

So, how do I instantiate a node stemming from a custom class that keeps all its parameters and can call functions of its custom superclass?

You’re gonna have to change things and create a switch statement for your arrow types, but:

var arrow = Arrow.new(speed, shots, preset)

However you have a lot of things I’d suggest changing.

  1. Use Area2D or RigidBody2D for your arrow instead of CharacterBody2D.
  2. Variable names should be snake_case to avoid confusion. ALL_CAPS is reserved for constants.
  3. Do not use _init(). As you are finding, it isn’t quite as flexible as a constructor. This is because it runs before _ready() runs. Instead, you should instantiate your object, then initialize any values.
  4. Only use preload() to initialize constants. Otherwise use load().
1 Like

I instantiate the object, then try to initialize my values via a function within the custom class but it just gives me an error.

How I instantiate:

arrow = load("res://Scenes/normal_arrow.tscn").instantiate()
arrow.set_properties(shots, type)

How I try to initialize values:

func set_properties(_shots : int, _type : types) -> void:
	shots = _shots
	match _type:
		types.NORMAL:
			type = _type
			speed = 300
		type.FUNNY:
			type = _type
			speed = 100

The error I get:

Invalid call. Nonexistent function 'set_properties' in base 'CharacterBody2D'.

Did you remove the _init function from your arrow script? You can’t use an _init function with mandatory parameters on a node that you plan to instantiate from a PackedScene since you can’t pass arguments into the instantiate function.

1 Like
const ARROW = preload("res://Scenes/normal_arrow.tscn")

spawn_func() -> void:
	var arrow: Arrow = ARROW.instantiate()
	arrow.set_properties(shots, type)

In addition to what @paintsimmon said (delete the _init() function), you need to make sure your arrow var is of type Arrow and not CharacterBody2D by implicitly declaring it as such.

I also recommend not using capital letters in folder names. You can end up with export issues for Windows and Mac games.

Would not statically typing the arrow variable actually matter? :thinking:

Based on the error being reported, it is statically typed somewhere else as a CharacterBody2D, otherwise it would be a Variant when assigned and that method would not have failed.

So TBH I put it in there so that the issue would come up in the code and could be resolved.

If you supply default values for all of the constructor argument the engine will be able to call it when creating the node.

Also, nothing is preventing you to call _init() explicitly after the instantiation, albeit this can be seen as a bit unconventional.