Unity AI Systems
Introduction to Unity AI
Artificial Intelligence (AI) is a crucial component in modern games, creating engaging and responsive non-player characters (NPCs) and enemies. Unity provides several tools and frameworks to implement various AI behaviors, from simple movement patterns to complex decision-making systems.
In this guide, you'll learn:
- Basic AI movement and navigation
- Decision-making systems including finite state machines
- Pathfinding with Unity's NavMesh system
- Implementing behavior trees
- Sensory systems for AI perception
- Advanced techniques for realistic behavior
Unity's NavMesh System
The Navigation Mesh (NavMesh) system is Unity's built-in solution for pathfinding and spatial reasoning for AI characters. It creates a simplified representation of the walkable areas in your game world, allowing characters to intelligently navigate around obstacles.
Setting Up a NavMesh:
- First, ensure your scene contains static obstacles and a floor/terrain
- Mark obstacles as "Navigation Static" in the Inspector (Object → Static dropdown)
- Open the Navigation window (Window → AI → Navigation)
- In the "Bake" tab, configure settings like agent radius, height, and max slope
- Click "Bake" to generate the NavMesh
Key NavMesh Components:
- NavMesh: The walkable surface map
- NavMesh Agent: Component added to characters that move on the NavMesh
- NavMesh Obstacle: Component for dynamic obstacles that modify the NavMesh at runtime
- Off-Mesh Link: Connections between separate NavMesh areas (doors, jumps, etc.)
Basic NavMesh Agent Movement:
using UnityEngine;
using UnityEngine.AI;
public class SimpleAIController : MonoBehaviour
{
// Reference to the NavMeshAgent component
private NavMeshAgent agent;
// Destination to move to
public Transform destination;
// Optional patrol points
public Transform[] patrolPoints;
private int currentPatrolIndex = 0;
// Movement speed settings
public float walkSpeed = 2.0f;
public float runSpeed = 4.0f;
// Reference to the animator (if any)
private Animator animator;
// Distance at which to consider a point reached
public float reachDistance = 1.0f;
void Start()
{
// Get the NavMeshAgent component attached to this GameObject
agent = GetComponent();
// Make sure we have a NavMeshAgent
if (agent == null)
{
Debug.LogError("NavMeshAgent component missing! Add it to the GameObject.");
return;
}
// Get animator if present
animator = GetComponent();
// Set initial speed
agent.speed = walkSpeed;
// Start with a destination if one is specified
if (destination != null)
{
MoveToDestination(destination.position);
}
else if (patrolPoints.Length > 0)
{
// Start patrolling
StartPatrolling();
}
}
void Update()
{
// Check if we've reached the destination (if we're moving)
if (!agent.pathPending && agent.remainingDistance < reachDistance)
{
// We've reached the destination
OnDestinationReached();
}
// Update animator if it exists
if (animator != null)
{
// Set the speed parameter in the animator based on agent's velocity
animator.SetFloat("Speed", agent.velocity.magnitude);
}
}
// Called when the agent reaches its current destination
void OnDestinationReached()
{
// If we're patrolling, move to the next point
if (patrolPoints.Length > 0)
{
// Move to next patrol point
currentPatrolIndex = (currentPatrolIndex + 1) % patrolPoints.Length;
MoveToDestination(patrolPoints[currentPatrolIndex].position);
// Debug info
Debug.Log("Moving to patrol point " + (currentPatrolIndex + 1));
}
else
{
// We're not patrolling, so just stop
agent.isStopped = true;
// Inform the animator if it exists
if (animator != null)
{
animator.SetBool("Idle", true);
}
}
}
// Start patrolling between points
public void StartPatrolling()
{
if (patrolPoints.Length == 0)
{
Debug.LogWarning("No patrol points assigned!");
return;
}
// Set speed to walk speed
agent.speed = walkSpeed;
// Start moving to the first patrol point
MoveToDestination(patrolPoints[currentPatrolIndex].position);
}
// Move to a specific position
public void MoveToDestination(Vector3 targetPosition)
{
// Ensure agent is not stopped
agent.isStopped = false;
// Set the destination
agent.SetDestination(targetPosition);
// Update animator
if (animator != null)
{
animator.SetBool("Idle", false);
}
}
// Chase a target (moves faster)
public void ChaseTarget(Transform target)
{
// Set to run speed
agent.speed = runSpeed;
// Start following the target
destination = target;
// Set destination
MoveToDestination(target.position);
// Update this every frame in Update() for dynamic targets
}
// Stop moving
public void StopMoving()
{
agent.isStopped = true;
if (animator != null)
{
animator.SetBool("Idle", true);
}
}
// Manually adjust speed
public void SetSpeed(float newSpeed)
{
agent.speed = newSpeed;
}
// Visualize the path in the editor
void OnDrawGizmos()
{
if (agent == null || !Application.isPlaying)
return;
// Draw the current path
Gizmos.color = Color.green;
var agentPath = agent.path;
Vector3 previousCorner = transform.position;
// Iterate through each corner in the path
foreach (var corner in agentPath.corners)
{
Gizmos.DrawLine(previousCorner, corner);
Gizmos.DrawSphere(corner, 0.1f);
previousCorner = corner;
}
// Draw patrol points
Gizmos.color = Color.blue;
foreach (var point in patrolPoints)
{
if (point != null)
Gizmos.DrawSphere(point.position, 0.5f);
}
}
}
NavMeshAgent key properties:
- Speed: How fast the agent moves
- Stopping Distance: How close the agent gets to destinations
- Angular Speed: How quickly the agent rotates
- Avoidance: How the agent avoids other agents
- Auto Braking: Whether the agent slows down as it nears the destination
NavMesh Tips
- Rebake the NavMesh whenever you change your level geometry
- Use NavMesh Obstacles for moving obstacles
- Create different agent types for different character sizes
- Use Off-Mesh Links for jumps or special movements
- For runtime-generated environments, use the NavMesh Components package from GitHub
Finite State Machines for AI
A Finite State Machine (FSM) is a behavioral design pattern that organizes AI behavior into discrete states with clear transitions between them. It's one of the most common and flexible approaches for creating game AI.
Implementing a Simple FSM:
using UnityEngine;
using UnityEngine.AI;
using System.Collections;
public class EnemyFSM : MonoBehaviour
{
// State enumeration
public enum EnemyState
{
Idle, // Standing still, looking around
Patrol, // Moving between patrol points
Chase, // Pursuing the player
Attack, // Attacking the player
Flee, // Running away from the player
Dead // Death state
}
// Current state
public EnemyState currentState = EnemyState.Idle;
// References
private NavMeshAgent agent;
private Animator animator;
private Transform player;
// Patrol settings
public Transform[] patrolPoints;
private int currentPatrolIndex = 0;
// Detection settings
public float detectionRadius = 10f;
public float attackRange = 2f;
public float safeDistance = 15f;
public LayerMask playerLayer;
// Combat settings
public int health = 100;
public int damage = 10;
public float attackCooldown = 2f;
private float lastAttackTime = 0f;
// Other settings
public float patrolWaitTime = 2f;
private float stateEnterTime;
void Start()
{
// Get components
agent = GetComponent();
animator = GetComponent();
// Find player
player = GameObject.FindGameObjectWithTag("Player")?.transform;
// Initial state setup
EnterState(EnemyState.Idle);
}
void Update()
{
// Check for state-independent transitions
if (health <= 0 && currentState != EnemyState.Dead)
{
EnterState(EnemyState.Dead);
return;
}
// Run the current state's behavior
switch (currentState)
{
case EnemyState.Idle:
UpdateIdleState();
break;
case EnemyState.Patrol:
UpdatePatrolState();
break;
case EnemyState.Chase:
UpdateChaseState();
break;
case EnemyState.Attack:
UpdateAttackState();
break;
case EnemyState.Flee:
UpdateFleeState();
break;
case EnemyState.Dead:
// No updates needed for dead state
break;
}
}
// Change to a new state
void EnterState(EnemyState newState)
{
// Exit the current state
ExitState(currentState);
// Remember when we entered this state
stateEnterTime = Time.time;
// Set and enter the new state
currentState = newState;
// Debug info
Debug.Log($"Enemy entered {newState} state");
// State-specific enter logic
switch (newState)
{
case EnemyState.Idle:
EnterIdleState();
break;
case EnemyState.Patrol:
EnterPatrolState();
break;
case EnemyState.Chase:
EnterChaseState();
break;
case EnemyState.Attack:
EnterAttackState();
break;
case EnemyState.Flee:
EnterFleeState();
break;
case EnemyState.Dead:
EnterDeadState();
break;
}
}
// Logic for exiting a state
void ExitState(EnemyState oldState)
{
// State-specific exit logic
switch (oldState)
{
case EnemyState.Attack:
// Stop attack animation
if (animator != null)
{
animator.SetBool("Attacking", false);
}
break;
// Other state-specific exit code...
}
}
#region State Enter Methods
void EnterIdleState()
{
// Stop the agent
agent.isStopped = true;
// Play idle animation
if (animator != null)
{
animator.SetBool("Idle", true);
animator.SetFloat("Speed", 0);
}
// Start a timer to eventually switch to patrol
StartCoroutine(IdleTimer());
}
void EnterPatrolState()
{
// Set movement speed for patrolling
agent.speed = 2f;
agent.isStopped = false;
// Play walk animation
if (animator != null)
{
animator.SetBool("Idle", false);
animator.SetFloat("Speed", 0.5f);
}
// Start moving to patrol point
if (patrolPoints.Length > 0)
{
agent.SetDestination(patrolPoints[currentPatrolIndex].position);
}
else
{
// No patrol points, revert to idle
EnterState(EnemyState.Idle);
}
}
void EnterChaseState()
{
// Set movement speed for chasing
agent.speed = 4f;
agent.isStopped = false;
// Play run animation
if (animator != null)
{
animator.SetBool("Idle", false);
animator.SetFloat("Speed", 1.0f);
}
}
void EnterAttackState()
{
// Stop moving during attack
agent.isStopped = true;
// Play attack animation
if (animator != null)
{
animator.SetBool("Attacking", true);
}
// Face the player
if (player != null)
{
Vector3 lookDirection = player.position - transform.position;
lookDirection.y = 0;
if (lookDirection != Vector3.zero)
{
transform.rotation = Quaternion.LookRotation(lookDirection);
}
}
}
void EnterFleeState()
{
// Set speed for fleeing
agent.speed = 5f;
agent.isStopped = false;
// Play flee animation
if (animator != null)
{
animator.SetBool("Idle", false);
animator.SetFloat("Speed", 1.0f);
animator.SetBool("Fleeing", true);
}
}
void EnterDeadState()
{
// Stop all movement
agent.isStopped = true;
// Play death animation
if (animator != null)
{
animator.SetTrigger("Die");
}
// Disable colliders
Collider[] colliders = GetComponents();
foreach (Collider col in colliders)
{
col.enabled = false;
}
// Schedule destruction (or object pooling return)
StartCoroutine(DestroyAfterDelay(5f));
}
#endregion
#region State Update Methods
void UpdateIdleState()
{
// Check for player in detection radius
if (player != null && IsPlayerDetected())
{
if (health < 30 && IsHealthLow())
{
// Low health, run away!
EnterState(EnemyState.Flee);
}
else
{
// Player detected, start chase
EnterState(EnemyState.Chase);
}
}
}
void UpdatePatrolState()
{
// Check for player detection first
if (player != null && IsPlayerDetected())
{
if (health < 30 && IsHealthLow())
{
// Low health, run away!
EnterState(EnemyState.Flee);
}
else
{
// Player detected, start chase
EnterState(EnemyState.Chase);
}
return;
}
// Check if we've reached the current patrol point
if (!agent.pathPending && agent.remainingDistance < 0.5f)
{
// Reached the waypoint, wait and then move to next
StartCoroutine(WaitAtPatrolPoint());
}
}
void UpdateChaseState()
{
// Check if we should flee
if (health < 30 && IsHealthLow())
{
EnterState(EnemyState.Flee);
return;
}
// If player is null or destroyed, go back to patrolling
if (player == null)
{
EnterState(EnemyState.Patrol);
return;
}
// Check if player is still in detection range
if (!IsPlayerDetected())
{
// Lost the player, go back to patrolling
EnterState(EnemyState.Patrol);
return;
}
// Update destination to follow player
agent.SetDestination(player.position);
// Check if close enough to attack
if (IsInAttackRange())
{
EnterState(EnemyState.Attack);
}
}
void UpdateAttackState()
{
// If player is null or destroyed, go back to patrolling
if (player == null)
{
EnterState(EnemyState.Patrol);
return;
}
// Check if we should flee
if (health < 30 && IsHealthLow())
{
EnterState(EnemyState.Flee);
return;
}
// Check if player is still in attack range
if (!IsInAttackRange())
{
// Player moved away, chase again
EnterState(EnemyState.Chase);
return;
}
// Face the player
Vector3 lookDirection = player.position - transform.position;
lookDirection.y = 0;
if (lookDirection != Vector3.zero)
{
transform.rotation = Quaternion.Slerp(transform.rotation,
Quaternion.LookRotation(lookDirection), Time.deltaTime * 5f);
}
// Attack logic with cooldown
if (Time.time >= lastAttackTime + attackCooldown)
{
PerformAttack();
lastAttackTime = Time.time;
}
}
void UpdateFleeState()
{
// If player is null or destroyed, go back to patrolling
if (player == null)
{
EnterState(EnemyState.Patrol);
return;
}
// Check if we've reached a safe distance
if (Vector3.Distance(transform.position, player.position) > safeDistance)
{
// We're safe, go back to idle/patrol
EnterState(EnemyState.Idle);
return;
}
// Keep running away from the player
Vector3 fleeDirection = transform.position - player.position;
fleeDirection.y = 0;
fleeDirection.Normalize();
// Find a point to flee towards
Vector3 fleeTarget = transform.position + fleeDirection * 10f;
// Create a path to the flee target
NavMeshPath path = new NavMeshPath();
if (agent.CalculatePath(fleeTarget, path))
{
agent.SetPath(path);
}
else
{
// Can't flee in that direction, try a random direction
Vector3 randomDirection = Random.insideUnitSphere * 10f;
randomDirection.y = 0;
agent.SetDestination(transform.position + randomDirection);
}
}
#endregion
#region Helper Methods
// Check if player is within detection radius
bool IsPlayerDetected()
{
if (player == null) return false;
// Check distance
float distance = Vector3.Distance(transform.position, player.position);
if (distance > detectionRadius) return false;
// Line of sight check
Vector3 dirToPlayer = (player.position - transform.position).normalized;
if (Physics.Raycast(transform.position + Vector3.up, dirToPlayer, out RaycastHit hit, detectionRadius))
{
// If we hit the player, they're visible
if (hit.transform == player)
{
return true;
}
}
return false;
}
// Check if player is within attack range
bool IsInAttackRange()
{
if (player == null) return false;
return Vector3.Distance(transform.position, player.position) <= attackRange;
}
// Check if health is low (as a percentage)
bool IsHealthLow()
{
return (health / 100f) < 0.3f; // Less than 30% health
}
// Perform an attack against the player
void PerformAttack()
{
// Trigger attack animation
if (animator != null)
{
animator.SetTrigger("Attack");
}
// Damage the player if in range
if (IsInAttackRange() && player != null)
{
// Get player health component
PlayerHealth playerHealth = player.GetComponent();
if (playerHealth != null)
{
playerHealth.TakeDamage(damage);
}
Debug.Log("Enemy attacked player for " + damage + " damage!");
}
}
// Take damage from player
public void TakeDamage(int damageAmount)
{
health -= damageAmount;
Debug.Log("Enemy took " + damageAmount + " damage. Health: " + health);
// Visual feedback
// You could implement a flash effect or play a hurt animation here
// Flee if health gets low
if (health < 30 && IsHealthLow() && currentState != EnemyState.Flee)
{
EnterState(EnemyState.Flee);
}
}
// Coroutine for waiting at patrol points
IEnumerator WaitAtPatrolPoint()
{
// Stop and play idle
agent.isStopped = true;
if (animator != null)
{
animator.SetBool("Idle", true);
animator.SetFloat("Speed", 0);
}
// Wait for the patrol wait time
yield return new WaitForSeconds(patrolWaitTime);
// Continue to next patrol point
currentPatrolIndex = (currentPatrolIndex + 1) % patrolPoints.Length;
agent.isStopped = false;
agent.SetDestination(patrolPoints[currentPatrolIndex].position);
// Resume animation
if (animator != null)
{
animator.SetBool("Idle", false);
animator.SetFloat("Speed", 0.5f);
}
}
// Idle timer coroutine
IEnumerator IdleTimer()
{
// Wait for a random time
yield return new WaitForSeconds(Random.Range(3f, 6f));
// Switch to patrol if we're still in idle state
if (currentState == EnemyState.Idle)
{
EnterState(EnemyState.Patrol);
}
}
// Destroy after delay
IEnumerator DestroyAfterDelay(float delay)
{
yield return new WaitForSeconds(delay);
Destroy(gameObject);
}
#endregion
// Visualization for debugging
void OnDrawGizmosSelected()
{
// Detection radius
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, detectionRadius);
// Attack range
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(transform.position, attackRange);
// Safe distance
Gizmos.color = Color.green;
Gizmos.DrawWireSphere(transform.position, safeDistance);
// Patrol points
Gizmos.color = Color.blue;
if (patrolPoints != null)
{
foreach (Transform point in patrolPoints)
{
if (point != null)
{
Gizmos.DrawSphere(point.position, 0.5f);
}
}
}
}