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.

Dariel de la Noval Sedano
Follow me
Dariel de la Noval Sedano
Follow me

Latest posts by Dariel de la Noval Sedano (see all)

Un pensamiento en “Implementando un AI Character con un arma en UE4

  1. Pingback: Introducción al HUD en Unreal Engine 4 – Parte 1 | El blog de Fernando Castillo Coello

Deja un comentario

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

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