Got stuck when trying to fix the "Attempt to call function 'instantiate' in base 'null instance' on a null instance." error... how do i fix it?

Godot Version

4.5.1.stable

Question

I found a plugin that i could use for my metroidvania. The plugin is called “Metroidvania System”. Now everything had been going fine until i made a script with extends "res://addons/MetroidvaniaSystem/Template/Scripts/MetSysGame.gd" and everything went well until i ran the game and i got this error:

Attempt to call function 'instantiate' in base 'null instance' on a null instance.

And the error happened from the plugin’s own script! In res://addons/MetroidvaniaSystem/Template/Scripts/MetSysGame.gd, there is this:

## Virtual method to be optionally overriden in your game class. Return a Node representing a scene under given path. Overriding it is mainly useful for procedurally generated maps.
func _load_room(path: String) -> Node:
	return load(path).instantiate()

And so i changed load to preload and then i get:

Parser Error: Could not resolve class "res://addons/MetroidvaniaSystem/Template/Scripts/MetSysGame.gd".

…even through the script is right there…
I just need help…

preload will attempt to load the path even before any functions like _ready are run. You won’t be able to preload a variable path. I think the issue is in how you are using the _load_room function. Could you show that code?

Here is the code, i just took it from the sample project of Metroidvania System (the plugin i’m using, i have modified it slightly, now Metroidvania System requires you to extend MetSysGame.gd):

# This is the main script of the game. It manages the current map and some other stuff.
extends "res://addons/MetroidvaniaSystem/Template/Scripts/MetSysGame.gd"
class_name Game

const SaveManager = preload("res://addons/MetroidvaniaSystem/Template/Scripts/SaveManager.gd")
const SAVE_PATH = "user://example_save_data.sav"

# The game starts in this map. Uses special annotation that enabled dedicated inspector plugin.
@export_file("room_link") var starting_map: String

# Number of collected collectibles. Setting it also updates the counter.
var collectibles: int:
	set(count):
		collectibles = count
		%CollectibleCount.text = "%d/7" % count

# The coordinates of generated rooms. MetSys does not keep this list, so it needs to be done manually.
var generated_rooms: Array[Vector3i]
# The typical array of game events. It's supplementary to the storable objects.
var events: Array[String]
# For Custom Runner integration.
var custom_run: bool
# See LoopScript.
var loop: String

func _ready() -> void:
	# A trick for static object reference (before static vars were a thing).
	get_script().set_meta(&"singleton", self)
	# Make sure MetSys is in initial state.
	# Does not matter in this project, but normally this ensures that the game works correctly when you exit to menu and start again.
	MetSys.reset_state()
	# Assign player for MetSysGame.
	set_player($Player)
	
	if FileAccess.file_exists(SAVE_PATH):
		# If save data exists, load it using MetSys SaveManager.
		var save_manager := SaveManager.new()
		save_manager.load_from_text(SAVE_PATH)
		# Assign loaded values.
		collectibles = save_manager.get_value("collectible_count")
		generated_rooms.assign(save_manager.get_value("generated_rooms"))
		events.assign(save_manager.get_value("events"))
		player.abilities.assign(save_manager.get_value("abilities"))
		
		if not custom_run:
			var loaded_starting_map: String = save_manager.get_value("current_room")
			if not loaded_starting_map.is_empty(): # Some compatibility problem.
				starting_map = loaded_starting_map
	else:
		# If no data exists, set empty one.
		MetSys.set_save_data()
	
	# Initialize room when it changes.
	room_loaded.connect(init_room, CONNECT_DEFERRED)
	# Load the starting room.
	load_room(starting_map)
	
	# Find the save point and teleport the player to it, to start at the save point.
	var start := map.get_node_or_null(^"SavePoint")
	if start and not custom_run:
		player.position = start.position
	
	# Add module for room transitions.
	add_module("RoomTransitions.gd")
	# You can enable alternate transition effect by using this module instead.
	#add_module("ScrollingRoomTransitions.gd")
	
	# Reset position tracking (feature specific to this project).
	await get_tree().physics_frame
	reset_map_starting_coords.call_deferred()
	
	# Make sure minimap is at correct position (required for themes to work correctly).
	%Minimap.set_offsets_preset(Control.PRESET_TOP_RIGHT, Control.PRESET_MODE_MINSIZE, 8)

# Debugging helper. Press F2 to quickly reload game.
func _input(event: InputEvent) -> void:
	var k := event as InputEventKey
	if k and k.pressed and k.keycode == KEY_F2:
		var cr: Script
		# CustomRunner can't be used directly, since the addon is optional.
		if ResourceLoader.exists("res://addons/CustomRunner/CustomRunner.gd"):
			cr = load("res://addons/CustomRunner/CustomRunner.gd")
		
		if cr and cr.is_custom_running():
			get_tree().change_scene_to_file.call_deferred("res://SampleProject/CustomRunnerIntegration/CustomStart.tscn")
		else:
			get_tree().reload_current_scene()

# Returns this node from anywhere.
static func get_singleton() -> Game:
	return (Game as Script).get_meta(&"singleton") as Game

# Save game using MetSys SaveManager.
func save_game():
	var save_manager := SaveManager.new()
	save_manager.set_value("collectible_count", collectibles)
	save_manager.set_value("generated_rooms", generated_rooms)
	save_manager.set_value("events", events)
	save_manager.set_value("current_room", MetSys.get_current_room_id())
	save_manager.set_value("abilities", player.abilities)
	save_manager.save_as_text(SAVE_PATH)

func reset_map_starting_coords():
	$UI/MapWindow.reset_starting_coords()

func init_room():
	MetSys.get_current_room_instance().adjust_camera_limits($Player/Camera2D)
	player.on_enter()
	
	# Initializes MetSys.get_current_coords(), so you can use it from the beginning.
	if MetSys.last_player_position.x == Vector2i.MAX.x:
		MetSys.set_player_position(player.position)

# Customized load function that handles maps generated in Dice.tscn and loops in LoopRoom.tscn.
func _load_room(path: String) -> Node:
	if not path.begins_with("GEN"):
		# See LoopScript.
		if not loop.is_empty():
			path = loop
			loop = ""
		return super(path) # gives the error, probably
	
	# Base scene that will be customized (Junction.tscn). Modified it for my game's scenes/main.tscn...
	var prototype := preload("uid://cmkq8vmxvy3yj").instantiate()
	prototype.scene_file_path = path
	
	var config := path.split("/")
	# Assign values to the scene (see the script in Junction.tscn).
	prototype.exits = config[2].to_int()
	prototype.has_collectible = config[3] == "true"
	# Apply the values. It has to happen before the scene enters tree.
	prototype.apply_config()
	
	return prototype

and at the # Gives the error, probably part, it tells me that there is a bug and takes me to MetSysGame.gd where it is:

func _load_room(path: String) -> Node:
	return load(path).instantiate()

Hope you won’t mind if i dump the whole MetSysGame.gd code… right?:

## Class designed for use in main game scenes.
##
## MetSysGame is responsible for map management and player tracking. You can extend it by adding MetSysModules.
extends Node

const MetSysModule = preload("res://addons/MetroidvaniaSystem/Template/Scripts/MetSysModule.gd")

var player: Node2D
var map: Node2D
var map_changing: bool

var modules: Array[MetSysModule]

## Emitted when [method load_room] has loaded a room. You can use it when you want to call some methods after loading a room (e.g. positioning the player).
signal room_loaded

## Sets the node to be tracked by this class. When player was assigned, [method MetroidvaniaSystem.set_player_position] will be called automatically at the end of every physics frame, updating the player position.
func set_player(p_player: Node2D):
	player = p_player
	player.get_tree().physics_frame.connect(_physics_tick, CONNECT_DEFERRED)

## Adds a module. [param module_name] is either a file located in [code]Template/Scripts/Modules[/code] or a full path to the script. The script must extend [code]MetSysModule.gd[/code]. Returns a module object that can be customized if needed.
func add_module(module_name: String) -> MetSysModule:
	# If a full path was passed in, use that. Otherwise assume it is a MetSys module.
	if not module_name.is_absolute_path():
		module_name = "res://addons/MetroidvaniaSystem/Template/Scripts/Modules/".path_join(module_name)
	
	var module: MetSysModule = load(module_name).new(self)
	modules.append(module)
	return module

func _physics_tick():
	if is_inside_tree() and can_process():
		MetSys.set_player_position(player.position)

## Loads a map and adds as a child of this node. If a map already exists, it will be removed before the new one is loaded. This method is asynchronous, so you should call it with [code]await[/code] if you want to do something after the map is loaded. Alternatively, you can use [signal room_loaded].
## [br][br][b]Note:[/b] If you call this method while a map is being loaded, it will fail silently. The earliest when you can load a map again is after [signal room_loaded] is emitted.
func load_room(path: String):
	if map_changing:
		return
	
	map_changing = true
	
	if map:
		map.queue_free()
		await map.tree_exited
		map = null
	
	map = _load_room(path)
	add_child(map)
	
	MetSys.current_layer = MetSys.get_current_room_instance().get_layer()
	map_changing = false
	room_loaded.emit()

## Virtual method to be optionally overriden in your game class. Return a Node representing a scene under given path. Overriding it is mainly useful for procedurally generated maps.
func _load_room(path: String) -> Node:
	return load(path).instantiate()

func get_save_data() -> Dictionary:
	var data: Dictionary
	data.merge(_get_save_data())
	
	for module in modules:
		data.merge(module._get_save_data())
	
	return data

## Virtual method to be overriden in your game class. Called by SaveManager's store_game(). Use it to return the data you want to save. Data of added modules is stored automatically.
func _get_save_data() -> Dictionary:
	return {}

func set_save_data(data: Dictionary):
	_set_save_data(data)
	
	for module in modules:
		module._set_save_data(data)

## Virtual method to be overriden in your game class. Called by SaveManager's retrieve_game(). The provided [Dictionary] holds your previously saved data.
func _set_save_data(data: Dictionary):
	pass

		if not loop.is_empty():
			path = loop
			loop = ""

It seems that path is assigned the value of loop, which isn’t a valid path for load(), which results in the error. Where is loop assigned?

The path is not valid.
You can print it out to see what it contains.
You can trace it all the way back to this line:
@export_file("room_link") var starting_map: String
Did you remember to add a value to that export in the editor?

1 Like

loop is assigned on line 24 using:

var loop: String

No? I really don’t know if i have added a value to that export in Godot-4 for my game, but i’ll try!

It works!!! My game just crashes immidiately…

edit: Can you explain to me why the crash happens, i suspect it is a stack overflow error…

1 Like

Are there any error messages?
Is it just your program that crashes or the entire editor itself?

Sure, there is a error message! The error is:

Stack overflow (stack size: 1024). Check for infinite recursion in your script.

The game only crashes, and that might mean something…
And it states Infinite Recursion, i’m looking for it right now…