Creando un Sistema de Inteligencia Artificial para Enemigos con Máquinas de Estados en Unity 🤖
Domina la creación de enemigos inteligentes en Unity implementando una Máquina de Estados Finitos en C#. Aprende a gestionar estados de patrullaje, persecución y ataque de forma modular y limpia.
Introducción al Comportamiento de Enemigos con IA 🤖
El diseño de inteligencia artificial (IA) para videojuegos es uno de los pilares fundamentales para crear experiencias inmersivas y desafiantes. Cuando los jugadores se enfrentan a enemigos que patrullan, reaccionan a su presencia, persiguen y atacan de manera coherente, el mundo virtual cobra vida. En este tutorial, aprenderemos a construir un sistema robusto y escalable utilizando el patrón de diseño de Máquina de Estados Finitos (FSM - Finite State Machine).
¿Qué es una Máquina de Estados Finitos?
Una Máquina de Estados Finitos es un modelo computacional que se encuentra en exactamente un estado a la vez de un conjunto finito de estados posibles. Nuestro enemigo cambiará de comportamiento basándose en eventos o condiciones específicas (llamadas transiciones).
Preparación del Entorno y Estructura de Clases 🛠️
Antes de escribir la lógica de nuestros estados, necesitamos diseñar la arquitectura base en C#. Vamos a estructurar nuestro código utilizando una clase abstracta para los estados y un controlador central que gestione el ciclo de vida del enemigo.
Creando la Clase Base para los Estados
Crea un nuevo script llamado State.cs. Esta clase servirá como molde para todos los comportamientos individuales de nuestro enemigo (Patrullar, Perseguir, Atacar).
using UnityEngine;
public abstract class State
{
protected EnemyController enemy;
public State(EnemyController enemyController)
{
enemy = enemyController;
}
public abstract void Enter();
public abstract void Update();
public abstract void Exit();
}
Implementando el Controlador Principal del Enemigo 🎮
El controlador principal (EnemyController.cs) será el componente que se encargará de mantener la referencia al estado actual y ejecutar sus métodos correspondientes en cada ciclo de actualización (Update).
Crea un script llamado EnemyController.cs con el siguiente código:
using UnityEngine;
using UnityEngine.AI;
public class EnemyController : MonoBehaviour
{
[Header("Configuración de Movimiento")]
public Transform[] waypoints;
public float moveSpeed = 3f;
public float chaseSpeed = 5f;
public float detectionRange = 7f;
public float attackRange = 1.5f;
public Transform player;
[HideInInspector]
public NavMeshAgent navAgent;
private State currentState;
// Instancias de los estados
public PatrolState patrolState;
public ChaseState chaseState;
public AttackState attackState;
void Start()
{
navAgent = GetComponent<NavMeshAgent>();
// Inicializar estados
patrolState = new PatrolState(this);
chaseState = new ChaseState(this);
attackState = new AttackState(this);
// Estado inicial por defecto
ChangeState(patrolState);
}
void Update()
{
if (currentState != null)
{
currentState.Update();
}
}
public void ChangeState(State newState)
{
if (currentState != null)
{
currentState.Exit();
}
currentState = newState;
currentState.Enter();
}
public bool CanSeePlayer()
{
if (player == null) return false;
float distanceToPlayer = Vector3.Distance(transform.position, player.position);
return distanceToPlayer <= detectionRange;
}
public bool IsPlayerInAttackRange()
{
if (player == null) return false;
float distanceToPlayer = Vector3.Distance(transform.position, player.position);
return distanceToPlayer <= attackRange;
}
}
Programando los Comportamientos Individuales 🧠
Ahora vamos a desarrollar los tres estados principales de nuestro enemigo: Patrullar por puntos de ruta, Perseguir al jugador al detectarlo y Atacar cuando esté lo suficientemente cerca.
1. Estado de Patrulla (PatrolState.cs)
El enemigo se moverá secuencialmente entre una serie de puntos definidos en la escena (waypoints). Si detecta al jugador, cambiará inmediatamente al estado de persecución.
using UnityEngine;
public class PatrolState : State
{
private int currentWaypointIndex = 0;
public PatrolState(EnemyController enemyController) : base(enemyController) { }
public override void Enter()
{
enemy.navAgent.speed = enemy.moveSpeed;
if (enemy.waypoints.Length > 0)
{
enemy.navAgent.SetDestination(enemy.waypoints[currentWaypointIndex].position);
}
}
public override void Update()
{
// Transición a persecución
if (enemy.CanSeePlayer())
{
enemy.ChangeState(enemy.chaseState);
return;
}
// Comprobar si llegó al waypoint actual
if (!enemy.navAgent.pathPending && enemy.navAgent.remainingDistance < 0.5f)
{
MoveNextWaypoint();
}
}
public override void Exit()
{
// Limpieza si es necesaria al salir del estado
}
private void MoveNextWaypoint()
{
if (enemy.waypoints.Length == 0) return;
currentWaypointIndex = (currentWaypointIndex + 1) % enemy.waypoints.Length;
enemy.navAgent.SetDestination(enemy.waypoints[currentWaypointIndex].position);
}
}
2. Estado de Persecución (ChaseState.cs)
En este estado, el enemigo utilizará una velocidad mayor para seguir al jugador por todo el escenario. Si el jugador se aleja demasiado, el enemigo volverá a patrullar.
using UnityEngine;
public class ChaseState : State
{
public ChaseState(EnemyController enemyController) : base(enemyController) { }
public override void Enter()
{
enemy.navAgent.speed = enemy.chaseSpeed;
}
public override void Update()
{
// Actualizar destino hacia el jugador constantemente
enemy.navAgent.SetDestination(enemy.player.position);
// Transición a ataque
if (enemy.IsPlayerInAttackRange())
{
enemy.ChangeState(enemy.attackState);
return;
}
// Transición si pierde al jugador
if (!enemy.CanSeePlayer())
{
enemy.ChangeState(enemy.patrolState);
return;
}
}
public override void Exit()
{
// Acciones al dejar de perseguir
}
}
3. Estado de Ataque (AttackState.cs)
Cuando el enemigo se encuentra a una distancia de combate, se detendrá y ejecutará la lógica de ataque (por ejemplo, infligir daño al jugador).
using UnityEngine;
public class AttackState : State
{
private float attackCooldown = 1.5f;
private float lastAttackTime;
public AttackState(EnemyController enemyController) : base(enemyController) { }
public override void Enter()
{
enemy.navAgent.ResetPath();
lastAttackTime = 0f;
}
public override void Update()
{
// Si el jugador se aleja del rango de ataque pero sigue visible, volver a persecución
if (!enemy.IsPlayerInAttackRange() && enemy.CanSeePlayer())
{
enemy.ChangeState(enemy.chaseState);
return;
}
// Si el jugador escapa por completo
if (!enemy.CanSeePlayer())
{
enemy.ChangeState(enemy.patrolState);
return;
}
// Lógica de ataque con temporizador
if (Time.time >= lastAttackTime + attackCooldown)
{
PerformAttack();
lastAttackTime = Time.time;
}
}
public override void Exit()
{
// Limpieza del estado de ataque
}
private void PerformAttack()
{
Debug.Log("¡El enemigo ataca al jugador inflijendo daño!");
// Aquí puedes invocar métodos del script de salud del jugador
}
}
Configuración en el Editor de Unity 🎯
Para poner en marcha nuestro sistema de IA, sigue estos pasos:
EnemyController al objeto del enemigo.Waypoints y arrastralos al array correspondiente en el inspector.Buenas Prácticas y Optimización Avanzada 🚀
Implementar una FSM es un gran comienzo, pero a medida que tu juego crece, es importante tener en cuenta ciertos aspectos de rendimiento y arquitectura:
- Caché de Componentes: Evita utilizar
GetComponentdentro de los métodosUpdatede los estados. Realiza todas las referencias necesarias al inicio en el métodoStart. - Frecuencia de Actualización del NavMesh: Si tienes decenas de enemigos en pantalla, recalcular el destino del
NavMeshAgenten cada fotograma puede sobrecargar la CPU. Considera actualizar el destino utilizando una corrutina con un intervalo de tiempo pequeño (por ejemplo, cada 0.2 segundos). - Transiciones Limpias: Asegúrate siempre de limpiar valores temporales dentro del método
Exit()de cada estado para evitar comportamientos residuales inesperados al cambiar de estado.
Preguntas Frecuentes (FAQ) 📖
¿Puedo añadir más estados a este sistema fácilmente?
¡Sí! Solo necesitas crear una nueva clase que herede deState, implementar los métodos abstractos Enter, Update y Exit, y añadir una instancia del mismo en el EnemyController.¿Cómo puedo integrar animaciones con este sistema?
Puedes añadir una referencia al componenteAnimator dentro de EnemyController y activar parámetros como "IsWalking", "IsChasing" o disparar triggers de ataque desde los métodos Enter o Update de cada estado.¿Es este sistema apto para juegos multijugador?
Este diseño básico está pensado para un solo jugador (Singleplayer). Para juegos multijugador en red, necesitarás sincronizar los estados y las posiciones mediante soluciones como Netcode for GameObjects o Photon.Conclusión y Siguientes Pasos 🎉
¡Felicidades! Has construido con éxito un sistema completo de Inteligencia Artificial basado en Máquinas de Estados Finitos en Unity. Este enfoque modular te permite escalar la complejidad de tus enemigos de forma limpia y ordenada.
Como siguientes pasos recomendados, te animo a:
- Añadir un estado de Búsqueda (
SearchState) cuando el enemigo pierda de vista al jugador. - Integrar un sistema de animaciones mediante transiciones del Animator Controller.
- Experimentar con la incorporación de campos de visión basados en ángulos además de simples distancias radiales.
¡Sigue programando y creando experiencias increíbles en el desarrollo de videojuegos! Avanzado
Tutoriales relacionados
- Creando un Sistema de Menús Interactivos y Navegación UI en Unity para tus Juegos 🎮intermediate25 min
- Creando un Sistema de Inventario Modular en Unity para RPGs y Juegos de Aventuraintermediate25 min
- Creando un Sistema de Movimiento y Combate Básico con Animaciones en Unity para Juegos Top-Down ⚔️intermediate15 min
- Creando un Sistema de Guardado y Carga Universal en Unity: Persistencia de Datos para tus Juegos 💾intermediate25 min
- Optimización Avanzada de Rendimiento en Unity: Sacando el Máximo a tus Juegos 🚀intermediate18 min
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!