Archivo del Autor: Dariel de la Noval Sedano

Introducción al HUD en Unreal Engine 4 – Parte 1

En los videojuegos, el HUD (Head Up Display) es el contenedor con toda la información que se encuentra sobre la vista de la cámara (como pegatinas en el lente de la cámara). Su propósito es informar al jugador del estado actual del juego, como puede ser, la salud de su personaje, la energía, las municiones o el mini mapa. El HUD no solo sirve para mostrar en un primer nivel la información propia del personaje protagónico, también nos sirve para mostrar cualquier información que pueda ser útil, por ejemplo, un caso que se usa mucho en los juegos de estrategia, es mostrar el indicador de salud sobre los personajes.

En este tutorial veremos cómo implementar el HUD de nuestro juego en Unreal Engine 4, para que el usuario tenga siempre a la vista la salud de su personaje, el arma que tiene equipada y las municiones que le quedan. También implementaremos un indicador de salud sobre los enemigos al estilo de los juegos de estrategia.

NOTA: Este tutorial ha sido desarrollado con Unreal Engine 4.6.1, 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.

El HUD en Unreal Engine 4

En Unreal Engine la clase GameMode es el núcleo de nuestro juego. Desde el Editor podemos definir fácilmente el GameMode que usará el juego dando clic en la opción World Settings del Toolbar y buscando la sección Game Mode, como hemos hecho ya en tutoriales anteriores.

La clase GameMode tiene cuatro atributos puntales de nuestro juego, hasta ahora hemos visto dos de ellos.

Default Pawn Class: Representa el Pawn del personaje protagónico, el que controlará el jugador

Player Controller Class: Es el Player Controller que poseerá al Pawn para poderlo controlar, es como la interfaz entre el jugador y el personaje dentro del juego.

Además de estos, el GameMode tiene dos atributos más, muy importantes:

Game State Class: Contiene el estado del juego, por ejemplo, la lista de misiones que has ganado, la puntuación actual del jugador y todas las cosas que en general determinen el estado del juego. Hasta ahora siempre hemos usado la clase por defecto que nos brinda el Engine.
Por último, el Game Mode tiene el atributo que veremos en este tutorial, el HUD Class.

HUD Class: Este atributo es una instancia de la clase AHUD que nos brinda el framework y nos permite definir la clase que implementará el HUD de nuestro juego. La clase AHUD es la que nos permitirá dibujar todo el contenido que se mostrará sobre la pantalla. Cada PlayerController tiene una instancia de su propio HUD y lo que se dibuje mediante ese HUD será dibujado en el ViewPort de ese Player Controller. En el caso de los juegos multiplayers que dividen la pantalla, varios ViewPorts comparten la misma pantalla, pero cada HUD dibuja su contenido en su propio Viewport, o sea, en su propio espacio en la pantalla.

La clase AHUD cuenta con un Canvas (instancia de UCanvas) sobre el que podemos pintar texturas, textos, rectángulos etc. Sobrescribiendo el método DrawHUD podemos dibujar sobre el HUD toda la información que queramos con métodos como DrawTexts o DrawRect y muchos otros que puedes ver en la declaración de la clase en los fuentes del Engine.

En versiones anteriores del Unreal este era el mecanismo por excelencia para implementar el HUD de nuestro juego. Crear una clase que herede de AHUD, sobrescribir el método DrawHUD para dibujar todo lo que necesitáramos e inicializar el HUDClass del GameMode con una instancia de esta clase. Pero en las versiones más reciente del Engine también contamos con una herramienta mucho más a “alto nivel“ y mucho más cómoda, que nos permite implementar nuestro HUD básicamente mediante “Drag and Drop“ de componentes, el Unreal Motion Graphics (UMG).

Unreal Motion Graphics (UMG) es una herramienta que nos brinda el Engine para crear los elementos UI como el HUD, menues del juego etc. El núcleo del UMG son los Widgets, que no son más que una serie de componentes preconstruidos que podemos usar para confeccionar la interfaz que queramos. Entre estos Widgets tenemos botones, checkboxes, barras de progreso y montón de componentes muy cómunes en las interfaz y que podemos incluir y customizar muy facil.

Estos Widgets son editados desde el Widget Blueprint. Un editor blueprint muy parecido al Blueprint Editor con el que hemos trabajado pero que tiene dos modos, el Graph y el Designer. El Graph ya sabemos para lo que es :) y el Designer es un espacio de trabajo donde podemos diseñar, al estilo “Drag and Drop“, la interfaz.

En este tutorial vamos a explorar ambos mundos. Vamos a implementar la parte del HUD que muestra toda la información del Player mediante el UMG y vamos a crear nuestra propia clase AHUD para mostrar también en el HUD, sobre los enemigos, una barra de salud a modo de indicador. De esta forma podremos dar un acercamiento a las dos variantes y quedará por tu parte seleccionar la vía que más te agrade cuando vayas a desarrollar el HUD de tu juego.

Nota: Para el desarrollo de este tutorial vamos a partir del proyecto que terminamos en el tutorial: Implementando un AI Character con un arma en UE4 donde ya tenemos todas las cosas necesarias del gameplay. Esto nos permitirá centrarnos exclusivamente en lo referente al HUD.

En este tutorial vamos a implementar el HUD para que el jugador tenga siempre a la vista la información del estado del juego (salud nuestra, salud del enemigo, arma equipada, cantidad de municiones, etc). Al terminar el HUD de nuestro juego se verá así:

Captura del juego donde se ve como quedará el HUD una vez que terminemos las dos partes que conforman este tutorial

Captura del juego donde se ve como quedará el HUD una vez que terminemos las dos partes que conforman este tutorial

 

Bien, despues de toda esta teoría ya podemos pasar a la práctica. Lo primero que vamos a agregar al HUD de nuestro juego será una barra de progreso que refleje la salud del personaje, y para esto usaremos los Widgets y el Unreal Motion Graphic.

Creando una barra de progreso que refleje la salud del personaje mediante el Unreal Motion Graphics (UMG)

Crea una nueva carpeta en el Content Browser, una vez dentro da clic derecho en un espacio en blanco y selecciona User Interface/Widget Blueprint, nómbralo HUDWidgetBlueprint y ábrelo.

Opción de widget blueprint para la creación del HUD

Opción de widget blueprint para la creación del HUD

 

Esto nos abrirá el editor de los Widget Blueprint. En el modo Designer es donde diseñaremos nuestro HUD. Has un poco de zoom-out al viewport viewport (usando la rueda del mouse) hasta que veas un cuadro azul. Este cuadro representa el espacio de la pantalla y será el contenedor del HUD de nuestro juego. Todos los componetes que agreguemos aquí se veran en un primer plano (como pegatinas en el lente de la cámara).

Captura del Widget Blueprint en el modo Designer

Captura del Widget Blueprint en el modo Designer

 

En el panel Palette (a la izquierda del editor) podrás encontrar agrupados por categoría todos los widgets que podemos incluir en nuestro HUD, tómate unos minutos para que le des un vistazo a todos los componentes que ya tenemos listos para usar. Busca el Progress Bar que se encuentra en la sección Common y arrástralo al contenedor del HUD en el centro de la pantalla y colócalo centrado en el borde inferior.

Paleta de componentes

Paleta de componentes

 

Una vez agregado a la escena, selecciónalo. A la derecha del editor sale el panel de detalles con las propiedades de este componente. También tómate unos minutos para dar un vistazo por todas las propiedades, para que tengas una noción de todo lo que se puede personalizar.

Panel de detalles

Panel de detalles

 

En la sección Appareance tenemos la propiedad Percent. Esta propiedad define el porciento de progreso de la barra. Inicialmente vamos a ponerle valor 1.0 (100%) aunque en realidad el valor de progreso de esta barra estará vinculado al valor de salud del personaje, más adelante veremos cómo hacerlo.

Captura del WidgetBlueprint con la barra de progreso agregada dentro del espacio de trabajo

Captura del WidgetBlueprint con la barra de progreso agregada dentro del espacio de trabajo

 

En este punto, es válido comentar un detalle. Al correr el juego en distintas dimensiones de pantalla no siempre nos saldrá el componente en la misma posición en la que lo colocamos aquí desde el Designer. Para solucionar este problema el UMG nos brinda varias herramientas para anclar los elementos a determinados lugares de la pantalla y que la posición que le definamos sea relativa a ese anclaje. Selecciona el Progress y en el panel Details busca la propiedad Anchors, despliega el combobox y verás un grupo de opciones que gráficamente nos muestran cual será el comportamiento del anclaje del elemento. En nuestro caso selecciona la que indica que el componente quedará anclado en el borde inferior al centro de la pantalla. Puedes jugar un rato con las otras opciones y moviendo la barra de progreso de lugar para que veas su comportamiento.

Modificación de Anchors del Progress

Modificación de Anchors del Progress

 

Por último en el panel Details, encima del buscador, a la izquierda del checkBox IsVariable se encuentra el nombre que tendrá este elemento. Por defecto Unreal le asigna uno, cámbialo para tenga un nombre más descriptivo y así poder identificarlo más rápido a la hora de buscarlo entre varios elementos. En mi caso le puse HealthProgress.

Bien, de momento solo tendremos eso en el HUD de nuestro juego, una barra de progreso en el borde inferior. Compila, salva los cambios y cierra este editor.

Agregando el HUDWidgetBlueprint al ViewPort

Ya tenemos diseñada una primera versión del HUD que queremos en nuestro juego, pero aún nos falta agregarlo al Viewport y esto lo haremos desde el Level Blueprint.

Mediante el botón Blueprint del Toolbar selecciona Open Level Blueprint. Lo que haremos aquí será muy simple. En el Event Begin Play vamos a crear una instancia del HUDWidgetBlueprint mediante el nodo CreateWidget. Una vez creada una instancia de este widget, la asignaremos a una variable que crearemos previamente, para en el futuro poder acceder fácilmente al HUD. Por último agregaremos este HUDWidgetBLueprint al ViewPort mediante el nodo Add to Viewport.

Captura del Level Blueprint donde se crea una instancia del WidgetBlueprint y se agrega al Viewport.

Captura del Level Blueprint donde se crea una instancia del WidgetBlueprint y se agrega al Viewport.

 

Compila, salva, cierra este editor y corre el juego. Verás el HUD en la pantalla. Pero tenemos pendiente un detalle, cuando nos acercamos al enemigo y este nos dispara, se disminuye la salud del PlayerCharacter porque vemos que llega a morir si recibe muchos disparos, pero la barra de progreso ni se entera. Vamos a ello.

Escenario con la salud del personaje

Escenario con la salud del personaje

 

Ya tenemos la barra de progreso que nos permitirá saber en todo momento la salud que tiene nuestro personaje, pero si te dejas dar un tiro por el enemigo verás que la barra no refleja nada, continua estando en el 100%. Pues es precisamente esto lo que vamos a solucionar ahora, vamos a “bindiar“ el progreso de la barra de estado a la salud del personaje para que cada vez que la salud de nuestro héroe disminuya la barra haga lo mismo.

Creando “Binding” para definir el progreso de la barra de forma dinámica

Abre el HUDWidgetBlueprint, selecciona la barra de progreso y en el panel Details en la sección Appearance en la propiedad Percent da click al boton Bind/Create Binding como se muestra en la imagen.

Creando un Binding para definir el dinámicamente el progreso de la barra de salud

Creando un Binding para definir el dinámicamente el progreso de la barra de salud

 

Una vez que des en el botón Create Binding se abrirá un blueprint editor donde podremos implementar el algoritmo necesario para definir en todo momento el porciento de progreso de esta barra. Como esta barra de progreso representa la salud del personaje será muy simple la implementación de este método. Obtenemos la referencia de nuestro héroe, obtenemos el valor de su variable Health, llevamos ese valor a porciento (dividiendo entre 100) ya que es un valor entre 0 y 1 el que espera este componente y finalmente conectamos este número al Return Value de esta función.

Función que determinará dinámicamente el porciento de progreso que tendrá el ProgressBar del HUD

Función que determinará dinámicamente el porciento de progreso que tendrá el ProgressBar del HUD

 

Compila, guarda, cierra ese editor y dale Play al juego. Acercate al enemigo para que te dispare y verás como cada vez que disminuye la salud de nuestro héroe esta queda perfectamente reflejada en la barra de progreso del HUD.

Escenario con nuestro enemigo disparando y se observa como se refleja en la salud del personaje

Escenario con nuestro enemigo disparando y se observa como se refleja en la salud del personaje

 

Personalizando el estilo de la barra de progreso

Ya tenemos un HUD con una barra de progreso que representa la salud de nuestro personaje, pero el estilo de esta barra es bastante insípido, verdad ? Vamos a jugar un poco con las propiedades que nos brinda este componente para mejorar su look. Lo que haremos será agregar una textura de fondo para el componente, otra a la barra de progreso y por último un icono a la derecha de la barra como indicador de lo que representa esa barra de progreso. Al final nuestra barra de progreso quedará así:

Estilo del Progress Bar para la salud del personaje

Estilo del Progress Bar para la salud del personaje

 

Primero descárgate los recursos para este tutorial desde aquí. Al descomprimir este .zip encontrarás los siguientes archivos:

HealthIcon: Icono para identificar la salud del personaje.
ProgressBackground: Fondo del progress bar para la salud del personaje.
ProgressFillImage: Imagen utilizada mientras se va llenando el progress bar para la salud del personaje.

De vuelta al Unreal Editor, en el Content Browser, dentro de la carpeta HUD, crea una nueva carpeta con el nombre Textures e importa aquí las texturas descargadas.

Al importar las texturas te saldrá un cartel como el que se muestra en la imagen, dale Yes.

Alerta al importar texturas ¨non power of two¨

Alerta al importar texturas ¨non power of two¨

 

El motivo de este alert viene dado por lo siguiente. Cada textura que importes al proyecto, por lo general su tamaño debe ser una potencia de 2, es decir 2n por ejemplo “8”, “16”, “32”, “64”, “128”, “256”, “512”, “1024”, “2048” … Texturas que se importen y que no cumplen esta regla, pierden calidad mientras más lejos estás de ellas. Además, el Engine no las puede optimizar para que puedan ser cargadas rápidamente y procesarla en la memoria. Puedes profundizar un poco sobre este tema en el siguiente link . En nuestro caso esto no nos afecta, por eso puedes aceptar el mensaje sin preocupación.

Una vez importadas las texturas, abre el blueprint del HUD y selecciona el Progress. En el panel Details, desplázate hasta la sección Style. Las propiedades dentro de esta sección nos sirven para modificar el estilo de los componentes. En nuestro caso vamos primero a modificar la propiedad Fill Image que nos permitirá definir el estilo del relleno de la barra. Expande Fill Image y en la propiedad Image despliega el combo a su lado y asígnale la imagen ProgressFillImage que acabamos de importar. Después, expande la propiedad Tint y en el parámetro A asígnale valor 0.7, para darle un poco de transparencia a la imagen.

Cambiando el estilo de la salud del personaje

Cambiando el estilo de la salud del personaje

 

Ahora vamos a personalizar el fondo de la barra. Repite el proceso pero en este caso en la propiedad Background y usa como imagen de fondo la textura ProgressBackground.

Ya casi terminamos, solo nos queda colocar un pequeño icono a la derecha de la barra para que el jugador pueda saber a la primera que esta es la barra de salud de su personaje. Para esto vamos a usar un componente de tipo Image.

Agrega un componente de tipo Image desde la paleta de componentes y asígnale como imagen, en la propiedad Brush/Image, el HealthIcon que importamos. Ahora aquí vamos a topar con un detalle, y es que este elemento se va a superponer sobre un pedazo de la barra de progreso. Como primero agregamos la barra de progreso y después agregamos este componente no tendremos problema, y la imagen nos saldrá por arriba, pero puede darse el caso que fuera al revés y el iconito quedará tapado por la barra de progreso. Para solucionar este problema en cada componente tenemos la propiedad ZOrder que define el orden en el eje z de cada elemento agregado al HUD.

Listo!! ya tenemos terminada la barra que reflejará la salud del personaje en el HUD de nuestro juego. Compila salva lo cambios y dale Play. Muévete hasta el enemigo y deja que te dispare para que veas como disminuye el progreso de la barra a medida que disminuyen las vidas del personaje, y ahora con el estilo que queríamos.

Captura de pantalla del juego donde se ve disminuyendo la barra de progreso que representa la salud del personaje.

Captura de pantalla del juego donde se ve disminuyendo la barra de progreso que representa la salud del personaje.

 

Perfecto !! si has llegado hasta aquí ya tendrás una noción básica del Unreal Motion Graphic y de cómo implementar el HUD de nuestro juego mediante esta herramienta, de momento vamos a dejarlo aquí. En el próximo tutorial vamos a continuar este proyecto y agregaremos nuevos componentes para mostrarle al jugador el arma que tiene equipada, el arma secundaria y la cantidad de municiones restantes.

Ahora . . . como hablamos al inicio, existe otra forma un poco más ¨a bajo nivel¨ pero que creo que vale la pena darle un vistazo. Para esto vamos a implementar un mecanismo para mostrar en el HUD de nuestro juego de forma dinámica, sobre la cabeza de cada enemigo, una barra de salud. Pero en este caso lo haremos heredando de la clase AHUD y sobrescribiendo el método DrawHUD.

Indicador de vidas sobre del enemigo (variante C++)

Lo primero que haremos será crear una nueva clase C++ que herede de HUD y con una instancia de esta clase inicializaremos el atributo HUDClass del Game Mode.

Crea una nueva clase (Add code to project) que herede de AHUD y nómbrala UE4DemoHUD (o como prefieras). Ahora abre el .cpp de tu clase GameMode e inicializa el atributo HUDClass con una instancia de esta nueva clase que acabamos de crear. Para esto, agrega lo siguiente antes de terminar la implementación del constructor del GameMode:

HUDClass = AUE4DemoHUD::StaticClass();

Recuerda incluir el .h de la clase que acabamos de crear para que desde aquí el compilador la pueda localizar.

Ahora vamos a sobrescribir el método DrawHUD de la clase HUD para dibujar lo que queremos. El método DrawHUD es llamado por el Engine automáticamente en cada frame del juego y es aquí donde tenemos que dibujar todo lo que queramos mostrar en el HUD en cada frame. En nuestro caso lo que queremos es dibujar una barra de progreso muy simple pero que siempre se vea sobre cada personaje enemigo y para esto tenemos que hacer algunas cosillas. Aquí te dejo como tiene que quedar el método, tómate unos minutos y ves línea a línea con los comentarios para que entiendas lo que hace.


//En el UE4DemoHUD.h
virtual void DrawHUD() override;

//En el UE4DemoHUD.cpp
#include “AIEnemyCharacter.h“

void AUE4DemoHUD::DrawHUD()
{
    ///Arreglo de actores para pasar por referencia al GetAllActorsOfClass
    //y obtener todos los actores del tipo de clase indicada
    TArray<AActor*> OutActors;
    
    //Guarda en el arreglo que pasamos como tercer parámetro todos los actores que estén en el nivel del tipo de clase indicado en el segundo parámetro
    UGameplayStatics::GetAllActorsOfClass(this, AAIEnemyCharacter::StaticClass(), OutActors);
    
    //Recorre cada uno de los actores devueltos por el GetAllActorsOfClass
    for (int32 Idx = 0; Idx < OutActors.Num(); ++Idx)
    {
        //Obtiene el actor actualmente en el loop casteado ya a la clase de nuestro enemigo
        AAIEnemyCharacter *AiEnemy = Cast<AAIEnemyCharacter>(OutActors[Idx]);
        
        // Si el cast fue satisfactorio y aún está vivo (su salud es mayor que 0)
        if (AiEnemy != NULL && AiEnemy->GetHealth()>0)
        {
            //Offset a la posición del personaje para que le quede el indicador de salud por encima de la cabeza
            FVector Offset = FVector(0, 0, 150);
            
            //Obtenemos la posición del Actor en el mundo
            FVector Location = AiEnemy->GetActorLocation();
            
            //Calculamos la posición donde queremos mostrar el indicador (encima de la cabeza del enemigo)
            //sumándole al vector de la posición del enemigo el offset
            FVector RectLocation = Location + Offset;
            
            //Este es uno de los puntos claves en este método. La clase AHUD cuenta con el método Project que nos permite convertir
            //coordenadas 3D al plano 2D del canvas. Necesitamos convertir las coordenadas RectLocation para poder dibujar el indicador
            //en el Canvas del HUD pero siempre en la posición correcta (sobre el enemigo)
            FVector RectLocation2D = Project(RectLocation);
            
            //Dibuja un primer rectángulo de color rojo que simulará el fondo de una barra de progreso
            //DrawRect nos permite dibujar un rectángulo en el canvas del HUD y espera los siguientes parámetro:
            //Color del rectángulo, posición en X en el canvas, posición en Y en el canvas ancho y alto
            DrawRect(FLinearColor(1.0, 0.0, 0.0, 1.0), RectLocation2D.X - 50, RectLocation2D.Y, 100, 15);
            
            //Dibuja un segundo rectángulo de color verde que será la propia barra de progreso
            //En este caso en vez de dibujar este rectángulo con un ancho de 100, lo dibujaremos con un ancho equivalente
            //a la salud del personaje que puede estar en un rango entre 0 - 100.
            //Como este rectángulo quedará sobre el anterior, la composición simulara una barra de progreso simple.
            DrawRect(FLinearColor(0.0, 1.0, 0.0, 1.0), RectLocation2D.X - 50, RectLocation2D.Y, AiEnemy->GetHealth(), 15);
        }
    }
} 

Listo !!. Compila el proyecto y en el editor desde el Toolbar ve hasta el World Settings y en la sección Game Mode asegúrate que se está usando el GameMode de nuestro juego y que al desplegarlo, en el atributo HUD Class esté indicado el nombre de la clase HUD que acabamos de crear.

Captura de nuestro World Settings donde se observa nuestro HUD

Captura de nuestro World Settings donde se observa nuestro HUD

 

En este punto me gustaría comentar un detalle. En versiones anteriores del Unreal Engine, al agregar una clase C++ al proyecto se tenía que cerrar el editor y volver a compilar y correr desde el IDE para que el editor pudiera encontrar esa clase. A partir de la 4.6.1 ya esto no es necesario. Una de las mejoras que incluyeron a partir de esta versión es que no es necesario cerrar el editor, simplemente al compilar el proyecto el Editor automáticamente detecta y recarga las nuevas clases o cambios hechos en las ya existentes. Para eso solo hace falta compilar el proyecto con el Editor abierto, fíjate que no tienes que Compilar y Correr, solo Compilar. Inmediatamente después que termine la compilación, verás como el Editor recarga los cambios.

Después de compilar, dale Play al juego y acércate al enemigo. Verás como sobre él se muestra una barrita como indicador de salud y si le disparamos la barra se actualiza. En el momento de morir deja de aparecer la barra por la condición que pusimos en el DrawHUD de solo dibujar si las vidas de ese Actor son mayores que 0.

Enemigo con su barra de salud

Enemigo con su barra de salud

 

Indicador de vidas sobre del enemigo (variante Blueprint)

A modo de referencia vamos a ver como sería para lograr esto mismo totalmente desde Blueprint.
Primero crea un nuevo blueprint dentro de la carpeta HUD que herede de HUD. En mi caso le puse HUDBlueprint.
Abre el archivo .cpp del GameMode y tenemos que cambiar para en vez de asignar al atributo HUDClass una instancia de la clase que creamos desde C++, asignarle este blueprint. Para esto sustituye la línea HUDClass = AUE4DemoHUD::StaticClass(); por lo siguiente:

//Obtiene la referencia al Blueprint acabado de crear
static ConstructorHelpers::FObjectFinder<UClass> HUDBPClass(TEXT("Class'/Game/HUD/HUDBlueprint.HUDBlueprint_C'"));

//Inicializa el atributo HUDClass con el HUDBlueprint creado y configurado desde el editor
if (HUDBPClass.Object != NULL)
{
  HUDClass = HUDBPClass.Object;
}

Compila nuevamente el proyecto y asegúrate desde el WorldSettings/GameMode que ahora el HUD Class del GameMode sea este blueprint que acabamos de crear. Una vez hecho esto abre el HUDBlueprint en el modo Graph y vamos a implementar aquí el mismo algoritmo que implementamos en el DrawHUD desde C++.

Agrega el nodo Event Received Draw HUD que es el nodo equivalente al DrawHUD que vimos desde C++ y en este evento implementa el mismo algoritmo que ya hicimos.

Event Received Draw HUD del HUDBlueprint para imprimir en el HUD el indicador de vidas sobre cada enemigo.

Event Received Draw HUD del HUDBlueprint para imprimir en el HUD el indicador de vidas sobre cada enemigo.

 

Compila, salva los cambios y corre el juego. El resultado será el mismo que ya logramos anteriormente desde C++ pero ahora hecho totalmente desde blueprint.

Conclusión

Como has podido notar el Unreal Motion Graphics nos facilita muchísimo la vida a la hora de crear las interfaz al tenernos todos estos Widgets listos para usar. Pero de cualquier forma el DrawHUD y los métodos para dibujar sobre el Canvas también son una alternativa (y muchas veces son el único camino posible).

Te recomiendo que le des un vistazo a la declaración de la clase HUD del Engine para que veas todos los métodos que puedes usar para dibujar distintos elementos en el Canvas. También puedes visitar los siguientes enlaces:

UCanvas
HUD, Canvas, Code Sample of 800+ Lines, Create Buttons & Draw Materials

Otro detalle importante a tener en cuenta es que hemos implementado funcionalidades para el HUD de nuestro juego de dos formas distintas, y como ves, pueden coexistir sin problema, así que no tienes que limitarte por una vía u otra, simplemente úsalas en conjunto para lograr un espectacular HUD en tu juego 😉

Bueno, esto es todo por hoy. Vamos a dejar aquí la primera parte de este tutorial de Introducción al HUD en Unreal Engine 4. En el próximo tutorial vamos a continuar este proyecto y agregaremos al HUD nuevos componentes, mediante el Unreal Motion Graphic, para mostrarle al jugador el arma que tiene equipada, el arma secundaria y la cantidad de municiones restantes. También veremos algunos truquitos para animar los elementos en el HUD.
Mientras tanto, bueno ya sabes . . . nos encantaría escuchar tus comentarios.

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.