Function is always null

Godot Version

4.1.2.stable

Question

I´m making an inventory system, and i have a function called “add_item_to_inventory” that, obviously, adds an item to the inventory array in player’s script by giving parameters of name, id, texture, description, and “on_use”, and that´s the problem.
“on_use” parameter is a function, but when i touch the button to execute the function “on_use”, the game crashes and the console says that the function is “null::on_use”, and i don’t know why and how to solve it. HELP!

Interactable.gd:

extends Area3D

@export var item_name: String
@export var item_id: int
@export var item_texture: Texture
@export var item_description: String

# Function to execute when "Use" button is touched
func on_use() -> Callable:
	return func():
		print("hello")

# Function to execute when player interact with something
func interact(interactor):
	GameManager.add_item_to_inventory(interactor, item_name, item_id, item_texture, item_description, on_use)
	queue_free()

game_manager.gd: (It has AutoLoad):

extends Node

var selected_item = null

func add_item_to_inventory(interactor, item_name: String, item_id: int, item_texture: Texture, item_description: String, on_use: Callable):
	var found_empty_slot = false
	
	# Add item to inventory
	for i in range(interactor.inventory.size()):
		if interactor.inventory[i] == null:
			interactor.inventory[i] = {
				"name": item_name,
				"id": item_id,
				"image": item_texture,
				"description": item_description,
				"on_use": on_use
			}
			found_empty_slot = true
			break

InventoryDisplay.gd:

extends Control

var on_item_use = null

@onready var slots = [
	$InventoryGrid/Slot1,
	$InventoryGrid/Slot2,
	$InventoryGrid/Slot3,
	$InventoryGrid/Slot4,
	$InventoryGrid/Slot5,
	$InventoryGrid/Slot6,
]

func update_inventory_display(inventory):
	$ItemName.text = ""
	$ItemDescription.text = ""
	on_item_use = null
	for i in range(slots.size()):
		if inventory[i] != null:
			# Update images of all items
			slots[i].get_node("TextureRect").texture = inventory[i]["image"]
			
			# Update display information
			if slots[i] == GameManager.selected_item:
				$ItemName.text = inventory[i]["name"]
				$ItemDescription.text = inventory[i]["description"]
				on_item_use = inventory[i]["on_use"]

# When "Use item" button is touched
func _on_use_item_pressed():
	on_item_use.call() # Here is when i call the function and the game crashes!

Slot.gd:

extends Button

@export var inventory_display : Control
@export var player : CharacterBody3D

func _on_pressed():
	GameManager.selected_item = self
	inventory_display.update_inventory_display(player.inventory)

In theory it should work.
In the following code you are deliberately setting on_item_use to null.
Because it is inside an if statement, there is no guarantee that you will get to this line:

on_item_use = inventory[i]["on_use"]

I would make sure you are actually getting to that line, that on_item_use is set to anonymous lambda, and that update_inventory_display isn’t being called again and nulling out the variable.

on_item_use = null #<--------------------------------------------
	for i in range(slots.size()):
		if inventory[i] != null:
			# Update images of all items
			slots[i].get_node("TextureRect").texture = inventory[i]["image"]
			
			# Update display information
			if slots[i] == GameManager.selected_item:
				$ItemName.text = inventory[i]["name"]
				$ItemDescription.text = inventory[i]["description"]
				on_item_use = inventory[i]["on_use"]  #<--------------------------------------------

Thank you for the answer. I tried to put the “on_item_use” assignment line outside the if, but it is still not working. :worried: Is there any example of how I can pass a function via a parameter? It would help.

I tried by assigning the variable “on_item_use” the value “func” instead of “null”:

var on_item_use = func on_use():
    print("something")

and it returns:

Invalid call. Nonexistent function '<null> (Callable)'.

So, I don’t understand why it is null if that is literally a function.

I think you did that part correctly.
I guess somewhere in update inventory it gets lost.

extends Node

var use_function : Callable

func _ready() -> void:
	set_use_function(on_use)
	use_function.call()
	
func set_use_function(callable : Callable):
	use_function = callable
	
func on_use():
	print("using")

prints:
using

Thank you for the answer, too.

I found something. If I comment out these two lines, the function will correctly print “something.” So, I think those were the lines that made the function null, but I still don’t know how to make the function that I pass as a parameter be the function to execute when the “Use Item” button is touched.

inventory_display.gd:

extends Control

var on_item_use = func on_use():
	print("something")

@onready var slots = [
	$InventoryGrid/Slot1,
	$InventoryGrid/Slot2,
	$InventoryGrid/Slot3,
	$InventoryGrid/Slot4,
	$InventoryGrid/Slot5,
	$InventoryGrid/Slot6,
]

func update_inventory_display(inventory):
	$ItemName.text = ""
	$ItemDescription.text = ""
	#on_item_use = null <- I commented this line
	for i in range(slots.size()):
		if inventory[i] != null:
			# Update images of all items
			slots[i].get_node("TextureRect").texture = inventory[i]["image"]
			
			# Update display information
			if slots[i] == GameManager.selected_item:
				$ItemName.text = inventory[i]["name"]
				$ItemDescription.text = inventory[i]["description"]
				#on_item_use = inventory[i]["on_use"] <- I commented this line

# When "Use item" button is touched
func _on_use_item_pressed():
	on_item_use.call()

Finally, I found the problem! It was that the “item” scene (that provides the “on_use” function) was deleted when you picked up the item, so the function disappears and it transforms to “null.”

I can’t believe this problem took me 2 days…

I actually tested this and if you have stored the Callable in a variable outside of that scene, then queue_free() the scene, it does not remove that Callable. The variable holds a copy of it in memory.
Its great that its working now but just want to be clear in that.

Yes, that’s another way to solve it. For now I just set the scene visibility to false and block the interact function so it’s as if the item disappears. It’s not the best solution, but I’m just testing so it works.