tutoriales.com

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.

Avanzado12 min de lectura21 views
Reportar error

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).

💡 Consejo: El patrón FSM es ideal para proyectos medianos y pequeños, ya que mantiene el código desacoplado, fácil de leer y muy sencillo de depurar en tiempo de ejecución.

¿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).

Patrullar Perseguir Atacar Jugador Detectado Perdido de vista En Rango Fuera de Rango Objetivo Eliminado Jugador Escapa

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();
}
📌 Nota: Usar constructores en los estados nos permite pasar una referencia directa del controlador principal (`EnemyController`), facilitando el acceso a variables como la velocidad, los puntos de ruta y las referencias a componentes de Unity.

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:

Paso 1: Crea un objeto 3D (por ejemplo, un cilindro) que represente al enemigo en tu escena.
Paso 2: Añade el componente NavMesh Agent desde el menú de componentes de Unity.
Paso 3: Asocia el script EnemyController al objeto del enemigo.
Paso 4: Configura la superficie navegable (Navigation Surface) y hornea el NavMesh de tu escenario.
Paso 5: Crea objetos vacíos (Empty GameObjects) en la escena para utilizarlos como Waypoints y arrastralos al array correspondiente en el inspector.
Inspector: Enemigo Static Transform Position X: 10 Y: 0 Z: -5 NavMesh Agent Steering: Humanoid Enemy Controller (Script) C# Script Speed 3.5 Detection Range 15.0 Waypoints (Array) Size 2 Element 0 Waypoint_Alpha (Transform) Element 1 Waypoint_Beta (Transform)

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 GetComponent dentro de los métodos Update de los estados. Realiza todas las referencias necesarias al inicio en el método Start.
  • Frecuencia de Actualización del NavMesh: Si tienes decenas de enemigos en pantalla, recalcular el destino del NavMeshAgent en 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.
⚠️ Advertencia: No olvides bakear correctamente el NavMesh de tu terreno. Si el NavMesh no está generado o hay obstáculos sin calcular, los enemigos generarán errores en la consola al intentar asignar destinos inalcanzables.

Preguntas Frecuentes (FAQ) 📖

¿Puedo añadir más estados a este sistema fácilmente?¡Sí! Solo necesitas crear una nueva clase que herede de State, 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 componente Animator 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:

  1. Añadir un estado de Búsqueda (SearchState) cuando el enemigo pierda de vista al jugador.
  2. Integrar un sistema de animaciones mediante transiciones del Animator Controller.
  3. 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

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!