Build new enemies on a timer while the game is already running, instead of placing them by hand in the editor.
Loading the game engine. This takes a moment the first time.
Every lesson before this one used nodes you placed by hand in Godot. This one builds brand new nodes while the game is already running.
None of the earlier lessons use these. Each one shows the line where it appears here.
A random whole number. randi() % 4 turns it into 0, 1, 2 or 3.
var side: int = randi() % 4Compares one value against a list of options and runs only the block under the one that fits. A tidier row of ifs.
match side:A random decimal between two numbers.
0: enemy.global_position = Vector2(randf_range(50, 1100), 10)The signal a node sends just before it leaves the scene, for example when it is freed.
enemy.tree_exiting.connect(_on_enemy_removed)Joins two conditions. At least one must be true.
if global_position.x < -50 or global_position.x > 1200:■ UI node · ■ 2D node · % unique name · .tscn instanced scene
extends Node2D
## Which scene to copy each time we spawn. Set to enemy.tscn in Godot.
@export var enemy_scene: PackedScene
## How many seconds to wait between spawns.
@export var spawn_interval: float = 1.5
## The most enemies allowed to be alive at the same time.
@export var max_enemies: int = 10
# How many enemies are alive right now.
var _enemy_count: int = 0
@onready var spawn_timer: Timer = %SpawnTimer
@onready var player: Player = %Player
func _ready() -> void:
spawn_timer.wait_time = spawn_interval
# "timeout" is a signal every Timer sends when it reaches zero. We put
# "_on_spawn_timer_timeout" inside ".connect()". That is the function
# Godot runs each time the timer finishes.
spawn_timer.timeout.connect(_on_spawn_timer_timeout)
# A Timer sits still until you start it.
spawn_timer.start()
func _on_spawn_timer_timeout() -> void:
# Already at the limit, so skip this one. "return" leaves early.
if _enemy_count >= max_enemies:
return
# Nobody picked a scene in Godot, so there is nothing to copy.
if enemy_scene == null:
return
# "instantiate()" makes one real copy of the saved scene. The copy exists
# now, but it is NOT in the game until "add_child()" near the bottom.
var enemy: Enemy = enemy_scene.instantiate()
# Pick one of the four screen edges at random.
# "randi()" gives a random whole number, and "% 4" chops it down to
# 0, 1, 2, or 3. "match" then runs only the line that matches.
var side: int = randi() % 4
match side:
0: enemy.global_position = Vector2(randf_range(50, 1100), 10)
1: enemy.global_position = Vector2(randf_range(50, 1100), 638)
2: enemy.global_position = Vector2(10, randf_range(50, 600))
3: enemy.global_position = Vector2(1140, randf_range(50, 600))
# Aim the new enemy at wherever the player is standing right now.
enemy.direction = (player.global_position - enemy.global_position).normalized()
# "tree_exiting" is a signal a node sends right before it gets erased.
# That is how we find out to subtract one from our count.
enemy.tree_exiting.connect(_on_enemy_removed)
# NOW the enemy is really in the game and starts moving.
add_child(enemy)
_enemy_count += 1
# Runs when any enemy we made is about to be erased.
func _on_enemy_removed() -> void:
_enemy_count -= 1class_name Enemy
extends CharacterBody2D
## How fast this enemy drifts along.
@export var speed: float = 80.0
# Which way to go. game.gd fills this in right after making us.
var direction: Vector2 = Vector2.ZERO
func _physics_process(delta: float) -> void:
# We change "global_position" directly instead of using
# "move_and_slide()". These enemies are meant to drift off the edge of
# the screen in a straight line, not to bump into anything on the way.
global_position += direction * speed * delta
# Once an enemy is off screen nobody can see it, but it would keep
# running forever and slowly clog the game up. "queue_free()" erases it.
if global_position.x < -50 or global_position.x > 1200:
queue_free()
if global_position.y < -50 or global_position.y > 700:
queue_free()class_name Player
extends CharacterBody2D
## How fast the player moves. "@export" puts "speed" at the top of the
## Inspector panel, so you can change it there instead of in this file.
@export var speed: float = 200.0
@onready var sprite: Sprite2D = $Sprite2D
# "_physics_process()" runs about 60 times a second. Godot calls it for you.
func _physics_process(_delta: float) -> void:
# "Input.get_vector(...)" checks all four keys at once and hands back
# an arrow pointing where to go. The four names in quotes are ours.
# See them under Project > Project Settings > Input Map.
var direction: Vector2 = Input.get_vector(
"move_left", "move_right", "move_up", "move_down"
)
# "direction" is only WHICH WAY (its length is always 1).
# "direction * speed" stretches it, so "velocity" is also HOW FAST.
velocity = direction * speed
# Nothing moves until "move_and_slide()" runs.
# "Slide" means we slide along walls instead of sticking to them.
move_and_slide()
# The warrior picture only faces right. Walking left mirrors it.
if direction.x != 0.0:
sprite.flip_h = direction.x < 0.0