
Adding an AI Bot to Your Phaser Game — Steering Behaviors Explained
Step-by-step tutorial on building an autonomous game bot using flee, wander, and edge-avoidance steering behaviors in Phaser.js 3 — with AI-generated code you can remix.
Ever watch an AI play your own game? There’s something satisfying about clicking a link and watching a bot dodge enemies better than you ever could. In this tutorial, you’ll learn how to add an autonomous bot to any Phaser game using three steering behaviors: flee, wander, and edge avoidance.
What We’re Building
A bot mode for a Phaser survival game. When you add ?bot=true to the URL, the AI takes over — it dodges enemies by steering away from them, wanders randomly when safe, and avoids getting stuck in corners. The result is a fairly competent autopilot in about 40 lines of logic.
How Steering Behaviors Work
Steering behaviors are a classic game AI technique popularized by Craig Reynolds. Instead of hard-coded paths, the bot calculates a desired direction each frame based on what’s around it. Three forces combine into one movement vector:
| Behavior | When It Activates | Result |
|---|---|---|
| Flee | Enemy is within 130px | Run directly away from nearest enemy |
| Wander | No nearby threats | Move in a random direction, changing every 0.5–1.5s |
| Edge Avoidance | Within 60px of any border | Push back toward center |
Each behavior produces a velocity vector. They blend together by weight, and the result is capped to a max speed. Let’s build each one.
Step 1: The Bot State
Add this to your scene’s create() method right after your player setup:
var botParam = new URLSearchParams(window.location.search).get('bot') || '';
this.botMode = botParam === 'true';
this.botTimer = 0;
this.botDir = { x: 0, y: 0 };
The botTimer controls how often the bot re-decides its wander direction. The botDir stores the current wander vector. Checking window.location.search once in create() is cleaner than parsing the URL every frame.
Step 2: Flee Behavior (Run from Danger)
The flee behavior is the core survival mechanic. Every frame, the bot finds the nearest enemy. If it’s closer than 130px, the bot runs directly away from it:
// In update(time) — note: 'time' parameter needed for bot timer
const enemies = this.enemies.getChildren();
let nearest = null, nearDist = Infinity;
enemies.forEach(e => {
const d = Phaser.Math.Distance.Between(
this.player.x, this.player.y, e.x, e.y
);
if (d < nearDist) { nearDist = d; nearest = e; }
});
if (nearest && nearDist < 130) {
const dx = this.player.x - nearest.x;
const dy = this.player.y - nearest.y;
const d = Math.sqrt(dx * dx + dy * dy) || 1;
const fleeWeight = Math.max(0, (130 - nearDist) / 130);
vx = (dx / d) * speed * fleeWeight;
vy = (dy / d) * speed * fleeWeight;
}
The key detail: fleeWeight ramps up as the enemy gets closer. At 130px the weight is 0 (no flee), at 0px the weight is 1 (full flee). This creates smooth pursuit — the bot doesn’t snap to full speed the moment an enemy appears on the radar — it eases into the escape.
Step 3: Wander Behavior (Move When Safe)
When no enemies are near, the bot shouldn’t just sit still — it should explore. Wander is a random direction that changes periodically:
if (time > this.botTimer) {
this.botTimer = time + Phaser.Math.Between(500, 1500);
this.botDir = {
x: (Math.random() - 0.5) * 2,
y: (Math.random() - 0.5) * 2
};
const len = Math.sqrt(
this.botDir.x ** 2 + this.botDir.y ** 2
) || 1;
this.botDir.x /= len;
this.botDir.y /= len;
}
vx = this.botDir.x * speed * 0.6;
vy = this.botDir.y * speed * 0.6;
The timer makes the bot change direction every 0.5–1.5 seconds. The * 0.6 multiplier keeps wander speed lower than flee speed — the bot meanders when safe but bolts when threatened. This makes the movement look more natural than constant random jitter.
Why use update(time) instead of update()? Phaser passes the current game time (in milliseconds) to update(time, delta). The time parameter lets the bot use timer-based decision cycles instead of checking every single frame (60 times per second). Without it, you’d need a separate counter or event timer for the wander direction changes.
Step 4: Combine and Weight the Forces
When both flee and wander are active (enemy moderately close but not panic distance yet), they blend together:
if (nearest && nearDist < 130) {
// ... calculate flee velocity ...
vx = (dx / d) * speed * fleeWeight + this.botDir.x * speed * (1 - fleeWeight * 0.7);
vy = (dy / d) * speed * fleeWeight + this.botDir.y * speed * (1 - fleeWeight * 0.7);
}
At distance 130px: fleeWeight is 0, so 100% wander. At distance 0px: fleeWeight is 1, wander drops to 30%. In between: the bot runs away while still wobbling a bit — making it harder to predict.
Step 5: Edge Avoidance
The most common way for simple bots to die is getting stuck in a corner. Add a push-back force when the bot approaches the game boundary:
const margin = 60;
if (this.player.x < margin) vx += speed * 0.5;
if (this.player.x > W - margin) vx -= speed * 0.5;
if (this.player.y < margin) vy += speed * 0.5;
if (this.player.y > H - margin) vy -= speed * 0.5;
This pushes the bot away from walls proportionally — the closer to the edge, the harder the push. The margin of 60px creates a “soft wall” zone that the bot tries to stay inside of.
Step 6: Speed Capping
After all forces are combined, cap the velocity so the bot doesn’t exceed game physics:
const maxSpeed = 220;
const currentSpeed = Math.sqrt(vx * vx + vy * vy);
if (currentSpeed > maxSpeed) {
vx = (vx / currentSpeed) * maxSpeed;
vy = (vy / currentSpeed) * maxSpeed;
}
this.player.body.setVelocity(vx, vy);
Without this, the flee + edge avoidance forces can stack and send the bot at absurd speeds (especially in corners where both forces push in the same direction).
The Complete Bot Mode
Here’s the full bot mode decision loop that goes into your update(time) method:
if (this.botMode) {
const enemies = this.enemies.getChildren();
let nearest = null, nearDist = Infinity;
// Step 1: Find nearest enemy
enemies.forEach(e => {
const d = Phaser.Math.Distance.Between(
this.player.x, this.player.y, e.x, e.y
);
if (d < nearDist) { nearDist = d; nearest = e; }
});
// Step 2: Update wander direction on timer
if (time > this.botTimer) {
this.botTimer = time + Phaser.Math.Between(500, 1500);
this.botDir = {
x: (Math.random() - 0.5) * 2,
y: (Math.random() - 0.5) * 2
};
const len = Math.sqrt(
this.botDir.x ** 2 + this.botDir.y ** 2
) || 1;
this.botDir.x /= len;
this.botDir.y /= len;
}
// Step 3: Flee + wander blend
if (nearest && nearDist < 130) {
const dx = this.player.x - nearest.x;
const dy = this.player.y - nearest.y;
const d = Math.sqrt(dx * dx + dy * dy) || 1;
const fleeWeight = Math.max(0, (130 - nearDist) / 130);
vx = (dx / d) * speed * fleeWeight +
this.botDir.x * speed * (1 - fleeWeight * 0.7);
vy = (dy / d) * speed * fleeWeight +
this.botDir.y * speed * (1 - fleeWeight * 0.7);
} else {
// Not threatened — just wander
vx = this.botDir.x * speed * 0.6;
vy = this.botDir.y * speed * 0.6;
}
// Step 4: Edge avoidance
const margin = 60;
if (this.player.x < margin) vx += speed * 0.5;
if (this.player.x > W - margin) vx -= speed * 0.5;
if (this.player.y < margin) vy += speed * 0.5;
if (this.player.y > H - margin) vy -= speed * 0.5;
// Step 5: Clamp speed
const maxSpeed = 220;
const currentSpeed = Math.sqrt(vx * vx + vy * vy);
if (currentSpeed > maxSpeed) {
vx = (vx / currentSpeed) * maxSpeed;
vy = (vy / currentSpeed) * maxSpeed;
}
} else {
// Human input
// ... your existing keyboard logic ...
}
this.player.body.setVelocity(vx, vy);
Try It Yourself
The bot mode is live in our AI Dodge game — open it with ?bot=true to see the math bot in action.
Built by DeepSeek V4 Flash — bot mode concept from Craig Reynolds’ steering behaviors, implementation refined with AI across 2 prompts
Remix Ideas
- LLM bot — Replace the math bot with an actual LLM call. The bot sends the game state (player position, nearest enemy, walls) to an API and gets a direction back. We built this too — it runs on Groq’s Llama 3.3 70B.
- Multiple weights — Make each enemy have its own flee radius. Small enemies get ignored at closer range than big ones.
- Predictive flee — Instead of fleeing the enemy’s current position, flee where it will be in 500ms based on its velocity.
- Group behavior — Make bots that stick together when safe (cohesion) but scatter when threatened (separation).
Adding Bot Mode to Your Own Game
To retrofit bot mode onto an existing Phaser game:
- Change
update()toupdate(time)in your scene - Add the bot state initialization in
create()after your player setup - Insert the bot branching logic at the start of
update()before your keyboard handling - Add a bot link to your controls:
<a href="?bot=true">Bot mode</a>
The full source is on GitHub — the main game at public/games/ai-dodge/index.html has the complete bot mode implementation.
What You Learned
- Steering behaviors combine multiple movement forces (flee, wander, edge avoid) into a single velocity vector
- Weighted blending lets the bot transition smoothly between behaviors based on distance
- Timer-based decision cycles (0.5–1.5s wander changes) look more natural than per-frame random
- Soft boundaries prevent corner-death without hard walls
- Speed capping prevents force stacking from creating absurd velocities
The same pattern scales to more complex behaviors — you can add pursuit, evasion, path following, or flocking by adding more force vectors and tuning their blend weights.