Archivo de la etiqueta: ia

Implementando un AI Character con un arma en UE4

Hola, soy Dariel de la Noval (@darielns), Lead Programmer en Spissa Software Solutions. He estado siguiendo los tutoriales de @nan2cc y la aceptación que han estado teniendo y me he embullado a sumarme en esta serie de tutoriales sobre el desarrollo de juegos con Unreal Engine 4, aquí voy con mi primer tuto, ya me dirás que te parece.

En este tutorial vamos a unir muchas de las cosas que ya hemos visto en tutoriales anteriores para agregar un enemigo a nuestro juego. Este enemigo estará patrullando una zona del nivel con un arma, al acercarnos, comenzará a dispararnos hasta matarnos. De igual forma, ya con el arma que configuramos para nuestro personaje en el tutorial pasado, podremos defendernos de estos ataques.

NOTA: Este tutorial ha sido desarrollado con Unreal Engine 4.4.3, si estás trabajando con otra versión puede que encuentres algunas diferencias, ya que el Engine está en constante actualización. De ser así, déjame tus comentarios al final del post y buscamos juntos la solución.

Importando los recursos necesarios al proyecto

La mayor parte de este tutorial toca muchos temas de Inteligencia Artificial que ya vimos a fondo en tutoriales anteriores, por este motivo en vez de desarrollar esta parte de nuevo, las importaremos directamente al proyecto.

Vamos a comenzar importando los recursos necesarios para tener rápidamente un personaje controlado por IA patrullando una zona del nivel y que detecte cuando nos acercamos a esa zona, como mismo hicimos en los tutoriales de IA anteriores.

Descarga los recursos necesarios desde aquí, al descomprimir el .zip tendrás lo siguiente:

Dentro de la carpeta M1Garand tendrás el FBX del modelo del arma que usará el enemigo. Este modelo al igual que el del arma del Player también fue descargada desde tf3dm.com

Dentro de la carpeta AIEnemy tendrás los siguientes recursos:

  • AIEnemyAnimBlueprint: Animation Blueprint del enemigo. Inicialmente solo contiene la lógica para las animaciones de reposo y caminando.
  • AIEnemyBehaviorTree: Behavior Tree (BT) encargado de la AI del enemigo. Inicialmente solo contiene el procesamiento para patrullar una zona y detectarnos si nos acercamos a esa zona.
  • AIEnemyBlackboard: BlackBoard asociado al BT del enemigo. Solo contiene 3 elementos: TargetPointNumber, TargetPointPosition e IsActorDetected. Los dos primeros son utilizados en el algoritmo de patrullado y el tercero en el algoritmo de chequear cuando nos acercamos.
  • AIEnemyCharacterBlueprint: Blueprint del enemigo.
  • AIEnemyController: Controlador del enemigo. Inicialmente solo contiene la asignación del BT.
  • AIEnemyTargetPoint: Target Point que se utilizara para definir los puntos claves en la zona de patrullado del enemigo.
  • CheckNearbyEnemy: Task del BT encargado de chequear cuando el player se acerca al enemigo.
  • IdleWalkBlendSpace1D: BlendSpace utilizado para hacer un blend entre la animación de Idle y Walk del enemigo.
  • UpdateNextTargetPointTask: Task del BT con la lógica para seleccionar el siguiente punto clave en el recorrido de patrulla del enemigo.

Para importar los recursos que están dentro de la carpeta AIEnemy, primero asegúrate de tener en tu proyecto el AnimStarterPack. Después, abre la carpeta donde se encuentra ubicado tu proyecto desde el explorador de ficheros del sistema operativo y copia dentro de Content, la carpeta AIEnemy.

Para el caso del FBX del arma que usará el enemigo, este impórtalo desde el FBX Import del Unreal como ya hemos hecho antes. Una vez que importes el arma, ábrela en el StaticMesh Editor, y como mismo hicimos para el arma del Player en el tutorial pasado, créale un Socket de nombre FireSocket en la punta del cañón.

Captura del StaticMesh Editor con el arma del enemigo cargada después de la creación del FireSocket

Captura del StaticMesh Editor con el arma del enemigo cargada después de la creación del FireSocket

 

Abre el editor y verás en el Content Browser los recursos importados. Puedes tomarte unos minutos para que le des un vistazo a cada elemento.

Por último, desde tutoriales anteriores estamos usando el AnimStarterPack que puedes descargar del MarketPlace. Pues, el personaje que viene en este paquete es el que usaremos como enemigo, pero como tendrá un arma en su mano, necesitamos crearle un socket como mismo hicimos para el Player en el tutorial anterior.

Abre el HeroTPP que viene en el AnimStarterPack y créale un socket en la mano, puedes usar como preview el arma que importamos para poder posicionar correctamente el socket, llámalo HandSocket.

Captura del Persona Editor después de crear el HandSocket para el enemigo

Captura del Persona Editor después de crear el HandSocket para el enemigo

 

Preparando la zona a patrullar

Prepara en el nivel la zona que quieres que patrulle el enemigo y enciérrala en un objeto de tipo NevMeshBoundVolume.

Agrega al nivel dos AIEnemyTargetPoints, estos los usaremos para definir el recorrido del personaje. Recuerda que nuestro juego es un scroll-side y nos movemos básicamente en un solo eje, por lo que el enemigo también se tendrá que mover por un solo eje, así que asegúrate de que los dos AIEnemyTargetPoints queden alineados y paralelos a la posición del PlayerStart.

A cada uno de estos AIEnemyTargetPoints hay que definirle el orden que representarán en el recorrido que estará haciendo el enemigo. Selecciona el primero y en el panel de Detalles, en la sección Default, dale valor de 0 a la variable Position. Selecciona el otro punto y dale valor de uno.

Captura del ViewPort de la escena con los dos TargetPoints agregados y la configuración correspondiente para la propiedad Position

Captura del ViewPort de la escena con los dos TargetPoints agregados y la configuración correspondiente para la propiedad Position

 

Por último, agrega al nivel dentro de esta zona, el AIEnemyCharacter que acabamos de importar y que tendremos en la carpeta AIEnemy. Recuerda ponerlo siempre alineado al Player Start.

Captura del ViewPort de la escena después de agregar el AIEnemyCharacter dentro de la zona que va a patrullar

Captura del ViewPort de la escena después de agregar el AIEnemyCharacter dentro de la zona que va a patrullar

 

Compila, lanza el juego y muévete hacia el enemigo. Verás cómo estará rondando de punto a punto y en cada punto esperará 2 segundos. Cuando detecte que estamos cerca de él se imprime en la pantalla, a modo de log, el mensaje: Detectando Enemigo Cercano!!

Te recuerdo que si tienes algún problema con esta parte por la que hemos pasado bastante rápido, dale un vistazo a este tutorial donde implementamos toda esta lógica de IA paso a paso.

Preparando el arma que usará el enemigo

Como ves, con lo hecho hasta ahora, tenemos el enemigo patrullando una zona del nivel pero por las animaciones que tiene puedes darte cuenta que le está faltando su arma. Vamos a agregar un arma en las manos de este personaje siguiendo el mismo principio que usamos en el tutorial pasado. En ese tutorial, colocábamos algunas armas en el escenario para que el Player las recogiera, pero si recuerdas, nunca creamos un blueprint de estas armas, implementamos toda su lógica desde C++ y después la agregábamos al nivel desde la sección All Classes.

Eso está bien, pero en el mundo Unreal una muy buena práctica es crear un Blueprint a partir de las clases que tengamos implementadas en C++. Esto nos permite, por ejemplo, modificar varios parámetros de la clase a nivel visual, una sola vez. Por ejemplo, imagínate que queramos tener en distintas zonas del nivel varias armas del mismo tipo. Si creamos un blueprint a partir de la clase Weapon que implementamos en C++, le definimos el Mesh y las propiedades que queramos una sola vez desde el blueprint, y después agregamos las instancias de este blueprint al nivel.

Vamos a crear un blueprint a partir de la clase Weapon que implementamos en C++. En el tutorial pasado implementamos toda la lógica del arma que usa el personaje, la SPAS12. Si quisiéramos usar la misma lógica de esta arma para el arma del enemigo, pudiéramos crear el blueprint a partir de esta clase que ya hicimos, pero por ejemplo, supongamos que en el arma de este enemigo la lógica del disparo sea distinta. En este caso creamos la clase para que herede de Weapon que es la clase base de las armas, y entonces implementamos en ella las funcionalidades específicas de esta arma.

Dentro de la carpeta Weapons crea un nuevo Blueprint que herede de la clase Weapon que implementamos en el tutorial pasado y ponle como nombre M1GarandWeaponBlueprint. Ábrelo, y en la sección Components, muévete hasta la propiedad WeaponMesh y en el panel de detalle dentro de la sección Static Mesh selecciona el Mesh del rifle que importamos al inicio.

Captura del M1GarandWeaponBlueprint que acabamos de crear.

Captura del M1GarandWeaponBlueprint que acabamos de crear.

 

Compila, salva los cambios y cierra este Blueprint.

Equipando al enemigo con un arma

Para el Player, en el tutorial pasado, acoplábamos el arma al socket una vez que este la recogía del escenario. Pero en este caso, desde el inicio del juego el enemigo tendrá equipada su arma. Para lograr esto le agregaremos un nuevo componente desde el Blueprint, el Child Actor Component.

Abre el AIEnemyCharacterBlueprint y desde la sección Components, despliega el combo Add Component, y selecciona Child Actor.

tuto8_imagen_06

El Chilld Actor nos permite agregar como componente de un Actor, otro Actor cualquiera. En este caso lo usaremos para agregarle el arma.

En este punto me gustaría comentarte otro componente parecido, el StaticMesh. Este nos permite agregar un StaticMesh cualquiera como componente hijo del Actor. Un ejemplo de su uso pudiera ser si este personaje tuviera un sombrero y el sombrero fuese un elemento independiente del modelo original. En este caso podemos agregarlo como StaticMesh al personaje, y después anclarlo a un socket determinado, como mismo haremos con el arma.

Entonces, al ChildActor que agregamos ponle como nombre WeaponComponent, selecciónalo y muévete en el panel de detalles del componente hasta la propiedad Child Actor Component y aquí selecciónale el blueprint del arma que preparamos para el enemigo.

7

Solamente nos queda anclar el arma al socket en la mano del personaje y esto lo haremos en el Construction Script. En el modo Graph selecciona la pestaña Construction Script, en la que inicialmente solo tenemos el nodo Construction Script.

Ya vimos en tutoriales pasados que el Construction Script se ejecuta en la inicialización del objeto, y es el lugar que tenemos para implementar las funcionalidades en el momento de la creación del Actor.

Vamos a anclar el arma al socket de la mano, y para ello utilizaremos el nodo Attach To. Este nodo permite adjuntar un elemento determinado a otro y recibe los siguientes parámetros:

Target: Este es el elemento que se desea adjuntar. En este caso, el componente WeaponComponent.
In Parent: A quien vamos a adjuntar dicho target. En este caso, al mesh del enemigo.
In Socket Name: Socket al cual se adjuntará el elemento Target. En este caso será al socket de la mano (HandSocket ).
Attach Type: Este será el modo en que se adjuntara. En este caso, Snap To Target.

Agrega al blueprint el nodo Attach To y asigna en cada parámetro los elementos correspondientes. Por último, enlaza el nodo Construction Script al Attach To. Te quedará de la siguiente forma:

Construction Script en el AIEnemyBlueprint para acoplar el arma en las manos de este personaje

Construction Script en el AIEnemyBlueprint para acoplar el arma en las manos de este personaje

 

Salva los cambios y compila. Cambia al modo componentes y podrás observar como ahora el rifle sale en las manos de este personaje.

Captura de la pre visualización del personaje enemigo después de compilar el Construction Script donde se acopla el arma al HandSocket

Captura de la pre visualización del personaje enemigo después de compilar el Construction Script donde se acopla el arma al HandSocket

 

Al correr el juego notarás que ya el enemigo tendrá el arma equipada y se moverá con ella en todo momento.

Implementado la lógica en el enemigo para que cuando nos detecte nos comience a disparar

Ya tenemos a nuestro enemigo patrullando con su arma una zona del nivel, pero a pesar de que el Player se le acerca, no le dispara, así que vamos a implementar la lógica para cuando nos detecte nos comience a disparar. Para esto crearemos un nuevo Task en el BT de este personaje, este Task se ejecutará cuando el enemigo detecte que tiene al Player cerca y básicamente lo que hará es rotarse hacia él y poner en true la variable que usaremos desde el Animation Blueprint para comenzar la animación del disparo.

Abre el AIEnemyAnimBlueprint y crea una nueva variable, dale de nombre IsShooting y de tipo bool. Guardar y cierra este Blueprint.

Crea un nuevo Task para el BT del enemigo y ponle como nombre RotateAndShoot y en él implementa el siguiente algoritmo.

RotateAndShoot Task, encargado de rotar el enemigo en la dirección del Player y pasar a true la variable para iniciar la animación de disparo

RotateAndShoot Task, encargado de rotar el enemigo en la dirección del Player y pasar a true la variable para iniciar la animación de disparo

 

Este Task se ejecutará cuando se detecte el personaje, pero fíjate que el enemigo puede detectar al Player estando en su dirección o no, por lo que primero tenemos que garantizar que este rote en la dirección del personaje antes que nos dispare. Esto lo logramos con un poco de matemática básica, rotando el vector de Location del enemigo y el vector de location del Player y a partir de ese vector resultante creamos un Rotator que afecte solo el eje X con el nodo Rotation From XVector y con el Rotator resultante actualizamos la rotación del enemigo. Hecho esto solo nos resta pasar a true la variable IsShooting para que comience la animación de disparo.

Este Task también tiene otro detalle. Si recuerdas cuando vimos los tema de IA a fondo comentamos que los Task puede tener 3 estados. Primero, que termina su ejecución satisfactoriamente, segundo, que termina su ejecución NO satisfactoriamente y un tercer estado que es mantener el Task en “pendiente“. Este es el caso en el que el Task sea una acción que demorará su ejecución, como es el caso. Recuerda que el task se ejecuta, el personaje rota hacia nosotros y comienza a disparar. Para hacer esto, y evitar que mientras el BT del personaje caiga en un ciclo sobre este Task mientras está disparando, fíjate que no llamamos al Finish Execute del Task.

Bien, entonces, como necesitamos que este Task se ejecute solamente si el personaje detecta que tiene al Player cerca, crearemos un nuevo Decorator en el BT para controlar esto. Arrastra del borde inferior del CheckNearbyEnemy y crea un nuevo Decorator de tipo Blackboard. Selecciona el decorator y en la sección Flow Control de panel de Detalles, en el atributo Observer aborts selecciona Both. En este caso lo que queremos es que inmediatamente que IsActorDetected esté en false se pase a ejecutar la otra rama, para que continúe como vigilante.

Selecciona el Decorator y en la sección Blackboard, en el atributo BlackBoard Key, selecciona IsActorDetected 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 IsActorDetected.

Por último arrastra del selector que tiene este Decorator y conéctalo al RotateAndShoot.

Captura del BT del enemigo después de agregar el RotateAndShoot y el decorator para controlar su ejecución.

Captura del BT del enemigo después de agregar el RotateAndShoot y el decorator para controlar su ejecución.

 

Listo ¡!! Guarda, compila, lanza el juego y muévete cerca del enemigo. Cuando este te detecta, se detiene de su tarea de patrullar, y en ese punto la variable IsShooting toma valor de true. Lo que nos queda es preparar la lógica de la animaciones para cuando esta variable esté en true, se comience a reproducir la animación del disparo.

Configurando animación de disparo del enemigo

Como las acciones de este enemigo son bastante básicas (solamente camina de un lado a otro y cuando nos detecta, se detiene ahí mismo y comienza a disparar desde el lugar) la lógica de la animación de disparar la podemos implementar como un nodo nuevo del StateMachine de este personaje, a este estado se pasará una vez que la variable variable IsShooting sea true y cuando sea falso se retornará a la animación de Idle/Walk.

Abre el StateMachine desde el AIEnemyAnimBlueprint y agrega el nodo Shooting con las transiciones correspondientes, fíjate que dentro de Shooting lo que hacemos es reproducir la animación Fire_Shotgun_Ironsights. Te quedará de la siguiente forma.

Maquina de estado del enemigo después de agregar el estado Shooting

Maquina de estado del enemigo después de agregar el estado Shooting

 

Salva los cambios, compila, lanza el juego y camina hasta el enemigo. Verás que cuando te le acercas comienza a ejecutar la animación de disparar, pero una vez que te alejas, comienza a caminar como si se estuviera deslizando y con la animación de disparando. Porque pasa esto?. Porque, a pesar de haber hecho la máquina de estado del enemigo correctamente, este solo regresa al estado de Idle/Walk una vez que la variable IsShooting está en false. En el Task RotateAndShoot ponemos esta variable en true una vez que el personaje está de frente al Player, pero en ningún punto del BT la ponemos en false.

Vamos a corregir este detalle y lo haremos en el Service CheckNearbyEnemy, en el punto en donde ya no se detecta que el Player está cerca vamos hacer que la variable isShooting vuelva a tomar su valor de false, para que el enemigo regrese de nuevo a su estado de caminando.

Señalado se encuentra la modificación al CheckNearbyEnemy para pasar la variable isShooting a false

Señalado se encuentra la modificación al CheckNearbyEnemy para pasar la variable isShooting a false

 

Compila, salva los cambios y corre el juego. Una vez que te acercas al enemigo este se detiene y comienza a disparar y si nos alejamos, continua caminando con la animación correcta.

Muy bien !!, ya tenemos a este personaje listo, pero aunque aparentemente nos dispara, por las animaciones que reproduce, en realidad en el momento del disparo no pasa absolutamente nada. Así que vamos ahora a implementar la lógica del disparo para poder determinar si le da al Player y poderle aplicar un daño a este.

Implementado lógica del disparo en el arma del enemigo

En el tutorial anterior, implementamos en C++ la lógica del disparo del arma que usa el Player. En este tutorial vamos a implementar la lógica del disparo del arma que usará el enemigo desde el Blueprint que creamos para esa arma. En realidad el modo de disparo es prácticamente idéntico y pudiéramos usar el ya implementado, pero lo vamos a hacer aquí de nuevo y desde blueprint, a modo de demostración.

En la clase Weapon, la clase base para todas las armas, tenemos el método virtual Fire, para poder implementarlo en cada arma según el tipo de disparo. Como tenemos el arma del enemigo en el blueprint M1GarandWeaponBlueprint, implementaremos aquí el método Fire de ella, pero antes tenemos que corregir algo que se nos escapó en el tutorial anterior.

En la declaración del método Fire de la clase Weapon, como atributos al macro UFUNCTION le pasamos BlueprintImplementableEvent. Este atributo permite que el método pueda ser implementado en el blueprint, pero tenemos que agregarle además el atributo BlueprintCallable, para que también se pueda llamar el método desde el Blueprint.

Abre el archivo Weapon.h y modifica los atributos del macro UFUNCTION del método Fire, para que te quede de la siguiente forma:

UFUNCTION(BlueprintImplementableEvent, BlueprintCallable, Category = "Weapon")
virtual void Fire();

Con esto ahora podemos implementar el método Fire en el Blueprint del arma del enemigo, y además llamarlo cuando se vaya a disparar el arma.

Abre el M1GarandWeaponBlueprint en el modo Graph y podrás agregar un nuevo evento, el Event Fire, y una vez agregado el nodo que representa este evento, podemos implementar toda la lógica que queramos. Vamos a implementar este evento de la siguiente forma:

VisualScript del evento  Fire del M1GarandWeaponBlueprint

VisualScript del evento Fire del M1GarandWeaponBlueprint

 

La lógica que seguimos aquí es prácticamente la misma que usamos en el tutorial pasado para el arma del Player. Primeramente lanzamos una línea imaginaria desde la posición del FireSocket, x unidades hacia delante. Recuerda que estas unidades están definidas en la variable ShotDistance, puedes darle el valor que prefieras a esa variable para definir el alcance del disparo.

Un detallito interesante a comentar, fíjate que para el rayo también restamos 30 unidades en el eje z, esto es un “parche“ por nuestro modo de juego scroll-side, las animaciones que tenemos y la posición en la que queda el rifle del enemigo cuando está disparando, que no queda alineado a la posición del Player, y debido a esto, si no hacemos este ajuste en la posición del rayo, este nunca colisionará con el personaje.

Otra posible solución y en realidad muy aplicada en este modo de juego, es que los personajes tengan una caja de colisión cuadrada algo ancha hacia los lados y estas colisiones controlarlas contra esta caja de colisión.

Bien, por último comprobamos si el rayo impactó con el Player y si es así le aplicamos un daño. Fíjate que, aunque no lo hacemos aquí, para las armas puede resultar interesante usar una variable para definir la cantidad de daño que esta ocasiona.

Por último, a diferencia del Fire del arma que usa el Player, en este caso no tenemos en cuenta las municiones. Un muy buen ejercicio que te puedes platear es implementar la lógica para que el enemigo tenga que recargar el arma si se queda sin municiones. Incluso, que pueda ir en búsqueda de municiones que estén en el escenario, recogerlas y después continuar con el ataque. En fin, lo puedes hacer todo lo complejo que quieras 😉

Muy bien, con esto ya tenemos el método de Fire de esta arma, ahora necesitamos llamar a este método en el momento en el que el enemigo hace el disparo. En el tutorial pasado implementamos un método llamado Attack en el HeroCharacter, donde comprobamos el arma que tiene equipada, e implementamos la lógica necesaria según el arma y finalmente llamamos al método Fire del arma. En realidad en este caso no es necesario un método así porque el enemigo ni tan siquiera tiene un inventario, tiene una sola arma y siempre la podrá disparar, pero vamos a aprovechar este punto para ver una cosilla nueva que tenemos en los blueprints. La posibilidad de crear funciones para encerrar determinada lógica y después poderla llamar simplemente como llamamos una función cualquiera.

Abre el AIEnemyCharacterBlueprint y fíjate que en el panel MyBlueprint, al lado del botón para crear una nueva variable, tenemos un botón para crear una función. Vamos a crear una nueva función de nombre Attack, en nuestro caso será muy simple, obtenemos la referencia del WeaponComponent y llamamos al método Fire, pero por ejemplo, si tu enemigo puede portar distintas armas la lógica de este proceso de atacar puede ser más compleja y por eso sería conveniente que la tengas en una función aparte, como mismo haríamos en C++.

Después de creada la función Attack, entra en su modo de edición e implementa lo siguiente:

Función Attack creada en el blueprint del enemigo. Esta función se ejecutará cuando el enemigo dispare su arma.

Función Attack creada en el blueprint del enemigo. Esta función se ejecutará cuando el enemigo dispare su arma.

 

Listo !! ya tenemos el método Fire del arma y el método Attack del enemigo, solo nos va quedando el evento que usaremos para llamar al método Attack.

Si abres la animación Fire_Shotgun_Ironsights en el Persona Editor y la analizas con detenimiento, verás que el momento exacto del disparo es un poquito después que inicia la animación. Pues bien, será en ese preciso momento donde ejecutaremos el método Attack que acabamos de crear y para esto usaremos los Notifies.

Crea un Notify casi al inicio de la animación Fire_Shotgun_Ironsights (donde se ve que es justamente el inicio del disparo) y dale de nombre FireNotify. Con esto haremos que el método que tiene toda la lógica del disparo del arma se llame en el momento justo.

Captura del Fire_Shotgun_Ironsights, después de crear el FireNotify

Captura del Fire_Shotgun_Ironsights, después de crear el FireNotify

 

Ahora abre el Blueprint Animation del enemigo, agrega el nodo del Notify que acabamos de crear y llama el método Attack, para que dispare el arma que tiene equipada cunado la animación pase por ese punto.

Trozo del AIEnemyAmimBlueprint con el algoritmo a ejecutar cuando se lanza el FireNotify

Trozo del AIEnemyAmimBlueprint con el algoritmo a ejecutar cuando se lanza el FireNotify

 

Compila, salva los cambios y corre el juego. Acércate al enemigo y verás que ya en el momento del disparo, se lanza el Trace para simular la trayectoria del proyectil, pero como notarás, el trace traspasa el Player. Esto pasa porque en la configuración de colisión del Mesh del Player, el Trace Response no lo tenemos configurado para que bloquee el trace.

Abre el HeroBlueprint en el modo de componentes, ve al árbol de componentes y selecciona el Mesh. Desplázate a la sección Colisiones y busca el combo Collision Presets y selecciona Custom. Luego marca como Block el parámetro Visibility de la sección Trace Responses.

Configuración del Trace Responses en el Mesh del Player para que el rayo que se lanza cuando el enemigo dispara su arma colisiones con este

Configuración del Trace Responses en el Mesh del Player para que el rayo que se lanza cuando el enemigo dispara su arma colisiones con este

 

Salva y lanza nuevamente el juego. Desplázate hacia el enemigo para que te dispare, verás como ahora el rayo si impacta en el personaje.

Captura del juego en ejecución en el momento en el que un disparo del enemigo (derecha) colisiona con el Mesh del personaje protagónico (izquierda)

Captura del juego en ejecución en el momento en el que un disparo del enemigo (derecha) colisiona con el Mesh del personaje protagónico (izquierda)

 

Implementado la lógica cuando el Player recibe daño

Ya tenemos implementado cuando el disparo del enemigo impacta con el Player y se le aplica un Damage, ahora vamos a implementar la lógica necesaria para restar salud al Player en cada disparo hasta que muera.

Primeramente necesitamos una variable en el Player que represente la salud de este. Abre el archivo HeroCharacter.h y agrégale el siguiente atributo:

/** Salud del Player, disminuye cuando recibe daño, al llegar a cero el personaje muere */
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Health)
float Health;

Por defecto, la salud del Player será 100. Abre el archivo HeroCharacter.cpp y agrégale al final del constructor lo siguiente:

//La salud del personaje inicialmente será 100
Health = 100;

Ahora, como vimos en el tutorial “Cómo causar daño a un personaje“, necesitamos implementar el evento “Any Damage“ que se dispara automáticamente en el Actor cuando recibe un Damage. Como toda la lógica de nuestro héroe, la hemos estado implementado en C++, implementaremos este método en C++ también. Aunque es válido aclarar, que si lo prefieres, lo puedes implementar en el Blueprint del Player.

De momento lo que haremos será restar la salud del Player cada vez que reciba daño, y cuando llegue a cero imprimir en la pantalla, a modo de log, que el Player ha muerto.

Abre el archivo HeroCharacter.h y agrega la declaración del método ReceiveAnyDamage para implementarlo.

/**
* Es llamado automáticamente por el Engine cuando se le aplica daño a este Actor
* @param Damage. Daño que se le aplica al player
* @param DamageType. Clase con la información del daño aplicado.
* @param InstigatedBy. Controller que causa el daño
* @param DamageCauser. Actor que causa el daño
*/
virtual void ReceiveAnyDamage(float Damage, const class UDamageType* DamageType, class AController* InstigatedBy, class AActor* DamageCauser) OVERRIDE;

Pasa ahora a HeroCharacter.cpp y vamos a implementar el método

/**
* Es llamado automáticamente por el Engine cuando se le aplica daño a este Actor
* @param Damage. Daño que se le aplica al player
* @param DamageType. Clase con la información del daño aplicado.
* @param InstigatedBy. Controller que causa el daño
* @param DamageCauser. Actor que causa el daño
*/
void AHeroCharacter::ReceiveAnyDamage(float Damage, const class UDamageType* DamageType, class AController* InstigatedBy, class AActor* DamageCauser)
{
	//Si el player esta vivo ...
	if (Health > 0)
	{
		//... decremento la vida con el daño aplicado
		Health -= Damage;
		
		//Mostramos un log en pantalla
		GEngine->AddOnScreenDebugMessage(-1, 15.0f, FColor::Red, FString::Printf(TEXT("La salud del personaje es : %f"), Health));
	}

	//Si la salud del Player llega a cero, muere !!
	if (Health == 0)
	{
		//Mostramos un log en la pantalla
		GEngine->AddOnScreenDebugMessage(-1, 15.0f, FColor::Red, "El player ha muerto");
	}
}

Compila y lanza el juego. Avanza hacia el enemigo para que te comience a disparar. Verás que cada vez que te dispara, se imprimen en pantalla las vidas restantes del Player y cuando llega a cero, se imprime el log que queremos.

Perfecto !!, ahora vamos a darle un poco de ”vida” a estos dos momentos, reproduciendo las animaciones en el Player cuando recibe los disparos y finalmente cuando muere.

Implementando la animación de Hit y Death del Player

Para la animación de muerte del Player usaremos la animación Death_1 que viene en el AnimStarterPack. Primero tenemos que hacerle el retarget para usarla en el esqueleto de nuestro personaje. Busca en el Content Browser, dentro del AnimStarterPack, la animación Death_1, dale clic derecho, Retarget Anim Assets/Duplicate Anim Assets and Retarget. Selecciona de aquí el esqueleto que usa nuestro HeroCharacter.

Abre el HeroCharacter.h y como mismo tenemos un atributo para cargar desde el editor el montage con las animaciones cuando el personaje tiene equipada el arma, vamos a crear otro atributo de tipo AnimationAsset para cargarle desde el editor la animación de muerte.

/* Animacion del personaje al morir */
UPROPERTY(EditDefaultsOnly, Category = "Animations")
UAnimationAsset *AnimationDeath;

Hecho esto, pasa a HeroCharacter.cpp y en el punto donde imprimimos el mensaje de muerte, vamos a cambiarlo para reproducir la animación de muerte:

//Reproducimos la animación de muerte
Mesh->PlayAnimation(AnimationDeath, false);

Compila y abre el editor. En el blueprint del personaje, asigna al atributo Animation Death, la animación Death_1 que preparamos.

Captura del HeroCharacterBlueprint donde le asignamos al atributo Animation Death el Asset de animación correspondiente

Captura del HeroCharacterBlueprint donde le asignamos al atributo Animation Death el Asset de animación correspondiente

 

Si en este punto corres el juego verás un errorcillo. Se reproduce la animación de muerte cuando la salud llega a cero, pero si en ese momento sigues tocando las teclas de moverte, el personaje sigue desplazándose por el escenario desde el piso, y esto evidentemente está terrible !!. Para arreglarlo, usaremos el método del Controller, UnPossess. Recuerda un poco la teoría que sigue el Framework de Unreal: El PlayerController “posee” al Pawn del personaje y mediante este es que se manejan las entradas del usuario. Con el método UnPossess hacemos que el Controller “desposea” al Pawn del personaje, y así quedan inutilizadas totalmente todas las entradas del jugador. Además, al hacer esto, el Service que tenemos en el enemigo que determina si estamos cerca del Player ya no nos retorna verdadero y con esto, una vez que el enemigo mate al Player, continuará patrullando la zona.

Regresa al HeroCharacter.cpp y después que reproducimos la animación de muerte agrega las siguientes líneas:

//Unposses del Controller
Controller->UnPossess();

//Destruye el component de collision básica del player
CapsuleComponent->DestroyComponent();

El DestroyComponent del Capsule Component lo llamamos para destruir la capsula de colisión del Player, para si se da el caso en el que el enemigo nos mata dentro de su zona de patrullaje, no colisione con esta capsula y puede pasar por arriba del “cadáver“ sin problemas.

Captura del juego en ejecución una vez que el enemigo nos mata, y nuestro personaje queda muerto, tendido en el suelo.

Captura del juego en ejecución una vez que el enemigo nos mata, y nuestro personaje queda muerto, tendido en el suelo.

 

Muy bien, solo nos va faltando un detallito, cada vez que el Player reciba un disparo, sería genial que reprodujera alguna animación no crees ? Pero, en este caso tenemos dos detalles importante a tener en cuenta.

Primero, esta animación de hit, tenemos que reproducirla mediante un Montage para poder mezclarlas con la animaciones de caminando/reposo, para que el personaje pueda caminar cuando reciba el disparo y estas dos animaciones se fusionen. Segundo, tenemos dos casos, cuando recibe el disparo teniendo el arma equipada y cuando lo recibe sin tener el arma equipada, por lo que tenemos que usar dos animaciones distintas.

Para este tutorial usaremos la misma animación, porque la verdad es que no tengo ninguna otra a mano y el AnimStarterPack no tiene ninguna animación para este caso :( . . . de todas formas creo que a modo de demostración es suficiente. En el AnimStarterPack localiza la animación Hit_React_1 y realiza todo el proceso de retarget de animación para usarla en el esqueleto de nuestro héroe.

Abre el UsingShotgunAnimMontage que preparamos en el tutorial anterior. Arrastra la animación que acabamos de hacerle el retarget para el final del montage, justo después del Reload. Crea una nueva sección de nombre Hit justo al comienzo de la animación de Hit y en el bloque Sections, agrega una que reproduzca el Hit y después caiga en el Idle en loop ya que es justamente lo que queremos. El Player recibirá el disparo, reaccionará a este disparo con un pequeño gesto y continuará en su reposo con su arma en la mano.

Captura de la sección Montage del UsingShotgunAnimMontage. Al final tenemos la última sección, de nombre Hit, con la animación Hit_React_1

Captura de la sección Montage del UsingShotgunAnimMontage. Al final tenemos la última sección, de nombre Hit, con la animación Hit_React_1

Captura de la sección Sections Preview del UsingShotgunAnimMontage. La última sección es la que acabamos de crear, armada con el Hit y a continuación el Idle en loop.

Captura de la sección Sections Preview del UsingShotgunAnimMontage. La última sección es la que acabamos de crear, armada con el Hit y a continuación el Idle en loop.

 

Ahora crea otro Montage para el caso en el que se reciba un disparo sin tener ningún arma equipada. Crea un nuevo montage con el nombre HitAnimMontage, ponle como nombre en el slot UpperBody, el mismo nombre de slot que hemos estado usando para los montages, y agrégale la misma animación Hit_React_1 o si tienes otra mano, para no repetir esta misma , usa la tuya 😉

Captura del HitAnimMontage.

Captura del HitAnimMontage.

Ya tenemos los dos montages configurados, ahora solo nos falta usarlos. Abre el HeroCharacter.h y agrega el atributo para desde el editor cargar el montage que usaremos cuando reciba el disparo sin ningún arma equipada.

/* AnimMontage para la animacion del personaje cuando le impacta un disparo del enemigo y este se encuentra sin el rifle equipado */
UPROPERTY(EditDefaultsOnly, Category = "Animations")
UAnimMontage* HitAnimMontage;

Ahora pasa al HeroCharacter.cpp y en el método ReceiveAnyDamage, luego de restar la salud del Player, agrega la lógica necesaria para reproducir estos montages según sea el caso. El método completo te quedará así:

void AHeroCharacter::ReceiveAnyDamage(float Damage, const class UDamageType* DamageType, class AController* InstigatedBy, class AActor* DamageCauser)
{
	//Si el player esta vivo ...
	if (Health > 0)
	{
		//... decremento la vida con el daño aplicado
		Health -= Damage;		

		//Reproducimos la animación de "hit" correspondiente segun tenga o no equipada el arma
		if (InventorySelectedSlot == EInventorySlot::Gun)
		{
			PlayAnimMontage(UsingShotgunAnimMontage, 1.f, "Hit");
		}
		else
		{
			PlayAnimMontage(HitAnimMontage, 1.f);
		}
		
		//Mostramos un log en pantalla
		GEngine->AddOnScreenDebugMessage(-1, 15.0f, FColor::Red, FString::Printf(TEXT("La salud del personaje es : %f"), Health));

	}

	//Si la salud del Player llega a cero, muere !!
	if (Health == 0)
	{
		//Mostramos un log en la pantalla
		GEngine->AddOnScreenDebugMessage(-1, 15.0f, FColor::Red, "El player ha muerto");

		//Reproducimos la animación de muerte
		Mesh->PlayAnimation(AnimationDeath, false);

		//Unposses del Controller
		Controller->UnPossess();

		//Destruye el component de collision básica del player
		CapsuleComponent->DestroyComponent();
	}
}

Compila, salva los cambios y por ultimo acércate al enemigo. Haz las pruebas con y sin el arma equipada y así veras al Player reproduciendo la animación correcta para cada estado

Ahora solo nos queda implementar algo muy parecido para el enemigo, un muy buen ejercicio sería que lo intentaras hacer por tu cuenta. De cualquier forma, aquí te dejo como sería.

Causando daño al enemigo

Si corres el juego en este punto y le disparan al enemigo, observarás que pasa lo mismo que nos pasaba con el disparo del enemigo al Player, la colisión con el rayo es ignorada por el Mesh del enemigo, ya sabes como solucionar esto. Selecciona al enemigo desde el nivel y en el panel de detalles muévete hasta la sección de colisiones y ponle block al parámetro visibility de trace response.

Abre el AIEnemyCharacterBlueprint y crea una nueva variable de nombre Health y asígnale como valor inicial 100. Esta será la salud del enemigo, como mismo hicimos con el Player desde C++.

Variable Health del AIEnemyCharacterBlueprint para representar la salud de este personaje

Variable Health del AIEnemyCharacterBlueprint para representar la salud de este personaje

 

Abre el archivo SPAS12Weapon.cpp (el arma que usa el Player) y agrega el siguiente código en el método Fire() justo después que le pedimos el nombre al actor impactado, básicamente lo mismo que hicimos en el Fire del arma del enemigo.

//Aplica un daño de 20 al Actor al que le dió el disparo.
UGameplayStatics::ApplyDamage(OutHit.GetActor(), 20, NULL, NULL, NULL);

El método ApplyDamage que nos brinda el UGameplayStatics es el mismo que hemos usado desde el blueprint. Recibe los mismos parámetros:

Actor al cual se le va a aplicar el daño.
Daño a aplicar.
Event Instigator se usa para definir el Controller que causa el daño.
Damage Causer se usa para definir el actor que causa el daño.
Damage Type Class nos permite definir una clase con propiedades específicas para extender la información del daño aplicado.

Una vez hecho esto, nos falta implementar el evento Any Damage en el enemigo como mismo hicimos para el Player. Salva todo y compila el código.

Cuando el enemigo reciba un disparo del Player, le restaremos salud, si llega a cero se reproduce la animación de muerte (Death_2) de lo contrario reproduciremos otra animación y además de esto usaremos una variable bool que nombraremos TakingDamage, para controlar que mientras esté recibiendo daño, no haga ninguna de las otras tareas que tiene configuradas en el Behavior Tree.

Primero vamos a crear esta variable. Abre el AIEnemyAnimBlueprint, crea una nueva variable de tipo bool y de nombre Taking Damage.

Ahora vamos a preparar las animaciones. La de muerte la reproduciremos directamente con el Play Animation, así que no usaremos ningún montage, pero para el caso de la animación cuando recibe un disparo pero aún no muerte si usaremos un montage.

Crear un nuevo montage, ponle como nombre HitAnimEnemyMontage y arrastra hacia él las animaciones Hit_React_4 e Idle_Rifle_Hip_Break1 en este mismo orden y dale como nombre de Slot HitSlot.

Ahora, fíjate, antes de reproducir este Montage, pondremos la variable TakingDamage en true, pero necesitamos que en cuanto termine esta animación esta variable pase a false y para eso usaremos un BranchPoint al final de la animación. Crea uno y ponle de nombre StopHit

HitAnimEnemyMontage con la creación del BranchPoint casi al final de la animación

HitAnimEnemyMontage con la creación del BranchPoint casi al final de la animación

 

Recuerda que para poder reproducir este montage en el enemigo, necesitamos tener el slot configurado en el Animation Blueprint. Abre AIEnemyAnimBlueprint y agrégale un nuevo Slot y ponle como nombre HitSlot para poder reproducir el Montage que acabamos de crear. Por último, conecta el StateMachine con este slot y luego este último al Final Animation Pose.

Captura del AIEnemyAnimBlueprint con el HitSlot para poder reproducir el Montage HitAnimEnemyMontage, usado cuando el personaje recibe un disparo pero aún no muere

Captura del AIEnemyAnimBlueprint con el HitSlot para poder reproducir el Montage HitAnimEnemyMontage, usado cuando el personaje recibe un disparo pero aún no muere

 

Bien, ya tenemos todos los recursos necesarios así que abre el AIEnemyCharacterBlueprint y vamos a implementar el evento Any Damage que básicamente hará lo siguiente: Primero le restamos la vida al enemigo. Luego, en el caso de que la vida llegue a 0 reproducimos la animación de Death_2 y eliminamos la capsula de colisión del enemigo. En caso contrario ponemos la variable TakingDamage en true y luego reproducimos el HitAnimEnemyMontage.

Any Damage del enemigo

Any Damage del enemigo

 

Fíjate que aquí, si es el caso en el que el personaje aún no ha muerto, antes de reproducir la animación, ponemos en true la variable Taking Damage y mediante el BranchPoint que creamos al final de esa animación la pondremos en false nuevamente, así que abre el AIEnemyAnimBlueprint implementa el evento MontageBranchingPoint_StopHit que simplemente lo que hará es poner esa variable en false nuevamente.

tuto8_imagen_30

Si corres el juego en este punto, notarás que ya se reproduce la animación, pero el enemigo como que se desliza cuando está recibiendo el disparo. Esto sucede porque el BT sigue el procedimiento normal y no “sabe“ que el enemigo está recibiendo disparos. Precisamente para esto fue que creamos esta especie de “bandera“ Taking Damage que ponemos en true cuando nos da el disparo y en false cuando terminamos de reproducir la animación. Vamos a usar esa variable ahora para mediante un Service en el BT para evitar que si el enemigo está recibiendo daño pase a hacer cualquiera cosa.

Agrega un nuevo key en el blackboard de nombre TakingDamage y de tipo booleano. Crea un nuevo Service de nombre TakingDamage y modifícalo para que te quede de la siguiente forma:

Service TakingDamage para actualizar el valor del Key TakingDamage del BlackBoard del enemigo con el valor de la variable TakingDamage que toma valor de true el tiempo en el que el enemigo se está recuperando del disparo.

Service TakingDamage para actualizar el valor del Key TakingDamage del BlackBoard del enemigo con el valor de la variable TakingDamage que toma valor de true el tiempo en el que el enemigo se está recuperando del disparo.

 

Con este Service listo, solo nos queda modificar el BT agregando al inicio del árbol este Service y a continuación el Decorator correspondiente para impedir que continúe la ejecución del árbol en el tiempo en el que el enemigo está “recuperándose“ de un disparo.

BehaviorTree final del enemigo

BehaviorTree final del enemigo

 

Listo !! Guarda, compila, lanza el juego y divierte un poco intentado matar al enemigo antes que él a ti 😉

Conclusión

Bueno, esto es todo por hoy, espero que te haya sido útil este tutorial. Un muy buen ejercicio que te puedo recomendar, es que intentes agregar más enemigos al nivel, tal vez en posiciones distintas, con distintas armas que causen más o menos daño, armas con lógicas de disparo distintas, en fin . . . todo lo que se te ocurra.

Otra cosa que te quería comentar. Como vez, la lógica de recibir daño y usar un arma, entre el enemigo y el Player es prácticamente idéntica. En proyectos reales, donde generalmente tendremos varios personajes que compartan lógicas muy parecidas o idénticas, no es nada recomendado implementar estas cosas de forma independiente por cada personaje. Lo ideal es crear una clase base, que encapsule la lógica común entre los personajes. Como las animaciones si serán distintas entre los personajes, estas las ponemos como propiedades de la clase, para que se pueda definir específicamente para cada personaje sin afectar la lógica (como hacemos aquí en el HeroCharacter). En este tutorial lo hemos hecho así sobre todo para demostrar las variantes C++ y Blueprint y que las puedas comparar e ir familiarizándote con ellas, pero no olvides aplicar este consejo en un proyecto real.

Ahora sí, esto es todo por hoy :). En próximos tutoriales veremos como implementar el HUD de nuestro juego, que es el mecanismo mediante el que el jugador tiene siempre en pantalla la salud del personaje, el arma equipada, la cantidad de municiones etc. También nos servirá para implementar una pequeña barra de salud sobre este enemigo. Además, veremos dos clases muy importantes para el núcleo del juego, el GameState y el GameMode. En fin, un montón de cosas interesantes vienen en próximos tutoriales, así que mantente al tanto, y mientras, bueno ya sabes . . . nos encantaría escuchar tus comentarios.

Introducción a la IA en UE4 (Variante en C++)

En Unreal Engine 4 tenemos dos grandes métodos a la hora de implementar alguna parte de nuestro juego. La variante en C++ y la variante en VisualScript mediante los Blueprint. Como ya hemos hablando antes, la decisión de irnos por una vía u otra es de cada cual, con la experiencia, se va logrando soltura a la hora de seleccionar que camino tomar.

En este tutorial no vamos a implementar ninguna funcionalidad nueva en nuestro juego. Vamos a implementar las mismas acciones que tiene el NPC del tutorial pasado, pero en C++. Esto nos va a servir para acercarnos un poco más al Framework C++ que nos brinda el Engine. Veremos como incluir nuevos módulos al proyecto. Cómo iterar por objetos del nivel. Cómo manipular la información guardada en el Blackboard que usa el AIController desde C++ y cómo crear Task y Services para el Behavior Tree totalmente desde C++.

Incluyendo el AIModule en nuestro proyecto.

Uno de los errores más comunes cuando se está comenzando en el mundo de C++ en UE4 y se crean clases que heredan de clases del framework que se encuentran en otros módulos, es que no se le dice a nuestro proyecto que vamos a usar clases de ese otro módulo. Cuando pasa esto e intentamos compilar el código nos da muchísimos errores de tipo “Linker“ que básicamente vienen dados porque estamos usando clases que el compilador no encuentra.

Para nuestro caso, como vamos a hacer referencia a varias clases del módulo de AI, tenemos que incluir el módulo AIModule. Cuando creamos un proyecto automáticamente se crea el archivo PROYECTO.Build.cs, es en este archivo donde tenemos que indicar los módulos que usamos en el código.

Abre la clase UE4Demo.Build.cs y verás que tiene la siguiente línea:

PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore"});

Como puedes notar, es un arreglo de strings con el nombre de los módulos que usa nuestro proyecto, por defecto ya vienen incluidos los módulos básicos. Como vamos a usar clases del modulo AIModule, agrega al final del arreglo otro string con el nombre del modulo. Te quedará de la siguiente forma:

PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "AIModule" });

Ahora si podemos incluir en nuestro proyecto cualquier clase del Módulo AI y no nos dará error. Si quieres, para probar exactamente lo que pasa, al final del tutorial prueba eliminar este ítem del arreglo y trata de compilar.

Recordando lo que hicimos en el tutorial pasado

En el tutorial pasado implementamos varios algoritmos mediante visualscripting y los usamos en nodos del Behavior Tree del AIController para definir el comportamiento del enemigo. Básicamente implementamos tres algoritmos:

CheckNearbyEnemy que lo usamos en un Service del Behavior Tree y nos sirve para determinar en cada update si el personaje principal está cerca del NPC haciendo uso del MultiSphereTraceForObjects.

UpdateNextTargetPoint que lo usamos en un Task del Behavior Tree para determinar el siguiente punto de la ruta que patrulla el enemigo

MoveToEnemy que lo usamos también en un Task y se llama cuando el algoritmo CheckNearbyEnemy detecta que estamos cerca del enemigo y haciendo uso del método AI Move To hacemos que el enemigo nos persiga.

Para este acercamiento a C++ en Unreal Engine 4 vamos a re implementar estos tres algoritmos pero totalmente desde C++.

Creando el AIController desde C++

Lo primero es crear nuestra clase C++ que heredará de AIController y que será el controlador del NPC. Crea una nueva clase desde el Editor File/Add Code To Project. Selecciona como clase base AIController y de nombre dale AIEnemyCppController, cuando el Editor te pregunte si quieres abrirla en el IDE le dices que sí. Tendrás la siguiente clase creada:


//AIEnemyCppController.h

#pragma once

#include "AIController.h"
#include "AIEnemyCppController.generated.h"

/** Clase Controladora del NPC del juego */
UCLASS()
class UE4DEMO_API AAIEnemyCppController : public AAIController
{
	GENERATED_UCLASS_BODY()
};

//AIEnemyCppController.cpp

#include "UE4Demo.h"
#include "AIEnemyCppController.h"

AAIEnemyCppController::AAIEnemyCppController(const class FPostConstructInitializeProperties& PCIP)
	: Super(PCIP)
{

}

Compila el proyecto y ejecútalo.

Creando el AIEnemyCppControllerBlueprint.

Aunque pudiéramos hacer completamente el AIController desde C++. Vamos a crear un Blueprint a partir de esta clase para poder extender el comportamiento del AIController desde VisualScripting, en caso que queramos. Crea un Blueprint que herede de AIEnemyCppController. Yo lo llamé AIEnemyCppControllerBlueprint

Abre el AIEnemyCharacter que creamos en el tutorial pasado selecciona el modo Defaults, sección AI, y en el atributo AI Controller Class selecciona AIEnemyCppControllerBlueprint. Guarda y Compila.

Ya cambiamos el controlador de nuestro enemigo, si corres el juego en este punto verás que el NPC no hará absolutamente nada, como es de esperar. Crea un nuevo Behavior Tree para este estudio, podemos usar el anterior, pero vamos a crear uno nuevo para no modificar el anterior y que te quede de referencia. Ya sabes como crear un Behavior Tree (desde el Content Browser: New/Miscellaneous/Behavior Tree) y ponle de nombre AIEnemyCppBehaviorTree simplemente para distinguirlo del otro.

Abre el AIEnemyCppControllerBlueprint y como mismo hicimos en el tutorial pasado, agrega al Blueprint el nodo Event Begin Play conéctalo al Run Behavior Tree y selecciona en BTAsset este nuevo Behavior Tree que creamos.

imagen_01

Creando el AIEnemyTargetPoint mediante C++

En el tutorial pasado definimos el recorrido del enemigo con puntos clave en el nivel. Estos puntos los creamos mediante un Blueprint que hereda de TargetPoint y le agregamos un atributo Position que nos sirve para indicar el orden del recorrido. Vamos a hacer esto mismo, pero totalmente desde C++, de esta forma también veremos algo nuevo, veremos que podemos agregar al nivel directamente una clase creada en C++, sin tener que crear un Blueprint que herede de ella como hemos hecho hasta ahora.

Agrega una nueva clase al código de nombre AIEnemyTargetPointCpp que herede de ATargetPoint. Modifica la declaración de la clase para que te quede de la siguiente forma:

//AIEnemyTargetPointCpp.h

#pragma once

#include "Engine/TargetPoint.h"
#include "AIEnemyTargetPointCpp.generated.h"

/** TargetPoint con el que definimos los puntos claves del recorrido del AIEnemyCharacter */
UCLASS()
class UE4DEMO_API AAIEnemyTargetPointCpp : public ATargetPoint
{
    GENERATED_UCLASS_BODY()

    /** Representa el orden que tiene este TargetPoint en el recorrido del personaje (siendo 0 el punto inicial) */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Tutorial Category")
    int32 Position;
};


//AIEnemyTargetPointCpp.cpp

#include "UE4Demo.h"
#include "AIEnemyTargetPointCpp.h"


AAIEnemyTargetPointCpp::AAIEnemyTargetPointCpp(const class FPostConstructInitializeProperties& PCIP)
    : Super(PCIP)
{
}

Simplemente agregamos a la clase el atributo Position de tipo entero. Gracias al macro UPROPERTY con el atributo EditAnywhere podemos ver Position desde el Editor y editar su valor. Con BlueprintReadWrite lo definimos para que también lo podamos manipular desde el Blueprint y Category representa un nombre de sección para mostrarlo en el Editor. Verás que el atributo Position sale en el panel de detalles del actor en una sección de nombre Tutorial Category.

Cambiando los TargetPoint que tenemos en el nivel por los AIEnemyTargetPointCpp.

Compila y ejecuta el código. Actualmente en el nivel tenemos los cuatro puntos que definen el recorrido del NPC, vamos a cambiar estos por los AIEnemyTargetPointCpp que acabamos de crear desde C++. Selecciona cada uno y elimínalos del nivel. Ahora, desde el panel Modes que tienes en la esquina superior izquierda tienes la sección All Classes, marca esa sección y localiza la clase AIEnemyTargetPointCpp.

Desde aquí podemos agregar al nivel instancias de esta clase sin tener que crear un blueprint como habíamos hecho hasta el momento. Arrastra cuatro instancias de esta clase al nivel, ve seleccionando cada una y modificando en el panel de Detalles dentro de la sección Tutorial Category el valor del atributo Position a 0,1,2,3 como mismo hicimos en el tutorial pasado.

Editor con los AIEnemyTargetPointCpp agregados al nivel. Fíjate en el panel de Detalles la sección Tutorial Category y el atributo Position

Implementando el algoritmo del UpdateNextTargetPoint desde C++

En el tutorial pasado lo primero que hicimos fue implementar el Task UpdateNextTargetPoint que se encargaba de determinar cual era el siguiente TargetPoint al que tenía que moverse el NPC y setearlo en el Blackboard. Vamos a hacer esto mismo pero totalmente desde programación.

Abre la clase AAIEnemyCppController y en la .h agrega la siguiente declaración

/** Usado desde el Task UpdateNextTarhetPointBTTaskNode del Behavior Tree para actualizar el siguiente punto en la ruta que patrulla */
UFUNCTION(BlueprintCallable, Category="Tutorial Category")
void UpdateNextTargetPoint();

Ahora pasa a la .cpp y agrega la implementación del método:

/** Usado desde el Task UpdateNextTarhetPointBTTaskNode del Behavior Tree para actualizar el siguiente punto en la ruta que patrulla */
void AAIEnemyCppController::UpdateNextTargetPoint()
{
    //Obtiene la referencia al BlackboardComponent del AIController
    UBlackboardComponent* BlackboardComponent = BrainComponent->GetBlackboardComponent();
    
    //Guarda en TargetPointNumber el valor que tiene el Blackboard en el KEY TargetPointNumber
    //Este numero representa el orden en el que se mueve el enemigo por los TargetPoint del nivel
    int32 TargetPointNumber = BlackboardComponent->GetValueAsInt("TargetPointNumber");
    
    //Como solo tenemos 4 TargetPoint, cuando ya esté en el último, que lo reinicie al primero.
    if(TargetPointNumber >= 4)
    {
        //Pone en 0 el valor del KEY TargetPointNumber del Blackboard
        TargetPointNumber = 0;
        BlackboardComponent->SetValueAsInt("TargetPointNumber", TargetPointNumber);
    }

    //Iteramos por todos los AAIEnemyTargetPointCpp que hay en el nivel
    for (TActorIterator<AAIEnemyTargetPointCpp> It(GetWorld()); It; ++It)
    {
        //Obtenemos el TargetPoint actualmente en el ciclo
        AAIEnemyTargetPointCpp* TargetPoint = *It;
        
        //Si el TargetPointNumber del BlackBoard es igual al valor del atributo Position del AAIEnemyTargetPointCpp
        //Este es el siguiente punto al que tiene que moverse el NPC por lo que setteamos el KEY TargetPointPosition con la posicion de ese Actor
        if(TargetPointNumber == TargetPoint->Position)
        {
            //Setteamos el KEY TargetPointPosition con la posicion de ese TargetPoint en el nivel y detenemos el ciclo con el break;
            BlackboardComponent->SetValueAsVector("TargetPointPosition", TargetPoint->GetActorLocation());
            break;
        }
    }
    
    //Por último, incrementamos el valor de TargetPointNumber del Blackboard
    BlackboardComponent->SetValueAsInt("TargetPointNumber", (TargetPointNumber + 1));
    
}

Ve con detenimiento por los comentarios de cada línea para comprender en detalles lo que se hace y como se usan las clases y métodos que brinda el Framework.

En la declaración del método usamos el macro UFUNCTION con el atributo BlueprintCallable. Esto es para poder llamar a este método desde un Blueprint en caso que nos haga falta. El método tiene de tipo de dato void porque no retorna nada, simplemente hace un procesamiento interno sin devolver ningún valor.

En la implementación si tenemos algunos puntos importantes en los que detenernos. Primero, fíjate que para obtener la referencia al BlackBoard usamos el atributo BrainComponent del AIController (la clase padre de nuestra clase). El BrainComponent tiene un método de nombre GetBlackboardComponent que nos permite obtener una referencia al BlackBoard que está usando este AIController para su base de conocimiento. Mediante este objeto de tipo UBlackboardComponent podemos usar el mismo principio que usamos en el Blueprint para settear u obtener un valor del BlackBoard.

Con SetValueAsTIPO_DE_DATO podemos settear el valor de un KEY determinado. El primer parámetro es el KEY y el segundo es el valor. Con el método GetValueAsTIPO_DE_DATO podemos obtener el valor almacenado en un KEY del blackboard, como parámetro espera el nombre del Key y retorna el valor almacenado en ese KEY.

Lo primero que tenemos en cuenta es comparar si el TargetPointNumber es superior al máximo que tenemos en el nivel, si es así, lo ponemos de nuevo en cero, para garantizar que el recorrido del personaje sea indefinido y siguiendo siempre el mismo orden.

A continuación usamos un Iterador muy útil que nos brinda el Framework. Desde C++ podemos usar TActorIterator para iterar por todos los Actores del nivel de la clase que indiquemos. En este caso nuestro objetivo es iterar por todos los AAIEnemyTargetPointCpp

Dentro del loop lo que hacemos es obtener la referencia del objeto actual en el iterador , comparamos el valor del atributo Position con el valor que tenemos en la variable TargetPointNumber que tiene el valor que tenemos en el KEY del BlackBoard, si son iguales, seteamos el KEY TargetPointPosition del blackboard con la posición de ese TargetPoint en el nivel usando el método GetActorLocation() que nos retorna el vector con la posición del actor.

Por ultimo, incrementamos el valor del KEY TargetPointNumber en 1 para que la siguiente vez que se llame este método se obtenga la posición del siguiente punto en el recorrido.

Fíjate que en esta clase estamos haciendo referencia a AAIEnemyTargetPointCpp por lo que tenemos que incluir al inicio, el fichero donde se define esta clase.

Creando el UpdateNextTargetPointTask del Behavior Tree desde C++

Ya tenemos listo el método para determinar el siguiente punto del recorrido, pero como sabes, este método lo tenemos que usar en un Task para que forme parte de un nodo del Behavior Tree. Vamos a crear este Task completamente desde C++

Compila y ejecuta el proyecto. Desde el editor agrega una nueva clase al proyecto que herede de UBTTaskNode y ponle de nombre UpdateNextTargetPointBTTaskNode. Abrela desde el IDE y modifica la .h para que te quede de la siguiente forma:

#pragma once

#include "BehaviorTree/BTTaskNode.h"
#include "UpdateNextTargetPointBTTaskNode.generated.h"

/** Task del Behavior Tree del AIEnemyCppController que ejecuta el método para seleccionar el siguiente punto del recorrido */
UCLASS()
class UE4DEMO_API UUpdateNextTargetPointBTTaskNode : public UBTTaskNode
{
    GENERATED_UCLASS_BODY()

    /* Se llama al iniciar este Task, tiene que retornar Succeeded, Failed o InProgress */
    virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent* OwnerComp, uint8* NodeMemory) override;

    /** Permite definir una descripción para este Task. Este texto se ve en el Nodo al agregarlo al Behavior Tree */
    virtual FString GetStaticDescription() const override;
};

Ahora pasa a la .cpp y agrega la implementación de estos métodos:

#include "UE4Demo.h"
#include "AIEnemyCppController.h"
#include "UpdateNextTargetPointBTTaskNode.h"


/** Constructor de la clase */
UUpdateNextTargetPointBTTaskNode::UUpdateNextTargetPointBTTaskNode(const class FPostConstructInitializeProperties& PCIP)
    : Super(PCIP)
{
    //Definimos el nombre que tendrá este Nodo en el Behavior Tree
    NodeName = "UpdateNextTargetPoint";
}

/* Se llama al iniciar este Task, tiene que retornar Succeeded, Failed o InProgress */
EBTNodeResult::Type UUpdateNextTargetPointBTTaskNode::ExecuteTask(UBehaviorTreeComponent* OwnerComp, uint8* NodeMemory)
{
    //Obtenemos la referencia al AIEnemyController
    AAIEnemyCppController* AIEnemyController = Cast<AAIEnemyCppController>(OwnerComp->GetOwner());
    
    //Llamamos al método UpdateNextTargetPoint que tiene la lógica para seleccionar el siguiente TargetPoint
    AIEnemyController->UpdateNextTargetPoint();
    
    //Finalmente retornamos Succeeded
    return EBTNodeResult::Succeeded;
}

/** Permite definir una descripción para este Task. Este texto se ve en el Nodo al agregarlo al Behavior Tree */
FString UUpdateNextTargetPointBTTaskNode::GetStaticDescription() const
{
    return TEXT("Actualiza el siguiente punto en el recorrido");
}

Es muy fácil crear un Task desde C++. Basta con heredar de UBTTaskNode. Para este caso solo necesitamos sobrescribir dos métodos. El método ExecuteTask que se llama cuando el Behavior Tree ejecuta este nodo. Simplemente obtenemos la referencia al AIEnemyController desde el parámetro OwnerComp con el método GetOwner, llamamos al método del AIEnemyController UpdateNextTargetPoint que acabamos de crear y que se encarga de toda la lógica necesaria para determinar y configurar el siguiente punto del recorrido. Por último retornamos Succeeded, como mismo hicimos en el tutorial pasado, para que el nodo padre, el Sequence, continúe con la ejecución del siguiente nodo.

El método GetStaticDescription nos sirve para retornar un string con una descripción para este Task. Esta descripción se ve en el Behavior Tree y resulta muy útil para el caso en el que sea otra persona la encargada de diseñar el Behavior Tree desde el Editor.

Compila y ejecuta el proyecto. Desde el Editor abre el Behavior Tree, verás que entre los Tasks que puedes agregar tendrás este que acabamos de crear completamente desde C++. Modifica el Behavior Tree para que te quede de la siguiente forma:

Behavior Tree con la secuencia y los Tasks para el recorrido por la zona del NPC. Fíjate que el título que muestra el nodo UpdateNextTargetPoint es el texto con el que inicializamos la variable NodeName y la descripción es el string que retornamos en el método GetStaticDescription.

Compila, guarda y ejecuta el juego. Como vez, tenemos el personaje moviéndose por los cuatro puntos exactamente a como lo logramos anteriormente, la diferencia es que ahora todo lo hemos hecho desde C++.

Implementado el método CheckNearbyEnemy

Vamos ahora a implementar el método CheckNearbyEnemy en el AIController, que es el método que hace uso del MultiSphereTraceForObjects para detectar si nos hemos acercado al enemigo. Agrega la declaración del método en la .h del AAIEnemyCppController

 /** 
 * Chequea si el personaje está cerca y setea una referencia a él en el BlackBoard 
 * Es usado en Service CheckNearbyEnemyBTService del Behavior Tree para estar constantemente vigilando si se acerca alguien a la zona de patrulla.
 */
UFUNCTION(BlueprintCallable, Category="Tutorial Category")
void CheckNearbyEnemy();

Ahora pasa al .cpp y agrega la siguiente implementación:

 /**
 * Chequea si el personaje está cerca y setea una referencia a él en el BlackBoard
 * Es usado en Service CheckNearbyEnemyBTService del Behavior Tree para estar constantemente vigilando si se acerca alguien a la zona de patrulla.
 */
void AAIEnemyCppController::CheckNearbyEnemy()
{
    //Obtenemos la referecia al Pawn del NPC
    APawn *Pawn = GetPawn();
    
    //Guardamos en MultiSphereStart la posición del NPC
    FVector MultiSphereStart = Pawn->GetActorLocation();

    //Creamos un nuevo vector a partir de la posición del NPC pero con un incremente en la Z de 15 unidades.
    //Estos dos valores serán los puntos de inicio y fin del MultiSphereTraceForObjects
    FVector MultiSphereEnd = MultiSphereStart + FVector(0, 0, 15.0f);
    
    //Creamos el arreglo que pasaremos como el parámetro ObjectTypes del MultiSphereTraceForObjects y define los tipos de objetos a tener en cuenta
    TArray<TEnumAsByte<EObjectTypeQuery>> ObjectTypes;
    ObjectTypes.Add(UEngineTypes::ConvertToObjectType(ECC_Pawn));

    //Creamos el arreglo de los actores a ignorar por el método, solamente con el Pawn del NPC que es el único que no puede estar en el resultado
    TArray<AActor*> ActorsToIgnore;
    ActorsToIgnore.Add(Pawn);
    
    //OutHits la usaremos como parámetro de salida. Como el método SphereTraceMultiForObjects recibe la referencia de este parámetro
    //al terminar la ejecución del método en este arreglo se encuentran el arreglo de FHitResult lleno con los Hits que detecte el método.
    TArray<FHitResult> OutHits;
    
    bool Result = UKismetSystemLibrary::SphereTraceMultiForObjects(GetWorld(),                      //Referencia del Mundo
                                                                   MultiSphereStart,                //Punto de Inicio de la recta que define la esfera
                                                                   MultiSphereEnd,                  //Punto de fin de la recta que define la esfera
                                                                   700,                             //Radio de la esfera
                                                                   ObjectTypes,                     //Tipos de objetos a tener en cuenta en el proceso
                                                                   false,                           //No queremos que se use el modo complejo
                                                                   ActorsToIgnore,                  //Los actores que se van a ignorar aunque esten dentro de la esfera
                                                                   EDrawDebugTrace::ForDuration,    //El tipo de Debug
                                                                   OutHits,                         //Parámetro por referencia donde se guardarán los resultados
                                                                   true);                           //si se ignora el propio objeto
    
    //Inicialmente seteamos en NULL el KEY del BlackBoard TargetActorToFollow para en caso de que en el Trace no se tenga resultados, se quede en NULL
    UBlackboardComponent* BlackboardComponent = BrainComponent->GetBlackboardComponent();
    BlackboardComponent->SetValueAsObject("TargetActorToFollow", NULL);

    //Si hay resultados en el Trace
    if(Result == true)
    {
        //Recorremos todos los objetos dentro del OutHits
        for(int32 i = 0; i < OutHits.Num(); i++)
        {
            //Obtenemos el FHitResult actualmente en el ciclo
            FHitResult Hit = OutHits[i];

            //Obtenemos la referencia el Character
            ACharacter* Character = UGameplayStatics::GetPlayerCharacter(GetWorld(), 0);

            //Si el Actor detectado en el Trace es igual al Character
            //setamos el KEY TargetActorToFollow del Blackboard con la referencia al Character
            if(Hit.GetActor() == Character)
            {
                BlackboardComponent->SetValueAsObject("TargetActorToFollow", Character);
            }
        }
    }
}

La lógica que sigue esté método tampoco es necesaria explicarla porque ya lo hicimos en el tutorial pasado cuando lo implementamos mediante VisualScripting, pero si vamos a pasar por el código para ver los detalles del Framework y las clases y método que nos permiten hacer esto mismo desde C++.

Primero preparamos las variables que vamos a pasar como parámetro al método del Trace. El único que creo que vale la pena comentar es la variable ObjectTypes que es un arreglo de TEnumAsByte Este es el tipo de dato del parámetro ObjectTypes que recibe el método del Trace. Fíjate que solamente le agregamos un elemento, el indicador de Pawn. UEngineTypes::ConvertToObjectType nos permite convertir el ECC_Pawn (valor de enum correspondiente al Pawn) a un EObjectTypeQuery. La siguiente variable que declaramos es el arreglo de Actors que va a ignorar el Trace, recuerda que aquí solamente incluimos al Pawn del enemigo, que lo tenemos en la variable Pawn y obtenemos la referencia a este mediante el método GetPawn.

Por último declaramos el arreglo OutHits que lo usaremos como parámetro de salida. El método SphereTraceMultiForObjects recibe este parámetro por referencia y al terminar la ejecución, el arreglo de FHitResults queda poblado con los Hits que detecte el método. Los parámetros por referencia son la variante que tenemos en C++ para tener más de un valor de retorno en una función.

Y ahora el punto clave de este método, la llamada al UKismetSystemLibrary::SphereTraceMultiForObjects. SphereTraceMultiForObjects es un método estático dentro de la clase UKismetSystemLibrary y ya sabemos lo que hace :)

Después de su ejecución seteamos en NULL el KEY TargetActorToFollow del Blackboard para que en caso de que no se encuentre el Pawn del personaje dentro del Trace, que se quede este KEY en NULL. Recuerda que el Decorator que tenemos en el Behavior Tree para determinar si se ejecuta o no el siguiente Task se basa en comprobar si el KEY TargetActorToFollow tiene valor.

Creando el CheckNearbyEnemyService desde C++

Como mismo hicimos para el Task anterior, tenemos que crear el Service del Behavior Tree donde se va a usar este método que acabamos de crear. Compila y ejecuta el proyecto, desde el editor crea una nueva clase que herede de UBTService y ponle de nombre UCheckNearbyEnemyBTService. Abrela en el IDE y modifica el .h para que te quede de la siguiente forma:

#pragma once

#include "BehaviorTree/BTService.h"
#include "CheckNearbyEnemyBTService.generated.h"

/** Service del Behavior Tree del AIEnemyCppController que chequea constantemente si el personaje está cerca de nosotros */
UCLASS()
class UE4DEMO_API UCheckNearbyEnemyBTService : public UBTService
{
    GENERATED_UCLASS_BODY()

    /** Se llama en cada update del Service */
    virtual void TickNode(UBehaviorTreeComponent* OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;   
};

Ahora pasa al .cpp y modifícalo para que te quede de la siguiente forma:

#include "UE4Demo.h"
#include "AIEnemyCppController.h"
#include "CheckNearbyEnemyBTService.h"

/** Constructor de la clase */
UCheckNearbyEnemyBTService::UCheckNearbyEnemyBTService(const class FPostConstructInitializeProperties& PCIP)
    : Super(PCIP)
{
    //Nombre del nodo en el Behavior Tree
    NodeName = "CheckNearbyEnemy";
    
    //Intervalo de Update
    Interval = 0.5f;

    //Aleatorio de desviación para el update
    RandomDeviation = 0.1f;
}

/** Se llama en cada update del Service */
void UCheckNearbyEnemyBTService::TickNode(UBehaviorTreeComponent* OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
    //Llamamos a la implementación de la clase base primero
    Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);
    
    //Obtenemos la referencia del AIEnemyController
    AAIEnemyCppController* AIEnemyController = Cast<AAIEnemyCppController>(OwnerComp->GetOwner());
    
    //Llamamos al método CheckNearbyEnemy del AIEnemyController que tiene toda la lógica para determinar si el enemigo está cerca o no
    //y configurar el KEY correspondiente del Blackboard
    AIEnemyController->CheckNearbyEnemy();
}

No hay mucho que explicar aquí verdad ? En el constructor inicializamos los valores del Services que ya conoces del tutorial anterior, y en el método TickNode obtenemos la referencia del AIEnemyController y llamamos al método CheckNearbyEnemy que se encarga de todo :)

Compila y ejecuta el proyecto. Abre el Behavior Tree y modifícalo para que te quede de la siguiente forma:

Behavior Tree con el Service que chequea si estamos cerca del enemigo

Guarda, Compila y ejecuta el juego.

Captura del juego corriendo en el Editor después de configurar el Behavior Tree con el Service para determinar si nos acercamos al enemigo.

Muy bien, ya solo nos queda el último paso para terminar el Behavior Tree que implementamos en el tutorial pasado pero en esta ocasión completamente desde C++.

Implementado el método MoveToEnemy

Vamos a implementar el último método del AIEnemyController, el encargado de llamar al MoveToActor para comenzar la persecución :). Abre AIEnemyCppController.h y agrega la siguiente declaración

/**
 *  Hace que el AIEnemyCharacter persiga al actor setteado en el KEY TargetActorToFollow del Blackboard
 *  Es usado en un Task del Behavior Tree para perseguir al personaje principal
 *  @return El resultado del método MoveToActor (Failed, AlreadyAtGoal o RequestSuccessful)
 */
UFUNCTION(BlueprintCallable, Category="Tutorial Category")
EPathFollowingRequestResult::Type MoveToEnemy();

Ahora pasa a la .cpp y agrega la siguiente implementación:

/**
 *  Hace que el AIEnemyCharacter persiga al actor setteado en el KEY TargetActorToFollow del Blackboard
 *  Es usado en un Task del Behavior Tree para perseguir al personaje principal
 *  @return El resultado del método MoveToActor (Failed, AlreadyAtGoal o RequestSuccessful)
 */
EPathFollowingRequestResult::Type AAIEnemyCppController::MoveToEnemy()
{
    //Obtenemos la referencia al BlackBoard
    UBlackboardComponent* BlackboardComponent = BrainComponent->GetBlackboardComponent();
    
    //Obtenemos la referencia al Actor guardado en el KEY TargetActorToFollow del BlackBoard
    AActor* HeroCharacterActor = Cast<AActor>(BlackboardComponent->GetValueAsObject("TargetActorToFollow"));
    
    //Iniciamos el proceso de perseguir al personaje
    EPathFollowingRequestResult::Type MoveToActorResult = MoveToActor(HeroCharacterActor);

    return MoveToActorResult;
}

Fíjate que usamos el método MoveToActor de AIController para comenzar la persecución, lo demás no creo que necesite explicación. Aquí usamos el método MoveToActor pasándole solo el parámetro del Actor que tiene que seguir, pero date una vuelta por la declaración del método para que veas los otros parámetros que se le puede pasar.

Creando el MoveToEnemyBTTaskNode desde C++

Por último nos queda crear el Task que usará este método. Pero, vamos a detenernos aquí para comentar un poco de teoría que se me pasó en el tutorial pasado.

En Unreal Engine 4 los Tasks pueden ser de dos tipos. Los que se ejecutan y tienen un resultado al instante, como el UpdateNextTargetPoint, y los que demoran un tiempo en terminar su ejecución. En este caso se encuentra este Task que vamos a crear, ya que la acción de este Task va a terminar cuando el enemigo llegue al personaje principal, y esto puede demorar un tiempo.

Este tipo de Task generalmente en su ejecución retornan InProgress para que el Behavior Tree sepa que demorará un poco en terminar, y se usa el método TickTask para estar comprobando la condición que se tiene que cumplir para que termine la ejecución del Task y en ese momento se termina la ejecución con una llama al método FinishLatentTask.

Vamos a verlo en la practica con la creación de este Task. Agrega una clase de nombre MoveToEnemyBTTaskNode que herede de UBTTaskNode modifica su .h para que te quede de la siguiente forma:

#pragma once

#include "BehaviorTree/BTTaskNode.h"
#include "MoveToEnemyBTTaskNode.generated.h"

/**
 * 
 */
UCLASS()
class UE4DEMO_API UMoveToEnemyBTTaskNode : public UBTTaskNode
{
    GENERATED_UCLASS_BODY()

    /* Se llama al iniciar este Task, tiene que retornar Succeeded, Failed o InProgress */
    virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent* OwnerComp, uint8* NodeMemory) override;
    
    /* Se llama constantemente. Es usado generalmente en Tasks que en el ExecuteTask retornan InProgress */
    virtual void TickTask(class UBehaviorTreeComponent* OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
    
    /** @return Una descripción para este Task. Este texto se ve en el Nodo al agregarlo al Behavior Tree */
    virtual FString GetStaticDescription() const override;
    
};

Ahora pasa a la .cpp y modifícala para que te quede de la siguiente forma:

#include "UE4Demo.h"
#include "AIEnemyCppController.h"
#include "MoveToEnemyBTTaskNode.h"

/** Constructor de la clase */
UMoveToEnemyBTTaskNode::UMoveToEnemyBTTaskNode(const class FPostConstructInitializeProperties& PCIP)
    : Super(PCIP)
{
    //Definimos el nombre que tendrá este Nodo en el Behavior Tree
    NodeName = "MoveToEnemy";
    
    //Activamos para que se llame el TickTask de este task
    bNotifyTick = true;
}

/* Se llama al iniciar este Task, tiene que retornar Succeeded, Failed o InProgress */
EBTNodeResult::Type UMoveToEnemyBTTaskNode::ExecuteTask(UBehaviorTreeComponent* OwnerComp, uint8* NodeMemory)
{
    //Obtenemos la referencia al AIEnemyController
    AAIEnemyCppController* AIEnemyController = Cast<AAIEnemyCppController>(OwnerComp->GetOwner());
    
    //Preparamos el resultado del Task. En este caso como es un Task que su ejecución no terminará al instante, tiene que retornar InProgress
    EBTNodeResult::Type NodeResult = EBTNodeResult::InProgress;
    
    //Llamamos al método MoveToEnemy del Controller y guardamos en MoveToActorResult el resultado
    EPathFollowingRequestResult::Type MoveToActorResult = AIEnemyController->MoveToEnemy();

    //Este caso sería si se ejecuta este Task estando delante del personaje. En este caso si retorna Succeeded
    if (MoveToActorResult == EPathFollowingRequestResult::AlreadyAtGoal)
    {
        NodeResult = EBTNodeResult::Succeeded;
    }
    
    return NodeResult;
}

/* Se llama constantemente. Es usado generalmente en Tasks que en el ExecuteTask retornan InProgress */
void UMoveToEnemyBTTaskNode::TickTask(class UBehaviorTreeComponent* OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
    //Obtenemos la referencia al AIEnemyController
    AAIEnemyCppController* AIEnemyController = Cast<AAIEnemyCppController>(OwnerComp->GetOwner());
    
    //Llamamos al método MoveToEnemy del Controller y guardamos en MoveToActorResult el resultado
    EPathFollowingRequestResult::Type MoveToActorResult = AIEnemyController->MoveToEnemy();
    
    //Si ya se llega al objetivo se termina la ejecución el Task con FinishLatentTask y un resultado
    if (MoveToActorResult == EPathFollowingRequestResult::AlreadyAtGoal)
    {
        FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
    }
}

/** @return Una descripción para este Task. Este texto se ve en el Nodo al agregarlo al Behavior Tree */
FString UMoveToEnemyBTTaskNode::GetStaticDescription() const
{
    return TEXT("Persigue al personaje principal");
}

Muy bien, la .h a estas alturas no creo que tenga nada que explicar. Ahora, en la cpp, primero en el constructor tenemos que poner en true el atributo bNotifyTick para que el Engine sepa que tiene que ejecutar el método Tick de esta clase.

En el ExecuteTask simplemente llamamos al método MoveToEnemy, este método nos retorna tres posibles valores Failed, AlreadyAtGoal o RequestSuccessful. Normalmente el ExecuteTask tendrá que terminar en InProgress a menos que el MoveToActor retorne AlreadyAtGoal que sería dado si se llama el método estando ya pegados al personaje. De lo contrario retornamos InProgress para dejarle saber el BT que este Task tomará un tiempo.

Entonces, en el TickTask es donde tenemos que estar comprobando si llega al objetivo, si esto pasa se fuerza a terminar el Task con el método FinishLatentTask.

Compila y ejecuta el proyecto. Modifica el Behavior Tree para agregar este último Task y el Decorator correspondiente como mismo lo hicimos en el tutorial pasado. Te quedará de la siguiente forma:

Behavior Tree completo

Guarda, compila y ejecuta el juego 😉

Captura del juego corriendo en el Editor después de terminar la configuración del Behavior Tree

Conclusión

A pesar de no implementar ninguna funcionalidad nueva en este tutorial, creo que sirvió para acercarnos un poco más al mundo de C++ en el Unreal Engine 4. Puedes bajarte de aquí las clases que hemos creado en este tutorial para una referencia directa.

Con esto terminamos por hoy, en el próximo tutorial le “enseñaremos“ a nuestro personaje a dar puñetazos para que se pueda defender cuando el enemigo lo atrape :) … mientras, me encantaría escuchar tus comentarios.

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 !!