Introducción a la Inteligencia Artificial en Unreal Engine 4

Hola de nuevo, aquí estamos con una nueva entrega de esta serie de tutoriales sobre el desarrollo de juegos en Unreal Engine 4. En el tutorial pasado comentaba que este próximo sería sobre el HUD y el GameMode, pero un amigo de MicropsiaGames, Marco Antonio, que dicho sea de paso, te recomiendo que no te pierdas su juego INFOCUS Extreme Bike, me comentó que sería genial si el próximo tutorial lo hacía sobre IA, y la verdad es que no tuvo que insistir mucho :)

En este tutorial vamos a agregar un enemigo que estará patrullando una zona del nivel. Cuando nos acerquemos a esa zona y el enemigo se de cuenta que estamos cerca, nos perseguirá, si pierde nuestro rastro volverá a su tarea de vigilante. Con este simple ejemplo veremos varios conceptos relacionados con la inteligencia artificial en Unreal Engine 4 como el Behavior Tree, Decorators, Task, Services, BlackBoard, AIController etc.

Manos a la obra !!

Modificando el nivel, la cámara y los controles, a un estilo top-down para analizar mejor la AI del enemigo.

Antes de comenzar con lo nuevo, vamos a hacer algunas modificaciones en el nivel, el estilo de cámara y los controles de nuestro juego, para poder analizar mejor el comportamiento del enemigo y el funcionamiento de la AI que implementaremos. Vamos a cambiar la cámara del side-scroller a una cámara top-down. Si has seguido los tutoriales anteriores, seguro que podrás hacer esto por tu cuenta, de todas formas, aquí te dejo lo que tienes que hacer.

Abre la clase HeroCharacter.h y agrega las declaraciones de los siguientes métodos:


/**
 * Inicializa las variables SpringArm y SideViewCamera con la configuracion necesaria
 * para una vista side-scroller
 */
void InitSideScrollerCamera(const class FPostConstructInitializeProperties& PCIP);

/**
 * Inicializa las variables SpringArm y SideViewCamera con la configuracion necesaria
 * para una vista topdown
 */
void InitTopDownCamera(const class FPostConstructInitializeProperties& PCIP);

/**
 *  Se llama cuando se detecta la entrada de tipo MoveForward (W o S).
 *  Determina la dirección en la que está el personaje y le aplica un movimiento (positivo o negativo) en esa dirección
 *
 *  @param Value es igual a 1 cuando se detecta W y -1 cuando se detecta S
 */
void MoveForward(float Value);

Ahora pasa a la .cpp, modifica el constructor y agrega las siguientes implementaciones.


AHeroCharacter::AHeroCharacter(const class FPostConstructInitializeProperties& PCIP)
	: Super(PCIP)
{
    //Por defecto esta propiedad viene en true para el Character.
    //Pero en nuestro modelo de desplazamiento, no queremos que el personaje rote en base a la rotación del Controller.
    bUseControllerRotationYaw = false;
    
    //Configuración del componente CharacterMovement
    
    //Al estar en true habilita para que el character se rote en la dirección del movimiento al comenzar el movimiento.
	CharacterMovement->bOrientRotationToMovement = true;
    
    //Factor de rotación para la propiedad anterior.
	CharacterMovement->RotationRate = FRotator(0.0f, 540.0f, 0.0f);
    
    //Bajamos un poco el valor por defecto de MaxWalkSpeed para que el personaje camine un poco más lento.
    CharacterMovement->MaxWalkSpeed = 400.0f;
    
    //Inicializa SpringArm y SideViewCamera para una vista de juego estilo side-scroller
    //InitSideScrollerCamera(PCIP);
    
    //Inicializa SpringArm y SideViewCamera para una vista de juego estilo top-down
    InitTopDownCamera(PCIP);
    
    CoinsCollected = 0;
}

/** Inicializa SpringArm y SideViewCamera para una vista de juego estilo side-scroller */
void AHeroCharacter::InitSideScrollerCamera(const class FPostConstructInitializeProperties& PCIP)
{
	//Inicializando la instancia del USpringArmComponent
	SpringArm = PCIP.CreateDefaultSubobject<USpringArmComponent>(this, TEXT("CameraBoom"));
    
    //Agregando el springArm al RootComponent del Character (la capsula de colisión)
	SpringArm->AttachTo(RootComponent);
    
    //bAbsoluteRotation nos permite definir si este apoyo para la cámara rotará junto con el player.
    //En este caso no queremos que rote junto al character
	SpringArm->bAbsoluteRotation = true;
    
    //La distancia entre el brazo y su objetivo. Este valor es el que define la distancia de la cámara.
    //Prueba con distintos valores para que veas el resultado.
    SpringArm->TargetArmLength = 500.f;
    
    //Offset que tendrá el Socket.
    //Un Socket es un punto de anclaje para otros componentes.
    //Por ejemplo, para el caso de un personaje podemos definir que tenga un socket en la zona de la mano
    //de esta forma le podemos agregar otro componente (como un arma, por ejemplo) en la mano
    //En nuestro SpringArm, en este socket es donde se agregará la cámara
	SpringArm->SocketOffset = FVector(0.f,0.f,75.f);
    
    //La rotación relativa que tendrá este brazo con respecto al padre.
    //En este caso queremos que este rotada en el eje Y 180 grados para que quede paralela al character a su mismo nivel.
    //De esta forma logramos el clásico estilo de cámara en los side-scrollers
	SpringArm->RelativeRotation = FRotator(0.f,180.f,0.f);
    
	// Creando la intancia del tipo UCameraComponent
	SideViewCamera = PCIP.CreateDefaultSubobject<UCameraComponent>(this, TEXT("SideViewCamera"));
    
    //El método AttachTo nos permite agregar un objeto a otro objeto en un socket determinado. Recibe dos parámetros.
    //el primero, el objeto al que vamos a anclarnos, en este  caso el springArm y el nombre del socket donde lo vamos a anclar
    //SocketName de USpringArmComponent retorna el nombre del Socket de este componente.
	SideViewCamera->AttachTo(SpringArm, USpringArmComponent::SocketName);

}

/** Inicializa SpringArm y SideViewCamera para una vista de juego estilo top-down */
void AHeroCharacter::InitTopDownCamera(const class FPostConstructInitializeProperties& PCIP)
{
	CharacterMovement->bConstrainToPlane = true;
	CharacterMovement->bSnapToPlaneAtStart = true;
    
	SpringArm = PCIP.CreateDefaultSubobject<USpringArmComponent>(this, TEXT("CameraBoom"));
	SpringArm->AttachTo(RootComponent);
	SpringArm->bAbsoluteRotation = true;
	SpringArm->TargetArmLength = 800.f;
	SpringArm->RelativeRotation = FRotator(-60.f, 0.f, 0.f);
	SpringArm->bDoCollisionTest = false;
    
	SideViewCamera = PCIP.CreateDefaultSubobject<UCameraComponent>(this, TEXT("TopDownCamera"));
	SideViewCamera->AttachTo(SpringArm, USpringArmComponent::SocketName);
	SideViewCamera->bUseControllerViewRotation = false;
}

/**
 *  Se llama cuando se detecta la entrada de tipo MoveForward (W o S).
 *  Determina la dirección en la que está el personaje y le aplica un movimiento (positivo o negativo) en esa dirección
 *
 *  @param Value es igual a 1 cuando se detecta W y -1 cuando se detecta S
 */
void AHeroCharacter::MoveForward(float Value)
{
	if ((Controller != NULL) && (Value != 0.0f))
	{
		//Determina la dirección del movimiento hacia delante
		const FRotator Rotation = Controller->GetControlRotation();
		const FRotator YawRotation(0, Rotation.Yaw, 0);
        
		// Crea el vector de la dirección y aplica el movimiento
		const FVector Direction = FRotationMatrix(Rotation).GetUnitAxis(EAxis::X);
		AddMovementInput(Direction, Value);
	}
}

/**
 *  Se llama cuando se detecta la entrada de tipo MoveForward (A o D).
 *  @param Value Value es igual a 1 cuando se detecta D y -1 cuando se detecta A
 */
void AHeroCharacter::MoveRight(float Value)
{
	if ( (Controller != NULL) && (Value != 0.0f) )
	{
		//Determina la dirección del movimiento hacia los lados
		const FRotator Rotation = Controller->GetControlRotation();
		const FRotator YawRotation(0, Rotation.Yaw, 0);
        
		// Crea el vector de la dirección y aplica el movimiento
		const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
		AddMovementInput(Direction, Value);
	}
}

Fíjate que aquí también modificamos la implementación del método MoveRight. Comenta la actual implementación y deja esta, para que el control del personaje se ajuste al estilo de cámara.

No vamos a detenernos aquí, porque, como te decía, si has seguido los tutoriales anteriores no tendrás problema con lo que hemos hecho. Básicamente creamos dos métodos distintos para inicializar los objetos SpringArm y SideViewCamera según el estilo que queramos. Dale una revisión al método InitTopDownCamera que es el que usaremos ahora y que configura el SpringArm para un estilo top-down, nada del otro mundo, de hecho el código fue copiado del proyecto base top-down que brinda el UE4 :).

Además de esto, agregamos la declaración del método MoveForward que usamos en el primer tutorial, ya que vamos a modificar el estilo de juego para que el personaje se mueva libremente por todo el mundo 3D y hacer un poco más compleja la implementación de la AI del enemigo :).

Po último agrega al método SetupPlayerInputComponent la línea siguiente


InputComponent->BindAxis("MoveForward", this, &AHeroCharacter::MoveForward);

Fíjate que para regresar a la implementación de la cámara al estilo side-scroller solo tenemos que eliminar la llamada en el constructor del HeroCharacter al método InitTopDownCamera y en cambio llamar al método InitSideScrollerCamera, regresar a la implementación del MoveRight que teníamos anteriormente y eliminar la entrada del MoveForward en el SetupPlayerInputComponent.

Compila y abre el Editor. Agrega un nuevo Input de nombre MoveForward para las teclas W y S como hicimos en el primer tutorial.

Modifica un poco el nivel, copiando el objeto Floor y usando las herramientas de Transformación para crear una plataforma que será la que estará patrullando el enemigo. A pesar que en el estilo final de nuestro juego no es necesario que la plataforma tenga mucho espacio hacia atrás, para este ejemplo crésela también en ese eje, con el objetivo de crear un buen espacio para que el enemigo se desplace libremente y poder probar y jugar mejor con la AI.

Por último, tenemos que encerrar la zona que va a patrullar el enemigo con un objeto de tipo NevMeshBoundsVolume. Arrastra al nivel, desde la sección Volumes, un Nav Mesh Bounds Volume y modifícalo para que encierre todo el nivel. A mi me quedó así:

Nivel modificado con el NavMeshBoundsVolume

Creado el enemigo a partir de un AIController

En el primer tutorial explicamos la filosofía que seguía el framework de UE4 en su jerarquía de clases en lo referente a los personajes de nuestro juego, hablamos que el personaje principal era un Pawn especial, un Character, y este era controlado por el PlayerController. Los personajes que usan inteligencia artificia, generalmente conocimos como non-player characters (NPC), como los enemigos por ejemplo, funcionan muy parecidos, su representación en el juego es mediante un Pawn y también son controlados por un Controller, pero en este caso por un AIController.

Por lo demás, básicamente contienen lo mismo que nuestro personaje principal. Un skeletal mesh, el sistema de animaciones, el Movement Component etc. Para nuestro ejemplo, haremos al enemigo a partir de las mismas animaciones y modelo de nuestro personaje protagónico, aunque si quieres variar te recomiendo de nuevo que te des una vuelta por el Marketplace, ahí podrás encontrar varios modelos de personajes con sus animaciones listos para usar y muchísimo más “cool“ que los que estamos usando … y GRATIS 😉

Crea una carpeta en el Content Browser, ponle el nombre que quieras, en mi caso le puse AIEnemy.

Crea dentro de esa carpeta un nuevo Blueprint que herede de AIController y ponle de nombre AIEnemyController. Crea otro Blueprint que herede de Character y ponle de nombre AIEnemyCharacter. Ahora vamos a configurar el Skeletal Mesh y las animaciones para el enemigo, que como dijimos, usaremos las mismas animaciones y el mismo mesh que el personaje principal. Da doble clic en AIEnemyCharacter, sección Componentes, selecciona el componente Mesh y en la sección Mesh para el atributo Skeletal Mesh selecciona el mismo Mesh que estamos usando para nuestro personaje protagónico y ajústalo dentro de la capsula como ya hemos hecho antes. En la sección Animation selecciona para el atributo Anim Blueprint Generated Class la misma que estamos usando para nuestro personaje. Selecciona el Capsule Component y cambia el atributo Capsule Half Height a 96 y Capsule Radius a 42. Esto es para ajustar un poco la capsula al modelo.

Selecciona el componente CharacterMovement y en la sección Movement Component despliega Nav Agent Props en el atributo Agent Radius ponle 42 (este atributo tiene que ser al menos del tamaño del radio de la capsula) y el atributo Agent Height ponlo en 192 (este tiene que ser al menos el doble del atributo Capsule Half Height). La propiedad NavAgentProps define como el sistema de navegación del NPC interactúa con este componente.

Ahora pasa al Max Walk Speed y ponle 400.

Por último, pasa al modo Defaults y en la sección AI, para el atributo AIControllerClass, selecciona el AIController que acabamos de crear, AIEnemyController. Con esto le decimos al AICharacter que será controlado por el AIEnemyController.

A diferencia de como hicimos con nuestro personaje, vamos a configurar a nuestro enemigo COMPLETAMENTE desde el Editor mediante el Blueprint. Aunque en un próximo tutorial veremos la variante de hacerlo desde C++ para que tu solo tomes la decisión de la variante que prefieres.

Ahora agrega el AIEnemyCharacter al nivel en la zona que creamos con bastante espacio. Guarda y corre el juego. Muévete hasta la zona donde pusimos al enemigo, como verás, ya lo tenemos ahí, pero está parado sin hacer nada, y aunque nos acerquemos a él, ni se inmuta :(. Vamos a arreglar esto, vamos a darle vida.

AIEnemyCharacter agregado al nivel en la zona que va a patrullar

Un poco de teoría sobre la inteligencia artificial en Unreal Engine 4 y el Behavior Tree

En UE4 hay muchas formas de implementar la lógica de un personaje AI, una muy simple es en el mismo Blueprint del Character, haciendo uso del evento Tick, implementar algún algoritmo para que el personaje se mueva de un lado a otro, que compruebe que está cerca de alguien con los métodos que brinda el Engine (y que veremos algunos hoy) y nos ataque o haga cualquier otra cosa. Esto se puede implementar directamente ahí, pero a medida que necesitemos complejizar las acciones que tendrá ese Actor, los estados en los que podrá estar y las cosas que podrá hacer en cada situación, se hará mucho más complejo el control y el mantenimiento de toda esa lógica, ya sea en VisualScript o desde C++.

Este es uno de los motivos por los que el UE4 nos facilita la creación de un Behavior Tree (árbol de comportamiento) para poder definir el comportamiento que tendrá el NPC según las condiciones en las que se encuentre. Behavior Tree es un concepto de mucho tiempo de la IA que no vamos a detenernos en él aquí, si quieres profundizar en la teoría detrás de los arboles de comportamientos para mecanismos IA puedes poner en Google “behavior tree ai“ y date una vuelta por el resultado que más te llame la atención 😉

Creando el Behavior Tree y el BlackBoard que definirá el comportamiento del enemigo

Después de está teoría vamos a la práctica. Entra en la carpeta AIEnemy en el Content Browser y selecciona New/Miscellaneous/Behavior Tree ponle de nombre AIEnemyBehaviorTree y New/Miscellaneous/Blackboard y ponle de nombre AIEnemyBlackboard

En el objeto Behavior Tree mediante el Blueprint vamos a diseñar de forma visual todo el árbol de comportamiento del personaje y el Blackboard es la “fuente de conocimiento“ de nuestro personaje AI, o sea, todas las variables globales que necesite el personaje para poder definir su comportamiento las definiremos aquí y los nodos que tenga nuestro árbol y necesiten esta información, harán referencia a ella mediante los métodos Get Blackboard Key As o Set Black Board Key As. Por ejemplo, una variable que define una parte del comportamiento del personaje es la referencia al actor que tiene que perseguir si lo ve. Pues en este caso, esa variable o ese ”conocimiento” que necesita el personaje la tenemos que definir aquí.

Un detalle a aclarar es que en el Blackboard lo que definimos en realidad son parejas de clave-valor donde el valor será del tipo de dato del que definimos para la clave.

Enemigo patrullando una zona del nivel

Vamos con la primera tarea que tiene el enemigo, patrullar una zona del nivel. Primero, pensemos… ¿cual es la lógica detrás de alguien que está vigilando un lugar ?… Simple, estará caminando de un lado hacia el otro, en cada punto crítico se detendrá unos segundos y pasará al siguiente, y así en ciclo infinito (a menos que se canse y se quede dormido :) …. )

En el modelo side-scroller que estamos usando en nuestro juego esto es muy simple porque como todo el desplazamiento se hace en un solo eje, básicamente el enemigo se estaría moviendo de un lado al otro y nada más. Por este motivo es que cambiamos el estilo de nuestro juego temporalmente, para complicar un poco el movimiento que tiene que tener el enemigo y hacer más interesante nuestro estudio ;). Vamos a hacer que este enemigo esté caminando por 4 puntos clave del nivel. Se moverá al primero, cuando llegue se detendrá unos segundos, pasará para el segundo, se volverá a detener y así…

Vamos primero a crear los objetos que agregaremos al nivel y usaremos como puntos clave en el camino. En estos puntos es donde se detendrá el personaje antes de pasar al siguiente y definirán la trayectoria que patrullará. En el Content Browser crea un nuevo Blueprint que herede de TargetPoint dale de nombre AIEnemyTargetPoint. TargetPoint es un simple Actor con una imagen que lo identifica en el Editor cuando lo agregamos al nivel, esta imagen no se ve en el juego, solo en el editor. Dale doble clic y créale una variable int de nombre Position y márcale el iconito del ojito que tiene a la derecha de la variable para hacerla pública. Esto nos permite acceder al contenido de esta variable desde un blueprint externo a ella, el mismo concepto de un atributo publico en programación orientada a objetos. De hecho, es exactamente esto, AIEnemyTargetPoint es una clase que hereda de TargetPoint y que tiene un atributo público de nombre position y tipo int. En este atributo vamos a tener el “orden“, por llamarlo de alguna manera, que tiene que seguir el personaje en su recorrido.

Blueprint del AIEnemyTargetPoints

Agrega al nivel cuatro AIEnemyTargetPoints y distribúyelos en el escenario por distintos puntos, definiendo el camino que quieres que recorra el personaje.

Ahora ve seleccionando uno a uno y en el panel de propiedades verás en la sección Default la variable Position, ve por cada uno y ponle 0, 1, 2, y 3. La idea es que el personaje comience en el punto 0, después se mueva el 1, después al 2, después al 3 y después de nuevo al 0. A mi me quedó así:

Nivel modificado con 4 AIEnemyTargetPoint

Muy bien, ya tenemos listo al enemigo y también los puntos que definen el camino que tendrá que patrullar, ahora solo falta implementarle la “inteligencia“ para que se mueva de un punto a otro en ciclo y esto lo haremos mediante el Behavior Tree y sus componentes. Vamos a agregarle las primeras ramas al árbol de comportamiento de nuestro personaje.

Antes, déjame comentarte en general cual sería la lógica que seguirá ese personaje para lograr su comportamiento. En el nivel tenemos 4 puntos que marcan el recorrido, el personaje necesita saber en que punto está actualmente del recorrido y la posición de ese punto en el espacio (vector 3D). Una vez que registre al punto al que se va a mover, que actualice su siguiente punto. Cuando termine el movimiento que espere unos segundos en la zona y pase al siguiente punto.

Abre el BT que creamos y en el en el panel de Detalles en la sección BT selecciona para el atributo Blackboard Asset el Blackboard que acabamos de crear. Ahora abre el Blackboard y vamos a agregar a este los pares Clave-Valor que necesitamos. Da clic en el botón de agregar nueva variable y ponle de key TargetPointNumber de tipo de dato int. Crea otra clave y ponle TargetPointPosition de tipo de dato Vector. En la primera vamos a guardar el punto al que se tiene que dirigir el personaje y en TargetPointPosition vamos a tener la posición de ese punto para poderle decir después que se mueve hacia ahí.

Blackboard con los dos primeros Keys: TargetPointNumber y TargetPointPosition

Ahora vamos a crear el primer Nodo de nuestro BT. Crea un nuevo Blueprint en el Content Browser que herede de BTTask_BlueprintBase y ponle de nombre UpdateNextTargetPointTask. BTTask_BlueprintBase es la clase que nos brinda el UE4 para los nodos de tipo Task del BT y que se van a usar mediante el Blueprint.

Los nodos Task serán las hojas del árbol (los nodos que no tienen hijos) y su objetivo es ejecutar una acción determinada. Este Task que acabamos de crear lo que hará será comprobar el valor que tiene la clave TargetPointNumber en el Blackboard y validar que si es mayor igual que 4 la reinicia a 0 para que del cuarto punto pase de nuevo al primero. Obtendrá la posición en el espacio del TargetPoint al que le toca moverse y setteará el valor de la entrada TargetPointPosition del Blackboard, para que en otro Task se ejecute la acción de moverse a ese punto. Esto se pudiera hacer en este mismo Task, pero vimos que uno de los objetivos más claros que tiene el BT es separar las acciones en subacciones concretas. Por lo que para esta tarea de patrullar en general la zona tendremos tres Task que serán nodos hijos de una secuencia.

Dale doble clic al Task que acabas de crear y agrega dos variables TargetPointNumber y TargetPointPosition ambas de tipo BlackboardKeySelector. Ahora comienza a agregar nodos y hacer conexiones hasta que el árbol te quede como el siguiente:

UpdateNextTargetPointTask. NOTA: Como el blueprint es un poco grande, para armar la imagen inventé un poco con capturas de pantalla de cada una de las zonas del blueprint y un poco de Photoshop. Si me puedes decir si existe alguna buena forma para exportar el blueprint completo como imagen sería genial :) …. Puedes darle clic a la foto para verla a tamaño completo.

… no voy a ir paso a paso en como tienes que hacer esto, porque si has seguido los tutoriales anteriores lo tienes que saber hacer. Por supuesto, sí vamos a explicar la lógica que sigue el algoritmo.

Comenzamos el algoritmo cuando se dispara el Evento Execute del Task, lo primero que hacemos es comprobar que el valor que esté registrado en el TargetPointNumber del BlackBoard sea mayor que 4. Recuerda, en esa variable tenemos el Position del TargetPoint al que tiene que moverse el personaje, por lo que tenemos que comprobar que si llega al último reinicie su valor al primero. Para obtener el valor de una variable que tenemos en el BlackBoard se usa el nodo GetBlackBoard As [Tipo de dato], en este caso int y le tenemos que decir cual es el Key que vamos a modificar y es para esto que creamos las dos variables al inicio de tipo BlackBoardKeySelector.

Una vez que tenemos el Position del TargetPoint al que le toca ir al NPC vamos a buscarlo en el nivel. Para esto usamos el Nodo Get All Actors of Class que le podemos definir en el puerto Actor Class el tipo de clase que queremos obtener, aquí seleccionamos AIEnemyTargetPoint y nos retornará en el puerto de salida un arreglo con todos los actores que hay en el nivel de tipo AIEnemyTargetPoint.

Después recorremos este arreglo con el nodo ForEach, tenemos que castear cada elemento del ciclo a AIEnemyTargetPoint y eso lo hacemos con la ayuda del nodo Cast To AIEnemyTargetPoint, dentro del ciclo preguntamos en cada iteración si la propiedad Position (que creamos como publica en el Blueprint del AIEnemyTargetPoint) es igual al valor que tenemos registrado en el TargetPointNumber del BlackBoard, y si es igual obtenemos la posición en el espacio de ese actor y seteamos el valor del otro Key que tenemos en el Blackboard, el TargetPointPosition para almacenar ahí el vector con la posición del siguiente TargetPoint al que se moverá el enemigo en su recorrido.

Por último, como dijimos, los Task tiene que terminar de dos formas: con Éxito o con Fallo. Según la forma en la que termine definirá el comportamiento de su nodo padre. En este caso, como todo el proceso lo tendremos dentro de un Sequence, necesitamos que termine en éxito para que el nodo Sequence siga ejecutando al siguiente nodo. Fíjate que al salir del puerto Completed del ForEach, el que se ejecuta cuando termina el ciclo, incrementamos primero el valor del TargetPositionNumber, para que la próxima vez que se ejecute este Task se obtenga la posición del siguiente TargetPoint en el nivel. Por último terminamos la ejecución del Task con el nodo FinishExecute, asegúrate de marcar el puerto Success. Guarda y compila.

Ahora solo queda configurar el Behavior Tree con este Task. Abre el AIEnemyBehaviorTree. Inicialmente solo tenemos un nodo, el nodo raíz del árbol. Arrastra desde el borde inferior del nodo Root y conecta a este un nodo Sequence, este nodo los usaremos cuando queramos ejecutar un grupo de Task en secuencia, una detrás de la otra, comenzando por la izquierda, siempre ten en cuenta que para que se ejecute el siguiente Task, el anterior tiene que haber retornado con Éxito.

Arrastra del borde inferior del nodo Sequence y agrega el Task UpdateNextTargetPointTask, selecciona el nodo y en el panel de detalles en la sección Default, el campo Target Point Number selecciónale la opción TargetPointNumber y en Target Point Position selecciona la opción Target PointPosition. Estos combos lo que listan son todos los keys que tenemos registrado en el Blackboard, lo que estamos haciendo es definiendo que esas dos variables que tenemos en el Task representan esos Keys del Blackboard

Un momento … sin tener que correr te darás cuenta que con esto no es suficiente, porque lo que hace el Task es solamente darle valor a las variables TargetPointPosition y TargetPointNumber y más nada. Arrastra del borde inferior del Sequence y agrega el Task: “Move To“. Este Task por defecto nos lo brinda UE4 y lo que hace es decirle al AICharacter que se mueva a una posición determinada. Selecciona en el panel de Detalles en la propiedad Blackboard Key la variable TargetPointPosition. Con esto estamos definiendo cual será el Key del Blackboard que tendrá la posición a donde se moverá el personaje, está posición es obtenida en el Task anterior según el TargetPoint al que le toque ir al personaje.

Con esto es suficiente, pero si corremos en este punto, el personaje irá caminando pasando por los 4 puntos sin detenerse, y evidentemente esto hará que se canse más rápido y se nos quede dormido al momento :S … para evitar esto agrega un tercer Task al sequence, ahora el Task ”Wait”. Este Task, también incluido en el UE4 nos permite detener la ejecución del árbol un tiempo determinado. En el panel de Detalles en el atributo Wait Time puedes definir el tiempo que estará detenido, yo puse 2.

Listo !!, el BT te tiene que quedar así.

Behavior Tree con los nodos necesarios para que el enemigo se mueva constantemente entre los TargetPoints que tenemos en el nivel

Es válido aclarar en este punto un detalle. El Nodo Wait lo que hace es detener completamente la ejecución del árbol, por lo que si en este intervalo de tiempo nos acercamos al personaje, el NPC no nos detectará, ya que todo el árbol está detenido. De cualquier forma creo que es un ejemplo válido del uso del Task Wait.

Configurando el AIController para que use el Behavior Tree que define su comportamiento

Solo nos falta una cosa, sí, ya tenemos configurado el Behavior Tree, pero en ningún lado le hemos dicho al AIController que use este BT para definir su comportamiento. Para esto, abre el AIEnemyController agrega el nodo Begin Play y conéctalo a un nodo de tipo Run Behavior Tree en el puerto BTAsset selecciona el AIBehaviorTree. Tan simple como eso.

AIEnemyController configurado para que ejecute el Behavior Tree

Compila, ejecuta el juego y muévete hacia el enemigo. Verás como estará rondando de punto a punto y en cada uno esperará 2 segundos antes de continuar. :) … puedes en lo que está en ejecución el juego abrir el Behavior Tree para que veas porque rama está el árbol en cada momento.

Juego en ejecución con el enemigo recorriendo los puntos que definimos

Creando un Service en el Behavior Tree para que el personaje esté al tanto de quien se acerque a la zona.

Ya tenemos al enemigo haciendo su recorrido rutinario, pero bastante poco eficiente es como vigilante no ? . . . por más que nos acerquemos a la zona, ni se inmuta :(. Vamos entonces a “enseñarle“ a vigilar la zona, o sea, a que detecte cuando nos acerquemos.

Como ya vimos, en el BT, los Task son para ejecutar una acción determinada cuando sea el momento adecuado y retornar un resultado. Pero en este caso no es exactamente esto lo que necesitamos, aquí necesitamos ejecutar un proceso constantemente ya que un buen vigilante tiene que estar atento, contantemente, para saber si alguien se aproxima :) . Para esto tenemos otro tipo de Nodo en el Behavior Tree, que son los Services. Service es el nodo que tenemos en el Behavior Tree para tener un proceso ejecutándose contantemente cada un intervalo de tiempo determinado, y es exactamente este tipo de nodo el que tenemos que usar para implementarle la tarea de vigilante a nuestro NPC.

En el Content Browser dentro de la carpeta AIEnemy crea un nuevo Blueprint que herede de BTService_BlueprintBase y ponle de nombre CheckNearbyEnemy. Antes de editar este Blueprint, tenemos que agregar una nueva entrada en el BlackBoard. Un nuevo “conocimiento“ que tendrá que tener el NPC, y es el actor al que va a seguir cuando detecte que hay alguien en la zona.

Abre el Blackboard y agrega un nuevo Key de tipo Object y ponle de nombre TargetActorToFollow. Aquí guardaremos la referencia a nuestro personaje protagónico cuando nos acercamos a la zona que vigila el enemigo.

Abre el CheckNearbyEnemy y primero crea las siguientes variables que usaremos en este visualscript:

DesiredObjectTypes de tipo EObjectTypeQuery y en la sección Default Value del panel de Detalles de esta variable, selecciona Pawn. En un segundo te explico donde la usaremos.

TargetActorToFollow: De tipo BlackBoardKeySelector y como ya hicimos en el Task que creamos, esta variable la usaremos para obtener el valor de la clave con este nombre en el Blackboard. Esta variable créala pública dando clic en el iconito del ojito que tiene a la derecha.

Te explico lo que tiene que hacer el algoritmo para que lo intentes crear por tu cuenta, de cualquier forma te dejaré también la imagen de referencia.

Los nodos de tipo Service en el BT cuentan con el evento Event Receive Tick, este evento es muy parecido al Tick que ya vimos en el tutorial pasado, solo que a diferencia de ese, este se ejecuta a un intervalo fijo configurable. Lo que vamos ha hacer constantemente, gracias al evento Tick del Service, es generar una esfera “imaginaria“ alrededor de todo el NPC mediante el Nodo Multi Sphere Trace for Objects. Este es un método súper útil que nos da el UE4 y nos permite generar un esfera imaginaria en una posición determinada con un radio que queramos y obtener todos los objetos que estén dentro de esa zona de la esfera. Este método espera los siguientes parámetros:

Puntos de Inicio y fin de la línea que usará como referencia para generar la esfera, en este caso usamos la posición del NPC como punto de inicio y el mismo punto en X y Y pero solo con un poco más de altura para que sean distintos los puntos (si inicio y fin son iguales, no funcionará). Usamos la posición del NPC para que siempre la esfera se genere alrededor de este personaje.

El parámetro Radio es bastante claro, es el radio de la esfera. Le damos un valor bastante grande y definirá el alcance de vigilancia del enemigo a la redonda.

El parámetro Object Types es un arreglo que define los tipos de objetos que queremos tener en cuenta para ese chequeo. En este caso queremos tener en cuenta a los objetos de tipo Pawn. Y para esto fue que creamos la variable DesiredObjectTypes y le dimos el valor de Pawn por defecto. Fíjate que este parámetro es un arreglo, podemos pasarle más de un tipo, pero en este caso solo queremos tener en cuenta a los Pawns por eso es que creamos un arreglo de un solo elemento.

Trace Complex: No necesitamos tenerlo en true, se usa para chequeo en mallas de colisiones complejas, pero requiere mayor consumo de recursos. Como este no es el caso de nosotros, lo podemos dejar en false.

Actors To Ignore: Otro muy útil parámetro que recibe este método. Nos permite definir todos los actores que queremos que ignore el método, o sea que aunque estén dentro de la esfera y sean un Pawn no se van a devolver en el resultado. Y aquí lo usamos para ignorar el propio Pawn del enemigo. Lógico no ?, el enemigo siempre va a estar dentro de la esfera pero él no será un resultado que nos interese.

Draw Debug Line es un muy útil parámetro que nos sirve para debugear la esfera en el nivel. Para esta prueba ponlo en For Duration y podrás ver el comportamiento de esta esfera imaginaria en el nivel cuando lo probemos.

Cada vez que se ejecute este método como resultado tendremos todos los actores que están dentro de la esfera, y por tanto cercanos al enemigo o false si no se encuentra ninguno. Esto es lo que haremos en la primera parte del algoritmo. Paso a paso sería:

1 – Casteamos el Actor que viene en el Tick a AIEnemyController que es el Controller de nuestro enemigo.

2 – Obtenemos el Pawn de ese Controller con el nodo Get Controller Pawn.

3 – Obtenemos la posición del Pawn con el nodo Get Actor Location

4 – Creamos u nuevo vector a partir de la posición del Pawn incrementándole solo en el eje Z un poco. Estas dos posiciones las pasamos como parámetro al nodo MultiSphereTrace for Objects. Además le pasamos los otros parámetros que necesita. Fíjate que la línea blanca define el flujo de ejecución del algoritmo.

Una vez que se ejecuta MultiSphereTrace for Objects tenemos en el puerto de salida Out Hits un arreglo de objetos HitResults y en el puerto Return Value, true, si se encontró algún objeto y false en caso contrario.

En este punto necesitamos una condición y para eso usamos el Nodo Branch que recibe el flujo de ejecución (los puertos blancos) y permite separar el script en dos flujos según el resultado de la condición, este nodo es el clásico IF de programación.

Si MultiSphereTrace for Objects retorna true quiere decir que encontró resultados. Entonces, continuamos el flujo del programa con el nodo ForEachLoopWithBreak, este nodo como su nombre lo indica, es el clásico for each de C++ que permite iterar un arreglo de elementos y detenerlo, llamando al break, cuando se cumpla la condición que buscamos. Iteramos entonces todos los actores que se encontraron dentro de la esfera. Fíjate que tenemos que conectar el puerto de salida Hits del MultiSphereTrace for Objects al puerto de entrada Array del ForEachLoopWithBreak para hacerle saber a este cuál es el array que va a iterar.

El puerto Loop Body del ForEachLoopWithBreak es el flujo del programa en cada iteración del ciclo, entonces, dentro del ciclo hacemos otro Branch para preguntar si el Actor que se encontró es el Player y si es así lo guardamos en el Blackboard en el key TargetActorToFollow. El MultiSphereTrace for Objects lo que retorna es un array de HitResults esta estructura tiene muchísima información del punto de interacción, no es solo el actor, por lo que necesitamos el nodo Break Hit Result para obtener el actor, este nodo en el puerto de salida Hit Actor nos da el Actor. Fíjate que para obtener el Pawn usamos el método Get Player Character que ya hemos usado en los tutoriales anteriores. Fíjate también que una vez que setteamos el valor del TargetActorToFollow pasamos el flujo al Break del ForEach porque ya no necesitamos más nada.

Bien, este es el caso donde se encuentra al personaje protagónico dentro de la esfera. Pero para el caso en el que el Actor que se encuentre dentro de la esfera no sea el personaje protagónico o que el método MultiSphereTrace for Objects retorne false, que quiere decir que no hay ningún Pawn dentro de la esfera, hay que dejar la variable TargetActorToFollow en NULL para poder determinar, con otro tipo de nodo del BT que veremos ahora, si tenemos al personaje protagónico cerca y lo seguimos, o no, y entonces seguimos con la rutina de vigilancia normal.

Por último, solo para testear, puedes agregar un nodo Print String cuando seteamos el valor del TargetActorToFollow con el texto Detectado Enemigo cercano. Este nodo imprime en la pantalla el texto que le indiquemos como parámetro y nos servirá aquí para ver el resultado de nuestro Service en ejecución

Finalmente, el Blueprint te tendrá que quedar así:

CheckNearbyEnemy Service

Agregando el Service CheckNearbyEnemy al Behavior Tree.

Solo resta agregar este nuevo nodo al BT. Abre el AIEnemyBehaviorTree, elimina el Link entre el nodo ROOT y el Sequence. Arrastra el borde inferior del ROOT y agrega un Selector nuevo. Da clic derecho sobre ese nuevo selector/Add Service y selecciona CheckNearbyEnemy. Se te agregará dentro del Selector. Selecciónalo y en el panel de Detalles la sección Default el atributo TargetActorToFollow (que es la variable que le declaramos al blueprint como pública) selecciónale como valor TargetActorToFollow del combo que hace referencia a las variables declaradas en el BlackBoard. Fíjate también que en la sección Service podemos modificar el intervalo del Tick y un aleatorio de desviación, para que no siempre se ejecute exactamente en un intervalo fijo. De momento podemos quedarnos con los valores por defecto.

Behavior Tree con el Service CheckNearbyEnemy y la secuencia de Tasks UpdateNextTargetPoint, MoveTo y Wait.

Guarda y ejecuta el juego. Veras el debug en rojo de la esfera que se ejecuta alrededor del enemigo. Recuerda que esto es un debug, después le quitas el valor que le dimos al parámetro Draw Debug Line y no se verá nada. Ahora camina con el personaje cerca del enemigo. Inmediatamente que entres dentro del espacio de la esfera, se imprime en la pantalla el texto “Detectado Enemigo Cercano“ (recuerda agregar ese nodo Print String al Blueprint para que puedas ver esta salida)

Captura del juego en ejecución, se puede ver el debug de la esfera alrededor del NPC definiendo su zona de alcance y al estar el personaje protagónico dentro de esa zona se imprime en la pantalla, a modo de debug, el texto: Detectado enemigo cercano

Creando otro Task en el Behavior Tree para perseguir al personaje cuando el enemigo detecte que está cerca.

Súper hasta ahora verdad !? :) … pero seguimos teniendo un enemigo un poco ”bobo” porque a pesar de ya detectar que nos acercamos a su zona de seguridad, no hace nada, sigue sin inmutarse. Vamos a solucionar esto, y para ello necesitamos otro Task. Como dijimos, los Task son para ejecutar acciones concretas y es exactamente eso lo que queremos. Cuando el enemigo nos detecte, que nos persiga.

Vamos al Content Browser, en la carpeta AIEnemy y crea un nuevo Blueprint que herede de BTTask_BlueprintBase y dale de nombre MoveToEnemyTask. Ábrelo para editarlo y agrega la variable TargetActorToFollow de tipo BlackBoardKeySelector y hazla pública. Después crea el siguiente algoritmo.

Algoritmo completo del MoveToEnemyTask en el Blueprint

Que hacemos aquí ?. Iniciamos el algoritmo cuando se lanza el evento Execute del Task, casteamos el Owner Actor a nuestro AIEnemyController, obtenemos el Pawn del enemigo y usamos un método SUPER GENIAL que nos da el UE4, el nodo AI Move To. Con este método podemos indicarle a un NPC que persiga a otro actor … deja que lo veas funcionando ¡!!, parece mentira el poder lograr algo tan complejo con un solo nodo :)

El nodo AI Move To espera como parámetros el Pawn, un Destino que será un vector fijo u otro Actor que será al que perseguirá el Pawn pasado en el primer parámetro. En este caso usaremos el puerto Target Actor y le pasaremos el actor que está guardado en el TargetActorToFollow del BlackBoard, que contiene la referencia a nuestro personaje cuando nos acercamos a la zona de vigilancia del enemigo gracias al Service que creamos hace unos minutos. Por último, llamamos al Finish Execute cuando el AI Move To retorna On Success que quiere decir que el enemigo nos alcanzó. Fíjate que aquí también podemos usar el Print String para ver en pantalla exactamente cuando el enemigo llega a nosotros.

Creando un Decorator en el Behavior Tree

Espera ! ! ! … de seguro que estás loc@ por conectar este Task al árbol y probar, pero tenemos un pequeño detalle. Este Task solamente lo podemos ejecutar si la variable TargetActorToFollow tiene valor en el BlackBoard, porque como vimos si el CheckNearbyEnemy determina que el personaje no está cerca, deja en NULL está variable y entonces no tiene porqué ejecutarse esta rama del árbol de comportamiento, sino que pasa a la sección de vigilante.

Para ejecutar condiciones en el Behavior Tree que determinen si continuar ejecutando una rama determinada del árbol tenemos otro tipo de nodo que son lo Decorators. Vamos entonces a crear un decorator para comprobar si la variable está seteada, y si es así, entonces se puede ejecutar este Task, sino, pasa a la otra rama del árbol.

Arrastra del borde inferior del CheckNearbyEnemy y crea un nuevo Selector, dale clic derecho y selecciona Add Decorator y selecciona Blackboard. En este caso no vamos a crear un Decorator personalizado, vamos a usar como mismo usamos el Task Wait y Move To que ya vienen con el UE4, el decorator Blackboard, que nos permite comprobar el valor de un key en el BlackBoard. Selecciona el decorator y en la sección Flow Control de panel de Detalles, en el atributo Observer aborts selecciona Both. Este parámetro define como se comportará el árbol cuando no se cumpla la condición. En este caso lo que queremos es que inmediatamente que TargetActorToFollow esté en NULL se pase a ejecutar la otra rama, para que continúe como vigilante. Prueba cambiar después este valor a Self para que notes la diferencia del comportamiento.

En la sección Blackboard, en el atributo BlackBoard Key, selecciona TargetActorToFollow para definirle que este es el key del blackboard que queremos comprobar y en Key Query selecciona Is Set para definirle el tipo de condición que queremos comprobar sobre el TargetActorToFollow.

Por último arrastra del selector que tiene este Decorator y conecta el Task MoveToEnemyTask, selecciónalo y en la sección Default, en el atributo Target Actor To Follow selecciona TargetActorToFollow.

Versión final del Behavior Tree del enemigo que vigila una zona alrededor de 4 puntos, si detecta que nos acercamos, se mueve hacia nosotros y si nos alejamos regresa a su tarea de vigilante

Listo ¡!! Guarda, compila y ejecuta el juego y muévete cerca del enemigo, cuando te vea corre rápido para que te le alejes, verás como inmediatamente que nos alejamos se incorpora a su tarea rutinaria. Si nos quedamos quietos, y nos alcanza, de momento solo imprimimos en la pantalla un mensaje. En próximos tutoriales haremos algo más interesante.

Vista del juego en ejecución y del Behavior Tree para analizar por donde va la ejecución del árbol,

Conclusión

Espero que este tutorial te sirva para iniciarte en el complejo mundo de la IA en los juegos desarrollados con Unreal Engine 4. Como has notado es un tema relativamente complejo, por lo que te recomiendo que le des más de una pasada al ejercicio, pruebes distintas cosas, juegua con los valores de retorno de los Tasks para que puedas lograr buena soltura y entender al 100% como es que funciona esta genial herramienta que nos da el Unreal Engine 4 para implementar la inteligencia artificial de los NPC en nuestros juegos.

En el próximo tutorial vamos a continuar con este ejemplo implementado los mecanismos para que este enemigo nos haga algo cuando llegue a nosotros. También le dedicaremos un tiempo a ver esta misma implementación pero desde C++, y así analizamos las dos variantes 😉

Mientras, me gustaría escuchar tus comentarios. Recuerda que puedes estar al tanto de los siguientes tutoriales siguiéndome en mi Twitter @nan2cc y si tienes alguna sugerencia de tema para un próximo tutorial me encantaría que me la hagas saber.

Hasta la próxima !!

Fernando Castillo Coello
Follow me

Fernando Castillo Coello

Gameplay Programmer at GameOlic
I've been into games development with Unreal Engine since 2014 and want to share with you some of the tips and tricks that I learned during my journey.
Fernando Castillo Coello
Follow me

8 pensamientos en “Introducción a la Inteligencia Artificial en Unreal Engine 4

  1. Pingback: Implementando un AI Character con un arma en UE4 | El blog de Fernando Castillo Coello

  2. Javier

    Muchas gracias por los tutoriales, es estupendo tener este material gratis, hay algún sitio que tenga una base de datos con modelos en 3d como para hacer un juego completo? así no tendría que aprender de momento a usar el 3ds max por ejemplo.

    Yo tambien te recomiendo lo del canal en youtube pero compaginandolo con el tutorial escrito, sería genial.

    Saludos y espero que sigas entusiasmado con la enseñanza!

    Responder

Responder a Marco Antonio González Núñez Cancelar respuesta

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *

Puedes usar las siguientes etiquetas y atributos HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>