Skip to content

Latest commit

 

History

History
274 lines (206 loc) · 8.51 KB

chapter-08.mdx

File metadata and controls

274 lines (206 loc) · 8.51 KB
sidebar_position 8
title Lesson 8 · Creating enemies

Lesson 8 · Creating enemies

Reference checkpoint: node scripts/course-create.mjs --lesson 8 builds this lesson's runnable end state beside your project — compare, never overwrite. See Practice checkpoints.

Nothing got in the way of lighting a beacon. You just walked up and stood there.

This lesson we put in a spirit. It appears from the dark, chases the ninja, and on contact the screen flashes red.

1. The finished screen

2. An enemy is the same structure as the player

spirit.tscn is almost the same as player.tscn.

Spirit  (CharacterBody2D)
  Shadow  (Sprite2D)
  Sprite  (AnimatedSprite2D)   4 directions × 4 frames
  Body    (CollisionShape2D)
  Hitbox  (Area2D)             ← only this is different
  Hover   (AnimationPlayer)

The value of extracting the player as a separate scene in Lesson 5 comes out here. You already know how to make it, so cutting the sprite sheet and attaching it is the end.

Player Spirit
Speed 96 62
Facing stick toward the player
Idle 1px breathing 3px hovering
Layer 2 4

The spirit is slower 96 versus 62. If the spirit is faster there is no way to run, and there is no game. You get chased, but you have to be able to shake them off. When we put in a fast enemy in Lesson 13, this balance is the baseline.

3. Check the official docs

We look at Godot's official Step by step / Creating the enemy together.

Official docs What we take from them
Creating the enemy Enemy as a separate scene · same structure as the player
Physics layers Deciding "who collides with what" with layers

The official sample has mobs cross the screen. Ours chase you.

4. Layers become the rule

This is the most important part.

In Lesson 7 we made the beacon's detect area see only layer value 2. Put the spirit on layer value 4 and the spirit cannot light a beacon.

[node name="Spirit" type="CharacterBody2D"]
collision_layer = 4
collision_mask = 0

Value 4 is inspector checkbox 3 — values double (1, 2, 4, 8), so ticking the fourth box would mean value 8, a layer nothing in this game uses.

We did not ask in code "is this the player?"

# do not do this
func _on_body_entered(body: Node2D) -> void:
	if body.name != "Player":
		return

Filter by name and the condition grows every time enemies grow. Even when there are three enemy types in Lesson 13, the layer setup stays as-is.

Layer What Who sees it
2 Player Beacon Reach · spirit Hitbox
4 Spirit Nobody. Nobody seeing it is the point of this table

5. Build it yourself

5-1. Chase, but do not stick tight

	var wish: Vector2 = (_target.global_position - global_position).normalized()
	velocity = velocity.lerp(wish * speed, TURN_RATE * delta)
	move_and_slide()

Snap with velocity = wish * speed and the spirit sticks tight to the player. When they turn they pivot at radius 0, and there is no way to dodge.

lerp turns them slowly so they take a wide corner, and you can cut and shake them off. Raise TURN_RATE and they get persistent; lower it and they get dull. It is a difficulty knob.

5-2. Who to chase is the arena's decision

func set_target(node: Node2D) -> void:
	_target = node

The spirit does not search the scene with something like get_tree().get_first_node_in_group("player"). The arena puts it in.

Same judgment as making the player not know about beacons in Lesson 7. Put the spirit in another scene and it still runs.

5-3. On contact

const HIT_COOLDOWN: float = 1.1

func _on_body_entered(_body: Node2D) -> void:
	if _cooldown > 0.0:
		return
	_cooldown = HIT_COOLDOWN
	touched_player.emit()

:::danger Without a cooldown you keep getting hit while overlapping body_entered fires once, at the moment of entry. But if the spirit pushes the player and jitters on the boundary, enter/exit repeats.

When health arrives in Lesson 9, that one burst would wipe all three hearts. We put it in now. :::

5-4. Red flash

Lay a red ColorRect over the whole screen on the Ui layer, and drop alpha from 1 to 0.

func _on_player_hit() -> void:
	if _flash != null and _flash.is_valid():
		_flash.kill()
	_hit_flash.modulate.a = 1.0
	_flash = create_tween()
	_flash.tween_property(_hit_flash, "modulate:a", 0.0, HIT_FLASH_SECONDS)

Same as what we learned in Lesson 4 — kill the previous tween before making a new one. Get hit in a row and two tweens pull the same alpha toward different targets.

Always set mouse_filter = 2 (Ignore). Skip it and this rectangle eats touches across the whole screen and the stick does not work.

5-5. Getting knocked back

A flash alone is weak. The body has to be pushed for the hit to reach your hands.

func knock_back(from_position: Vector2) -> void:
	var away: Vector2 = (global_position - from_position).normalized()
	velocity = away * KNOCKBACK_SPEED
	_knock_left = KNOCKBACK_SECONDS

:::warning While being pushed you have to ignore the stick Without _knock_left, the next frame move_toward puts velocity back toward the stick. The hand holding the stick erases the knockback as-is.

We do not read the stick for only 0.18 seconds. Short enough that it does not feel like control was stolen, and the push is clearly visible. Measured, it is 48px. :::

There is still no health Right now it is enough that a hit only looks like a hit. Three hearts and game over are Lessons 9 and 10.

6. Check your work so far

godot --headless --path . --import

(godot is your Lesson 1 GODOT_BIN invocation, quoted when the path has spaces. The import validates that the editor can open the project and that every resource imports: exit code 0 with no ERROR lines in the output. It does not compile your scripts or run gameplay — press Play in the editor for that, then close the window to quit. The reference verifier additionally compiles every script and drives each gameplay check headless.)

  • A spirit chases the ninja
  • It is slower than the ninja (you can shake it off)
  • On contact the screen flashes red
  • Overlapping does not keep hitting you
  • A spirit can walk past a beacon and the beacon does not light
  • With a spirit present, the stick still works

7. Break it on purpose

① Change the spirit's collision_layer to 2 → The spirit lights beacons. Stand still and the game finishes itself. One layer is one rule.

② Change to velocity = wish * speed → The spirit sticks tight and follows. Cut and it still does not come off.

③ Set HIT_COOLDOWN to 0 → It keeps flashing the whole time you overlap. When health arrives in Lesson 9, it is instant death.

④ Set HitFlash's mouse_filter to Stop → The stick does not work. The rectangle covering the whole screen eats touches.

⑤ Skip _knock_left and only set velocity in knock_back() → Hold the stick and you are not pushed. The next frame you go back toward the stick.

⑥ Set spirit speed to 120 → Faster than the ninja, so you cannot run. There is no game.

8. Completion checklist

  • [ ] A spirit chases you, and it is slower than the ninja
  • [ ] On contact the screen flashes red and the ninja is knocked back
  • [ ] While being pushed, you are still pushed even if you hold the stick
  • [ ] Overlapping hits only once per 1.1 seconds
  • [ ] A spirit cannot light a beacon (with a layer, not with code)
  • [ ] With a spirit present, the stick and beacon ignition still work
  • [ ] godot --headless --path . --import finishes with exit code 0 and no errors in its output

9. Exercises

  1. Change TURN_RATE to 1.0 and 20.0. 1.0 takes a wide turn and is easy to shake off; 20.0 sticks like a shadow.

  2. Place one more spirit. Call set_target() once more from the arena. Same as placing three beacons in Lesson 3.

  3. Give the spirit's Sprite light_mask = 0. It does not brighten even beside a beacon. It looks ghostly — pick which side is better.