Room system for metroidvanias

Godot Version

4.4.1 stable

Question

So every level in my game is a scene and its called like this: “room_1_1” / “room_1(horizontal)_1(vertical)” andin my game scene I have a Node2D called"RoomSpawner"

I want that if the player exits a room, it goes to the next room, I tried just putting areas and calling them in the map system global script, but that would be bad if i have multiple areas in multiple rooms.

Sooo I’m trying to make a system, that when you exit the room, it deletes the current room and brings you to the next_room that you selected in the inspector, and because of the game being a metroidvania. I also want a room to be connected with multiple rooms + can know if you exited the room at the x or y axis + backtracking is possible. I tried, but failed:

class_name map_system
extends Area2D

var SAVE_PATH: String = "user://rooms.cfg"

var config: ConfigFile = ConfigFile.new()
var current_room: PackedScene

@export var next_room: String

# Called when the node enters the scene tree for the first time.
func _ready() -> void:
	var room_spawner: Node2D = get_tree().get_root().get_node("Main/GameContainer/Game/Game/RoomSpawner")
	
	room_spawner.add_child(preload("res://areas/cave/normal rooms/room_1_1.tscn").instantiate())


func change_room() -> void:
	var room_spawner: Node2D = get_tree().get_root().get_node("Main/GameContainer/Game/Game/RoomSpawner")
	var player: CharacterBody2D = get_tree().get_root().get_node("Main/GameContainer/Game/Game/Player")
	
	var err = config.load(SAVE_PATH)
	if err == OK:
		room_spawner.remove_child(config.get_value("Map", "CurrentRoom"))
	current_room = load(next_room)
		
	var room = current_room.instantiate()
	room_spawner.add_child(room)

and I don’t want to use Met Sys, because it breaks my game :frowning:

How did it fail? Did you get a error?

Why are you using a “user://” config file?

I didn’t get an error, it just didn’t bring me to other rooms and was really bad and will make me waste time when using areas(unproductive). I use a “://user” config file for saving scenes, but that isn’t really important now

The code you have there isn’t doing anything with the player. I presume they’re a child of the room? You presumably need to remove them from the old room and add them to the new.

I’d think RoomSpawner could be a global script, something like:

# extends Node

func change_room(index: Vector2i, player: CharacterBody2D) -> void:
    var tree_loc: Node2D = $/root/GameContainer/Game/Game/Rooms
    var old:      Node2D = tree_loc.get_child(0)
    var path:     String = "res://rooms/room_%d_%d" % [index.x, index.y]
    var tgt:      Node2D = load(path)

    old.remove_child(player)
    tree_loc.remove_child(old)
    tree_loc.add_child(tgt)
    tgt.add_child(player)

Ideally, you’d have another function in the same set to do background loading for rooms so you could have all the nearby rooms preloaded, cached and ready for change_room() to jump to immediately. You could also just preload() everything.

Edit: This is skating past other things, mind you; presumably changing the room is also going to involve populating it (unless that’s happening in _ready() or something) and placing the player in the room. Presumably the player isn’t always starting in the same place in every room; if nothing else, where they start is going to depend on which door they came in.

The player is a child of the game scene and not of the room

So, when you say it didn’t place the character in the room, do you mean the player is just beside the room? Behind it? In the room but not in the right place?

One option you have is just to have all the rooms loaded and in the tree, but make the rooms that aren’t in use invisible. Their _process() and _physics_process() functions will still be called, but you can check is_visible_in_tree() to guard against that. It might seem excessive to have everything loaded and running simultaneously, and back in the SNES days or even the PS1 days it would have been, but these days even on mobile devices you have vast resources available.

the player is in the scene root and not in the room scenes, the player exist and when the first room gets instantiated, the player is in that room bcause the roomSpawner node is in the scene root too, I don’t want all rooms to be loaded, because that will make it lag and its really unproductive, my goal is a room system that instantiates rooms when you exit the previous ones

You probably still want to cache nearby rooms, then. Note that preload() is pre-loading everything you feed to it, so if you don’t want everything loaded upfront you’re going to need to manually load() things.

If I use preload, I would have to make a code line for every room wich isn’t good both for performance and productivity

After 2 days, I finally fixed it by myself, now it works perfectly :star_struck:
For those who have the same problem, what I did is, making the next_room variable an export + packed scene, so you can connect rooms just by dragging the room you want to connect in the inspector, I also made a transition system(optional) now also the rooms gets called thanks to that export variable so now it can switch from rooms really easily, now you can build rooms and connect them without even coding, you just need to use the “Door” Node(custom node, but is an invisible area2D) and choose the next room(the room that you will go to if you enter that area) + you can use multiple doors and connect multiple rooms! By the way, here is the code:

class_name Door
extends Area2D

@export var next_room: PackedScene = preload("res://areas/cave/normal rooms/room_1_1.tscn")
@export var player_position: Vector2

var SAVE_PATH: String = "res://areas/rooms.cfg"

var config: ConfigFile = ConfigFile.new()
var current_room: PackedScene

func _on_area_entered(area: Area2D) -> void:
	change_room()

func change_room() -> void:
	var room_spawner: Node2D = get_parent().get_parent().get_parent()
	var player: CharacterBody2D = get_tree().get_root().get_node("Main/GameContainer/Game/Game/Player")
	var transition: AnimationPlayer = get_parent().get_parent().get_parent().find_child("RoomTransition").find_child("AnimationPlayer")
	var main: Node2D = get_tree().get_root().get_node("Main")
	
	var err = config.load(SAVE_PATH)
	if err == OK:
		pass # I don't need it now, but maybe later 😊
	var last_room: Node2D = get_parent().get_parent()
	current_room = next_room
	
	transition.stop()
	transition.play("transition")
	player.cutscene = true
	player.velocity.x += 20 * player.direction
	main.cutscene = "transition"
	await get_tree().create_timer(0.7).timeout
	
	var room = current_room.instantiate()
	room_spawner.add_child(room)
	
	player.global_position = self.player_position
	player.velocity.x = 0
	
	await get_tree().create_timer(0.5).timeout
	player.cutscene = false
	last_room.queue_free()