How do I stack TileMaps and swap them on demand?

Godot Version

4.2.1, will probably upgrade to whatever newest as the project goes

Question

TL;DR is the title, but I’ll explain everything from the beginning.

As a coding exercise, I’m remaking a 2005 Chinese Flash game Tower of Sorcerer 1.4, which was based on a PC98 Japanese Title Tower of Sorcerer.

(Playthrough Video as the reference)
I did it mainly for the auto-progressing combat system, and I’m struggling but making progress consistently.

However, the overworld side of the game isn’t going well.
I got something working, but it’s not really a comfortable base to build upon, so I want to hear what people has to say about my current approach.

The goal, is to have a 11 * 11 playable area, which will have about 50 floors, swapping when player hit a trigger or teleport.
My first approach was simple. Just stack a lot of invisible 11 * 11 TileMap, and show() the current floor on demand.

It worked…for the version. I didn’t dig into it, but I think Godot “Fixed” the bug where invisible TileMap won’t have collision.
Now hiding or even DISABLE PROCESSING won’t stop walls from another floor to block player’s way.
I don’t know why should invisible stuff have collision in the first place, but okay.
Guess I’ll find another way.

So what I did after that, is I just store all the data in an array, queue_free() all the 11 *11 TileMap, and use the Array to paint the tiles onto an one and only TileMap node.

It looks like this:

extends Node2D
class_name LevelManager

signal level_changed(ascend: bool)

## Will cause issue when floor value is negetive
var levels_tilemap_data: Array
var level_names: Array
var tilemap: TileMap = preload("res://stage/levels/level_default.tscn").instantiate()

@export var current_level: int = 0:
	set(new_level):
		var has_level: bool = false
		for level: String in level_names:
			if level == str(new_level):
				has_level = true
				break
		if has_level:
			_clear_level(current_level)
			_set_level(new_level)
			level_changed.emit(new_level > current_level)
			current_level = new_level

func _ready() -> void:
	for child: Node in get_children():
		if child is TileMap:
			levels_tilemap_data.append(_get_level_tilemap_data(child))
			level_names.append(child.name)
			child.queue_free()
	add_child(tilemap)
	_set_level(current_level)

func _get_level_tilemap_data(level: TileMap) -> Array[Array]:
	var level_data: Array[Array]
	for layer: int in level.get_layers_count():
		var used_cells_coords: Array[Vector2i] = level.get_used_cells(layer)
		for cell_coords: Vector2i in used_cells_coords:
			var source_id := level.get_cell_source_id(layer, cell_coords)
			var atlas_coords := level.get_cell_atlas_coords(layer, cell_coords)
			var alter_id := level.get_cell_alternative_tile(layer, cell_coords)
			level_data.append([layer, cell_coords, source_id, atlas_coords, alter_id])
	return level_data

func _set_level(level: int) -> void:
	for tile_info: Array in levels_tilemap_data[level_names.find(str(level))]:
		tilemap.set_cell(tile_info[0], tile_info[1], tile_info[2], tile_info[3],  tile_info[4])

func _clear_level(level: int) -> void:
	for tile_info: Array in levels_tilemap_data[level_names.find(str(level))]:
		tilemap.set_cell(tile_info[0], tile_info[1], -1, Vector2i(-1, -1))

And…it works. For now at least, there will be issue with special stages later, but that’s not hard to work around.
Using another array to point to other array is probably a bad idea as well, but I’ll put that aside for now, for there’s more important stuff is not functioning:
How do I save the state of the TileMap?

As it stands, when I leave the level and come back, everything respawns, which is not how the game works.
It’s pretty easy to archive the whole game into an array and load back from it, but modifying it is a nightmare.
Even if I succeed, is it really a good idea to mess around a giant array that makes the whole game functional?
I can probably dig around and find a way around it, but I have a feeling I’ve already made some gigantic stupid mistakes and shouldn’t build upon it further.

A potential solution is to just make a giant TileMap, say 110 * 55, then just shift the TileMap around to create the illusion of level switching. Never worry about save and load since it’ll never be unload.
But…I planned to open source this project once it’s done, also I want it to run on the web. I want to make it as simple and elegant as possible.

I just feel like I’m missing something really fundamental here.
What’s your thought? Thanks for reading.

If you need to have 50 unique map then you will need to make a the floor editor and save the data in json or something then read it and generate the floor

or really can manually make 50 scenes of the map, since it’s not randomly generated right? then you save it and make custom switch scene when entering floor.

so the scene tree will look like

-Core(core.gd) (Node):
–Tilemaps (Node)
—TilemapSceneContainer (Control):
----here you add child and switch tilemap scene
–CoreUI (Canvas layer)
health/mp/coin ui (control)

I did plan to make 50 scenes, the problem is the scene will have collision even if the tilemap is hiden or disabled, so I can’t just stack them upon each other, the player will be block by nothing in sight
Making other floors not in the scene tree is easy, the part I can’t figure out is how to make them exit and enter the tree while remaining the state of the level (item picked, enemy defeated etc.)

yes that’s why instantiate a tilemap scene one at a time, so the collider wont be stacked

well um use switch scene like this Gapless Scene Change - #3 by zdrmlpzdrmlp

when a tilemap scene is about to be added as a child, there should be a checker to get saveData to see if this tilemap scene (can be by id), already has chest/item pick/taken or enemy defeated, so it wont respawn every time we created the scene

That’s the part I can’t get my head around with.
I placed the items and enemies with the TileMap as well, and I don’t know how do I pin point their existence in the tilemap, and erase them by code.
…maybe I just record the coords of things that should be erased, and upon loading use set_tile to clean the tile?
I think that would work, but would require me to do layer managing precisely, and at some point someone toying with the project will be like “WHY IS THIS PARTICULAR SLIME KEEP RESPAWNING”, which shouldn’t be necessary annoyance.
Also, that means I’d have to save each and every single level into a scene file, instead of multiple TileMap nodes under a manager scene. Which…would be a pain to do manually with tens of levels.
It can be automated, just…have to do compilation within the project folder itself…I have a feeling this will create more managing problems later.

It’s better than what I have now though. I’ll do this if no better idea pop up.

there’s no need for this, like i said use save system, but to reference it will come back to your own game design.
here is an example, you set id for each enemy
so if there are 10 enemy
they are named in from 1 to 10
if there are 5 item scattered across the map, it will also be given id of 1 to 5
the idea is to add the collected or dead enemy to an Array for that scene
so there are 2 Array
Enemy Array contains list of ID has died
Item Array contains list of ID has collected

so from that when player collected the item, because the item has an ID, you add/append the ID to the Item Array
if player defeated enemy with ID 6, then you append it to Enemy Array

so next time when player reenter this map, it will check these 2 array first before spawning any of the item or enemy
then you might ask what if there are 50 Map, how does the 2 Arrays works then
well you can put these 2 Array mentioned into a dictionary
the dictionary key will be your your tilemap scene order
example you beat enemy ID 2 in tilemap scene 1
then you put access Dictionary[1] and get the enemy array, and append the ID 2 there
likewise when you enter the map of tilemap scene 1, it will check dictinary with key 1

That would mean I don’t append then through TileMap, instead just append them as a scene in the editor.
…That makes sense, but also makes me rethink everything. If modifying level through TileMap is no longer necessary, a giant array of every level becomes viable again.
I guess I meshed 2 problems into 1 here. Swapping levels and saving loading doesn’t have to be the same thing.

what do you mean by this?
you know you can load scene by path right, there’s only need to call the right string path to the scene to access it. what append do?

Edit: Wrong quote

I meant in TileMap you can use the scene as a tile. It’s easy to paint, but pretty convoluted to do modification later on.
I’m talking about NOT doing that.

What if I just…remove the child, let them hang in mamory as orphan nodes, and only add them back when needed? How bad can it be?

i mean yeah, it’s come back to your game design. changes will affect it different way, but if it’s for the better, then why not change it to better one?

i dont really get it, isnt it the same paint it and modify it for tilemap? i mean just click one of the tileset and left click it to the tilemap to paint?

I’m talking about identifying what has been instanced at runtime, and repaint the modified data through code, like how do I erase the defeated enemy from an array that’s just full of coords info?
I just had an idea of using dictionary to isolate out every single level, when I exit a level I just override that level’s info and read the next with a key, simultaneously get rid of the stupid pointer array…
Just haven’t figure out why it assigned my keys but not my value, dictionary probably wasn’t intended for this much on-the-go modification

well i did tell about how to make it possible, you will need to have enemy spawner controller, or really just something to detect if enemy already died or not before any enemy is spawned in a level

i read you do keep the coordinate for the each of the enemy? then it’s pretty similar to the array of died enemy, because each floor has its own arrays in a dictionary and to access it, just call the key and get the arrays

with this, there no need to erase a specific enemy from array, because it’s an array of dead enemy, any dead enemy ids will be put here, and will not be cleared unless user demand or new game
also because it’s an array for A single floor enemy IDs,
so next floor wont use the same Array

That’s what I don’t get, it’s in the TileMap! How do I spawn them after checking their stats, when all there is bunch of number inside an array?


(Find the enemy by coords is possible, but the layer managing problem remains)

I want to assign the array on _ready(), but just doesn’t work

print(levels_tilemap_data)
## prints out tons of stuff
levels_data[child.name] = levels_tilemap_data

## After the loop is done
print(level_datas)
## prints "{ "0": [], "1": [], "2": []...}", keys are there but values aren't

i thought it would be like you set ID in editor in a scene of a tilemap level, and set visible to false so it wont just appear and disappearing for a short milliseconds when player entered the floor if they already died

what is this even mean and from?
the enemy id is basically something you assigned yourself from editor, before making it run

then you might ask how to set id from editor for each scene enemy?
simply use
@export var enemy_id
put this in your enemy.gd sccript or whatever is the enemy being spawned

The original post, this line:

level_data.append([layer, cell_coords, source_id, atlas_coords, alter_id])

And I repaint using this data array

You can’t alter exported variables if it’s painted with TileMap