Quick answer: Use a text instance variable as a state machine ("patrol", "chase", "attack", "flee") combined with the Line of Sight behavior for detection and the Pathfinding behavior for navigation. Each tick, run only the logic for the current state and transition between states when conditions are met. This approach scales from simple back-and-forth guards to complex multi-phase boss encounters.
Enemy AI does not have to be complex to feel intelligent. A guard that patrols, spots the player, gives chase, and returns to its route when it loses sight creates a convincing experience with just four states. Construct 3 provides built-in behaviors like Line of Sight and Pathfinding that handle the hard parts, letting you focus on designing enemy behavior rather than writing navigation algorithms from scratch.
The State Machine Pattern
Every enemy AI in this guide is built on the same foundation: a state machine stored as an instance variable. The enemy has a text variable called state that holds its current behavior mode. Each tick, your event sheet checks this variable and runs only the relevant logic.
Start by setting up your enemy sprite with these instance variables:
• state (text): Current AI state, default "patrol"
• health (number): Hit points, default 100
• speed (number): Movement speed in pixels per second, default 120
• detectionRange (number): How far the enemy can see, default 300
• attackRange (number): Distance to trigger attack, default 50
• attackCooldown (number): Timer tracking time until next attack, default 0
In your event sheet, the structure looks like this:
Event group: Enemy AI
Sub-event: Enemy state = "patrol"
• Move toward current patrol point
• If reached patrol point, switch to next point
• If Line of Sight has sight of Player → set state to "chase"
Sub-event: Enemy state = "chase"
• Move toward Player
• If distance to Player < attackRange → set state to "attack"
• If Line of Sight does not have sight of Player → set state to "patrol"
Sub-event: Enemy state = "attack"
• If attackCooldown ≤ 0, deal damage, reset cooldown
• If distance to Player > attackRange → set state to "chase"
Sub-event: Enemy state = "flee"
• Move away from Player
Patrol Behavior
The simplest patrol uses two invisible marker sprites placed where you want the enemy to walk. Give each marker an instance variable patrolIndex (0 and 1). The enemy stores which index it is heading toward.
With the MoveTo behavior on the enemy, the event sheet logic is clean:
// Patrol logic in scripting
const enemy = runtime.objects.Enemy.getFirstPickedInstance();
const markers = runtime.objects.PatrolMarker.getAllInstances();
// Find the current target marker
const target = markers.find(
m => m.instVars.patrolIndex === enemy.instVars.targetIndex
);
if (target) {
const dist = runtime.objects.Sprite.testOverlap
? 0 : distanceTo(enemy, target);
if (dist < 10) {
// Reached marker, switch to the other one
enemy.instVars.targetIndex =
enemy.instVars.targetIndex === 0 ? 1 : 0;
} else {
// Move toward marker
enemy.behaviors.MoveTo.moveToPosition(
target.x, target.y
);
}
}
For more complex patrol routes with multiple waypoints, use a patrol path approach: store waypoint indices 0, 1, 2, 3 and increment the target index each time the enemy reaches a point. When it reaches the last point, reset to 0 for a looping path or reverse direction for a ping-pong path.
Detection with Line of Sight
Add the Line of Sight behavior to your enemy sprite. In the behavior properties, configure:
• Range: Match your detectionRange instance variable (e.g. 300 pixels)
• Cone of view: 120 degrees for a forward-facing detection cone, or 360 for omnidirectional awareness
• Obstacles: Set to the Solid behavior so walls block line of sight
The key condition is Line of Sight → Has line of sight to object. This returns true only when the target is within range, within the cone angle, and not blocked by any obstacle with the Solid behavior.
For a more realistic detection system, combine Line of Sight with distance tiers:
// Tiered detection system
function updateDetection(enemy, player) {
const dist = Construct.distanceTo(
enemy.x, enemy.y, player.x, player.y
);
const hasLOS = enemy.behaviors.LineOfSight
.hasLineOfSightTo(player);
if (dist < 60) {
// Close range: always detect, even behind
return "detected";
} else if (dist < enemy.instVars.detectionRange && hasLOS) {
// Medium range: need line of sight
return "detected";
} else {
return "undetected";
}
}
This creates enemies that are hard to sneak up on at close range but can be avoided at a distance by staying behind cover.
Chase and Pathfinding
For open areas without obstacles, chasing is simple: move toward the player each tick using Move at angle with the angle from the enemy to the player. But most game levels have walls, and enemies that walk through walls break immersion immediately.
Add the Pathfinding behavior to the enemy sprite. When entering the chase state, calculate a path to the player:
Event: Enemy state = "chase" + Every 0.5 seconds
• Enemy Pathfinding → Find path to Player.X, Player.Y
Event: Enemy Pathfinding → On path found
• Enemy Pathfinding → Move along path
Recalculating every 0.5 seconds instead of every tick keeps performance reasonable while still tracking a moving player. The pathfinding behavior uses the obstacle map you define (typically all objects with the Solid behavior) to navigate around walls and furniture.
In scripting, the pathfinding API looks like:
// Recalculate path periodically during chase
async function chasePath(enemy, player) {
const pf = enemy.behaviors.Pathfinding;
// Find path to player position
const result = await pf.findPath(player.x, player.y);
if (result) {
pf.startMoving();
} else {
// No valid path, fall back to direct movement
const angle = Construct.angleTo(
enemy.x, enemy.y, player.x, player.y
);
enemy.x += Math.cos(angle) * enemy.instVars.speed
* runtime.dt;
enemy.y += Math.sin(angle) * enemy.instVars.speed
* runtime.dt;
}
}
Boss Patterns and Multi-Phase AI
Boss encounters extend the state machine with phases. Each phase has its own set of states and transitions. Use a phase instance variable alongside state:
// Boss phase transitions based on health
function updateBossPhase(boss) {
const healthPercent = boss.instVars.health
/ boss.instVars.maxHealth * 100;
if (healthPercent > 66) {
boss.instVars.phase = 1;
// Phase 1: slow projectiles, long pauses
} else if (healthPercent > 33) {
boss.instVars.phase = 2;
// Phase 2: faster attacks, adds spawns
boss.instVars.speed = 180;
boss.instVars.attackCooldown = 1.0;
} else {
boss.instVars.phase = 3;
// Phase 3: enraged, rapid attacks, new patterns
boss.instVars.speed = 240;
boss.instVars.attackCooldown = 0.5;
}
}
// In the boss attack state, check phase for pattern
function bossAttack(boss) {
switch (boss.instVars.phase) {
case 1:
fireProjectileRing(boss, 8); // 8 bullets in a ring
break;
case 2:
fireProjectileRing(boss, 16); // 16 bullets
spawnMinion(boss);
break;
case 3:
fireProjectileSpiral(boss); // Spiral pattern
dashTowardPlayer(boss);
break;
}
}
Difficulty Scaling
Rather than creating separate enemy types for each difficulty level, scale the instance variables dynamically. This lets you use the same AI logic with different parameters:
// Apply difficulty multipliers on enemy creation
function applyDifficulty(enemy, level) {
const multipliers = {
easy: { health: 0.7, speed: 0.8, detection: 0.6 },
normal: { health: 1.0, speed: 1.0, detection: 1.0 },
hard: { health: 1.5, speed: 1.2, detection: 1.4 }
};
const m = multipliers[level] || multipliers.normal;
enemy.instVars.health *= m.health;
enemy.instVars.speed *= m.speed;
enemy.instVars.detectionRange *= m.detection;
enemy.behaviors.LineOfSight.range =
enemy.instVars.detectionRange;
}
You can also scale difficulty dynamically based on player performance. Track deaths per room or time between hits, and adjust enemy parameters in real time for a smoother difficulty curve.
Related Issues
If your event sheet conditions are not triggering as expected, see Fix Construct 3 Event Sheet Conditions Not Working. For collision detection issues that affect AI attacks, see Fix Construct 3 Collision Not Detected Between Objects. For performance concerns with many AI enemies, see Construct 3 Performance Tips for Large Projects.
Start with the simplest AI that feels right and only add complexity when playtesting reveals the need. A patrol-chase-attack loop with tuned speeds and detection ranges is more fun than a complex behavior tree that nobody notices.