Skip to content

M4.1 — The Enemy Actor

What you'll learn

  • Assemble an enemy from the same components as the player (Hurtbox, Health, StateMachine).
  • Mirror the layer/mask config so the enemy hits the player and is hit, but never itself.
  • Fan Health.died out to SignalBus.enemy_died, and derive enemies via scene inheritance.

How it applies

  • An ARPG is mostly enemies. The genre lives or dies on having many varied enemies cheaply. If each enemy is built from scratch, you get three; if each is a small configuration of shared components, you get thirty. This chapter is where the reuse investment from M2–M3 pays its first dividend.
  • The death event is the loop's hinge. "Enemy died" is what triggers loot, XP, and progress. Routing it through the SignalBus means the enemy announces its death once and every interested system — present or added later — hears it, without the enemy holding references to any of them.
  • Correct layers keep combat fair. The enemy must take the player's hits, deal its own, and never hit itself or (usually) other enemies. That is entirely a layer/mask configuration; getting it right here is what makes the fight read correctly.
  • Scene inheritance is the variation engine. A base enemy scene with shared structure, inherited by per-enemy scenes that swap the sprite, stats, and behavior tuning, is how you produce a roster without duplicating wiring — and a fix to the base propagates to every enemy.

Concepts

The enemy is the player, minus input

Look at what the player became by end of M3: a CharacterBody2D with a CollisionShape2D, an AnimatedSprite2D, a Hurtbox, a Health, an attack Hitbox, and a StateMachine of states. An enemy is the same assembly with two differences:

  1. Its states read the world (where is the player? am I in range?) instead of the keyboard.
  2. Its layer/mask bits are mirrored (it is on the enemy side of the matrix).

Everything else — the body, the components, the FSM scaffolding — is identical and reused. You are not writing a new combat system; you are configuring the one you have for an actor whose brain is AI instead of a human.

Layer/mask for the enemy

Using the M3.1 layer names:

  • Enemy body: on enemy (layer 3); mask world (collides with walls). It need not collide with the player body unless you want bodies to block each other (often you do — set its mask to include player too, and the player's mask to include enemy, if pushing should happen).
  • Enemy Hurtbox: on enemy_hurtbox (layer 5); mask includes player_hitbox (6) so the player's attack is detected (the hurtbox is the detector, M3.1).
  • Enemy attack Hitbox: on enemy_hitbox (layer 7); the player's Hurtbox masks layer 7, so the enemy's swing is detected by the player.

The asymmetry guarantees the player's hitbox hits enemy hurtboxes and the enemy's hitbox hits the player's hurtbox, while neither side's hitbox touches its own hurtbox (different layers, masks don't include own side).

Death fans out to the bus

When an enemy's Health emits died (M3.2), the enemy turns that into the global event other systems want:

Example

The base enemy script connects its own Health's died to a handler that announces the death globally, then removes itself:

# res://scripts/enemy.gd
class_name Enemy
extends CharacterBody2D

@onready var health: Health = $Health
@onready var hurtbox: Hurtbox = $Hurtbox

func _ready() -> void:
    hurtbox.hurt.connect(_on_hurt)
    health.died.connect(_on_died)

func _on_hurt(amount: int, source_position: Vector2) -> void:
    health.take_damage(amount)
    _flash()
    apply_knockback(source_position, 200.0)   # from M3.4

func _on_died() -> void:
    SignalBus.enemy_died.emit(self, global_position)
    queue_free()

The enemy emits enemy_died with itself and where it died, then queue_free()s. The loot system (M6) will connect to SignalBus.enemy_died and spawn a drop at at_position; the XP system (M5) will award based on the enemy. The enemy references neither — it just announces.

Scene inheritance for variation

Build one base enemy scene (enemy.tscn) with the full assembly and the enemy.gd script. Then make each concrete enemy an inherited scene: Scene → New Inherited Scene → enemy.tscn, saved as enemy_skeleton.tscn. The inherited scene keeps the structure and wiring, and you override only what differs — the sprite's SpriteFrames, the Health.max_health, movement speed, attack damage, and the concrete AI states. A change to the base (say, a fix in enemy.gd) propagates to every inherited enemy.

This mirrors how the player was a single scene; enemies are a family descending from one base, which is the cheapest path to a varied roster.

Walkthrough

  1. Build the base enemy. Scene → New Scene → CharacterBody2D, rename Enemy. Add children: CollisionShape2D (body footprint), AnimatedSprite2D named Sprite, and instances of hurtbox.tscn and health.tscn (M3) as Hurtbox and Health. Add a Node named StateMachine with state_machine.gd (M2.4) — its states come in M4.2.
  2. Attach res://scripts/enemy.gd (the class_name Enemy body above) to the Enemy root.
  3. Set layers/masks in the Inspector: Enemy body on enemy (3), mask world (1) (+player if bodies should block); Hurtbox on enemy_hurtbox (5), mask player_hitbox (6); (the enemy's attack Hitbox comes with its attack state in M4.2, on enemy_hitbox (7)).
  4. Set Health.max_health on the base to a modest value (e.g., 8). Save as res://scenes/actor/enemy.tscn.
  5. Make a concrete enemy: Scene → New Inherited Scene → enemy.tscn. Set the Sprite's SpriteFrames to skeleton art (or placeholder), adjust Health.max_health if desired. Save as res://scenes/actor/enemy_skeleton.tscn.
  6. Drop a skeleton into the world: open main.tscn, drag enemy_skeleton.tscn onto World, position it near the player's start.
  7. Press F5 and attack the skeleton with the player's M3.3 attack. It should flash, take knockback, lose HP, and — when HP hits 0 — disappear (queue_free). Add a temporary SignalBus.enemy_died.connect(func(e, pos): print("died at ", pos)) in GameState._ready to confirm the death event fires with the correct position. Remove the temp listener after.

Optional sanity check

Attack the skeleton and watch for two failure signatures of a wrong layer/mask: if hits do nothing, the enemy Hurtbox's mask is missing player_hitbox (the M3.1 bug); if the player damages itself when attacking, the player's hitbox layer or the player hurtbox's mask is misconfigured to overlap its own side. Fix the bits, not the code — the scripts are correct.

Self-check quiz

Q1 — What makes building the enemy fast, given the work in M2–M3?

A. Enemies use a simpler physics body than the player. B. The enemy reuses the same component scenes (Hurtbox, Health, StateMachine) and the same body type; only its states (AI vs input) and layer/mask side differ. C. Godot generates enemies from the player scene automatically. D. Enemies don't need collision shapes.

Reveal answer

B. The architecture was built for this: the enemy is the player's assembly with AI states instead of input states and mirrored layer bits. Nothing about the combat system is rebuilt. A is false (same CharacterBody2D). C is false (you inherit a scene you authored, not auto-generated). D is false (it needs a footprint to collide with walls and a hurtbox to be hit).

Q2 — Why does the enemy emit SignalBus.enemy_died instead of calling the loot and XP systems directly?

A. Direct calls are slower. B. So the enemy stays ignorant of which systems care about its death; loot, XP, counters, and sound subscribe to the bus independently, and new listeners can be added without changing the enemy. C. Because the enemy is freed before loot can be called directly. D. Because SignalBus is the only way to pass a position.

Reveal answer

B. The bus decouples the death event from its consumers: the enemy announces once, any number of systems react, and adding the M6 loot system later requires zero change to the enemy. A is not the point. C is a real concern (it queue_frees) but the bus's value is decoupling, not timing — the signal is emitted before freeing regardless. D is false (you could pass a position in a direct call; the bus's benefit is the decoupling).

Q3 — Why build concrete enemies as inherited scenes of a base enemy.tscn?

A. Inherited scenes run faster. B. They keep the shared structure and wiring while overriding only what differs (sprite, stats, states), so a roster is cheap and a base fix propagates to all of them. C. Godot requires enemies to be inherited scenes. D. To avoid using components.

Reveal answer

B. Scene inheritance is the variation engine: one base holds the assembly, each enemy overrides the few things that make it distinct, and fixing the base fixes every descendant. A is false. C is false (it's a choice). D is backwards — inherited enemies use the components; inheritance and components compose.

Integration question

Q4 — open

The enemy and the player share the Hurtbox, Health, and StateMachine scenes, yet one is driven by a keyboard and the other by AI, and attacking one must not damage the other's owner-self. Explain the two axes along which the enemy differs from the player (behavior and the layer/mask matrix), and describe precisely what would go wrong if you copied the player's hurtbox layer/mask onto the enemy verbatim instead of mirroring it.

Reveal expected answer

The enemy differs from the player along exactly two axes. Behavior: the FSM states read world state (player position, detection, range) instead of Input, so the same StateMachine/State scaffolding runs an AI brain rather than a controller. Layer/mask side: the enemy sits on the enemy side of the matrix — body on enemy, hurtbox on enemy_hurtbox masking player_hitbox, attack hitbox on enemy_hitbox — the mirror of the player's player/player_hurtbox/ player_hitbox. If you copied the player's hurtbox config onto the enemy verbatim, the enemy's hurtbox would be on player_hurtbox and mask enemy_hitbox: the player's attack hitbox (on player_hitbox) would no longer be in the enemy hurtbox's mask, so player attacks would stop registering on enemies, while the enemy's hurtbox would now react to enemy hitboxes — meaning enemies could damage each other or an enemy could be hit by its own attack. The components are identical; correctness lives entirely in mirroring the matrix, which is why the layer names from M3.1 exist.