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