Introducción al Desarrollo de Videojuegos con GameMaker · Module 7: Enemies, Basic AI, and Gameplay Mechanics

Designing and Programming a Basic Patrol Enemy

Lesson 31 of 60 · 55 min

Also available in: ES

Introduction to Autonomous Behavior in 2D Games

Up to this point in the course, gameplay in our platformer project has been driven primarily by player input: the player presses a key or pushes a stick, and the character responds on screen. However, for a game world to feel alive, challenging, and dynamic, it is essential to introduce entities whose movements follow internal logical rules independent of player controls. This set of predefined behaviors is what we call basic Artificial Intelligence (AI) or behavior scripting in game development.

The fundamental AI archetype in 2D platformers is the patrol enemy. From the iconic Goombas in Super Mario Bros. to the Metools in Mega Man or wandering sentries in Hollow Knight, the constant horizontal pacing from left to right—rebounding off walls or turning at ledges—is a timeless design convention. In this lesson, we will build a patrol enemy from scratch that traverses the level autonomously, interacts with existing terrain (`obj_wall`), and reverses direction upon encountering a solid obstacle.

Asset Preparation: Sprite and Collision Mask

Before writing a single line of code, we must prepare the graphical asset for our enemy and properly configure its collision geometry in GameMaker. Precise physical collisions and visual rendering depend directly on how the sprite origin and mask are defined.

Start by creating a new Sprite asset in the Asset Browser (right-click the Sprites folder -> Create -> Sprite) and name it canonically `spr_enemy_patrol`. To maintain visual consistency with our retro character, use a resolution of 16x16 or 32x32 pixels. If you do not yet have a finished walk cycle, draw a simple stylized block or creature with a distinct color (such as red or purple) featuring two visible eyes pointing clearly to the right.

💡 Golden rule of orientation: By universal convention in 2D game development, all side-scrolling sprites with horizontal facing should be drawn facing RIGHT by default. This simplifies mathematical calculations, as the positive horizontal axis (+X) directly corresponds to a positive image scale.

There are two critical settings to configure inside the `spr_enemy_patrol` sprite editor:

PropertyRecommended SettingTechnical Justification
Origin PointBottom Centre (or Middle Centre)When flipping the sprite horizontally via `image_xscale`, the pivot point must sit exactly at the horizontal center. Placing it at Top Left (0,0) would cause the enemy to abruptly snap backward by several pixels when turning around.
Collision MaskRectangle / Manual or Full ImageMust be perfectly symmetrical. An asymmetrical mask will cause the enemy to get stuck inside a wall the exact instant its orientation flips.
Interpolation / SizeMatching the player (e.g., 32x32 px)Ensures level gaps and tunnels designed for the player work consistently with the enemy's dimensions.

Once the sprite is created and configured, create the corresponding object. Right-click the Objects folder, select Create -> Object, name it `obj_enemy_patrol`, and assign the `spr_enemy_patrol` sprite. There is no need to check GameMaker's built-in 'Solid' box; we will manage all physical interactions through explicit GML Code to guarantee precise control free of engine quirks.

Variable Structure in the Create Event

The enemy must track its kinematic state in memory: its walking speed, the direction it is currently moving, its vertical velocity, and the gravitational force keeping it grounded. All of these variables must be initialized in the **Create** event of `obj_enemy_patrol`.

Open `obj_enemy_patrol`, add the **Create** event, and enter the following GML code block:

/// @description Patrol enemy initialization

// Horizontal movement parameters
move_speed = 1.5;         // Movement speed in pixels per frame
move_dir = 1;             // Direction: 1 = Right, -1 = Left
hsp = 0;                  // Calculated horizontal speed (custom hspeed)

// Vertical movement and gravity parameters
vsp = 0;                  // Current vertical speed
grav = 0.3;               // Downward gravitational acceleration
vsp_max = 8;              // Terminal fall speed limit

// Initial visual facing
image_xscale = move_dir;

Let us examine the logic behind these variables. Instead of using GameMaker's built-in `hspeed` and `vspeed`—which automatically translate the instance internally before we can perform precise collision checks—we declare custom variables `hsp` and `vsp`. This technique, introduced when programming the player character in previous modules, lets us calculate exactly where the entity wants to go and verify that the space is clear before modifying the actual `x` and `y` coordinates.

The `move_dir` variable acts as a 1D unit vector. It will only take two values in this implementation: `1` (moving right) or `-1` (moving left). Multiplying `move_dir` by `move_speed` directly yields the exact velocity and direction without requiring cumbersome nested conditional statements.

Schematic diagram illustrating 2D patrol logic: horizontal speed calculation, forward collision prediction vector with place_meeting, and turnaround upon wall detection.
Predictive patrol mechanic: the enemy checks for collisions prior to moving; upon detecting a solid, it inverts its direction vector. · AI-generated image

Movement and Collision Logic in the Step Event

The **Step** event runs once every frame (typically 60 times per second). Here we process basic physics, evaluate collisions against solid geometry (`obj_wall`), and reverse the movement direction whenever the enemy collides head-on with an obstacle.

Add the **Step -> Step** event to `obj_enemy_patrol` and insert the following code structured into distinct phases: applying forces, collision resolution, and coordinate updates.

/// @description Patrol logic and physics collisions

// =========================================================================
// 1. APPLY GRAVITY
// =========================================================================
if (vsp < vsp_max) {
    vsp += grav;
}

// =========================================================================
// 2. HORIZONTAL SPEED CALCULATION
// =========================================================================
hsp = move_dir * move_speed;

// =========================================================================
// 3. HORIZONTAL COLLISION DETECTION & RESOLUTION (WALLS)
// =========================================================================
// Check if there is a solid object at the target position
if (place_meeting(x + hsp, y, obj_wall)) {
    // Move pixel by pixel until flush against the obstacle
    while (!place_meeting(x + sign(hsp), y, obj_wall)) {
        x += sign(hsp);
    }
    
    // Halt movement for this frame
    hsp = 0;
    
    // Reverse direction for subsequent frames
    move_dir = -move_dir;
}

// Apply horizontal displacement to X axis
x += hsp;

// =========================================================================
// 4. VERTICAL COLLISION DETECTION & RESOLUTION (FLOORS AND CEILINGS)
// =========================================================================
if (place_meeting(x, y + vsp, obj_wall)) {
    // Move pixel by pixel vertically toward the surface
    while (!place_meeting(x, y + sign(vsp), obj_wall)) {
        y += sign(vsp);
    }
    
    // Reset vertical speed upon landing or hitting ceiling
    vsp = 0;
}

// Apply vertical displacement to Y axis
y += vsp;

// =========================================================================
// 5. VISUAL FACING (SPRITE FLIPPING)
// =========================================================================
if (move_dir != 0) {
    image_xscale = move_dir;
}

Mathematical Breakdown of Direction Inversion

Pay close attention to the line `move_dir = -move_dir;`. This concise mathematical operation eliminates the need for branching `if/else` checks. If `move_dir` is `1`, `-1 * 1` evaluates to `-1`. On the next flip, `-(-1)` evaluates to `1`. In a single line, we toggle the heading indefinitely whenever a wall is hit.

The `sign(hsp)` function returns `1` for positive values, `-1` for negative values, and `0` for zero. Paired with the `while (!place_meeting(x + sign(hsp), y, obj_wall))` loop, it guarantees a pixel-perfect contact point. The enemy will never float 0.5 pixels away from a wall or embed itself inside solid geometry before turning around, preventing visual jitter and clipping.

The place_meeting Function vs. Alternatives

GameMaker provides three core functions for checking object overlap: `place_meeting`, `position_meeting`, and `instance_place`. Understanding their differences is crucial for robust AI design:

FunctionWhat It ChecksReturn ValuePrimary Use in This Context
place_meeting(x, y, obj)The full collision mask of the calling instance positioned at (x, y) against masks of 'obj'.Boolean (true / false)Determining whether the enemy's entire body will physically collide with a wall or floor.
position_meeting(x, y, obj)A single infinitesimal point (x, y) in room space against masks of 'obj'.Boolean (true / false)Point sensors, such as ground probes placed ahead of the enemy.
instance_place(x, y, obj)The full mask of the calling instance at (x, y) against masks of 'obj'.Instance ID (or noone)Required when you need to know exactly which instance was hit to read or modify its variables.

Fall Prevention: Ledge Detection

With the code implemented so far, the enemy will reliably patrol from wall to wall. However, if we place it on an elevated, free-floating platform with no enclosing walls, it will walk straight off the edge and fall under gravity until reaching the bottom of the room.

In level design, we frequently want certain patrol enemies to remain confined to their assigned platform, guarding a ledge without dropping off. To achieve this, we equip our enemy with a forward ground probe.

The logic works by projecting a virtual check slightly ahead of the enemy and a couple of pixels below its feet. If that projected point lacks solid ground (`!position_meeting` or `!place_meeting`), the enemy knows an edge is ahead and can immediately halt and reverse direction, just as if it had struck a wall.

This check must run before `x += hsp` is applied, allowing the enemy to respond to the ledge before taking the step that would send it falling into the abyss.

Complete and Refined Code for obj_enemy_patrol

Below is the complete, modular integration of both systems ready to be implemented across your project's levels.

Create Event for obj_enemy_patrol

/// @description Initial patrol setup

// Movement constants and variables
move_speed = 1.5;            // Constant patrol speed
move_dir = 1;                // 1 = Right, -1 = Left
hsp = 0;                     // Current horizontal speed

// Gravity variables
vsp = 0;                     // Current vertical speed
grav = 0.35;                 // Gravitational acceleration
vsp_max = 9;                 // Terminal fall speed

// Platform ledge behavior
avoid_ledges = true;         // true: stay on ledge; false: fall off

// Initial visual orientation
image_xscale = move_dir;

Step Event for obj_enemy_patrol

/// @description Patrol routine, collisions, and sensors

// 1. Gravity
if (vsp < vsp_max) {
    vsp += grav;
}

// 2. Ledge / cliff check (if enabled)
var _is_grounded = place_meeting(x, y + 1, obj_wall);

if (avoid_ledges && _is_grounded) {
    // Project sensor ahead of the enemy's base in the current direction
    var _probe_x = x + (move_dir * (abs(sprite_width) * 0.5));
    var _probe_y = bbox_bottom + 2;
    
    // If there is no solid ground below the probe point, turn around
    if (!position_meeting(_probe_x, _probe_y, obj_wall)) {
        move_dir = -move_dir;
    }
}

// 3. Target horizontal speed
hsp = move_dir * move_speed;

// 4. Collision and turnaround against solid walls
if (place_meeting(x + hsp, y, obj_wall)) {
    while (!place_meeting(x + sign(hsp), y, obj_wall)) {
        x += sign(hsp);
    }
    hsp = 0;
    move_dir = -move_dir;
}
x += hsp;

// 5. Vertical collision with floor or ceiling
if (place_meeting(x, y + vsp, obj_wall)) {
    while (!place_meeting(x, y + sign(vsp), obj_wall)) {
        y += sign(vsp);
    }
    vsp = 0;
}
y += vsp;

// 6. Flip sprite to match movement direction
if (move_dir != 0) {
    image_xscale = move_dir;
}

Placement and Testing in the Room

To verify that our enemy behaves as intended, open one of the test rooms created in earlier lessons (such as `rm_level_1` or a dedicated sandbox like `rm_test_mechanics`).

Follow these steps inside the Room Editor:

1. Select the Instances layer (`Instances`) in the Layers panel on the left side of the screen. Remember that gameplay logic objects must never be placed on Tilemap or Background layers.

2. Drag an instance of `obj_enemy_patrol` into an enclosed corridor between two solid columns of `obj_wall` blocks.

3. Drag a second instance of `obj_enemy_patrol` onto a raised floating platform with open sides to test the ledge sensor.

4. Launch the game by pressing F5 (or clicking the Run button in the top toolbar). Observe how both instances behave:

The enemy in the corridor should move steadily to the right, stop cleanly against the wall, immediately flip its sprite to face left, and walk back to the opposite wall in a continuous loop. Meanwhile, the enemy on the elevated platform should pace to the edge, detect the drop, and turn around before falling.

💡 Quick Debugging: If you notice the enemy vibrating or getting stuck indefinitely inside a wall, check two things immediately: 1) Ensure the origin point of `spr_enemy_patrol` is horizontally centered at 'Bottom Centre'. 2) Ensure the collision mask is set to a perfectly symmetrical rectangle rather than 'Precise per-frame' detection.

Practical Exercises

Complete the following activities to reinforce your understanding of patrol logic and instance variable overrides in GameMaker.

Exercise 1: Individual Speed Variation via Variable Definitions

**Task:** Rather than hardcoding all enemies to the same speed (`move_speed = 1.5;`), open the `obj_enemy_patrol` editor and click on the 'Variable Definitions' tab. Add a variable named `move_speed` with a default value of `1.5` and type Real. Remove the direct assignment of `move_speed` in the Create event code so it does not overwrite this definition. Next, in the Room Editor, double-click two different enemy instances and customize their individual speeds (for example, set a slow enemy to `0.8` and a fast one to `3.0`).

**Verification:** When pressing F5, both enemies should patrol at clearly different speeds within the same room, demonstrating the power of per-instance variable overrides without duplicating object assets.

Exercise 2: Invisible Patrol Markers

**Task:** Create a new object called `obj_marker_patrol`. Assign it a bright, semi-transparent sprite (such as a 16x16 px yellow or cyan marker) and uncheck the object's 'Visible' box (or leave it visible for debugging). Update the enemy's Step event so that, in addition to colliding with `obj_wall`, it also turns around when colliding with `obj_marker_patrol`. Place two markers along an open hallway and place an enemy between them.

**Verification:** The enemy should patrol back and forth between the markers in open space without requiring solid walls, giving you flexible control over patrol boundaries during level design.

Exercise 3: Turn Delay Pause State

**Task:** Realistic or mechanical enemies often pause briefly before reversing course rather than snapping around instantly. Using an alarm (`alarm[0]`) or a manual timer variable (`turn_timer`), program the enemy so that upon reaching a wall or ledge, its horizontal speed drops to `0` for 30 frames (half a second at 60 FPS) before resuming its patrol in the opposite direction.

**Verification:** When encountering an obstacle, the enemy comes to a complete stop for half a second, faces the opposite direction, and resumes walking at standard speed.

Summary

  • A basic patrol enemy relies on an autonomous horizontal velocity vector combined with continuous gravity applied each frame.
  • GameMaker convention requires side-facing sprites to be drawn facing right with a horizontally centered origin (Bottom Centre) to allow clean flipping via image_xscale without offset jitter.
  • Direction reversal is efficiently achieved using move_dir = -move_dir upon detecting collisions with place_meeting.
  • A pixel-perfect while loop paired with sign(hsp) ensures instances move flush against solid boundaries without clipping into obstacles.
  • Ledge detection anticipates drops by using forward virtual sensors with position_meeting ahead of the sprite's base, keeping patrol enemies confined to floating platforms.

Practise what you learned

Exercise 1 · 1 point

Why does the lesson implement movement using custom variables like `hsp` and `vsp` instead of GameMaker's built-in `hspeed` and `vspeed` variables?

You are practising without an account: nothing is saved. Sign up to keep your progress.

Exercise 2 · 1 point

Order chronologically the actions performed by the Step event code to detect and resolve the patrol enemy's horizontal collision with a wall (`obj_wall`):

  1. 1Reverse movement direction for upcoming frames using `move_dir = -move_dir`.
  2. 2Check with `place_meeting(x + hsp, y, obj_wall)` whether there will be a collision at the future position.
  3. 3Apply the resulting horizontal displacement by adding `x += hsp`.
  4. 4Advance pixel by pixel until flush against the wall using `while (!place_meeting(x + sign(hsp), y, obj_wall))`.
  5. 5Stop the current frame's horizontal speed by assigning `hsp = 0`.
You are practising without an account: nothing is saved. Sign up to keep your progress.

Exercise 3 · 2 points

Complete the GML Step event code to: 1) Calculate the horizontal speed `hsp` by multiplying `move_dir` by `move_speed`. 2) Mathematically reverse `move_dir` using the formula taught in the lesson (without using if/else conditionals).

Your code is not executed: specific requirements are checked on the text.

You are practising without an account: nothing is saved. Sign up to keep your progress.

Sign in to save your progress — Sign in