0 votes

Hello everyone,
I'm working on my first game and trying to implement a generic system that would allow me to define the monster waves of my game + apply custom properties in case I need them.

For example:

  • Wave 1

    • 4 Knight enemies
  • Wave 2

    • 4 Knight enemies
    • 1 Archer enemy
  • Wave 3

    • 4 Knight enemies + They all have 1 extra health point
    • 1 Boss

But I'm still struggling to make things work, primarily by my lack of understanding of how Resources and how the language works.

The idea is to have a ResourceStash
I can pre-load all my enemy's scenes + export an enum which would be used by a Resource

# ResourceStash.gd
extends Node

enum ENEMIES {KNIGHT}
const EnemiesScenes = {
    KNIGHT = preload("res://Enemies/Knight.tscn")
}

func load_enemy_scene_for(enemy: ENEMIES):
    match enemy:
        ENEMIES.KNIGHT:
            return EnemiesScenes.KNIGHT

Then I have a EnemyStats resource:

# EnemyStats.gs
extends Resource
class_name EnemyStats

signal enemy_died

@export var max_speed: int
@export var max_health: int
@export var enemy: ResourceStash.ENEMIES
var health: set = set_health

# Here, the values are not being properly set 
func _init(max_speed = 15, max_health = 1):
    max_speed = max_speed
    max_health = max_health
    health = max_health

func set_health(value):
    health = clamp(value, 0, max_health)
    if health == 0:
        enemy_died.emit()

And finally, I can define my "generic" Resource that would allow me to build my waves dynamically:

## Waves.gs
extends Resource
class_name Waves

@export var waves: Array[Wave]

    #########
## Wave.gs
extends Resource
class_name Wave

@export var level: String
# Maybe later I could use a dictionary to provide more metadata about the waves
@export var enemies: Array[EnemyStats]

func _init(level = 0, enemies = []):
    level = level
    enemies = enemies

# .....
func _on_has_waved_cleared():
pass

    #########
## WaveSpawner.gd
extends Node2D

var Waves = preload("res://Waves/Waves.tres")

@onready var Spawner = $Spawner

func _ready():
    for enemy in Waves.waves[0].enemies:
        var enemy_scene = ResourceStash.load_enemy_scene_for(enemy.enemy)
        var main = get_tree().current_scene
        var instance = enemy_scene.instantiate()
        instance.stats = enemy
        main.add_child(instance, true)
        instance.global_position = position
        await get_tree().create_timer(randf_range(0.5, 1.5)).timeout

But I still do not understand:

  • Why is my WaveSpawner when I do instance.stats = enemy does not start my instance with the custom data
  • Does this approach seems reasonable? + Am I doing something wrong?

Thank you so much for your attention

Godot version v4.0.rc2.official [d2699dc7a]
in Engine by (19 points)
edited by

Could you please explain what a "wave spammer" is?

Hello Ertain,
Sorry for not providing details + I was also using the wrong word... I meant an "Enemy Spawner".

I tried to add details to the description.

But in general, I'm building a game similar to
https://js13kgames.com/entries/norman-the-necromancer

The idea is to have defined a resource that would allow me to specify my enemy waves.
For example:

  • Wave 1

    • 4 Knight enemies
  • Wave 2

    • 4 Knight enemies
    • 1 Archer enemy
  • Wave 3

    • 4 Knight enemies + They all have 1 extra health point
    • 1 Boss

Then based on that config I can create a class that would spam that + takes if the wave is done or not.

1 Answer

0 votes
Best answer

I think now I have managed to make what I wanted.
Now I need to make improvements.

Most of my issues were due to the following:

  • Instantiating resources in the wrong lifecycle callbacks
  • Bugs in Godot 4 Engine, sometimes you have to restart the engine after setting up a .tres file

Here is the code draft with the idea:

### RESOURCES

## Waves.gd
extends Resource
class_name Waves

@export var waves: Array[Wave]

## Wave.gd
extends Resource
class_name Wave

@export var level: String
@export var enemies: Array[EnemyStats]


## EnemyStats.gd
extends Resource
class_name EnemyStats

signal enemy_died

@export var max_speed: int
@export var max_health: int:
    set(value):
        max_health = value
        health = max_health
@export var enemy: ResourceStash.ENEMIES
var health: set = set_health

func set_health(value):
    health = clamp(value, 0, max_health)
    if health == 0:
        enemy_died.emit()

##### SCENES

## WaveSpawner.gd
extends Node2D

var Waves = preload("res://Waves/Waves.tres")

@onready var Spawner = $Spawner

func spawn_enemies():
    # TODO: work with signals to automatically handle each wave
    for enemy in Waves.waves[0].enemies:
        var enemy_scene = ResourceStash.load_enemy_scene_for(enemy.enemy)
        var instance = enemy_scene.instantiate()
        instance.stats = enemy.duplicate()
        get_tree().current_scene.add_child(instance, true)
        instance.global_position = Spawner.global_position
        await get_tree().create_timer(randf_range(0.5, 1.5)).timeout

## Enemy.gd
extends CharacterBody2D

@export var stats: EnemyStats:
    set(value):
        stats = value
        if not stats is EnemyStats: return

func _ready():
    self.stats = stats.duplicate()
    stats.enemy_died.connect(_on_EnemyStats_enemy_died)

func _on_Hurtbox_hit(damage):
    stats.health -= damage

func _on_EnemyStats_enemy_died():
    Utils.instance_scene_on_main(EnemyDeathEffect, global_position)
    queue_free()

## World.gd
extends Node2D

@onready var WaveSpawner := $WaveSpawner

func _ready():
    WaveSpawner.spawn_enemies()

With this definition, I can define each wave the way I want directly in a .tres file.
It is possible to provide as many levels as I need + customize my enemies.

Waves.tres

Then in my main scene, I can use my WaveSpawner scene to automatically handle each enemy wave.

Main scene

I still didn't implement the features that will detect if the wave is done or not, but I hope this draft can show my general idea.

by (19 points)
edited by

This looks very ellegant, but I am not sure if setting it up in export Array menu is comfortable. I would propably try to compose it with whole Nodes, so I could define waves by childing nodes and setting their variables. I believe it is more visual and doesn't require to dig through nested submenus of exported arrays and dicitonaries.

Wow, thank you so much for this suggestion!

It's my first time diving into game development (as a Hobby), and coming from a different area I ended up thinking of something way more complex than what it should have been.

I've just played around and definitely, this is waaay easier right now.

I've removed the @export var enemy: ResourceStash.ENEMIES and kept my EnemyStats (since I will evolve this file and create more configs for enemies later)

# EnemyStats.gs extends Resource
class_name EnemyStats

signal enemy_died

@export var max_speed: int
@export var max_health: int:
    set(value):
        max_health = value
        health = max_health

var health: int:
    set(value):
        health = clamp(value, 0, max_health)
        if health == 0:
            enemy_died.emit()

Removed aaalll my resources.
Created a scene for each wave:

Wave1 scene

Then I just load the scene I want to spawn

# WaveSpawner.gd
extends Node2D

@onready var Spawner = $Spawner

func spawn_enemies(scene_path):
    var scene = load(scene_path)
    Utils.instance_scene_on_main(scene, Spawner.global_position)

# World.gd
extends Node2D

@onready var WaveSpawner := $WaveSpawner

func _ready():
    RenderingServer.set_default_clear_color(Color.BLACK)
    # TODO: Transform this to work with signals/load dynamically the waves
    WaveSpawner.spawn_enemies("res://Waves/Wave1.tscn")

What do you think about that?

That is exactly what I was thinking about :).
Lately I noticed that Resources are being some hot topic in tutorials, but in fact they are very overrated in my opinion

Welcome to Godot Engine Q&A, where you can ask questions and receive answers from other members of the community.

Please make sure to read Frequently asked questions and How to use this Q&A? before posting your first questions.
Social login is currently unavailable. If you've previously logged in with a Facebook or GitHub account, use the I forgot my password link in the login box to set a password for your account. If you still can't access your account, send an email to [email protected] with your username.