How do I dynamically spawn a new object by giving it a Resource that contains its definition?

Godot Version

4.6

Question

I have an inventory system that I like to think is fairly robust. All data about an item (value, weight, damage, etc.) is stored in a custom Resource called ItemResource. These resources are stored in an array within a custom Inventory class, which also has methods for adding, removing, and sorting the items.
I also have a custom ItemActor class, which contains an exported ItemResource variable. This class is used to represent an item in the game world. If I want to place an object in the world, I just place an ItemActor somewhere in the scene assign the desired ItemResource, and its create_item() method automatically sets the object’s sprite, collision shape, and other relevant variables.
When the player interacts with the ItemActor, it sends its ItemResource to the player’s Inventory through a custom Signal, and then deletes itself from the game world. Easy and simple.
However I’m having trouble figuring out how to create a brand-new ItemActor and call this method from within the game, such as when the player wants to drop something from their inventory.
I’d like to have a generic method which takes an ItemResource as a parameter, creates a new ItemActor object from it, and then spawns the item in the game world at the player’s feet.
I’m stumbling over how best to do this because I still don’t have a firm grasp of how and when to use init(), ready(), instantiate(), new(), etc. They’re all so similar that it’s hard to understand exactly what needs to run when.

Could I get some advice on the best way to implement this? I’ve tried creating the following methods in the Actor class (the custom class for the player character), but they’re throwing errors which prevent them from working.

Thanks in advance for any and all advice!

func place_item(item : ItemResource) → void:
    print("The player is attempting to place " + item.name)
    # Spawn a new ItemActor with the ItemResource’s data
    var item_to_spawn : PackedScene = load("res://models/item_actor.tscn")
    item_to_spawn.instantiate() as ItemActor
    item_to_spawn.item_resource = item

This throws the error Invalid assignment of property or key 'item_resource' with value of type 'Resource (SeedResource)' on a base object of type 'PackedScene'.
SeedResource extends ItemResource, but I don’t think that’s relevant in this case because the scene is loaded as a PackedScene so I apparently can’t access the internal item_resource variable.

func place_item(item : ItemResource) → void:
    print("The player is attempting to place " + item.name)
    # Spawn a new ItemActor with the ItemResource’s data
    var item_to_spawn = ItemActor.new()
    item_to_spawn.item_resource = item
    item_to_spawn.create_item()

create_item() is the ItemActor method which processes the ItemResource data to assign the collision shape and sprite (full code below). This approach throws Invalid assignment of property or key ‘texture’ with value of type ‘AtlasTexture’ on a base object of type ‘Nil’.

Here are the relevant definitions of my ItemActor and ItemResource classes:

item_actor.gd:

@tool
class_name ItemActor extends CharacterBody3D


@export var item_resource : ItemResource # Contains the definition of the item


@onready var collision_shape: CollisionShape3D = $CollisionShape3D
@onready var sprite: Sprite3D = $Sprite3D
@onready var item_name : String
@onready var interactable : Interactable = $Interactable
@onready var highlight_material = preload("res://materials/highlight_interact.tres")


## Called when the node enters the scene tree for the first time.
func _ready() -> void:
	create_item()


## Reads the assigned ItemResource and creates a Sprite, CollisionShape, 
## and InteractionShape for the item
func create_item() -> void:
	if self.item_resource == null:
		push_error("This item has no item resource assigned!")
		return
	self.item_name = item_resource.name
	var size = add_sprite(item_resource.sprite)
	var box = add_collision_shape(size)
	add_interaction_shape(box)
	

## Sets the sprite of the ItemActor and returns a Vector2 describing its size
func add_sprite(new_sprite) -> Vector2:
	self.sprite.texture = new_sprite
	var size : Vector2 = self.sprite.texture.get_region().size
	return size


## Sets a CollisionShape3D for the object based on the size of the sprite.
## Returns the collision shape so it can be re-used to create the interaction shape
func add_collision_shape(size : Vector2) -> BoxShape3D:
	# Figure out the 3rd dimension of the box
	var dimensions = Vector3(size.x / 100, size.y / 100, size.x / 100)
	var new_box = BoxShape3D.new()
	new_box.set_size(dimensions)
	collision_shape.shape = new_box
	return new_box


## Sets up the Interactable component. For now it will have the same shape
## as the collision box.
func add_interaction_shape(shape : BoxShape3D) -> void:
	var child = CollisionShape3D.new()
	var new_box = shape.duplicate()
	child.shape = new_box
	interactable.add_child(child)
	interactable.interact = self._on_pickup

item_resource.gd:

## This serves as the base Resource class for all items in the game.
## Items are created by loading an Item Node, then populating its data from an
## ItemResource.
class_name ItemResource extends Resource

enum ITEM_CLASS {WEAPON, ARMOR, TOOL, RESOURCE}

@export var name : String = ""
@export var sprite : Texture2D
@export var price : int = 5
@export var count : int = 1
@export var tags : Array[String]
@export var item_class : ITEM_CLASS
1 Like

Here you need to assign the instantiated scene into a variable and only then provide the item_resource.

func place_item(item : ItemResource) → void:
    print("The player is attempting to place " + item.name)
    # Spawn a new ItemActor with the ItemResource’s data
    var item_to_spawn : PackedScene = load("res://models/item_actor.tscn")
    var item_actor: ItemActor = item_to_spawn.instantiate() as ItemActor
    item_actor.item_resource = item

Thanks for the super fast reply! This fixes the problem in my first iteration, but I still need to run the ItemActor.create_item() method before I can place the item. This throws the same error as in my second iteration:

func place_item(item : ItemResource) -> void:
	print(self.name + " is attempting to place " + item.name)
	# Spawn a new ItemActor with the ItemResource's data
	var item_to_spawn : PackedScene = load("res://models/item_actor.tscn")
	var item_actor: ItemActor = item_to_spawn.instantiate() as ItemActor
	item_actor.item_resource = item # Assign the ItemResource
	item_actor.create_item() # Define the ItemActor's properties before placing it

This throws
Invalid assignment of property or key 'texture' with value of type 'AtlasTexture' on a base object of type 'Nil'.

I’m not sure I understand what the message is trying to tell me. What is the “base object” in this case? Is it supposed to be the new item_actor? If so, why does it not have a type?

It means that somewhere you’re trying to do something.texture = some_atlas_texture and that something is null.

Yes, I assign the sprite in the add_sprite function in my ItemActor class:

## Sets the sprite of the ItemActor and returns a Vector2 describing its size
func add_sprite(new_sprite) -> Vector2:
	self.sprite.texture = new_sprite
	var size : Vector2 = self.sprite.texture.get_region().size
	return size

I don’t think I understand what being null means in this context. Does that mean the Sprite3D node I’m trying to assign a texture value to doesn’t exist yet? If not, what sequence do I need to follow to ensure that the structure PackedScene is fully in place before I start modifying it?

The Sprite3D should exist, but your reference doesn’t.

@onready means, the value (in this case, a reference to the Sprite3D node) gets assigned when the node gets added to the SceneTree. Before that, variables like sprite will be null.

So you have to add your ItemActor instance to the SceneTree before calling create_item() to prevent this.

add_child(item_actor)

Depending on your scene structure, you might want to call add_child() on some specific node, other then the node containing the place_item function.
And if create_item() gets called in _ready() anyways, you don’t need to manually call it for the new instance.

It means that self.sprite hasn’t been properly initialized. It can happen for various reasons; typically bad path or initialization code hasn’t been executed yet.

That did it, thank you so much! I’ll have to do some refactoring for efficiency (no need to load the item_to_spawn separately on every function call) but here’s a full working solution:

## Spawn an item using an ItemResource
func place_item(item : ItemResource) -> void:
	var item_to_spawn : PackedScene = load("res://models/item_actor.tscn")
	var item_actor: ItemActor = item_to_spawn.instantiate() as ItemActor
	item_actor.item_resource = item # Assign the ItemResource to the ItemActor
	var world = get_tree().get_first_node_in_group("GameWorld") # Get the game world node
	var player_position = self.position # Get player's current position
	player_position.z += 0.05 # Place the item just in front of the player
	world.add_child(item_actor) # Add item to the scene tree, which triggers _ready
	item_actor.position = player_position # Set the item's position
	self.actor_data["inventory"].remove_item(item) # Remove item from inventory

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.