Archivo de la etiqueta: Inventario

Implementando un inventario y usando un arma en UE4

En este tutorial crearemos un sistema de inventario para equipar armas e implementaremos la lógica para poder recoger, recargar, disparar el arma y detectar con qué colisiona el disparo. Esto nos permitirá ver varios conceptos y técnicas nuevas para nuestro juego. En este tutorial veremos:

  • Introducción a los Sockets, el mecanismo que nos brinda UE4 para anclar un elemento a otro en un punto determinado.
  • Uso del AnimMontage para las animaciones al usar el arma.
  • Uso del LineTrace para detectar colisión con una línea imaginaria. Con el uso de este método simularemos la trayectoria del disparo.
  • Implementaremos un sistema de inventario genérico para nuestro juego, donde el personaje podrá seleccionar, de las armas que tenga disponible, cuál usar.
  • Agregaremos los efectos de sonido al disparar y recargar el arma.
  • Veremos una introducción a los AnimNotifies para lanzar eventos en puntos exactos de una animación.
  • Y muchas cosas más, así que no te lo pierdas.

Preparando los recursos necesarios

Puedes descargarte de aquí los recursos que usaremos en este tutorial. Al descomprimir el .zip encontrarás varios efectos de sonidos para el arma y el FBX de una SPAS-12. Aprovecho para decirte que el modelo de la SPAS-12 lo descargué de: tf3dm.com. En ese sitio podrás encontrar montón de modelos 3D gratis, listos para importarlos en tus proyectos de pruebas y prototipos, así que si con el MarketPlace no estas conforme, ahí te dejo otro lugar donde encontrar recursos ;).

Además del modelo del arma y los efectos de sonidos, también está incluido en los recursos las clases Weapon, SPAS12Weapon, HeroCharacter como quedarán al terminar el tutorial, para una referencia más rápida.

Importa todos estos recursos al proyecto. Es bueno que mantengas los elementos organizados en el Content Browser, por ejemplo, puedes crear una carpeta de nombre Weapons, dentro de ella, las carpetas particulares para cada tipo de arma que tengas en tu juego. Dentro de cada una coloca todos los recursos que son de esta arma. Puedes también crear dentro de esta carpeta, otra carpeta llamada Sounds, para tener ahí los efectos de sonido. En fin, organízalo como mejor sea para ti, pero siempre es bueno tener el Content Browser bien organizado 😉

Después que importes los .wav de los efectos de sonidos, crea los Sound Cue correspondientes.

. . . Ya ?? . . . vale, comenzamos.

Introducción a los Sockets en Unreal Engine 4

Nuestro personaje tendrá la posibilidad de encontrar armas en el escenario, recogerlas y equiparlas. Cuando recogemos un arma, tendremos que agregar el modelo del arma en el guarda pistola del personaje, en este caso será en la parte de atrás del cinturón. De igual forma, cuando equipa el arma para usarla, la tendremos que anclar a la mano del personaje. Pues bien, para este tipo de cosas Unreal Engine nos brinda los Sockets

Desde el Persona Editor podemos crear un socket en una posición relativa a un hueso del esqueleto. Estos sockets, que básicamente son puntos invisibles, los podemos rotar o trasladar relativos a la posición del hueso en donde se ha creado y podemos anclar otros objetos en esta posición.

Por ejemplo, en nuestro caso lo que haremos será crear dos sockets, el primero lo crearemos relativo al hueso spine_01, para que quede justo donde nuestro personaje guardará la escopeta. El segundo lo crearemos relativo a la mano del personaje, para anclar el arma a este socket cuando la vaya a usar.

Creando los sockets necesarios en el esqueleto del personaje

Abre el esqueleto que usa nuestro personaje (HeroTPP) y en el panel Skeleton Tree (esquina superior izquierda) tenemos la estructura de huesos del esqueleto, aquí se muestra el árbol de huesos de este esqueleto. Selecciona el hueso spine_01, da clic derecho y selecciona Add Socket y dale de nombre HolsterSocket, este nombre lo usaremos desde programación para poderle decir a la escopeta en que socket se va a anclar cuando el personaje la recoja.

Captura del Persona Editor agregando el Socket relativo al hueso spine_01

Captura del Persona Editor agregando el Socket relativo al hueso spine_01

 

Ahora da clic derecho sobre el HolsterSocket desde el Skeleton Tree, selecciona Add Preview Asset y selecciona el StaticMesh de nuestra escopeta. Esto nos permite agregar a este socket a modo de pre-visualización el objeto que finalmente anclaremos aquí. De esta forma podemos ajustar la posición y rotación del HolsterSocket en base al punto de pivote del otro objeto.

Agregando al HolsterSocket el StaticMesh de la SPAS-12, a modo de pre-visualización.

Agregando al HolsterSocket el StaticMesh de la SPAS-12, a modo de pre-visualización.

 

En este punto vale aclarar una cosa. Como notarás, al anclar la escopeta aquí, el punto de anclaje está en la culata de la escopeta, que es el punto de pivote del modelo. El punto de pivote de un objeto en Unreal Engine 4 determina el punto sobre el que se hará cualquier transformación (traslación, rotación o escala). Este punto de pivote siempre está localizado en el origen (0,0,0) cuando se exporta el modelo desde el software de modelado 3D. Para el caso de las armas, es buena idea antes de exportar el modelo, garantizar que este punto de pivote esté sobre el gatillo del arma, de esta forma evitamos conflictos a la hora de colocar distintos modelos de armas en un mismo socket.

Puedes ver la posición del punto de pivote de cualquier StaticMesh, abriéndolo desde el Content Browser y marcando en el Toolbar del StaticMesh Editor, el botón Pivot Point.

Muy bien, ahora necesitamos mover y rotar el socket para que la SPAS-12 quede en la posición correcta, ya teniendo la pre-visualización del modelo, es muy fácil ajustar la posición y rotación correcta para el socket. Selecciona el HolsterSocket y con las herramientas de rotación y transformación ve rotando y moviendo el socket hasta que la SPAS-12 te quede en la posición correcta.

Captura del Persona Editor después de rotar y trasladar el HolsterSocket, dejando la SPAS-12 en la posición correcta

Captura del Persona Editor después de rotar y trasladar el HolsterSocket, dejando la SPAS-12 en la posición correcta

 

Un truco bastante útil para lograr la posición exacta es pre-visualizar la animación que tendrá que ver con este socket, en este caso es la animación de desenfundar la escopeta. Así puedes detener la animación en frames determinados y mover o rotar el socket teniendo como referencia la postura del personaje.

Muy bien, ahora crea un nuevo socket en el hueso hand_r (mano derecha) y dale de nombre HandSocket. Agrega la pre-visualización de la SPAS-12 a este socket, y de la misma forma que acabamos de hacer, traslada y rota el socket hasta que la escopeta quede en la posición correcta. Puedes cargar la animación de Idle_Rifle_Hip para que el preview del esqueleto se vea con esta animación. De esta forma te será más fácil posicionar el socket.

Captura del Persona Editor después de configurar el HandSocket

Captura del Persona Editor después de configurar el HandSocket

 

Perfecto !!, ya tenemos listo los dos sockets que necesitamos en nuestro personaje. Vamos ahora ha implementar la lógica de nuestra arma.

Implementando las clases Weapon y SPAS12Weapon

Vamos a implementar las clases para las armas. Tendremos una clase base llamada Weapon, esta clase tendrá la lógica común para cualquier tipo de arma y será una clase abstracta, o sea, no podemos tener instancias de ella, solamente servirá de clase base para las armas especificas. Además, tendremos la clase SPAS12Weapon que heredará de Weapon y será nuestra escopeta.

Crea una nueva clase que herede de Actor y nómbrala Weapon y crea otra clase que herede de Weapon y nómbrala SPAS12Weapon.

Abre el archivo Weapon.h y modifícalo para que te quede de la siguiente forma:

#pragma once

#include "GameFramework/Actor.h"
#include "Weapon.generated.h"

/**
 * Clase Base abstracta de todas las armas.
 */
UCLASS(abstract)
class UE4DEMO_API AWeapon : public AActor
{
    GENERATED_UCLASS_BODY()

protected:
    
    /**
     * USphereComponent es un componente en forma de esfera generalmente usado para detectar colisiones simples
     * Este será el Root del arma y con él detectaremos las colisiones entre el personaje y el arma
     */
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Weapon")
    TSubobjectPtr<USphereComponent> BaseCollisionComponent;
    
    /** StaticMesh del arma.  */
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Weapon")
    TSubobjectPtr<UStaticMeshComponent> WeaponMesh;

    /** Cantidad máxima de municiones */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Weapon")
    int32 MaxAmmo;

    /** Alcance del disparo */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Weapon")
    int32 ShotDistance;
    
    /** Efecto de sonido del disparo */
    UPROPERTY(EditAnywhere, Category="Sounds")
    USoundCue* ShotSoundEffect;
    
    /** Efecto de sonido cuando no hay munición */
    UPROPERTY(EditAnywhere, Category="Sounds")
    USoundCue* EmptySoundEffect;
    
public:

    /** Cantidad de municiones actuales */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Weapon")
    int32 Ammo;
    
    /** True cuando el arma está en el escenario y false cuando es recogida por el Pawn */
    bool bIsEquipped;
    
    /**
     * Es llamado desde el Pawn cuando colisiona y recoge el arma
     * De momento solamente pone en true el flag bIsEquipped y muestra un log en la pantalla
     */
    void OnCollected();
    
    /**
     * Dispara el arma y obtiene el objeto que colisionó con el disparo
     * Método abstracto para sobreescribir en las clases hijas según las características especificas del disparo de cada arma
     */
    UFUNCTION(BlueprintImplementableEvent, Category="Weapon")
    virtual void Fire();
    
    /** Reinicia las municiones del arma */
    void Reload();
};

Aquí lo único nuevo que tenemos es el uso de UCLASS(abstract). Así es como le decimos al Engine que esta clase es abstracta. La clase Weapon está compuesta por los siguientes atributos:

BaseCollisionComponent: Componente que usaremos como ROOT de este actor y para detectar la colisión.

WeaponMesh: Mesh del arma y que cargaremos desde el Editor.

MaxAmmo: Máxima cantidad de municiones

Ammo: Cantidad de municiones disponibles, se decrementa con cada disparo.

ShotDistance: Alcance del disparo. Para la implementación del disparo, como veremos más adelante, lo que haremos es lanzar un rayo imaginario una x distancia hacia delante, el primer objeto que colisione con ese rayo será el que reciba el disparo. Para variar un poco el comportamiento entre las armas, el largo de ese rayo lo definiremos usando esta variable, esto nos permitirá definir un alcance especifico para cada arma.

ShotSoundEffect: Efecto de sonido del disparo de esta arma. Lo cargaremos desde el editor.

EmptySoundEffect: Efecto de sonido cuando se intenta disparar el arma estando sin municiones. También lo cargaremos desde el editor.

bIsEquipped: Este será un flag que usaremos para controlar que no se continúe detectando la colisión cuando el arma ya sea recogida por el Character. Como mismo hicimos con las monedas en el segundo tutorial.

Además de esto tenemos dos métodos

OnCollected: Este método será llamado desde el Character cuando recoja el arma. Le daremos una implementación general aquí. Solamente lo usaremos para poner en true el bIsEquipped y para imprimir un log en la pantalla. Si quisiéramos hacer algo en particular con cada arma en el momento en el que es recogida por el personaje, entonces tendrías que tener una implementación especifica de este método en las clases hijas.

Fire: En este método estará toda la lógica cuando se dispara el arma, decremento de las municiones, reproduce el efecto de sonido correspondiente y lanza el rayo para determinar con que a colisionado el disparo. Es un método virtual, que no tendrá implementación en esta clase, sino que lo implementaremos según el arma en especifico. Es válido aclarar que si en tu juego la lógica del disparo será la misma entre todas las armas, ya que puede ser el caso de que no tengas una gran diferencia del modo de disparo entre las armas, puedes dejar la implementación en esta clase base y que la hereden todas las armas.

Reload: Este método es llamado por el jugador cuando decide recargar el arma (tocando la tecla R) y simplemente reinicia la cantidad de municiones disponibles con el valor de MaxAmmo

Abre ahora el archivo Weapon.cpp y modifícalo para que te quede de la siguiente forma:

#include "UE4Demo.h"
#include "Weapon.h"


AWeapon::AWeapon(const class FPostConstructInitializeProperties& PCIP)
    : Super(PCIP)
{
    bIsEquipped = false;
    
    //Crea la instancia del USphereComponent
    BaseCollisionComponent = PCIP.CreateDefaultSubobject<USphereComponent>(this, TEXT("BaseSphereComponent"));
    
    //Inicializa el RootComponent de este Actor con el USphereComponent
    RootComponent = BaseCollisionComponent;
    
    //Crea la instancia del UStaticMeshComponent
    WeaponMesh = PCIP.CreateDefaultSubobject<UStaticMeshComponent>(this, TEXT("WeaponMesh"));
    
    //Settea el Profile de collision del este objeto en OverlapAll para que no bloquee al personaje
    WeaponMesh->BodyInstance.SetCollisionProfileName(FName(TEXT("OverlapAll")));
    
    //Agregamos el UStaticMeshComponent como hijo del root component
    WeaponMesh->AttachTo(RootComponent);
}

void AWeapon::OnCollected()
{
    GEngine->AddOnScreenDebugMessage(-1, 15.0f, FColor::Red, "Weapon Collected !!");
    
    bIsEquipped = true;
}

void AWeapon::Reload()
{
    Ammo = MaxAmmo;
}

En el constructor tenemos la inicialización del RootComponent y el MeshComponent. A estas alturas no debes tener problema con entender eso. Además setteamos el profile de colisión del arma en OverlapAll ya que usaremos el evento que se dispara al estar haciendo overlap con la pistola para poder recogerla del escenario.

También tenemos la implementación de los métodos OnCollected y Reload que son súper simples.

Ahora vamos a implementar la lógica de nuestra escopeta. Abre el archivo SPAS12Weapon.h y agrégale la declaración del override para el método Fire, te quedará así:

#pragma once

#include "Weapon.h"
#include "SPAS12Weapon.generated.h"

UCLASS()
class UE4DEMO_API ASPAS12Weapon : public AWeapon
{
    GENERATED_UCLASS_BODY()

    virtual void Fire() OVERRIDE;
};

Pasa ahora a SPAS12Weapon.cpp y modifica su contenido para que te quede de la siguiente forma:

#include "UE4Demo.h"
#include "SPAS12Weapon.h"


ASPAS12Weapon::ASPAS12Weapon(const class FPostConstructInitializeProperties& PCIP)
    : Super(PCIP)
{
    //Cantidad máxima de municiones para esta arma
    MaxAmmo = 4;
    
    //Cantidad de municiones para esta arma
    Ammo = 4;

    //Alcance máximo de la escopeta
    ShotDistance = 5000;
}

void ASPAS12Weapon::Fire()
{
    //Si la escopeta aún tiene municiones ...
    if(Ammo > 0)
    {
        //HitResult de la primera colisión del trace
        FHitResult OutHit;
        
        //Punto inicial para el Trace. La posición del FireSocket en el Mesh de la escopeta
        FVector TraceStart = WeaponMesh->GetSocketLocation("FireSocket");
        
        //Punto final para el Trace. Otro punto en linea recta a [ShotDistance] unidades de distancia
        FVector TraceEnd = (WeaponMesh->GetRightVector() * ShotDistance) + TraceStart;
        
        //Parámetros adicionales usados para el trece, en este los valores por defecto son suficiente.
        FCollisionQueryParams TraceParams;
        
        //Lanza un rayo desde TraceStart hasta TraceEnd, retorna true si se encontró alguna colisión y en OutHit el HitResult de la primera colisión
        bool bHit = GetWorld()->LineTraceSingle(OutHit, TraceStart, TraceEnd, ECC_Visibility, TraceParams);
        
        //Si el rayo colisionó con algo ...
        if (bHit)
        {
            //... Imprime en pantalla el nombre del Actor (a modo de prueba)
            GEngine->AddOnScreenDebugMessage(-1, 15.0f, FColor::Red, OutHit.Actor->GetName());
            
            //Dibuja una linea, a modo de Debug, desde la posicion inicial hasta el punto de impacto
            DrawDebugLine(GetWorld(), TraceStart, OutHit.ImpactPoint, FColor::Red, false, 5.f);
            
            //Dibuja un punto en la zona del impacto, a modo de Debug.
            DrawDebugPoint(GetWorld(), OutHit.ImpactPoint, 16.f, FColor::Red, false, 5.f);
        }
        else //el rayo no colisionó con nada ...
        {
            //Dibuja una linea, a modo de Debug, desde la posicion inicial hasta la final
            DrawDebugLine(GetWorld(), TraceStart, TraceEnd, FLinearColor::Red, false, 5.f);
        }
        
        //Reproduce efecto de sonido del disparo en la posición de la pistola
        if (ShotSoundEffect)
        {
            UGameplayStatics::PlaySoundAtLocation(this, ShotSoundEffect, GetActorLocation());
        }
        
        //Decrementa la cantidad de municiones disponibles
        Ammo--;
    }
    else //Si la pistola no tiene municiones ...
    {
        GEngine->AddOnScreenDebugMessage(-1, 15.0f, FColor::Red, "Sin municiones :(");
        
        //Reproduce efecto de sonido de la escopeta vacía
        if (EmptySoundEffect)
        {
            UGameplayStatics::PlaySoundAtLocation(this, EmptySoundEffect, GetActorLocation());
        }
    }
}

En el constructor solamente necesitamos definir por defecto la cantidad de municiones y el alcance del disparo, recuerda que de todas formas gracias al macro UPROPERTY, estos valores los podemos modificar desde el editor, en la sección Weapon, en el panel de propiedades de este objeto. Además, tenemos el método estrella de nuestra arma :), el Fire. Vamos paso a paso con lo que hacemos en este método:

Primero tenemos que comprobar que el arma tenga municiones, si es así, preparamos los parámetros que usaremos en el método GetWorld->LineTraceSingle. Este método nos permite lanzar un rayo invisible desde un punto a otro, nos retorna si se detectó alguna colisión con él y por referencia en la variable OutHit viene toda la información del primer Hit, encapsulada en la estructura FHitResult. Puedes ver la implementación de esta estructura en los fuentes del Engine para que tengas una idea de toda la información que tenemos del impacto. De momento solo nos interesa el Actor con el que se ha colisionado.

El método recibe como parámetros la variable OutHit (de salida), además los vectores de inicio y fin del rayo. Aquí vamos a detenernos para ver de que forma armamos este rayo. Si lo que queremos es un rayo que simule la trayectoria del proyectil, el primer punto que necesitamos estaría en el cañón de la escopeta y el otro punto lo necesitamos alineado a este punto, en la dirección del cañón, una x distancia por delante. Acordamos que esta distancia la íbamos a definir mediante la variable ShotDistance, para tener la posibilidad de variar el alcance de cada arma.

Muy bien, pero tenemos un problema. Como obtenemos el punto exacto en la salida del cañón de la escopeta ? . . . se te ocurre algo ? . . . pues claro !!, mediante un socket. Los Sockets no solo los podemos crear en un esqueleto, también los podemos crear en un StaticMesh. Además, este socket no solo nos servirá para saber este punto, también nos puede servir, por ejemplo, para en el momento del disparo anclar y emitir desde aquí un sistema de partículas para simular el efecto de humo y fuego del arma al dispararse. Aún no hemos visto los sistemas de partículas en estos tutoriales, pero un muy buen ejercicio investigativo es que busques por tu cuenta un poco de información sobre estos e intentes acoplar aquí ese efecto en el momento del disparo. Después me encantaría que nos dejaras tu solución en los comentarios y la agregamos al tutorial como complemento 😉

Muy bien, si te fijas en el código, para obtener el punto inicial del rayo lo hacemos a partir de la línea WeaponMesh->GetSocketLocation(“FireSocket”). El método GetSocketLocation nos retorna la posición del socket con el nombre pasado como parámetro. Por supuesto, este socket tiene que existir en el Mesh. Vamos entonces al editor para configurar este socket en el StaticMesh de nuestra escopeta.

Abre el StaticMesh de la escopeta. En el Toolbar del Static Mesh Editor, hay un botón para activar o desactivar la pre-visualización de los sockets, asegúrate de tenerlo activado. Desde el menú principal del Static Mesh Editor selecciona Window/Socket Manager. Esto te agregará en la esquina inferior derecha del editor, debajo del panel Details, el panel Socket Manager. Desde este panel podrás agregar y configurar sockets a este StaticMesh prácticamente de la misma forma que hicimos con el esqueleto del personaje.

Selecciona el botón Create Socket, dale de nombre al nuevo socket FireSocket y muévelo para posicionarlo en la punta del cañón.

Captura del Static Mesh Editor con el modelo de la escopeta y con el FireSocket en la posición correcta.

Captura del Static Mesh Editor con el modelo de la escopeta y con el FireSocket en la posición correcta.

 

Listo !!, guarda y cierra el editor. Regresamos al código del método Fire para seguir analizándolo. Ya tenemos el punto de inicio del rayo, ahora nos falta el punto final. El tercer parámetro que recibe el método es el punto final del rayo y este lo obtenemos gracias a unos métodos súper útiles que tenemos para los Mesh, son los método GetForwardVector, GetRightVector, GetUpVector. Estos métodos nos retornan un vector unitario en cada una de las direcciones del Mesh. En este caso necesitamos el método GetRightVector que nos da un vector unitario en la misma dirección del cañón. Con este vector y un poco de matemática muy simple, conseguimos nuestro punto final. Para obtener el punto final necesitamos multiplicar este vector por el valor que queremos para la distancia del disparo, en este caso guardado en la variable ShotDistance, y el resultado lo sumamos al vector inicial. Esta operación nos dará el punto final del rayo con el que vamos a simular la trayectoria de la bala.

El cuarto parámetro del LineTraceSingle es el “canal“ que usará este rayo para detectar las colisiones. Recuerdas que en el tutorial anterior vimos una introducción a las colisiones en Unreal Engine 4 y que hablamos de los Collision Responses de cada objeto y que se dividían en dos grupos: los Trace Responses para las colisiones con rayos, como es el caso, y los Object Response para las colisiones con otros objetos. Recuerdas ? … pues bien, este parámetro permite definir que tipo de Trace Response usará el rayo, en este caso indicamos Visibility. Si le das un vistazo a cualquier objeto en la sección Collision verás que para los Trace Response existen dos opciones Visibility y Camera. Una cosa muy importante, los objetos que puedan recibir un disparo, o sea, que este rayo pueda colisionar con ellos, tienen que tener marcado en Block el Trace Response: Visibility, ya que este es el canal que estamos usando para este rayo. De lo contrario, aunque visualmente notemos que el rayo colisiona, no se detectará la colisión.

El último parámetro son opciones adicionales que podemos usar para el rayo, de momento con sus valores por defecto tenemos suficiente. Puedes revisar la implementación de esta estructura para que veas los atributos que tiene.

Al llamar al método LineTraceSingle, en la variable bHit tendremos si se colisionó con algo o no, y en la variable OutHit tendremos la información del primer hit. Con esto, lo único que nos queda es comprobar si bHit es true, si es así, imprimimos un mensaje en la pantalla con el Nombre del objeto con el que colisionó el rayo y además con la ayuda de los métodos DrawDebugLine dibujamos, a modo de debug, una línea para poder ver la trayectoria del disparo y validar que todo esté funcionando como tiene que ser.

En próximos tutoriales vamos a determinar en este punto, si la colisión con el disparo a sido con un enemigo, y en ese caso le aplicaremos un daño, aunque los mecanismos para aplicar daño ya los hemos visto en tutoriales anteriores, y sería súper bueno que te adelantes e implementes esto por tu cuenta.

En caso que bHit esté en false, es que no se detectó colisión con ningún objeto, esto puede pasar porque no hay ningún objeto en el trayecto del disparo, al menos a una distancia menor que ShotDistance. En ese caso también imprimimos en pantalla un texto temporal y dibujamos una línea a modo de debug.

Después de esto, si ShotSoundEffect es válido, reproducimos el efecto de sonido en la posición de la pistola. Recuerda que ShotSoundEffect y EmptySoundEffect los tenemos como propiedades de la clase y la idea es que desde el editor se pueda cargar, para cada una de estas propiedades, el efecto de sonido correspondiente.

Por último, decrementamos la cantidad de municiones.

En caso de que la pistola se quede sin municiones, simplemente preguntamos si EmptySoundEffect es válido, y si lo es, reproducimos el efecto de sonido.

Listo !!, ya tenemos la implementación completa de la escopeta que podrá usar nuestro personaje, pero aún no tenemos nada “visible“ para probar, ya que necesitamos antes, implementar el mecanismo para que el personaje pueda recoger y equipar la escopeta.

Tómate unos minutos y seguimos con eso 😉

Implementando un sistema de inventario

Nuestro personaje tendrá distintas formas de defenderse. Podrá usar las manos y defenderse a base de puñetazos, como ya vimos en los dos tutoriales anteriores, pero además, podrá equipar distintas armas y usarla contra sus enemigos.

En los juegos que tienen estas características, es clásico contar con un sistema de inventario, donde el jugador puede agregar el arma al espacio del inventario correspondiente a ese tipo de objeto y seleccionar del inventario el objeto que quiere equipar para usar.

Para no extender el tutorial, solamente implementaremos la lógica para usar un arma, en este caso una escopeta, pero al terminarlo verás lo fácil que resultará agregar otras armas.

Primero, vamos a explicar un poco el principio que usaremos. Básicamente un inventario lo podemos ver como un cajón imaginario con distintos compartimentos. Cada uno de estos compartimentos puede estar vacío o almacenar un elemento específico. En nuestro caso, sería un cajón con dos compartimentos. El primero puede tener una escopeta, y en el caso que la tenga, la podemos seleccionar para defendernos con ella, el segundo sigue el mismo principio solo que en vez de escopeta o armas pesadas, estará destinado para pistolas.

Ejemplo gráfico del inventario. En este caso el inventario estaría lleno, pero si tuviéramos, por ejemplo, solo la escopeta, el segundo elemento del cajón estaría vacío.

Ejemplo gráfico del inventario. En este caso el inventario estaría lleno, pero si tuviéramos, por ejemplo, solo la escopeta, el segundo elemento del cajón estaría vacío.

 

El elemento ideal que tenemos para implementar este tipo de cosas a nivel de programación son los arreglos. Un arreglo, viéndolo abstractamente, es un cajón en la memoria con distintos compartimentos. Podemos obtener el elemento que se encuentra en un compartimento a partir del índice (su posición en el arreglo, siendo 0 la primera posición).

El único detalle a tener en cuenta es que en un arreglo todos los elementos tienen que ser del mismo tipo, pero usando la herencia y el polimorfismo (conceptos básicos de la programación orientada a objetos) podemos definir que nuestro arreglo será de elementos de una clase base, digamos Weapon y podemos tener instancias de objetos que heredan de Weapon, como sería Pistol para el caso de la pistola, por ejemplo, y Shotgun para el caso de la escopeta.

Muy bien, ya basta de teoría y vamos a la practica.

Abre el archivo HeroCharacter.h y agrega al inicio del fichero, debajo de los includes, lo siguiente:

#define INVENTORY_GUN_INDEX         0
#define INVENTORY_PISTOL_INDEX      1

UENUM()
namespace EInventorySlot
{
    enum Type
    {
        Gun,
        Pistol,
        None
    };
}

Los dos #defines son simplemente constantes para usar a la hora de acceder a cada posición del arreglo. A continuación creamos un enum que usaremos para identificar el tipo seleccionado en el inventario. En nuestro caso tendremos tres variantes Pistola, Escopeta o Ninguno. Este último caso sería si el usuario no ha seleccionado ningún elemento del inventario, ya sea porque no tiene o porque no ha querido, en este caso se estaría defendiendo a puñetazos.

Muévete, hasta el final de la clase y agrega los siguientes atributos:

/** Evento cuando este AActor se superpone con otro AActor */
virtual void ReceiveActorBeginOverlap(class AActor* OtherActor) OVERRIDE;

/* Inventario del player. En el primer index se tendrá un arma tipo escopeta y en el segundo un arma tipo pistola */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Inventory")
TArray<class AWeapon*> Inventory;

/* Compatimento del inventario actualmente seleccionado por el player */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Inventory")
TEnumAsByte<EInventorySlot::Type> InventorySelectedSlot;

El atributo Inventory es una arreglo de Weapons . En este guardaremos, en el índice correspondiente, la referencia de cada una de las armas recogidas. En este caso usaremos el índice 0 para las armas de tipo escopeta y el índice 1 para las pistolas.

El atributo InventorySelectedSlot lo usaremos para saber en cada momento cual es el elemento del inventario actualmente en uso.

Bien, pasa ahora a HeroCharacter.cpp y agrega al final del constructor las siguientes líneas

//Reserva el espacio para los items en el inventario. En este momento cada uno de los espacios está en NULL
Inventory.AddZeroed(2);

//Inicialmente no hay ningún elemento del inventario seleccionado
InventorySelectedSlot = EInventorySlot::None;

Con la primera línea lo que hacemos es reservar el espacio en memoria para nuestro inventario. El método AddZeroed de TArray nos permite agregar la cantidad de elementos que indiquemos como parámetro al arreglo, pero en NULL, o sea, vacíos, no hay nada ahí. Es como tener el cajón con todos sus compartimentos vacíos.

Además de esto, inicializamos la variable InventorySelectedSlot en None, o sea, no tendremos nada equipado, en este caso la única forma que tendríamos de defendernos son las manos.

Ahora vamos a implementar la lógica para poder recoger el arma del escenario. De momento lo haremos muy simple, las armas estarán dispersas por el nivel y cuando le pasemos por arriba, automáticamente esta se agregará al inventario si no tenemos ninguna otra arma de ese tipo. Para esto usaremos el método ReceiveActorBeginOverlap del personaje. Este método es el que se dispara cuando se detecta un Overlap entre este Actor y otro. Recuerda que para que se dispare este método tiene que estar en true el atributo Generate Overlap Events.

Muy bien, sabiendo esto, agrega la implementación del método ReceiveActorBeginOverlap en HeroCharacter.cpp:

/** Evento cuando este AActor se superpone con otro AActor */
void AHeroCharacter::ReceiveActorBeginOverlap(class AActor* OtherActor)
{
    Super::ReceiveActorBeginOverlap(OtherActor);
    
    //Chequea si el objeto con el que se está colisionando es un Weapon
    if(OtherActor->IsA(AWeapon::StaticClass()))
    {
        //Casteamos OtherActor a AWeapon dado que el parámetro de ReceiveActorBeginOverlap es de tipo AActor
        AWeapon *OtherActorAsWeapon = Cast<AWeapon>(OtherActor);
        
        //Si el arma con la que estamos colisionando NO ha sido equipada, o sea, que está en el escenario ...
        if(OtherActorAsWeapon->bIsEquipped == false)
        {
            //Chequea si el arma con la que se está colisionando es un ASPAS12Weapon
            if(OtherActor->IsA(ASPAS12Weapon::StaticClass()))
            {
                //Si el espacio para las armas de tipo escopeta del inventario está libre...
                if(Inventory[INVENTORY_GUN_INDEX] == nullptr)
                {
                    //Colocamos en el primer index del arreglo Inventory una referencia a este objeto
                    Inventory[INVENTORY_GUN_INDEX] = OtherActorAsWeapon;
                    
                    //Adjunta la pistola al Socket del Mesh del personaje, donde guarda las armas de tipo escopeta.
                    OtherActorAsWeapon->AttachRootComponentTo(Mesh, "HolsterSocket", EAttachLocation::SnapToTarget);
                    
                    //Ejecuta la lógica que tenga el arma cuando es tomada por el personaje
                    //En este caso simplemente se pone en false bIsEquipped y se muestra un log en la pantalla
                    OtherActorAsWeapon->OnCollected();
                }
                else //... si ya tienes en el compartimento del inventario para las escopetas, una escopeta ...
                {
                    // ... mostramos un log en la pantalla.
                    //Otra variante puede ser, incrementar las municiones de esa arma
                    GEngine->AddOnScreenDebugMessage(-1, 15.0f, FColor::Red, "Ya tienes una escopeta equipada !!");
                }
            }
            
            //@TODO:
            //Aquí puedes agregar el chequeo de otro tipo de arma, como una pistola por ejemplo,
            //y agregarlo el Inventory usando el index INVENTORY_PISTOL_INDEX
            
        }
    }
}

Ve con detenimiento por los comentarios, línea a línea, para que puedas entender en cada paso lo que se hace. A modo de resumen, lo que hacemos es determinar si estamos colisionando con un arma, si es así, miramos que tipo de arma es, para agregarla al índice indicado en el arreglo Inventory. Además, la anclamos al HolsterSocket del personaje.

Muy bien !!, vamos a probar esto. Compila y abre el editor. Desde el panel Mode selecciona All Classes y agrega al escenario una SPAS12Weapon, recuerda colocarla alineada al personaje para que este pueda colisionar con ella.

Ahora, desde el panel Details en la sección Static Mesh selecciona el Mesh de la SPAS12. En este mismo panel, busca la sección Sounds y verás las dos propiedades Shot Sound Effect y Empty Sound Effect. Carga en cada una el Sound Cue correspondiente que creamos al inicio del tutorial, después de importar los .wav.

Captura del editor con la SPAS12Weapon agregada al escenario después de seleccionarle el Mesh

Captura del editor con la SPAS12Weapon agregada al escenario después de seleccionarle el Mesh

 

Como notarás el SphereComponent de la escopeta tiene su centro en la culata de la escopeta. Este es otro problemita que se crea por tener el punto de pivote de este StaticMesh en esa posición, por eso hay que tener en cuenta el punto de pivote al exportar el modelo desde el software de modelado 3D. De momento, para “disimular“ un poco el problema, podemos rotar la escopeta para que salga apuntando hacia arriba.

Muy bien !!, crea una copia de la escopeta y colócala en otra posición, esto para rectificar que tal como programamos, cuando el personaje tenga en el inventario una escopeta, si le pasa por arriba a otra, no la podrá recoger. Por supuesto, esta lógica la podemos cambiar si queremos, por ejemplo, se pudiera comprobar que si tienes ya en el inventario esa arma lo que se obtengan sean municiones. En fin, ya esto depende a lo que quieras en tu juego, de momento lo haremos así para mantenerlo simple.

Listo!! guarda, corre el juego y camina hacia la escopeta. Cuando le pases por encima a la primera, se detectará el evento Overlap y se agregará la pistola al Mesh del personaje en el HolsterSocket (en la parte de atrás del cinturón). Ahora pasa por arriba de la segunda escopeta, verás que esta se mantendrá en el escenario y se mostrará en la pantalla el log: “Ya tienes una escopeta equipada !! ”, tal como queríamos.

Captura del juego una vez que se recoge la escopeta del escenario y esta queda anclada en el HolsterSocket del personaje.

Captura del juego una vez que se recoge la escopeta del escenario y esta queda anclada en el HolsterSocket del personaje.

 

Muy bien !!, ya tenemos el mecanismo necesario para recoger las armas, vamos ahora con la segunda parte, cómo usarlas.

Preparando el AnimMontage para las animaciones cuando se está usando la escopeta

Para el uso de la escopeta en general se necesitan varias animaciones. Necesitamos una animación para equipar la escopeta, otra para dispararla, otra para recargarla cuando se le agoten las municiones y una última para guardarla. Vamos a usar para esto algunas de las animaciones que vienen en el AnimStarterKit que bajamos del MarketPlace en el tutorial pasado, recuerdas ?. En ese paquete de animaciones tenemos todas las que necesitamos, no son perfectas, pero suficiente para el desarrollo del tutorial.

Pero, antes de poderlas usar tenemos un pequeñito problema, y es que estas animaciones vienen con su propio esqueleto, así que tenemos que apoyarnos de una opción que nos da Unreal Engine 4 que es genial. Y es la posibilidad de hacer un retarget de una animación de un esqueleto a otro. Para esto el esqueleto tiene que ser el mismo, como es nuestro caso.

Busca en el Content Browser las siguientes animaciones: Equip_Rifle_Standing, Idle_Rifle_Hip, Fire_Shotgun_Hip, Reload_Shotgun_Hip y ha cada uno dale clic derecho Retarget Anim Assetes/Duplicate Anim Assets and Retarget. Esto te abrirá una ventana para seleccionar el nuevo esqueleto. Selecciona de aquí el esqueleto que usa nuestro HeroCharacter. Repite esto para cada animación y al terminar ya tendremos las animaciones necesarias.

Ventana de selección del esqueleto para hacer el Retarget de la animación

Ventana de selección del esqueleto para hacer el Retarget de la animación

 

Ahora vamos a preparar un AnimationMontage con estas animaciones. En el Content Browser, dentro de la carpeta donde tengas las animaciones del personaje, crea un nuevo AnimMontage y nómbralo UsingShotgunAnimMontage. Dale como nombre de Slot: UpperBody. Todas estas animaciones queremos que se fusionen con las animaciones del State Machina para, por ejemplo, en lo que caminamos poder disparar.

Arrastra las siguientes animaciones en este mismo orden a la sección Montage del AnimMontage: Equip_Rifle_Standing, Idle_Rifle_Hip, Fire_Shotgun_Hip, de nuevo Equip_Rifle_Standing (vamos a usar esta misma animación para la acción de guardar el arma, pero en realidad deberíamos tener una animación específica para esta acción), por último Reload_Shorgun_Hip. Debes tener el Montage así:

Sección Montage del UsingShotgunAnimMontage después de agregar las animaciones

Sección Montage del UsingShotgunAnimMontage después de agregar las animaciones

 

Ahora crea los siguientes Montage Sections: EquipShotgun para el inicio, sustituye el Default por este. IdleShotgun para el inicio de la animación Idle_Rifle_Hip. FireShotgun para el inicio de la animación Fire_Shotgun_Hip, HolsterShotgun para el inicio de Equip_Rifle_Standing (recuerda que la usaremos también para guardar la escopeta) y por último ReloadShotgun al inicio de Reload_Shotgun_Hip. Te quedará de la siguiente forma:

Sección Montage del UsingShotgunAnimMontage después de crear los Montage Sections

Sección Montage del UsingShotgunAnimMontage después de crear los Montage Sections

 

Perfecto, vamos a preparar ahora las secciones para este AnimMontage. En el bloque Sections da clic en el botón Clear. Ahora, como mismo hicimos en el tutorial de los puñetazos, crea las siguientes secciones:

 

Bloque Sections del UsingShotgunAnimMontage después de crear configurar los Montage Sections

Bloque Sections del UsingShotgunAnimMontage después de crear configurar los Montage Sections

 

La idea de esto es obtener el siguiente comportamiento. Para la primera sección, queremos que el personaje desenfunde la escopeta y se quede en el estado Idle_Rifle_Hip en loop. Para la segunda sección queremos que el personaje dispare la escopeta y también se quede en Idle, para la cuarta sección, es simplemente guardar la escopeta, así que después de guardar ya no se usaría más este Montage porque se regresaría a las animaciones del StateMachine. Por último, para la última sección, queremos que el personaje recargue la escopeta y regresa a su estado idle en loop también.

Perfecto !!, ya casi estamos listo para probar, pero nos queda un detalle. Si te fijas en la animación Equip_Rifle_Standing, esta animación comienza y es más o menos por la mitad de la animación cuando el personaje llega a tener la mano sobre la escopeta. O sea que es exactamente en este momento donde tendríamos que desanclar la escopeta del HolsterSocket del personaje y anclarla al HandSocket. Bien, seguro que ya sabes como resolver esto verdad ?? . . . exacto! Con los Branch Point del Montage.

Esto lo necesitamos tanto para la animación de equipar el arma, como para enfundarla así que crea en la posición correcta, apoyándote con la pre-visualización, estos dos Branch Points y nombralos EquipShotgun y HolsterShotgun. Te quedarán así:

BranchsPoints creados en el UsingShotgunAnimMontage en los puntos exactos donde se enfunda y desenfunda el arma.

BranchsPoints creados en el UsingShotgunAnimMontage en los puntos exactos donde se enfunda y desenfunda el arma.

 

Muy bien, ya tenemos casi listo nuestro AnimMontage. Digo casi listo, porque aún nos queda incorporarle un detalle para la reproducción de los efectos de sonido, pero de momento vamos a probar sin ellos. Guarda estos cambios y cierra.

Antes de pasar para el código tenemos que refactorizar dos cosillas en el AnimGraph del HeroAnimBlueprint y también en el Blueprint del personaje, además tenemos que configurar los nuevos inputs que tendremos en nuestro juego. Primero, abre el Blueprint del personaje y elimina el InputAction Punch donde ponemos en true/false la variable Is Punching del personaje. Esta lógica la vamos a cambiar para que se ajuste a nuestro nuevo mecanismo de defensa que ahora cuenta con un inventario con armas, además de los puños.

Por otro lado, abre el AnimGraph del HeroAnimBlueprint y cambia el nombre del Slot a UpperBody (después lo cambiaremos en el AnimMontage de los puñetazos, no te preocupes). Además de esto, selecciona el nodo Layered blend per bone y en el panel Details pon en true la opción Mesh Space Rotation Blend. Esto es necesario para evitar que al hacer el Blend entre las animaciones que usaremos para el uso de la escopeta y las de locomoción, quede rotada la columna del personaje, esto nos permite asegurarnos que la dirección a la que apuntará el cuerpo será siempre la correcta. Puedes probar después poner esta propiedad en false para que veas cual es el efecto más claramente.

tuto7_imagen_13

Muy bien, ahora vamos a ajustar los nuevos controles de nuestro juego. Abre el Project Settings/Input y configura los Bindings de tipo Action de la siguiente forma:

Nueva configuración de los Action Mappings

Nueva configuración de los Action Mappings

 

Como notarás tenemos cuatro Action Mappings nuevos. ReloadWeapon (R) lo usaremos para recargar el arma. Attack (Clic izquierdo del Mouse) lo usaremos para atacar. ActivateInventorySlot1 y ActivateInventorySlot2 (teclas 1 y 2 del teclado respectivamente). Estas las usaremos para activar cada elemento del inventario, con la tecla 1 activaríamos el elemento en el primer compartimento, si tenemos alguno, y con la tecla 2 el elemento del segundo compartimento.

Listo !! guarda y cierra el editor que nos regresamos al código.

Abre el archivo HeroCharacter.h y agrega al final de la declaración los siguientes atributos y métodos

/*
 * Flag para saber si el player está atacando.
 * Se hace true cuando se presiona el click derecho del mouse y false cuando se suelta
 */
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category="Pawn")
bool IsAttacking;

/*
 * Es llamado cuando se presiona la tecla 1.
 * Enfunda/desenfunda el arma que esté en el primer compartimento del inventario (si hay alguna)
 */
void ActivateInventorySlot1();

/*
 * Es llamado cuando se presiona la tecla 2.
 * Enfunda/desenfunda el arma que esté en el segundo compartimento del inventario (si hay alguna)
 */
void ActivateInventorySlot2();

/* AnimMontage para las animaciones del personaje cuando está usando la escopeta */
UPROPERTY(EditDefaultsOnly, Category="Animations")
UAnimMontage* UsingShotgunAnimMontage;

//@TODO: Aquí puedes incluir los AnimMontage para el uso de otras armas

public:

/** 
 * Se llama en el BranchPoint de la animación en el momento de equipar el arma
 * Ancla el arma seleccionada a la mano del personaje
 */
UFUNCTION(BlueprintCallable, Category="Animations")
void OnEquipActiveWeapon();

/**
 * Se llama en el BranchPoint de la animación en el momento de guardar el arma
 * Ancla el arma al cinturón del personaje
 */
UFUNCTION(BlueprintCallable, Category="Animations")
void OnHolsterWeapon();

/** Inicia el ataque con el arma equipada */
void Attack();

/** Recarga el arma equipada */
void ReloadWeapon();

IsAttacking: Lo usaremos como un flag para saber cuando el personaje esté atacando. Lo pondremos en true mientras esté presionado el clic izquierdo del Mouse y en false cuando se suelte.

ActivateInventorySlot1: Este método lo llamaremos cuando el jugador presione la tecla 1 y activará el primer elemento del inventario.

ActivateInventorySlot2: Este método lo llamaremos cuando el jugador presione la tecla 2 y activará el segundo elemento del inventario. En este tutorial no le daremos ninguna implementación, así que queda por tu cuenta agregar otro tipo de arma para el segundo espacio en el inventario 😉

UsingShotgunAnimMontage: Es la instancia del AnimMontage que usará el personaje para las animaciones del uso de la escopeta. Tiene que ser configurado desde el editor.

OnEquipActiveWeapon: Este método será llamado por el BranchPoint que creamos en la animación de desenfundar la escopeta y en este preciso momento se desanclará el arma de HolsterSocket del personaje y se anclará en HandSocket.

OnHolsterWeapon: Es lo contrario del método anterior. Será llamado por el BranchPoint que creamos en la animación de guardar la escopeta y en ese preciso momento se desanclará el arma del HandSocket del personaje y se anclará en el HolsterSocket.

Attack: Este método lo llamaremos cuando el jugador presione/suelte el clic izquierdo del ratón y se encargará de iniciar el ataque con el arma equipada.

ReloadWeapon: Por último, este método lo usaremos para recargar el arma equipada. Será llamado cuando el jugador presione la tecla R.

Muy bien, pasa ahora a HeroCharacter.cpp. Inicializa en false el atributo IsAttacking al final del constructor y agrega las siguientes líneas al final del método SetupPlayerInputComponent:

InputComponent->BindAction("ActivateInventorySlot1", IE_Released, this, &AHeroCharacter::ActivateInventorySlot1);
InputComponent->BindAction("ActivateInventorySlot2", IE_Released, this, &AHeroCharacter::ActivateInventorySlot2);

InputComponent->BindAction("Attack", IE_Pressed, this, &AHeroCharacter::Attack);
InputComponent->BindAction("Attack", IE_Released, this, &AHeroCharacter::Attack);

InputComponent->BindAction("ReloadWeapon", IE_Released, this, &AHeroCharacter::ReloadWeapon);

Agrega ahora al final de la clase la implementación de los métodos ActivateInventorySlot1 y ActivateInventorySlot2

/*
 * Es llamado cuando se presiona la tecla 1.
 * Enfunda/desenfunda el arma que esté en el primer compartimento del inventario (si hay alguna)
 */
void AHeroCharacter::ActivateInventorySlot1()
{
    //Si el Slot actualmente seleccionado es distinto al de la escopeta ...
    if(InventorySelectedSlot != EInventorySlot::Gun)
    {
        //Si el slot para la escopeta está vacío ...
        if(Inventory[INVENTORY_GUN_INDEX] == nullptr)
        {
            //Mostramos un log en la pantalla
            GEngine->AddOnScreenDebugMessage(-1, 15.0f, FColor::Red, "No tienes ninguna escopeta para equipar");
        }
        else //... si el slot de la escopeta NO está vacío ...
        {
            //Mostramos un log en la pantalla
            GEngine->AddOnScreenDebugMessage(-1, 15.0f, FColor::Red, "Equipando la escopeta");
         
            //Reproduce AnimMontage en la primera sección, que es la animación de equipar la escopeta
            if(UsingShotgunAnimMontage)
            {
                PlayAnimMontage(UsingShotgunAnimMontage);
            }
            
            //Actualiza el valor de InventorySelectedSlot para saber en otro momento que actualmente tenemos seleccionada la escopeta
            InventorySelectedSlot = EInventorySlot::Gun;
        }
    }
    else if(InventorySelectedSlot == EInventorySlot::Gun) //Si tenemos equipada la escopeta ...
    {
        //Lanza animación para guardarla (Section HolsterShotgun del UsingShotgunAnimMontage)
        if(UsingShotgunAnimMontage)
        {
            PlayAnimMontage(UsingShotgunAnimMontage, 1.f, "HolsterShotgun");
        }
    }
}

void AHeroCharacter::ActivateInventorySlot2()
{
    //@TODO: Implementar la lógica para activar/desactivar el arma del segundo compartimento del inventario
}

Como siempre, ve detenidamente línea a línea y apoyándote en los comentarios para que entiendas a fondo lo que se hace. En general al presionar la tecla 1 comprobamos si NO tenemos equipada la escopeta, si no la tenemos equipada rectificamos que en el primer index del arreglo Inventory, que es el espacio destinado para las armas de tipo escopeta, exista algún elemento. Si está vacío no podemos equipar nada por lo que de momento mostramos un log en la pantalla , solo para que el usuario lo sepa. Si NO está vacío, entonces reproducimos la animación para equipar la escopeta y actualizamos el valor de InventorySelectedSlot a Gun. Del resto se encargará el método OnEquipActiveWeapon en cuanto se dispare el BranchPoint correspondiente. Por último, en caso que ya tengamos una escopeta equipada, pues lo que hacemos es reproducir la animación de guardarla.

El método ActivateInventorySlot2 te lo dejo para que lo implementes tú 😉 teniendo como referencia el ActivateInventorySlot1 te será muy fácil hacerlo, además sería un muy buen ejercicio.

Vamos ahora a implementar los métodos OnEquipActiveWeapon y OnHolsterWeapon. Agrega al final del HeroCharacter.cpp las siguientes implementaciones:

/**
 * Se llama en el BranchPoint de la animación en el momento de equipar el arma
 * Ancla el arma seleccionada a la mano del personaje
 */
void AHeroCharacter::OnEquipActiveWeapon()
{
    //Rectificamos que InventorySelectedSlot sea igual a Gun para poder saber que index de Inventory usar
    if(InventorySelectedSlot == EInventorySlot::Gun)
    {
        //Obtenemos la referencia al arma en el INVENTORY_GUN_INDEX del arreglo
        AWeapon *Weapon = Inventory[INVENTORY_GUN_INDEX];
        
        if(Weapon)
        {
            //Desancla el arma del HolsterSocket, para anclarla en la mano
            Weapon->DetachRootComponentFromParent(true);
            
            //Ancla la pistola al Socket en la mano
            Weapon->AttachRootComponentTo(Mesh, "HandSocket", EAttachLocation::SnapToTarget);
        }
    }
}

/**
 * Se llama en el BranchPoint de la animación en el momento de guardar el arma
 * Ancla el arma al cinturón del personaje
 */
void AHeroCharacter::OnHolsterWeapon()
{
    //Rectificamos que InventorySelectedSlot sea igual a Gun para poder saber que index de Inventory usar
    if(InventorySelectedSlot == EInventorySlot::Gun)
    {
        //Obtenemos la referencia al arma en el INVENTORY_GUN_INDEX del arreglo
        AWeapon *Weapon = Inventory[INVENTORY_GUN_INDEX];
        
        if(Weapon)
        {
            //Desancla el arma del HandSocket, para anclarla en la mano
            Weapon->DetachRootComponentFromParent(true);
            
            //Ancla la pistola al HolsterSocket
            Weapon->AttachRootComponentTo(Mesh, "HolsterSocket", EAttachLocation::SnapToTarget);
            
            //Reinicia el valor de InventorySelectedSlot a None, para saber que no tenemos ningún arma equipada
            InventorySelectedSlot = EInventorySlot::None;
        }
    }
}

Estos dos métodos son casi uno la inversa de otro. El primero es llamado una vez que el usuario selecciona la tecla 1 para activar el arma que tiene en el primer compartimento del inventario. En ese momento, inicia la animación para desenfundar el arma, y cuando la animación está en el punto donde el personaje tiene la mano ya sobre la escopeta, gracias al BranchPoint que creamos, se llama el método OnEquipActiveWeapon. Este método obtiene la referencia del arma desde el inventario, la desancla del HolsterSocket y la ancla al HandSocket del personaje, para que este la pueda tomar en la mano. Por otro lado, el método OnHolsterWeapon es lo contrario.

Muy bien, ya tenemos implementada la lógica para equipar/guardar el arma. Ahora vamos a implementar la lógica para dispararla. Agrega al final del HeroCharacter.cpp la implementación del método Attack

/** Inicia el ataque con el arma equipada */
void AHeroCharacter::Attack()
{
    //Si IsAttacking es false, se está presionando el clic izquierdo del mouse ...
    if(IsAttacking == false)
    {
        IsAttacking = true;
        
        //Hacemos un switch por los posibles valores que puede tomar el InventorySelectedSlot
        //para saber que arma está equipada e iniciar la lógica de correspondiente
        switch (InventorySelectedSlot)
        {
            case EInventorySlot::Gun: //Si está equipada la escopeta
            {
                //Obtenemos la referencia de la escopeta
                AWeapon *Weapon = Inventory[INVENTORY_GUN_INDEX];
                
                //Si aún tiene municiones ...
                if(Weapon->Ammo > 0)
                {
                    //Reproducimos la animación del disparo con la escopeta
                    if(UsingShotgunAnimMontage)
                    {
                        PlayAnimMontage(UsingShotgunAnimMontage, 1.f, "FireShotgun");
                    }
                }
                
                //Llamamos al método Fire del arma
                Weapon->Fire();
                
                break;
            }
            case EInventorySlot::Pistol:
            {
                //@TODO: Iniciar ataque con pistola
                
                break;
            }
            case EInventorySlot::None:
            {
                //@TODO: Iniciar ataque con golpes
                
                break;
            }
            default: break;
        }
    }
    else //Esta atacando, detiene la acción de atacar (se soltó el clic izquierdo del mouse)
    {
        IsAttacking = false;
    }
}

Este método es llamado cuando en el juego presionamos/soltamos el clic izquierdo del mouse. Al ser presionado, hacemos un switch para determinar que valor tiene la variable InventorySelectedSlot, para saber que arma es la que tenemos equipada, y con la que vamos a atacar. Si es EInventorySlot::Gun quiere decir que tenemos equipada la escopeta. En este caso obtenemos la referencia a la escopeta desde el inventario. Si la escopeta tiene municiones, reproducimos la animación del disparo de la escopeta y por último llamamos al método Fire del arma, que es el encargado de toda la lógica del disparo, como ya vimos.

Casi terminamos, pero si recuerdas cuando implementamos el método Fire de la escopeta, en cada disparo se decrementa la cantidad de municiones de esta. Si las municiones llegan a cero nuestro personaje no podrá disparar más. Para solucionar esto agregamos el método ReloadWeapon. Este método lo podemos llamar al presionar la tecla R y se encargará de comprobar cual es el arma equipada, reproducir la animación correspondiente y llamar al método Reload del arma, que tiene la lógica correspondiente para recargarla. En nuestro caso simplemente reiniciamos la cantidad de municiones. Pues bien, agrega la implementación del método ReloadWeapon al final de HeroCharacter.cpp

/** Recarga el arma equipada */
void AHeroCharacter::ReloadWeapon()
{
    //Si está activa la escopeta
    if(InventorySelectedSlot == EInventorySlot::Gun)
    {
        //Obtenemos la referencia
        AWeapon *Weapon = Inventory[INVENTORY_GUN_INDEX];
        
        if(Weapon)
        {
            //Reproducimos la animación de recargando el arma
            if(UsingShotgunAnimMontage)
            {
                PlayAnimMontage(UsingShotgunAnimMontage, 1.f, "ReloadShotgun");
            }
            
            //LLamamos al método Reload del arma
            Weapon->Reload();
        }
        
        return;
    }
    
    if(InventorySelectedSlot == EInventorySlot::Pistol)
    {
        //@TODO: Aquí puedes agregar la lógica para recargar la pistola
    }
}

Listo !! compila y abre el editor. Antes de darle Play, abre el Blueprint del personaje en el Modo Defaults y busca la propiedad Using Shotgun Anim Montage y selecciona el UsingShotgunAnimMontage que creamos hace un rato, compila y guarda los cambios.

tuto7_imagen_15.1

Por último, en el UsingShotgunAnimMontage creamos dos BranchPoints. Estos BranchPoints los usaremos para llamar a los métodos OnEquipActiveWeapon y OnHolsterWeapon en el momento exacto, como marcamos estos métodos como BlueprintCallable los podemos llamar desde el Blueprint sin problemas. Abre el Animation Blueprint del personaje y agrega lo siguiente:

Trozo del HeroAnimBlueprint donde se agrega la llamada a los método OnEquipActiveWeapon y OnHolsterWeapon a partir de los eventos generados por los BranchPoints creados en el UsingShotgunAnimMontage

Trozo del HeroAnimBlueprint donde se agrega la llamada a los método OnEquipActiveWeapon y OnHolsterWeapon a partir de los eventos generados por los BranchPoints creados en el UsingShotgunAnimMontage

 

Ahora sí !! listo para la acción !?? . . . compila, guarda y ejecuta el juego.

tuto7_imagen_17

Camina hacia la escopeta y cuando la tengas equipada toca la tecla 1 para activarla. Después que la tengas lista, da clic con el mouse para que comiences a disparar. Como configuramos la escopeta para que tenga un máximo de 4 balas, después de dar el cuarto disparo no podrás disparar más, porque te quedaste sin municiones, pero esto no es problema :) … toca la tecla R para que recargues el arma.

Agrega algún objeto al nivel al que le puedas disparar, asegúrate de configurarle su Collision Traces Response en Block para los Trace de tipo Visibility. Equipa la escopeta y dispárale. Verás que en la pantalla se muestra el log con el nombre del objeto, además se dibuja un punto rojo en el punto del impacto, que fue exactamente lo que implementamos. En próximos tutoriales haremos cosas más interesantes ;).

Por último, vuelve a tocar la tecla 1 para que guardes la escopeta.

Bueno, hasta ahora va genial todo, eh ?! . . . pero aún tenemos un detallito pendiente por agregar. Si revisas en los efectos de sonido que tenemos, aún nos quedan por usar dos efectos el shotgload.wav que es el efecto de sonido al recargar la escopeta y shotgr1b.wav que es el efecto al preparar la escopeta para próximo disparo. Pues bien, vamos a usar estos efectos en cada momento y para esto usaremos los Anim Notifiy.

Introducción a los Anim Notifies en Unreal Engine 4

Los Animation Notifications o simplemente AnimNotifies nos brindan una forma de definir eventos en puntos específicos de una animación. Son comúnmente usados para agregar efectos de sonido en la animación, por ejemplo, el efecto del pasos en el momento exacto donde en la animación queda el pie en el suelo. O para emitir un sistema partículas.

Vamos a usarlos en este caso para agregar el efecto de sonido al preparar la escopeta y al colocar las balas al recargarla.

Abre el UsingShotgunAnimMontage y muévete hasta la sección Notifies. Usa el timeline para pararte en el punto de la animación donde el personaje coloca la bala en la escopeta. Esta acción la hace cuatro veces, o sea, coloca cuatro balas, por lo que tendremos que agregar cuatro Notifies. En la sección Notifies da clic derecho justo en el punto exacto y selecciona Add Notify … fíjate que se despliegan varias opciones. Tenemos, PlayParticleEffect y PlaySound, estos dos son notifies pre-creados. Además podemos seleccionar New Notify para crear un Notify específico.

Una vez creado un Notify podemos recibir este evento desde el Animation Blueprint del personaje, como mismo hemos hecho con los Branch Point. En este caso no es necesario irnos por esta vía gracias al notify pre-construido PlaySound.

Entonces, en el punto exacto donde se colocan las balas da clic derecho/Add Notify/PlaySound y en el panel de detalles de ese notify, en la propiedad Sound selecciona del Content Browser el Sound Cue que creaste a partir del efecto shotgload.wav.

tuto7_imagen_18

Repite el proceso para los 4 momentos de la animación donde el usuario agrega una bala. Por último, muévete en el timeline hasta el punto en la animación del disparo donde el usuario prepara la escopeta para el próximo disparo y agrega otro PlaySoundNotify con el efecto shotgr1b.wav.

Listo !!, guarda y dale Play. Dispara la escopeta y podrás escuchar detrás de cada disparo, justo en el momento exacto, sincronizado con la animación, el efecto shotgr1b.wav. Ahora toca la tecla R para recargar y podrás escuchar también el efecto shotgload.wav justo en el momento exacto. . . súper verdad !! :)

Refactorizando el Animation Blueprint para los puñetazos.

Antes de terminar, tenemos que refactorizar el Animation Blueprint del personaje para poder dar puñetazos cuando no se tenga equipada ningún arma. Primero, abre el PunchingAnimMontage y cambia el nombre de slot a UpperBody. Ahora, abre el Animation Blueprint del personaje y modifícalo para que te quede de la siguiente forma:

Animation Blueprint del personaje. El bloque marcado en rojo es lo que se cambió.

Animation Blueprint del personaje. El bloque marcado en rojo es lo que se cambió.

 

Anteriormente aquí obteníamos directamente el valor de la variable IsPunching del personaje, esta variable se hacía true cuando se detectaba el evento Press el input Punch y false con el evento Release. Esto lo cambiamos para ajustarlo al nuevo mecanismo, ahora tenemos que comprobar si la variable IsAttacking está en true y el InventorySelectedSlot está en None, si estas dos condiciones se cumplen es que el personaje no tiene equipada ningún arma y está atacando, así que no queda otra que los puños 😉

Listo !! guarda, y dale play al juego. Sin recoger ningún arma, da clic y verás como el personaje comienza a lanzar puñetazos, puedes recoger el arma del escenario y dar clic y seguirá dando puñetazos. Ahora toca la tecla 1 para equiparla, cuando des clic lo que hará será disparar el arma. Toca de nuevo la tecla 1 para guardarla y vuelve a dar clic, volverá a lanzar puñetazos. Genial, no !?

Pues esto es todo por hoy. En próximos tutoriales estaremos agregando enemigos que nos dispararan al vernos, implementaremos el HUD del juego con el Unreal Motion Graphics y muchas otras cosas. Mientras tanto, me encantaría escuchar tus comentarios y recuerda que puedes seguirme en Twitter (@nan2cc), para que estés al tanto de los próximos tutoriales.