Level memory for ennemy killed and item picked

Hi everyone !

I’ve searched the web before asking but I can’t find what I’m looking for. It’s pretty common in games so I must phrase it wrong. Can anyone point me in the right direction with a link or some keywords to look for ?

So my game is divided into multiples areas/levels (scenes). The player can go freely between the areas and can come back to previously visited areas. I’m looking for a simple way to let the map or the ennemy nodes on the map know if they have already been killed or hurt before and adjust the map when the player re-enters. Actually they are created each time the player enters the area.

Ideally I would like to let them respawn after a while, but I would like to prevent the player to farm them by enter kill and come back and kill again. Ideally I would delay this with a timer of some sort. I would also apply this logic to items and stuff on the map. (It’s a kind of survival rpg where you can mine stuff and chop trees…)

I can’t believe there is no info or tutorial about that somewhere… ?!?

Thank you very much !

Sounds like you need to make a save file for your world objects.

you need to keep track of all the enemies that exist in your game. preferably have a node that spawns them when the game start.

I would create an autoload with a dictionary, there I would store a bunch of bools.
give each enemy a unique ID or name, when they are killed they would update themselves in the autoload.
when the level loads it would check each enemy to see if they need to spawn or not.
you can do this for hundreds of enemies across all levels.

if there’s too much data, you would need to find a way to store an enemy list into a file so you can free the memory, and access it when it’s relevant, like with levels that the player can no longer go back to, or ones that are too far away (doors-wise). you could also use more than one dictionary in this case.

I guess this would work but I hoped I could find a solution without using save game features. It seems out of my league for now as I am still a beginner… :wink: the intro of your link scares me : " save games can be complicated" :rofl:

Thank you

I tought of something like that but could not figure out how to create unique identifier automatically. I figure I could set a var int at init and use it as key and add1 between the ennemy added in the dict, but I could not figure out how to make the spawners or the level code to remember the keys and not just recreate an entry each time the player enter the level…

Thank you

This is a challenge I recently solved for my game, maybe not fully yet as I’ve not dealt with all the edge cases. It is not that hard, it just requires some moving parts.

This is for you and anyone in the future looking for a solution for something like this.

The way I do it is that I let each enemy handle its own spawning in a level. For each area/level, there is an area_state config file attached to it that contains information about dead enemies and other persistent activities. The state file looks like this:

##Area 1 state file area_1.config
[Enemy1]
is_dead=true
death_position=Vector2(2802.87, -328.575)

[BreakableBox1]
destroyed=true

[Enemy2]
is_dead=true
death_position=Vector2(2890.36, -325.202)

[Enemy3]
is_dead=true
death_position=Vector2(6575.32, 695.425)

As you can see, each enemy is referenced by name, and it stores the needed information to know if it should respawn, or where it should respawn its carcass as dead.

Each enemy or interactable object registers itself with the GameManager autoload when it spawns in the level for the first time. It also updates its status when it dies or gets destroyed.

I have a class called enemy that all enemies extend from. In that class, when the enemy node enters the tree, it checks the the area_state file, if is_dead is true, it destroys itself if not, it spawns normally. Below is what a sample of the code look like in the enemy script;

func _enter_tree() -> void:
	##I set some enemies to persist in the level so they always respawn
	if not persist_world_state:
		return

	##Enemy gets its death state from the GameManager or registers itself for the first time
	var dead = await GameAreaManager.load_object_state(self, "is_dead", is_dead)
	
	if dead:
		##If enemy is dead, it destroys the enemy
		var death_position: Vector2 = await GameAreaManager.load_object_state(self, "death_position", global_position)
		queue_free()

Destroyable objects also share a similar script.

The saving and loading of the area_state looks something like this;

##check if area state exists, then load or create it
var config_file: ConfigFile = ConfigFile.new()
var err = config_file.load("user://areas/area_1/area_1_state.config")
if err != OK:
	config_file.save("user://areas/area_1/area_1_state.config")

func save_object_state(object: Node2D, property: String, value: Variant):
	config_file.set_value(object.name, property, value)
	save_config_file(config_file, dir_path, file_path)

func load_object_state(object: Node2D, property: String, default_value: Variant = null) -> Variant:
	return config_file.get_value(object.name, property, default_value)

In summary, the interaction looks like this:
Object -> GameManager -> AreaStateFile.config

I hope this helps you. It may not be something you can get done immediately, but it will give you an idea of how to get it done. At the end of the day, you really can’t escape making a save system, and this architecture makes it modular enough for all object types.