Lesson 06

Enemy Patrols

An enemy that walks a fixed route, switching to the next spot on its list each time it arrives.

Loading the game engine. This takes a moment the first time.

Perspective

Each enemy in the level is a copy of enemy.tscn, so all of them share this one script.

HOW PATROLLING WORKS

The enemy keeps a numbered list of spots. It walks toward spot 0, and once it gets close it switches to spot 1, then spot 2, then back to 0. Wrapping back around to 0 is what makes it loop forever.

New code in this lesson

None of the earlier lessons use these. Each one shows the line where it appears here.

[ ]

A list of values in square brackets. Counting starts at 0, so list[0] is the first item.

enemy.gd
@export var patrol_points: PackedVector2Array = PackedVector2Array([

PackedVector2Array

A list that holds only Vector2 values, like the spots on a patrol route.

enemy.gd
@export var patrol_points: PackedVector2Array = PackedVector2Array([

Vector2(x, y)

Makes a Vector2 from two numbers, like Vector2(100, 50).

enemy.gd
Vector2(-100, 0), Vector2(100, 0)

is_empty()

True when a list has nothing in it.

enemy.gd
if patrol_points.is_empty():

%

The remainder after dividing: 7 % 3 is 1. Handy for making a number wrap back around to 0.

enemy.gd
_current_index = (_current_index + 1) % patrol_points.size()

size()

How many items are in a list.

enemy.gd
_current_index = (_current_index + 1) % patrol_points.size()

The scenes

■ UI node · ■ 2D node · % unique name · .tscn instanced scene

game.tscn
  • Game Node2D
  • Player CharacterBody2D player.tscn
  • Enemy1 CharacterBody2D enemy.tscn
  • Enemy2 CharacterBody2D enemy.tscn
  • Enemy3 CharacterBody2D enemy.tscn
enemy.tscn
  • Enemy CharacterBody2D enemy.gd
  • Polygon2D
  • CollisionShape2D
player.tscn
  • Player CharacterBody2D player.gd
  • Sprite2D
  • CollisionShape2D

FileSystem

  • game.tscn
  • enemy.tscn
  • enemy.gd
  • player.tscn
  • player.gd

The code

Perspective
extends CharacterBody2D

## How fast the enemy walks.
@export var speed: float = 100.0
## The list of spots to walk between, measured from where the enemy starts.
## Vector2(-100, 0) means "100 pixels left of my start spot", and
## Vector2(100, 0) means "100 pixels right of my start spot".
@export var patrol_points: PackedVector2Array = PackedVector2Array([
	Vector2(-100, 0), Vector2(100, 0)
])

# Which slot of the list we are walking toward. Lists start at 0.
var _current_index: int = 0
# Where this enemy was standing when the level began.
var _origin: Vector2

func _ready() -> void:
	# Remember our starting spot. Every patrol point is measured from here.
	_origin = global_position

# "_physics_process()" runs about 60 times a second. Godot calls it for you.
func _physics_process(_delta: float) -> void:
	# An empty list means there is nowhere to walk. "return" leaves early.
	if patrol_points.is_empty():
		return

	# Add the patrol point onto our starting spot to get the real target.
	var target_pos: Vector2 = _origin + patrol_points[_current_index]
	# An arrow pointing from us to that spot, shrunk down to length 1.
	var direction: Vector2 = (target_pos - global_position).normalized()
	velocity = direction * speed
	move_and_slide()

	# Are we basically there? 5 pixels counts as close enough. We cannot
	# check for exactly 0, because we move in jumps and would sail past it.
	if global_position.distance_to(target_pos) < 5.0:
		# Switch to the next slot in the list.
		# The "%" wraps back around to 0 when we run off the end, so the
		# enemy loops forever instead of crashing. With 2 spots it counts
		# 0, 1, 0, 1, 0, 1 and never stops.
		_current_index = (_current_index + 1) % patrol_points.size()