What's the best way to create a modular wave-spawning node?

Godot Version

4.2.1

Question

I’m creating a tower defense game, I want to be able to easily create enemy waves for creating ‘deterministic-spawning’ levels. Here’s my current implementation:

class_name spawnerNode
extends Node

var slime_scene = preload("res://scenes/enemies/slime.tscn")
var zombie_scene = preload("res://scenes/enemies/zombie.tscn")
var orc_scene = preload("res://scenes/enemies/orc.tscn")
var x
var wave = 1
var status = 'idle'
signal wave_changed(int)

@export var spawner_tile_index = 0
@export var waves = {
	'wave1' : 
	{ 
		'enemy_sequence1' :
		{
			'name': 'orc', 'amount' : 2, 'time' : 3
		},
	},
	'wave2' : 
	{
		'enemy_sequence1' :
		{
			'name': 'orc', 'amount' : 1, 'time' : 2
		},
		'enemy_sequence2' :
		{
			'name': 'zombies', 'amount' : 4, 'time' : 2
		},
	},
	'wave3' : 
	{
		'enemy_sequence1' :
		{
			'name': 'orc', 'amount' : 4, 'time' : 1
		},
		'enemy_sequence2' :
		{
			'name': 'zombies', 'amount' : 2, 'time' : 1
		},
		'enemy_sequence3':
		{
			'name': 'slime', 'amount' : 3, 'time' : 1
		},
		'enemy_sequence4':
		{
			'name': 'orc', 'amount' : 4, 'time' : 1
		},
	},
}
var n_waves = waves.size()

func spawn_next_wave():
	if wave > n_waves or status == 'spawning':
		return
	status = 'spawning'
	emit_signal('wave_changed', wave)
	for enemy in waves['wave%d' % wave]:
		await spawn_unit(waves['wave%d' % wave][enemy]['name'], waves['wave%d' % wave][enemy]['time'], waves['wave%d' % wave][enemy]['amount'])
	status = 'idle'
	wave += 1
	
	
func spawn_unit(enemy_name, time, amount):
	for i in amount:
		if enemy_name == 'zombies':
			x = zombie_scene.instantiate()
		elif enemy_name == 'slime':
			x = slime_scene.instantiate()
		elif enemy_name == 'orc':
			x = orc_scene.instantiate()
		x.startIndex = spawner_tile_index
		get_parent().add_child.call_deferred(x)
		await get_tree().create_timer(time).timeout
	
	
func _unhandled_input(_event):
	if Input.is_action_just_pressed("space"):
		spawn_next_wave()

it works like a charm, but I was expecting to modify it from the editor for each level, but the editor doesn’t seem to be able to follow the pattern for adding more enemies on a single wave or more waves. I have to set up each individual type for the key and value of the dictionaries, which takes longer than coding itself. how could I improve this?

Hi! If I was doing this, I would use a custom resource. I’d create one for the sequence, containing the type of enemy, the amount and the time as properties. And I’d give each one the
@export tag so I could quickly edit it.

Then I would create a resource for the wave, which would contain an array of sequences, again with the @export tag and casted as an array of sequence resources.

Finally, I’d create an `@export var waves : Array[WaveResource] and begin building my resources there!

1 Like

This seems like a perfect scenario to use Resources. In this case, you can use a Resource to define the data structure of a “wave”, and every “enemy sequence”.

Every EnemySequence contains the same three values, let’s create enemy_sequence.gd:

extends Resource
class_name EnemySequence

@export var name: String
@export var amount: int
@export var time: float

Every Wave consists of multiple EnemySequences so let’s create wave.gd to define a Resource that has this array of enemy sequences.

extends Resource
class_name Wave

@export var enemy_sequences: Array[EnemySequence]

Finally let’s add an array of Wave to your spawner!

@export var enemy_data: Array[Wave]

And now you have a very organized data structure, cleaner code, and a nice way to modify it on the editor.

Resources are great ways to store data, you can do nice things with them like save a Resource to an external file. Check the documentation for more info.

1 Like

thanks! I didn’t know how resources worked, you guys are doing god’s work