Waypoint Patrol AI
Enemy patrol between waypoints with configurable speed, wait times, and patrol patterns (loop, ping-pong, random).
How to Use
Create empty GameObjects as waypoints, position them in the scene
Attach WaypointPatrol to your enemy
Drag waypoints into the array in inspector
Choose patrol pattern: Loop, PingPong, or Random
Adjust speed and wait time at each waypoint
Gizmos show patrol path in Scene view (select the enemy)
Features
- Three patrol modes: Loop, PingPong, and Random
- Configurable move speed, rotation speed, and arrival threshold
- Wait timer at each waypoint with adjustable duration
- Smooth rotation towards movement direction via Slerp
- PausePatrol and ResumePatrol methods for state integration
- Editor Gizmos draw waypoint path and connections in Scene view
When to Use This
Ideal for stealth games, RPGs, tower defense, and any game with guard enemies or NPCs that walk predefined routes. Use Loop mode for circular patrols, PingPong for back-and-forth hallway guards, and Random for unpredictable patrol behavior. Combine with EnemyChaseAI to switch from patrol to chase when the player is detected.
Common Mistakes
The waypoints array must be populated with Transforms in the Inspector — empty or null entries cause the enemy to freeze. Waypoints are world-space positions, so moving the parent of waypoint objects at runtime will not update the patrol path. Setting arrivalThreshold too small (below 0.1) can cause the enemy to orbit the waypoint endlessly without triggering arrival.
Source Code
using UnityEngine;
/// <summary>
/// Moves an enemy between waypoints with configurable patterns.
/// Supports loop, ping-pong, and random patrol modes.
/// </summary>
public class WaypointPatrol : MonoBehaviour
{
public enum PatrolMode { Loop, PingPong, Random }
[Header("Waypoints")]
[SerializeField] private Transform[] waypoints;
[Header("Movement")]
[SerializeField] private float moveSpeed = 3f;
[SerializeField] private float rotationSpeed = 5f;
[SerializeField] private float arrivalThreshold = 0.2f;
[Header("Waiting")]
[SerializeField] private float waitTime = 1f;
[Header("Pattern")]
[SerializeField] private PatrolMode patrolMode = PatrolMode.Loop;
private int currentIndex;
private int direction = 1;
private float waitTimer;
private bool isWaiting;
private void Update()
{
if (waypoints == null || waypoints.Length == 0) return;
if (isWaiting)
{
waitTimer -= Time.deltaTime;
if (waitTimer <= 0f)
isWaiting = false;
return;
}
MoveToWaypoint();
}
private void MoveToWaypoint()
{
Transform target = waypoints[currentIndex];
if (target == null) return;
Vector3 targetPos = target.position;
Vector3 direction3d = (targetPos - transform.position);
direction3d.y = 0f; // Keep on same plane
float distance = direction3d.magnitude;
if (distance <= arrivalThreshold)
{
ArrivedAtWaypoint();
return;
}
// Move
Vector3 moveDir = direction3d.normalized;
transform.position += moveDir * moveSpeed * Time.deltaTime;
// Rotate towards movement direction
if (moveDir != Vector3.zero)
{
Quaternion targetRotation = Quaternion.LookRotation(moveDir);
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime);
}
}
private void ArrivedAtWaypoint()
{
isWaiting = true;
waitTimer = waitTime;
switch (patrolMode)
{
case PatrolMode.Loop:
currentIndex = (currentIndex + 1) % waypoints.Length;
break;
case PatrolMode.PingPong:
currentIndex += direction;
if (currentIndex >= waypoints.Length - 1 || currentIndex <= 0)
direction *= -1;
break;
case PatrolMode.Random:
int next;
do { next = Random.Range(0, waypoints.Length); }
while (next == currentIndex && waypoints.Length > 1);
currentIndex = next;
break;
}
}
/// <summary>
/// Pause patrol at current position.
/// </summary>
public void PausePatrol() => enabled = false;
/// <summary>
/// Resume patrol from current position.
/// </summary>
public void ResumePatrol() => enabled = true;
#if UNITY_EDITOR
private void OnDrawGizmosSelected()
{
if (waypoints == null || waypoints.Length == 0) return;
Gizmos.color = Color.cyan;
for (int i = 0; i < waypoints.Length; i++)
{
if (waypoints[i] == null) continue;
Gizmos.DrawWireSphere(waypoints[i].position, 0.3f);
if (i < waypoints.Length - 1 && waypoints[i + 1] != null)
Gizmos.DrawLine(waypoints[i].position, waypoints[i + 1].position);
}
// Close loop if in Loop mode
if (patrolMode == PatrolMode.Loop && waypoints.Length > 1
&& waypoints[0] != null && waypoints[waypoints.Length - 1] != null)
{
Gizmos.DrawLine(waypoints[waypoints.Length - 1].position, waypoints[0].position);
}
}
#endif
}