Archivo de la etiqueta: UE4

Cómo causar daño a un personaje con puñetazos en UE4 – Parte 1

Hola, seguimos con esta serie de tutoriales sobre el desarrollo de juegos con Unreal Engine 4. En este tutorial vamos a enseñarle a nuestro personaje sus primeras habilidades para defenderse. Vamos a enseñarle a dar puñetazos. Esto nos servirá para hacer una introducción al Animation Montage, al Animation Composite, a los mecanismo de colisión que nos brinda UE4, a los métodos para causar daño a los Pawn, a reproducir efectos de sonido y muchas cosas más. Lo dividiremos en dos partes por su extensión, aquí vamos con la primera.

Como siempre, vamos a partir del mismo proyecto con el que hemos estado trabajando desde el inicio, pero antes de comenzar con el plato fuerte vamos a hacer una pequeña modificación en la clase del HeroCharacter para facilitar el proceso de cambiar entre un estilo de cámara y otro. Abre el código fuente del proyecto y modifica la clase HeroCharacter para que te quede de la siguiente forma:


//--------------------------------------------------------------------
// File: HeroCharacter.h
//--------------------------------------------------------------------

#pragma once

#include "GameFramework/Character.h"
#include "HeroCharacter.generated.h"

/**  Enum ayudante con los estilos de camara del juego */
typedef enum
{
    CameraGameStyleSideScroller, //Estilo Side Scroller
    CameraGameStyleTopDown       //Estilo Top Down
}
CameraGameStyle;

/** Constante para definir el estilo de juego */
#define GAME_STYLE CameraGameStyleSideScroller

/** Clase base del personaje principal */
UCLASS()
class UE4DEMO_API AHeroCharacter : public ACharacter
{
    GENERATED_UCLASS_BODY()

    /** Brazo para apoyar fijar la cámara al Character al estilo side-scroller */
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category=Camera)
    TSubobjectPtr<USpringArmComponent> SpringArm;
    
    /** Cámara del juego, es adjuntada al socket del brazo para lograr el estilo de cámara de un side-scroller */
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category=Camera)
    TSubobjectPtr<UCameraComponent> SideViewCamera;
    
    /** Cantidad de monedas recolectadas por el personaje */
    UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category=Coins)
    int32 CoinsCollected;
    
    /**
     * Se llama cuando el motor detecta la entrada Run
     * Intercambia el estado de correr del personaje
     */
    void ToggleRunState();
    
    /**
     * Inicializa las variables SpringArm y SideViewCamera con la configuracion necesaria
     * para una vista side-scroller
     */
    void InitSideScrollerCamera(const class FPostConstructInitializeProperties& PCIP);
    
    /**
     * Inicializa las variables SpringArm y SideViewCamera con la configuracion necesaria
     * para una vista topdown
     */
    void InitTopDownCamera(const class FPostConstructInitializeProperties& PCIP);
    
    /**
     * Se llama cuando el motor detecta la entrada configurada para 'MoveRight'.
     * En este caso cuando el usuario toca la tecla A o D del teclado
     */
    void MoveRight(float Value);
    
    /**
     *  Se llama cuando se detecta la entrada de tipo MoveForward (W o S).
     *  Determina la dirección en la que está el personaje y le aplica un movimiento (positivo o negativo) en esa dirección
     *
     *  @param Value es igual a 1 cuando se detecta W y -1 cuando se detecta S
     */
    void MoveForward(float Value);
    
    /** Se llama constantemente en el Tick del personaje para determinar si se está colisionando con una moneda */
    void CollectCoins();
    
    /**
     * Se ejecuta automáticamente por el Engine en cada frame del juego
     * @param DeltaSeconds la diferencia en segundos entre el frame pasado y el actual
     */
    virtual void Tick(float DeltaSeconds) OVERRIDE;
    
    /**
     * Metodo de la clase APawn que permite configurar los 'binding' de los controles
     * Es llamado automaticamente por el Engine
     */
    virtual void SetupPlayerInputComponent(class UInputComponent* InputComponent) OVERRIDE;
    
};


//--------------------------------------------------------------------
// File: HeroCharacter.cpp
//--------------------------------------------------------------------


#include "UE4Demo.h"
#include "Coin.h"
#include "HeroCharacter.h"


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

    CoinsCollected = 0;
}

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

}

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

void AHeroCharacter::SetupPlayerInputComponent(class UInputComponent* InputComponent)
{
    //Le dice al motor que cuando detecte las entrada de tipo MoveRight que llame al metodo AHeroCharacter::MoveRight
    InputComponent->BindAxis("MoveRight", this, &AHeroCharacter::MoveRight);
    
    //Solo este input se usa en el estilo TopDown
    if(GAME_STYLE == CameraGameStyleTopDown)
        InputComponent->BindAxis("MoveForward", this, &AHeroCharacter::MoveForward);
    
    //Le dice al motor que cuando detecte la entrada de tipo Jump (barra espaciadora) llame al metodo Jump de la clase ACharacter (la clase padre de esta)
    InputComponent->BindAction("Jump", IE_Pressed, this, &ACharacter::Jump);
    
    //Le dice al motor que cuando detecte la entrada de tipo Run (Shift) estando presionada la tecla, llame al metodo ToggleRunState.
    InputComponent->BindAction("Run", IE_Pressed, this, &AHeroCharacter::ToggleRunState);
    
    //Le dice al motor que cuando detecte la entrada de tipo Run (Shift) al soltar la tecla, llame al metodo ToggleRunState.
    InputComponent->BindAction("Run", IE_Released, this, &AHeroCharacter::ToggleRunState);
}

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

/**
 *  Se llama cuando se detecta la entrada de tipo MoveForward (A o D).
 *  @param Value Value es igual a 1 cuando se detecta D y -1 cuando se detecta A
 */
void AHeroCharacter::MoveRight(float Value)
{
    //Si el estilo de juego es CameraGameStyleTopDown procesamos la entrada MoveRight para lograr el movimiento que se aplique a este estilo
    if(GAME_STYLE == CameraGameStyleTopDown)
    {
        if ( (Controller != NULL) && (Value != 0.0f) )
        {
            //Determina la dirección del movimiento hacia los lados
            const FRotator Rotation = Controller->GetControlRotation();
            const FRotator YawRotation(0, Rotation.Yaw, 0);
            
            // Crea el vector de la dirección y aplica el movimiento
            const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
            AddMovementInput(Direction, -Value);
        }
        return;
    }
    
    //Si el estilo de juego es CameraGameStyleSideScroller procesamos la entrada MoveRight para lograr el movimiento que se aplique a este estilo
    if(GAME_STYLE == CameraGameStyleSideScroller)
    {
        if ( (Controller != NULL) && (Value != 0.0f) )
        {
            // Agrega un vector de movimiento hacia la derecha o la izquierda segun el valor de Value
            AddMovementInput(FVector(0.f,-1.f,0.f), Value);
        }
        return;
    }
}


/**
 * Se llama cuando el motor detecta la entrada Run
 * Intercambia el estado de correr del personaje
 */
void AHeroCharacter::ToggleRunState()
{
    //Si el atributo MaxWalkSpeed del CharacterMovement está en 400.f lo aumentamos a 900.f para que el personaje se mueva mas rápido
    //De lo contrario lo volvemos a poner en 400.f para que regrese a su velocidad de caminar.
    if(CharacterMovement->MaxWalkSpeed == 400.0f)
        CharacterMovement->MaxWalkSpeed = 900.0f;
    else
        CharacterMovement->MaxWalkSpeed = 400.0f;
}

/** Se llama constantemente en el Tick del personaje para determinar si se está colisionando con una moneda */
void AHeroCharacter::CollectCoins()
{
    //Arreglo de AActors para guardar temporalmente todos los Actors que se detecten que están colisionando con el personaje
    TArray<AActor*> CollectedActors;
    
    //CapsuleComponent cuenta con el método GetOverlappingActors. Este metodo nos retorna en la llamada dentro del arreglo que le pasamos por parámetro
    //todos los objetos que estan dentro de la capsula en ese momento.
    CapsuleComponent->GetOverlappingActors(CollectedActors);
    
    //Recorremos todos los objetos dentro del CapsuleComponent
    for(int32 i = 0; i < CollectedActors.Num(); i++)
    {
        
        //Como el arreglo es de AActors tenemos que catear cada elemento a ACoin antes de usarlo
        ACoin *Coin = Cast<ACoin>(CollectedActors[i]);
        
        //Nos aseguramos que la moneda está activa y que no ha sido llamado aún el método Destroy
        if(Coin != NULL && !Coin->IsPendingKill() && Coin->bIsActive)
        {
            //Incrementamos la cantidad de momendas recolectadas
            CoinsCollected++;
            
            //Por último llamamos al OnCollected de la moneda para ejecutar toda la lógica de la moneda cuando esta es tomada por el personaje
            Coin->OnCollected();
        }
    }
}

/**
 * Se ejecuta automáticamente por el Engine en cada frame del juego
 * @param DeltaSeconds la diferencia en segundos entre el frame pasado y el actual
 */
void AHeroCharacter::Tick(float DeltaSeconds)
{
    Super::Tick(DeltaSeconds);
    
    //En cada update del juego llamamos al CollectCoins para estar constantemente determinando si se está colisionando con alguna moneda
    CollectCoins();
}


Aquí no hemos hecho nada especial. Recuerdas que ya teníamos un método para adaptar el juego para el estilo side-scroller y otro para el top-down? Pues lo que agregamos fue la constante GAME_STYLE y una enum con las dos variantes de estilo que tenemos. Para alternar entre un estilo de cámara y otro basta con modificar el valor de esta constante.

Guarda, compila y ejecuta. Yo además modifiqué el SpringArm del personaje desde el editor en la sección Transform, la propiedad Rotation la tengo en Absolute y con los valores X = 0, Y = – 10 y Z = 200. Esto para darle un poco de inclinación a la cámara, pero es cuestión de gusto, déjala como la teníamos hasta ahora o ponla a tu gusto, como prefieras.

Captura de la vista de nuestro juego

Captura de la vista de nuestro juego

En estos momentos nuestro héroe camina, corre y salta, pero no tiene forma de defenderse, verdad ? . . . solo puede salir corriendo si tiene algún problema :). Pues bien, vamos hoy a enseñarle a dar puñetazos. Lo primero que necesitamos es el FBX de las animaciones de puñetazos. Si le das un vistazo a los recursos que descargaste en el primer tutorial, y que los puedes descargar de aquí si aún no los tienes, verás los siguientes FBX:

MontageExample_Start.FBX: Animación de inicio del movimiento del puñetazo

MontageExample_Punch_1.FBX: Animación del puñetazo con el brazo derecho

MontageExample_Punch_2.FBX: Animación del puñetazo con el brazo izquierdo. Esta animación está hecha como continuación de la anterior, con el objetivo de poder mantener al personaje dando golpes alternando derecha / izquierda sin parar.

MontageExample_End_1.FBX: Animación de fin del puñetazo con la mano derecha

MontageExample_End_2.FBX: Animación de fin del puñetazo con la mano izquierda.

Estos recursos que estamos usando son de unos geniales video-tutoriales que preparó el equipo de Epic Games y los puedes ver aquí. Nosotros los hemos tomado prestados para nuestros tutoriales, pero tienen un problema, estas animaciones de puñetazos NO son hechas para el esqueleto de nuestro personaje, lo que implica que al reproducirlas, nuestro héroe se deformará ligeramente, verás que por ejemplo se le separan un poco las extremidades, esto no es un problema para el desarrollo de este tutorial, pero te lo comento para que no pienses que estas haciendo algo mal :) . . . simplemente no tenemos un animador en disposición de estos tutoriales, por eso estamos usando los recursos que podamos … igual, para aprender y jugar con el UE4 vienen de maravilla :)

Importa estos FBX al proyecto dentro de la carpeta Animation o donde prefieras. Ya sabes, en el Content Browser, Import/Seleccionas los FBX. En la ventana de Import del UE4 selecciona Animation y el esqueleto que usa nuestro personaje y da clic en el Botón Import. Listo, ya tenemos los recursos necesarios, puedes darle doble clic a cada una de las animaciones importadas para que les des un vistazo en el Persona Editor.

Vamos a aprovechar esta situación en la que tenemos una animación en partes para dar una rápida introducción al Animation Composite, uno de los assets de animación que nos brinda el Unreal Engine 4.

Introducción al Animation Composite en Unreal Engine 4

Animation Composite es un animation asset que nos permite unir más de una animación una detrás de la otra y tratar esta secuencia de animaciones como una sola animación. En este ejemplo lo vamos a usar para unir las animaciones MontageExample_Start, MontageExample_Punch_1 y MontageExample_End_1 en una sola animación.

En el Content Browser selecciona: New/Animation/Animation Composite, selecciona el esqueleto de nuestro héroe y ponle de nombre PunchAnimComposite. Dale doble clic para editarlo.

Nuevo PunchAnimComposite

En el panel de configuración tenemos tres secciones, la primera es la sección Composite. Desde esta sección haremos la composición de la animación, arrastrando hasta aquí las distintas animaciones que formarán la secuencia final.

Las otras dos secciones (Notifies y Curves) no las veremos en este tutorial, pero sobre todo los Notifies son súper útiles, nos sirven para definir eventos en puntos exactos de la animación y que podemos usar desde el Blueprint para implementar alguna función determinada en ese preciso momento. Por ejemplo, reproducir un efecto de sonido cuando la animación está en el punto exacto en donde el pie toca el piso y así logramos sincronizar el efecto de sonido de los pasos a la animación de caminar.

Arrastra desde el Asset Browser las animaciones MontageExample_Start, MontageExample_Punch_1 y MontageExample_End_1 a la sección Composite. Fíjate que se crean dos filas y las animaciones se agregan alternadamente entre una y la otra fila para facilitar la visualización. Con los controles de reproducción puedes ver como queda la composición de esas tres animaciones en una sola.

PunchAnimComposite configurado con las 3 animaciones en secuencia

Este nuevo asset de animación que tenemos lo podemos usar como un AnimationSequence normal. Vamos a ver un ejemplo simple. Iré por este proceso sin detenernos mucho ya que es algo que a estas alturas no debes tener problema en entenderlo si has seguido los tutoriales anteriores. Sino, te recomiendo que les des un vistazo primero para que entiendas bien todo lo que haremos aquí.

Primero, desde Edit/Project Settings/Input, agrega una nueva entrada de tipo Action dale de nombre Punch y selecciona la tecla R.

Ventana Edit/Project Settings/Engine – Input con la configuraciones de los Bindings

Abre el Blueprint del Character y crea una nueva variable de tipo bool de nombre IsPunching. Esta variable estará en true cuando el usuario toque la tecla R y en false cuando la suelte. Para lograr esto, abre el Event Graph del Blueprint del Character y agrega lo siguiente:

HeroCharacterBlueprint con el algoritmo a ejecutar cuando se detecte la entrada “Punch“

Hasta ahora, la programación de todo lo que tenía que ver con las entradas del usuario y las acciones que se ejecutaban, y en general la lógica del character, las teníamos implementadas desde C++. Para este tutorial quise hacer esto por aquí para mostrarte la variante blueprint, yo en lo personal este tipo de cosas las prefiero hacer en C++, pero es a gusto de cada cual . . . después me dices cual prefieres tú 😉

Como vez, desde el blueprint podemos agregar un nodo que representa al evento Pressed/Released de cada uno de los inputs que tenemos configurados. Cuando el usuario toque la tecla R (que es la tecla que definimos para el Input de nombre Punch) se va disparar este Evento en el blueprint por el puerto del Pressed, cuando la suelta se dispara de nuevo por el puerto del Released.

Simplemente lo que hacemos es darle valor de true a la variable IsPunching cuando se presione la tecla R y de false cuando se suelte.

Ahora abre el AnimationBlueprint del Character, agrega una nueva variable con el mismo nombre, isPunching también de tipo bool. No es necesario que la variable tenga el mismo nombre pero creo que es más claro.

Modifica el Event Graph del AnimationBlueprint del Character para que te quede de la siguiente forma:

Animation Blueprint del Character donde le agregamos el proceso para darle valor a la variable IsPunching según el valor que tenga la variable de mismo nombre en el Character. Recuerda que en el Character la variable IsPunching toma valor según se presione o no la tecla R (“Punch” input)

Recuerda que este algoritmo se ejecuta constantemente en cada update del Animation.

Muy bien, ahora pasa a la maquina de estado que tenemos para el Character y modifícala para que te quede de la siguiente forma:

Máquina de estado del Character con el nuevo estado Punch. A este estado se llega desde el Idle/Walk si IsPunching está en true. Se regresa al Idle/Walk cuando se termina de reproducir la animación

Nada nuevo aquí tampoco, con esto acabamos de darle un nuevo estado al personaje (Punch), a este estado entrará cuando esté golpeando y al terminar de golpear volverá al Idle/Walk. Al agregar el nodo PunchAnimComposite al Final Animation Pose dentro del estado Punch, asegúrate de desmarcar la opción de Loop, ya que no queremos que esta animación se reproduzca en loop.

Compila y ejecuta el juego:

Captura de pantalla del juego en ejecución. Cuando se presiona la tecla R el personaje reproduce el PunchAnimComposite ejecutando toda la secuencia de animaciones que conforman el puñetazo.

Vale, esto a primera vista se ve bastante bien, pero con este mecanismo tenemos varios problemas. Primero, al tenerlo como un estado en el Locomotion State Machine del personaje, solamente se podrá reproducir la animación del golpe en cada momento. No podrá fusionar dos animaciones. Por ejemplo, si queremos que pueda golpear en lo que camina tendremos un problema. Prueba para que veas, camina, y en lo que te desplazas toca la R para golpear, verás como se reproduce la animación del golpe pero en ese momento se estará desplazando por el escenario patinando. Como único pudiéramos solucionar esto es cancelando el desplazamiento cuando se esté moviendo, aunque no es una mala idea, no es lo que queremos hacer, queremos algo mas cool !! :)

De cualquier forma, creo que este simple ejemplo ha servido para ver el Animation Composite que en muchos casos nos puede ser útil. Pero este no es el plato fuerte de este tutorial, vamos con algo mucho más genial que nos brinda el UE4 y es el Animation Montage.

Introducción al Animation Montage en Unreal Engine 4

El AnimMontage es un animation asset que nos otorga una libertad fenomenal para el trabajo con animaciones. Con este tipo de asset podemos exponer el control de la animación hacia el Blueprint, permitiéndonos en cualquier momento, pausar la animación, reproducir una sección determinada de la animación, definir ”al vuelo“ que animación se va a reproducir después de la actual, saltar a reproducir otra animación, loopear de forma súper simple un grupo de animaciones y disparar eventos en cualquier punto de la animación, semejante a los Notifies. En fin, nos da un control total de las animaciones a nivel de código (ya sea C++ o VisualScripting).

En este ejemplo usaremos un AnimMontage para configurar las animaciones relacionadas con los puñetazos del personaje, ya que en el proceso de golpear tenemos que tener total control de la animación en cada momento.

En el Content Browser dentro de la carpeta Character da clic en New/Animation/AnimMontage, selecciona el esqueleto que usa nuestro personaje. Ponle de nombre PunchingAnimMontage y dale doble clic para editarlo.

PunchingAnimMontage acabado de crear y abierto en el Persona Editor

El panel de configuración del AnimMontage es algo parecido al AnimComposite. La primera sección es Montage, aquí es donde agregaremos todas las animaciones que conformarán el AnimMontage, pero a diferencia del AnimComposite, estas animaciones no tienen que ser una secuencia. La idea es que sean animaciones relacionadas a una acción determinada pero que no precisamente las reproduciremos una detrás de otra, sino que podremos crear secciones para seleccionar en cada momento que animación o animaciones reproducir.

Primero tenemos que definir un Slot Name para este AnimMontage. En el campo Slot Name que tiene el alert en rojo “Please provide a slot name for this asset“ escribe PunchSlot

Arrastra desde el Asset Browser hasta la sección Montage las animaciones MontageExample_Start, MontageExample_Punch_1, MontageExample_Punch_2, MontageExample_End_1 y MontageExample_End_2. Las animaciones se agregan alternándose la fila para facilitar la visualización.

PunchingAnimMontage en edición: Agregadas todas las animaciones que formaran el AnimMontage

En este punto, si pre-visualizas la animación, esta no tiene mucho sentido. Se reproducen todas estas animaciones en secuencia, cuando no son una secuencia en realidad. Pues bien, el AnimMontage nos permite, a partir de secciones que definamos, hacer una mezcla entre estas animaciones y después decidir cual vamos a reproducir en cada momento.

Ahora vamos a crear las secciones que tendrá este AnimMontage. En la primera fila del panel Montage da clic derecho y selecciona New Montage Section y ponle de nombre PunchStart. Fíjate que se agrega una línea verde clara vertical y en la primera fila se muestra el nombre de esta sección. Ahora, por defecto tenemos una sección de nombre Default al inicio del gráfico, pon el cursor sobre la línea verde que representa el inicio de esa sección Default y arrástrala hacia delante de la sección PunchStart que acabamos de crear, verás que automáticamente la sección PunchStart se posiciona en el inicio. Ahora puedes dar clic derecho sobre esta sección Default y eliminarla, ya que no la vamos a usar.

Crea una nueva sección y llámala Punch1 arrastra la línea verde de esta sección para que quede exactamente en la unión entre las animaciones PunchStart y Punch1. Notarás que con la rueda del mouse puedes hacer zoom en esta gráfica para ver bien la unión entre las dos animaciones, también notarás que el Editor te ayuda a posicionar la sección en la intersección de dos animaciones.

Repite el proceso creando 3 secciones más de nombre Punch2, PunchEnd1 y PunchEnd2 y posiciónalas al inicio de la animación que le corresponde. Lo que acabamos de crear aquí son las distintas secciones que componen el animation montage. Te quedará de la siguiente forma:

Panel Montage del PunchingAnimMontage después de agregar y posicionar las secciones.

Ya tenemos las secciones que conforman nuestro AnimMontage solo nos falta el último paso: Configurar la relación entre cada una de las secciones. Fíjate que debajo de la zona Montage tienes la zona Sections con dos botones Create Default y Clear. Primero da clic en el botón Clear para restaurar cualquier configuración que tomen por defecto las secciones. Debajo de estos botones tendrás la lista de botones alineados horizontalmente que representan cada una de las secciones que acabamos de crear y debajo de esos botones tendremos finalmente como relacionaremos cada una de las secciones. Después de dar en el botón Clear tienes 4 fila que representan cada una de las secciones en donde ninguna sección tiene relación con otra.

Vamos a modificar esto, vamos conformar la primera sección a partir de la animación PunchStart después de reproducirse PunchStart queremos que seguidamente se reproduzca Punch1 para lanzar el puñetazo con la mano derecha. Bien, vamos a hacer esto hasta aquí.

Primero da clic en el botón verde que dice PunchStart de la primera fila, debajo del botón Preview All Sections, notarás que se pondrá en amarillo ahora toca de los botones alineados horizontalmente arriba, el de nombre Punch1. Verás que automáticamente se agrega el Punch1 a continuación del PunchStart. Te quedará así:

tuto5_imagen_12

Muy bien, ahora vamos a complicar un poquito más la estructura de esta sección. Cuando el personaje esté dando puñetazos lo que queremos es que si se deja la tecla R presionada el personaje continúe dando golpes indistintamente con la mano derecha y la izquierda. Muy bien, pues para esto tenemos que crear un ciclo entre estas dos animaciones Punch1 y Punch2. O sea que el proceso completo sería Punch Start -> Punch 1 -> Punch2 -> Punch 1 -> Punch2 … y así.

Toca en el botón Punch1 de la primera fila debajo del botón Preview All Sections , se te pondrá en amarillo. Ahora toca de los botones alineados horizontalmente arriba el de nombre Punch2. Verás que automáticamente, al igual que con el caso anterior, se te agrega el Punch2 a continuación del Punch1.

Ahora solo nos queda definir para que estas animaciones se mantengan reproduciéndose en ciclo. Pues para esto es tan simple como tocar el botón Punch2 de la primera fila debajo del botón Preview All Sections, cuando se ponga en amarillo toca de los botones alineados horizontalmente arriba, el de nombre Punch1, verás como a diferencia de antes que se agregaba a continuación, ahora se ponen Punch1 y Punch2 en azulitos. Lo que quiere decir que estas dos animaciones se van a reproducir en ciclo. La configuración de la primera sección te quedará así:

Configuración final de las secciones del PunchingAnimMontage

Con esto terminamos de configurar las secciones que componen nuestro PunchingAnimMontage. Fíjate que puedes dar en el botón Preview al inicio de cada fila para que veas la pre-visualización de cada sección. Las dos últimas secciones las queremos así de simples, con una sola animación, ya que las usaremos para llamar a una de las dos, dependiendo en el punto en el que el jugador deje de presionar la tecla R, para empatar el medio del puñetazo con el final de este para el brazo correcto.

Perfecto ¡! Ya tenemos listo el PunchingAnimMontage compila y guarda los cambios. Ahora hay que modificar el Animation Blueprint de nuestro personaje para que use este Montage.

Primero abre el Locomotion State Machine del personaje y elimina el estado Punch que creamos para ver el ejemplo del AnimComposite. Ahora, abre el AnimGraph, en este momento nuestro AnimGraph tiene el nodo de la maquina de estado conectado directamente al Final Animation Pose. Pues bien, para reproducir animaciones desde el código usando AnimMontage necesitamos un nodo especial en el AnimGraph, un nodo de tipo “Slot“. Da clic derecho en el AnimGraph y selecciona dentro de la sección Blends, Slot (No slot name). Esto agregará un nodo de tipo Slot al AnimGraph. A este nodo tenemos que decirle que AnimMontage representará y para esto basta con seleccionarlo y en el panel de propiedades darle el mismo nombre que pusimos en el campo Slot en el PunchingAnimMontage cuando lo creamos, que fue: PunchSlot. Ahora conecta el StateMachina al Slot y este al Final Animation Pose. Listo !! , te quedará así:

AnimGraph del AnimationBlueprint del personaje con el PunchSlot para usar el PunchingAnimMontage

Muy bien, ahora pasa al EventGraph y modifícalo para que te quede de la siguiente forma:

Captura del EventGraph donde hemos agregado la lógica para usar el PunchingAnimMontage. (puedes dar clic para agrandar la imagen)

Vamos a analizar lo que hemos hecho aquí, aunque estoy bastante seguro que si has seguido los tutoriales anteriores no tendrás ningún problema con entender esto. Desde el EventGraph podemos usar varios nodos para trabajar con los AnimMontage. En este caso usamos dos de ellos. El Montage Is Playing que nos permite saber si se está reproduciendo alguna animación de las que están en un AnimMontage y Montage Play que nos permite reproducir un AnimMontage determinado. Al usar el Montage Play se reproduce la primera sección que tenemos definida en el AnimMontage, lo que quiere decir, que en este caso cuando llegue aquí se va a producir la secuencia PunchStart->Punch1Punch2.

En el algoritmo lo que hacemos es determinar el valor de IsPunching, si este está en true preguntamos si se está reproduciendo el PunchingAnimMontage, fíjate que el segundo puerto (o parámetro) de este nodo, nos permite definir a que AnimMontage nos referimos. Entonces, si no se está reproduciendo es que vamos a reproducir el PunchingAnimMontage. Aquí igual, el parámetro Montage To Play nos permite definir que AnimMontage reproducimos, incluso nos permite indicar una velocidad de reproducción mediante el parámetro In Play Rate.

Esta validación del Montage Is Playing la hacemos porque recuerda que este algoritmo se reproduce en cada update del proceso de animación del personaje, y en el tiempo en el que se reproduce cualquiera de las secciones que tenemos definidas en el PunchingAnimMontage este algoritmo se ejecuta más de una vez. Si no hacemos esto, en todo momento estaríamos interrumpiendo la animación para comenzarla de nuevo.

Compila, guarda, ejecuta el juego, y toca la tecla R. Perfecto !!, ya nuestro personaje está dando puñetazo como un loco, fíjate que comienza la animación del puñetazo y después se queda alternando entre el golpe con la mano derecha y con la izquierda. Pero tenemos un problema, a pesar de soltar la tecla R nuestro héroe se queda como loco sin dejar de lanzar puñetazos.

Si analizas lo que hicimos, tiene lógica que esto pase, recuerda que el Montage Play lanza la reproducción de la primera sección que tenemos definida en el AnimMontage, y la primera sección que tenemos en el PunchingAnimMontage no termina, porque después del PunchStart entra en un loop entre Punch1 y Punch2 y de ahí no sale nunca. Pues bien, vamos a solucionar esto.

Recuerdas que tenemos además de esa primera sección en el PunchingAnimMontage dos secciones más no? PunchEnd1 y PunchEnd2 que son la animación del terminado del puñetazo para la mano derecha e izquierda respectivamente. Pues bien, vamos a usarlas, pero tenemos un problema, ¿cómo saber cuando el usuario suelta la tecla R exactamente que mano es la que está lanzando el golpe, para a partir de ahí reproducir la correspondiente animación de fin del golpe según la mano y terminar le proceso de puñetazo ? Pues aquí es donde entra a ayudarnos otra genial funcionalidad del AnimMontage, los Brach Points.

Configurando los Branch Points en el PunchingAnimMontage

Branch Points es la forma que tenemos de lanzar eventos que podemos capturar en el código en puntos exactos de cualquiera de las animaciones que forman parte del Montage, son similares a los Notifies solo que los Notifies son asincrónicos y los Brach Points son sincrónicos. Esto quiere decir que estos últimos tienen más precisión en el tiempo que los Notifies, pero por supuesto, requieren de más procesamiento.

Abre el PunchingAnimMontage y en la cuarta fila del grafico en la sección Montage, debajo de la animación Punch1, da clic derecho/ New Branch Point y ponle de nombre IsPunching1. Ahora arrastra este branch point para colocarlo casi al final de la gráfica del Punch1. Repite el proceso para agregar otro branch point de nombre IsPunching2 debajo de la animación Punch2 y colócalo casi al final de la gráfica de esta animación. Te quedará de la siguiente forma:

Branch Points agregados al final de las animaciones Punch1 y Punch2 en el PunchingAnimMontage.

Esto que acabamos de hacer nos permitirá ejecutar un código determinado en el Blueprint inmediatamente que la reproducción de la animación pase por este punto. O sea, que cuando el personaje esté reproduciendo la animación del puñetazo, casi al terminar la animación del brazo derecho se va a lanzar el evento IsPunching1 y cuando esté terminando con el brazo izquierdo se va a lanzar el evento IsPunching2.

Ahora lo que necesitamos es intervenir estos eventos y preguntar si IsPunching está en false, o sea si el usuario ya soltó la tecla R, y si es así, actualizamos para que al terminar esa animación pase a reproducir la animación de fin según el brazo que sea.

Abre el EventGraph del AnimationBlueprint del personaje. En un espacio en blanco da clic derecho y con el check Context Sensitive marcado fíjate que tienes una sección que dice Add Montage Branching Point Event y esta sección lista los dos branch points que acabamos de crear. Agrégalos los dos al blueprint y continua modificándolo para que te quede de la siguiente forma:

Trozo del AnimationBlueprint del personaje donde se capturan los eventos IsPunching1 y IsPunching2 del AnimMontage para definir la siguiente sección a reproducir del PunchingAnimMontage

Para el trabajo con Montage desde el blueprint también contamos con el nodo Montage Set Next Section. Este nodo nos permite definir cual es la siguiente sección que se va a reproducir después que termine una determinada. Con el parámetro Section Name to Change definimos cual es de la que vamos a partir, en cada caso según el evento, será Punch1 o Punch2 y con el parámetro Next Section definimos la siguiente sección que se va a reproducir, que igual, según sea el evento sería PunchEnd1 o PunchEnd2.

También contamos con el nodo Jump Tu Section, que nos permite exactamente en ese momento interrumpir la animación que se está reproduciendo y reproducir otra sección.

Listo, guarda, compila y ejecuta el juego. Toca la tecla R indistintamente, prueba tocarla y dejarla unos segundos y después soltarla. Notarás que cuando se suelta la tecla R según la mano en la que se quedó dando el puñetazo es la que se usa para terminar la animación del puñetazo … genial, verdad ¡!!??

Vale, pero aún nos va quedando un problemita. Si intentas caminar en lo que estás dando golpes verás que patinas en vez de caminar. Como dijimos, esto lo podemos solucionar simplemente eliminado la posibilidad de caminar cuando se esté dando golpe, pero eso no es lo que queremos en nuestro juego, en nuestro juego queremos poder salir corriendo sin parar y dando puñetazos “de todos colores“ :)

Pues bien, para lograr esto tenemos que usar en el AnimGraph un nodo súper útil que se llama Layered blend per bone. Este nodo nos permite fusionar distintas animaciones en una, pudiendo definir que una parte del esqueleto reproduzca una animación y la otra parte otra animación. Es esto exactamente lo que necesitamos, queremos que la animación del puñetazo se reproduzca en la parte de arriba del esqueleto y la de caminar en la parte de abajo.

Abre el AnimGraph del AnimationBlueprint del personaje. Ahora mismo lo tenemos configurado de la siguiente forma:

AnimGraph del personaje antes de usar el Layered blend per bone

Da clic derecho en una zona en blanco y agrega el nodo de nombre Layered blend per bone. Por defecto este nodo solo tiene un puerto de entrada de nombre Base Pose y otro de salida. Selecciona el nodo, da clic derecho sobre él, y después da clic en Add Blend Pin. De esta forma agregamos un segundo puerto de entrada al nodo.

Muy bien, pero si te fijas tenemos un problema. Nosotros en este visualscript en la salida del StateMachine tenemos la animaciones de locomoción del personaje, y en la salida del PunchSlot tenemos la animación del puñetazo, pero Layered blend per bone necesita también como parámetro lo que nos retorna el StateMachine en limpio. Por tanto, necesitamos el StateMachine conectarlo a dos puertos de entrada y sin embargo este tiene un solo puerto de salida y no lo podemos duplicar. Bien, solucionar esto es muy simple. Tenemos que usar un nodo que nos permite guardar en memoria (en caché) la salida del StateMachine y este nodo si lo podemos duplicar todas las veces que queramos en el blueprint.

Usando el nodo Save Cache Pose para guardar en caché el resultado del SateMachine y poder usarlo varias veces en el blueprint.

En un espacio en blanco del blueprint da clic derecho y con el check de Context Sensitive marcado agrega un New Save cached pose… dentro de la sección Cached Poses. Ahora conecta la salida del StateMachine a este nodo. Bien, ya tenemos en cache el State Machine. Ahora vuelve a dar clic en una zona en blanco y con el check Context Sensitive marcado, agrega Use cached pose ‘HeroLocomotionCache‘. Este nodo es una referencia a esa caché en donde tenemos la salida del StateMachine, y lo mejor que tiene es que lo podemos duplicar todas las veces que necesitemos.

Crea un duplicado de este nodo y conecta uno de ellos al primer puerto de parámetro del Layered blend per bone. El otro conéctalo al PunchSlot y la salida del PunchSlot conéctala al segundo parámetro del Layered blend per bone. Por último el Layered blend per bone conéctalo al Final Animation Pose. Te quedará el gráfico de la siguiente forma:

AnimGraph del AnimationBlueprint del personaje haciendo uso del Layered blend per bone y la caché del StateMachine.

Muy bien, ahora solo nos falta definir en el Layered blend per bone cual va a ser la parte del esqueleto que usaremos para dividir la animación. Selecciona el Layered blend per bone y en el panel de propiedades de este nodo en la sección Config tiene la propiedad Layer Setup que es un arreglo con un solo elemento. Despliega el elemento 0 y este también es un arreglo con un solo elemento de nombre Branch Filters. En este punto, Branch Filters no tiene ningún elemento. Da clic en el botón de + para agregar un elemento y en la propiedad Bone Name escribe spine_01. Spide_01 es el nombre del hueso que vamos a usar para el blend, puedes dar clic en el Modo Skeleton (en la esquina superior derecha) para que veas la estructura del esqueleto, busca y selecciona el spide_01 para que veas que hueso es y que parte del cuerpo controla.

Finalmente el AnimGraph te quedará de la siguiente forma:

AnimGraph del AnimationBlueprint. La zona marcada en rojo es la configuración del Layered blend per bone donde definimos el hueso para hacer el blend de las dos animaciones.

Listo, compila, guarda y ejecuta el juego. Una cosa importante a mencionar y que notarás en cuanto tires el primer puñetazo. Verás que el modelo del personaje se deforma un poco, esto pasa por lo que hablamos al inicio, estas animaciones que puñetazos que estamos usando NO son para este esqueleto. Por supuesto que en nuestro juego vamos a tener un animador que nos dará los FBX correctos para el esqueleto de nuestro personaje :)

Ahora prueba correr y dar puñetazos al mismo tiempo, verás que funciona a la perfección, a medida que la parte inferior del personaje mueve los pies con la animación de caminando o corriendo la parte de arriba reproduce al puñetazo. Tal como queríamos.

Conclusión

Ya nuestro personaje tiene las habilidades de dar puñetazos, así que ya se puede defender. Vamos a dejar esta primera parte aquí, en la segunda parte de este tutorial vamos a agregar a un enemigo para golpearlo, y entrenar un poco con él :), en cada golpe el enemigo recibirá daño y cuando su salud se termine morirá. Eso nos permitirá dar una introducción a los mecanismos de colisión que nos brinda el UE4, a los mecanismo de daño, a como reproducir efectos de sonidos, veremos como usar los geniales recursos que tenemos a la mano en el marketplace y varias cosas más. No te lo pierdas, puedes seguirme en Twitter (@nan2cc) para que estés al tanto.

Mientras, te recomiendo que le des un vistazo a la serie de video-tutoriales que te comenté al inicio: Introduction to Third Person Blueprint Game, y sinceramente de donde tomé la idea y los recursos para este tutorial :). Es en inglés, pero aunque no se te de bien el inglés, después de pasar por este tuto podrás entender muy bien todo. Además verás un ejemplo de uso de los Notifies que comentamos aquí. Te recomiendo no te lo pierdas.

Hasta la próxima !!

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

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

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

Incluyendo el AIModule en nuestro proyecto.

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

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

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

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

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

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

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

Recordando lo que hicimos en el tutorial pasado

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

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

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

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

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

Creando el AIController desde C++

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


//AIEnemyCppController.h

#pragma once

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

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

//AIEnemyCppController.cpp

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

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

}

Compila el proyecto y ejecútalo.

Creando el AIEnemyCppControllerBlueprint.

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

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

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

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

imagen_01

Creando el AIEnemyTargetPoint mediante C++

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

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

//AIEnemyTargetPointCpp.h

#pragma once

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

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

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


//AIEnemyTargetPointCpp.cpp

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


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

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

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

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

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

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

Implementando el algoritmo del UpdateNextTargetPoint desde C++

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Creando el UpdateNextTargetPointTask del Behavior Tree desde C++

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

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

#pragma once

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

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

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

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

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

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


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

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

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

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

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

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

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

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

Implementado el método CheckNearbyEnemy

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Creando el CheckNearbyEnemyService desde C++

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

#pragma once

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

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

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

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

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

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

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

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

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

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

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

Guarda, Compila y ejecuta el juego.

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

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

Implementado el método MoveToEnemy

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

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

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

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

    return MoveToActorResult;
}

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

Creando el MoveToEnemyBTTaskNode desde C++

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

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

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

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

#pragma once

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Behavior Tree completo

Guarda, compila y ejecuta el juego 😉

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

Conclusión

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

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

Introducción a la Inteligencia Artificial en Unreal Engine 4

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

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

Manos a la obra !!

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

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

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


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

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

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

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


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

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

}

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

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

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

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

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

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

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


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

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

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

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

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

Nivel modificado con el NavMeshBoundsVolume

Creado el enemigo a partir de un AIController

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

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

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

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

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

Ahora pasa al Max Walk Speed y ponle 400.

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

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

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

AIEnemyCharacter agregado al nivel en la zona que va a patrullar

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

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

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

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

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

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

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

Enemigo patrullando una zona del nivel

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

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

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

Blueprint del AIEnemyTargetPoints

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

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

Nivel modificado con 4 AIEnemyTargetPoint

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

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

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

Blackboard con los dos primeros Keys: TargetPointNumber y TargetPointPosition

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

AIEnemyController configurado para que ejecute el Behavior Tree

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

CheckNearbyEnemy Service

Agregando el Service CheckNearbyEnemy al Behavior Tree.

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

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

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

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

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

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

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

Algoritmo completo del MoveToEnemyTask en el Blueprint

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

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

Creando un Decorator en el Behavior Tree

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

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

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

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

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

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

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

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

Conclusión

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

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

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

Hasta la próxima !!

Tutorial: ¿Cómo hacer un juego side-scroller 3D con UE4 ?

Introducción

En el tutorial pasado hicimos una introducción general al Unreal Engine 4. Vimos varios aspectos importantes como la jerarquía de clases que sigue el framework, el visual scripting, la configuración de las animaciones para el personaje, definimos temporalmente una cámara fija a nuestro juego, entre otras cosas. Si no lo has visto, debieras darle un vistazo antes de seguir con este.

Hoy vamos a crear la base del estilo de nuestro juego: un side-scroller 3D. Actualmente estoy trabajando con mi equipo (www.spissa.com) en un Runner automático, side-scroller 2D para iOS y Android. Puedes darte una vuelta por nuestro sitio en Facebook para que estés al tanto de la salida. Si te gusta este estilo de juego, te aseguro que te encantara el nuestro 😉

Como te decía, en este tutorial vamos a configurar la cámara del juego para lograr una vista side-scroller. Vamos a agregar unas monedas al escenario y usar un mecanismo simple de colisiones para que el personaje las pueda recolectar. Vamos a “enseñarle“ a nuestro personaje a correr y saltar :). Vamos a ver varios macros de variables y funciones de clase para la integración entre el código C++ y el Editor . . . y muchas cosas más. Listo ?! .. pues “manos a la obra“ !!

Modificando el nivel desde el editor

Vamos a comenzar modificando un poco el nivel actual desde el editor para que se acople más al estilo de juego que queremos implementar. Como ya decía, vamos a trabajar en un juego 3D side-scroller. La base de este estilo de juego es tener la cámara paralela al personaje protagónico siguiéndolo contantemente en la misma posición. El personaje, por su parte, se mueve solamente en dos direcciones: hacia arriba/abajo cuando salta y hacia izquierda/derecha cuando se desplaza.

Abre el editor con el proyecto UE4Demo que dejamos del tutorial pasado y comienza eliminando los objetos visibles que no vamos a usar (las sillas, la mesa, la estatua). Solo quédate con un objeto floor (piso). Después, modifica el objeto floor usando las herramienta de transformación y escalado que tienes en la esquina superior derecha del viewport. Después crea copias del objeto, modifícalas indistintamente y repártelas por el nivel. Asegúrate de dejar el Actor Play Start sobre una de las plataformas, para evitar que el personaje cuando inicie el juego se caiga al abismo. En mi caso quedó así ( tu puedes usar mucho más tu imaginación para lograr algo mejor :) ):

Nivel modificado en el Editor para que se acople más al estilo de nuestro juego

Nivel modificado en el Editor para que se acople más al estilo de nuestro juego

 

Como notarás, al lanzar el juego se muestra un cartel rojo que dice: LIGHTING NEEDS TO BE REBUILT. Esto pasa porque modificamos la geometría en el nivel y el motor necesita recompilar las luces para que la iluminación se adapte a la nueva geometría. Puedes recompilar las luces desde el Toolbar/Build/Build Lighting Only. Vuelve a correr, verás que todo volvió a la normalidad.

Bien, ya tenemos un nivel, muy básico, pero suficiente para implementar y probar todo lo que haremos hoy.

Configurando la cámara para un juego side-scroller.

En este punto es importante aclarar que el UE4 ya trae por defecto una plantilla para comenzar un juego de este estilo. De hecho usaremos prácticamente lo mismo que usa esta plantilla, pero la idea es hacerlo desde cero en este tutorial para entender bien el porqué de las cosas.

Lo primero será cambiar la cámara del juego. En el tutorial pasado configuramos una cámara fija bastante simple, pero que nos sirvió para introducirnos tanto en el visual scripting con el Blueprint Editor, como en la programación en C++. Ya no usaremos más esta cámara. Elimina la actual implementación que tengas de la cámara, si te quedaste con la solución por código, comenta el código dentro del Begin Play y si te quedaste con la solución en el Blueprint, elimina todos los nodos o elimina la conexión que sale del Nodo BeginPlay con esto último se rompe el algoritmo porque no continua la ejecución al lanzarse el BeginPlay pero se mantienen el resto de las conexiones y los nodos en el Editor, por si los quieres para una referencia.

Vamos ahora a configurar un nuevo estilo de cámara. Esta vez lo haremos desde C++ en nuestra clase Character. Siempre ten en cuenta que esto mismo lo puedes hacer también desde el Editor, recuerda que tenemos una clase Blueprint que hereda de nuestra clase HeroCharacter.h. Una buena práctica sería que te aventuraras una vez que veas lo que haremos aquí, ha hacerlo por tu parte desde el Editor modificando el HeroCharacterBlueprint.

Abre la clase HeroCharacter.h y y después del macro GENERATED_UCLASS_BODY() agrega las siguientes declaraciones:


/** Brazo para apoyar fijar la cámara al Character al estilo side-scroller */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category=Camera)
TSubobjectPtr<USpringArmComponent> SpringArm;
    
/** Cámara del juego, es adjuntada al socket del brazo para lograr el estilo de cámara de un side-scroller */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category=Camera)
TSubobjectPtr<UCameraComponent> SideViewCamera;

Lo que hicimos fue agregar dos variables de clase a nuestro HeroCharacter, pero lo que seguro más te llama la atención es el macro UPROPERTY que incluimos en cada variable. Este macro nos sirve para definir si queremos reflejar o no estos atributos en el Editor y que opciones tendremos. Como vimos, una de las cosas geniales que tiene Unreal Engine 4 es la perfecta integración entre el código y el editor y este macro es uno de los que nos ayuda a esto. A este macro le podemos pasar parámetros:

VisibleAnywhere: Indica que esta propiedad será visible en el panel de propiedades en el Editor.

BlueprintReadOnly: Esta propiedad podrá ser leída desde un VisualScript en el Blueprint, pero no modificada.

Category: Permite especificar un nombre de categoría bajo la que se mostrará esta propiedad en el Editor

Puedes ver una referencia detallada de todos los parámetros que podemos indicar para UPROPERTY en la clase Runtime/CoreUObject/Public/UObject/ObjectBase.h dentro del namespace UP. Para llegar rápido puedes dar clic sobre cualquiera de las categorías con la tecla cmd presionada.

Después que implementemos el constructor donde inicialicemos estas variables, vamos a ver en el editor el resultado que tiene definirlas con este macro.

Estas dos variables de clase que creamos para HeroCharacter son de un nuevo tipo de datos que no hemos usado antes. La primera variable que creamos la nombramos SpringArm y es de tipo USpringArmComponent, pero fíjate que usamos para definirle el tipo, la clase parametrisada TSubobjectPtr. Con TSubobjectPtr podemos usar el método CreateDefaultSubobject del objeto PCIP que recibimos en el constructor para crear una instancia de cualquier tipo de dato. Lo haremos en un segundo.
USpringArmComponent nos permite fijar un componente a su padre a una distancia fija. Usaremos este componente para fijar la cámara del juego a una distancia fija del personaje.

Además, creamos la variable de clase SideViewCamera de tipo UCameraComponent. El nombre de la clase es bastante descriptivo, esta será la cámara de nuestro juego :)

Hecho esto vamos a inicializar estas variables en el constructor de la clase. Abre el archivo HeroCharacter.cpp y agrega antes de terminar la implementación del constructor el siguiente bloque:


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

Préstale atención a los comentarios de cada línea para que puedas entender cada una lo que hace. En general creamos y configuramos el objeto USpringArmComponent y lo agregamos al Character. Después, creamos y configuramos el objeto UCameraComponent (la cámara del juego) y la agregamos al USpringArmComponent para fijarla a una distancia del Character. Notar que las instancias de los objetos las creamos a partir del objeto que recibimos en el constructor, con PCIP.CreateDefaultSubobject.

Listo, compila y ejecuta el juego, ahora tendrás la vista del juego al estilo side-scroller :). Puedes probar moverte por el nivel que creaste para que veas como la cámara te sigue en todo momento a la misma distancia.

Nueva vista del juego con la cámara configurada al estilo de un side-scroller

Nueva vista del juego con la cámara configurada al estilo de un side-scroller

 

No cierres el editor, vamos a ver ahora a HeroCharacterBlueprint. En el tutorial pasado este blueprint lo creamos dentro de la carpeta Character. Ábrela y activa el modo Components (esquina superior derecha). Fíjate que ahora en el panel de Componentes del Character tienes un nuevo objeto SpringArm y este tiene como hijo a SideViewCamera. Puedes revisar para que veas que tiene las propiedades que definimos desde programación. Desde aquí también puedes variar todas las propiedades por defecto. Juega un poco con ellas para que veas todo lo que puedes lograr.

Para que veas en la practica el efecto de los atributos que le pasamos al UPROPERTY: Regresa al código y elimina el atributo VisibleAnywhere que le pusimos al macro UPROPERTY y compila de nuevo. Cuando abras el HeroCharacterBlueprint en el editor a pesar de poder ver los componentes, al seleccionarlo no se muestran las propiedades en el panel de detalles.

Modificando los controles del juego para un side-scroller

Ya nuestro juego tiene el estilo de cámara que queremos, pero te habrás dado cuenta de que en este estilo de juego no sirve mucho el control actual, ya que el personaje puede moverse tanto en el eje X como en el Y, cosa que es muy poco común en los side-scroller. Por tanto, vamos a modificar un poco los controles para ajustarlos más a este estilo de juego. Además agregaremos dos nuevos controles y acciones nuevas para nuestro personaje: correr y saltar.

Supongo que ya tengas ideas de como hacer esto si seguiste el tutorial anterior. Entra desde el Editor en Edit/ProjectSettings/Input. Deja solamente MoveRigth. Ahora vamos a agregar otro tipo de input, ActionBinding. Estas entradas, a diferencia de los AxisBinding, son usadas para ejecutar acciones determinadas, saltar por ejemplo o abrir una puerta. Crea una nueva entrada de este tipo, dale de nombre Jump y selecciona la barra espaciadora. Además si te fijas puedes definirle que para que se ejecute tenga que estar presionada otra tecla simultáneamente. En este caso no marques ninguna, solamente con tocar la barra espaciadora el personaje saltará.

Nueva configuración de los controles del juego. Ahora con un control para saltar

Nueva configuración de los controles del juego. Ahora con un control para saltar

 

Ahora vamos al código. Primero, ya no necesitamos el método MoveForward de HeroCharacter, elimina la declaración de este método en el .h y la implementación del .cpp. Además, dentro del método SetupPlayerInputComponent elimina el BindAxis del MoveForward. Por último, tenemos que modificar el código del método MoveRight para cambiar su implementación, ya que actualmente lo que hace el personaje cuando tocamos las teclas A o D es rotar su posición. En este caso lo que queremos es que se mueve hacia delante y atrás respectivamente. Modifica el método MoveForward para que quede así:


/**
 *  Se llama cuando se detecta la entrada de tipo MoveForward (Cuando el usuario toca las teclas A o D).
 *  @param Value Value es igual a 1 cuando se detecta D y -1 cuando se detecta A
 */
void AHeroCharacter::MoveRight(float Value)
{
	if ( (Controller != NULL) && (Value != 0.0f) )
	{
        // Agrega un vector de movimiento hacia la derecha o la izquierda segun el valor de Value
        AddMovementInput(FVector(0.f,-1.f,0.f), Value);
	}
}

Como puedes notar, el método quedó mucho mas simple que la anterior implementación y ya debes entender sin problema que hace. Simplemente aplicamos un vector de movimiento que afecta solo un eje con el valor de Value. Por lo que cuando el usuario toque la tecla D el personaje se moverá hacia delante (hacia la derecha) y cuando toque la tecla A se moverá hacia la izquierda.

Ahora tenemos que implementar el método que se llamará cuando el usuario toque la barra espaciadora para hacer saltar a nuestro héroe. Muy simple, agrega la siguiente línea al método SetupPlayerInputComponent:

InputComponent->BindAction("Jump", IE_Pressed, this, &ACharacter::Jump);

Fíjate en una cosa, el método que se llamará es ACharacter::Jump. O sea, Jump es un método implementado en la clase base de nuestra clase, no en nuestra propia clase HeroCharacter. Este método ya tiene implementado todo lo necesario para hacer saltar al personaje. No es poca la ayuda que nos da el Framework ehh ?? :)

Listo, compila y ejecuta el juego y toca las teclas A y D para que veas como el personaje se mueve correctamente por el escenario, y aunque toques las teclas W o S no pasará nada. Ahora toca la barra espaciadora, eehhh !!! nuestro personaje ya sabe saltar también :) . . . pero algo está mal aquí. Sí, el personaje salta, pero a pesar de estar saltando sigue reproduciendo la animación de caminar. Pues claro, recuerda que solo le tenemos dos animaciones, y no hemos hecho nada para el caso en el que está saltando. Vamos a solucionar esto.

Configurando las animaciones para el salto del personaje

Del tutorial pasado tenemos los recursos que supuestamente nos entregó nuestro equipo de diseño para el personaje principal. Entre ellos tenemos 3 FBX de animaciones Jump_End.FBX, Jump_Loop.FBX, Jump_Start.FBX y Run.FBX. Impórtalas dentro de la carpeta Animations. Puedes abrirlas en el Persona Editor para que les des un vistazo.

Como notarás tenemos tres fbx distintos, básicamente tres animaciones separadas para el salto. Y te preguntarás: ¿Esto por qué?… te cuento. Cuando comenzamos a desarrollar nuestro actual proyecto (puedes darle un vistazo aquí :)) en este juego el personaje salta mucho y a distintas alturas, lo mismo hace un salto simple chiquito, que un salto desde una gran altura y está más tiempo en el aire. El problema que teníamos con esto es que cuando el salto era de gran altura, donde el personaje estaba mucho tiempo en el aire, cuando iniciaba el salto reproducíamos la animación de saltando, que la teníamos en una sola parte, y cuando la animación terminaba su tiempo, como el personaje aun no había caído en el suelo, se quedaba con el ultimo frame de la animación y totalmente tieso, hasta que caía en el suelto y entonces seguía animándose normalmente. Por este motivo es que en la mayoría de los casos es necesario separar las animaciones del salto. Tendremos el pedazo de la animación cuando inicia el salto, después una animación que reproduciremos en repetición (en loop) mientras el personaje este en el aire, y por ultimo el trozo de animación cuando llega al suelo. De esta forma tendremos todo el ciclo del salto animándose perfectamente independientemente de la altura.

Vamos a configurar esto en el Blueprint de las animaciones del personaje. Como ya sabes, para el sistema de animación se usa una maquina de estado que nos permite separar perfectamente todos los estados en los que estará el personaje y configurar la animación para cada estado. Actualmente la maquina de estado de nuestro personaje es sumamente simple. Solamente tiene un estado Idle/Walk. Vamos a modificarla para agregar los estados del salto.

Dentro del HeroAnimBlueprint/AnimGraph, entra en la maquina de estado. Arrastra desde el nodo Idle/Walk para crear un nuevo nodo, ponle el nombre de JumpStart, arrastra desde JumpStart para crear otro nodo y ponle JumpLoop. Repite el procedimiento desde JumpLoop para crear un tercer estado JumpEnd y por ultimo conecta el estado JumpEnd con Idle/Walk para cerrar el ciclo. Te quedará así:

Nueva maquina de estado del Character, con los estados del salto

Nueva maquina de estado del Character, con los estados del salto

 

Lo que acabamos de hacer aquí es definirle 3 nuevos estados que va a tener el personaje y el orden en los que podrá alcanzarlos. O sea, el personaje podrá estar en reposo/corriendo y de ahí puede pasar al estado de JumpStart (inicio del salto). De este estado JumpStart puede pasar a JumpLoop (ciclo del salto). De JumpLoop puede pasar a JumpEnd y por ultimo de JumpEnd regresa nuevamente a Indle/Walk. Fíjate que la conexión entre los estados es una flecha que marca la dirección, o sea, cual es el estado origen y cual el destino. Es bastante lógico, si lo imaginas un poco o te paras un segundito y saltas :) reproducirás estos 4 estados y la transición por cada uno de ellos.

Cada estado tiene sobre la flecha de la transición un iconito arriba que representa la condición que define cuando es que se pasará de un estado a otro. Esta condición las tenemos que programar nosotros (mediante visual scripting con el Blueprint Editor). El primer caso sería cuando el personaje está en Idle/Walk y comienza un salto. La condición que usaremos para pasar a este estado será muy simple, crearemos una variable BOOL que nos permita saber si el personaje está en el aire o no y si está en el aire va a pasar al estado de JumpStart.

Da doble clic en el icono de la transición y entrarás en el modo edición de este elemento, Por defecto tiene un nodo Result con la descripción Can Enter Transition, este nodo espera un parámetro bool (true/false) que le dirá si la maquina activa este estado o no, o sea, si se efectúa la transición. Crea y agrega una nueva variable, como mismo hicimos en el tutorial pasado, pero de tipo bool. Ponle de nombre IsInAir o cualquier otro nombre identificativo que se te ocurra. Agrégala al blueprint en modo GET. Ahora conecta el Puerto de salida de esta variable al Puerto de entrada del nodo Result.

Lo que acabamos de programar aquí mediante visualscripting es: Si la variable IsInAir tiene valor true, entonces ejecuta esta transición. Lo que quiere decir, que el personaje pasara para el estado de JumpStart.

VisualScript de la transición entre Idle/Walk y JumpStart

VisualScript de la transición entre Idle/Walk y JumpStart

 

Guarda y sale del modo de edición de la transición de Idle/Walk a JumpStart y entra en el modo de edición del nodo JumpStart para definir que animación se va a reproducir aquí. Arrastra hacia al blueprint la animación JumpStart que tienes en los recursos y conéctala al nodo final. Para el caso de las animaciones JumpStart y JumpEnd, queremos que se reproduzcan solo una vez, o sea que no se reproduzcan en ciclo como hemos hecho hasta ahora con las animaciones de reposo y caminando, ya que la idea es reproducir JumpStart, al terminar, reproducir JumpLoop (esta sí en ciclo) hasta que el personaje llegue al suelo y por ultimo reproducir JumpEnd, una sola vez. Selecciona el nodo de la animación JumpStart y en el panel de propiedades desmarca la opción Loop.

VisualScript del nodo JumpStart

VisualScript del nodo JumpStart

 

Ahora, ¿cual sería la condición para pasar de JumpStart a JumpLoop?. Pues para este caso usaremos un nuevo nodo. La idea es determinar cuando se esté a punto de acabar la animación de JumpStart para comenzar a reproducir el loop. Da doble clic en el icono de transición de JumpStart a JumpLoop, como ya vimos, por defecto tenemos el nodo Result al que hay que conectar la salida del algoritmo que preparemos aquí y que determina cuando pasa el personaje a este estado. Agrega un nuevo nodo de tipo Time Remaining (Ratio) para Jump_Start Asset. Este nodo nos permite tener en todo momento el tiempo que le va quedando a la animación para que termine. Agrega ahora otro nodo de tipo float < float. Este nodo es un método que nos permite saber si un parámetro A es menor que otro B. Conecta el puerto de salida del nodo TimeRemaining al puerto de arriba de entrada de la comparación. Para el segundo parámetro de la función menor que, no vamos a conectar ningún nuevo nodo, sino que vamos a definir un valor a mano. Como queremos que se comiese a reproducir Jumploop ya cuando esté a punto de terminar el JumpStart, pon en el campo del segundo parámetro de la función menor que: 0.1. Por ultimo conecta la salida de esta funcione al nodo Result. Te quedaría de la siguiente forma: [caption id="" align="alignnone" width="630"]VisualScript de la transición de JumpStart a JumpLoop VisualScript de la transición de JumpStart a JumpLoop[/caption]

 

Básicamente lo que programamos aquí fue: Si el tiempo que le queda a la animación por terminar es menor que 0.1 pasa al siguiente estado.

Guarda estos cambios, sale del modo de edición de la transición y entra en el modo edición del estado JumpLoop, agrega la animación JumpLoop y conéctala al Result. A diferencia de JumpStart, JumpLoop si queremos que se reproduzca en ciclo ya que es esta la animación que se estará reproduciendo mientras el personaje esté en el aire. Para esto, rectifica que en el panel de propiedades del nodo que representa la animación, tenga marcado el atributo Loop.

VisualScript del nodo JumpLoop

VisualScript del nodo JumpLoop

 

Vamos ahora a configurar la transición de JumpLoop a JumpEnd. JumpEnd es la animación cuando termina el salto, para pasar al estado JumpEnd simplemente sería cuando el personaje deje de estar en el aire. Para esto ya tenemos la variable bool isInAir así que vamos a usarla. Da doble clic en el icono que representa la transición entre JumpLoop y JumpEnd, agrega la variable isInAir como GET… un segundo !! isInAir nos dice si el personaje está en el aire, pero, cómo saber si el personaje NO está en el aire ?. Sería la negación de isInAir verdad ?. Entonces, agrega al blueprint un nodo de tipo NOT. Este nodo retorna la negación de su entrada. Conecta isInAir a NOT y NOT a Result. Listo ¡!, en cuanto el personaje deje de estar en el aire pasará a este estado.

VisualScript de la transición de JumpLoop a JumpEnd

VisualScript de la transición de JumpLoop a JumpEnd

 

Sale del modo de edición de esta transición, entra en el modo de edición del estado JumpEnd. Arrastra y conecta al Final Pose la animación JumpEnd y desmárcale la opción de loop.

VisualScript del estado JumpEnd

VisualScript del estado JumpEnd

 

Por último, tenemos que definir las condiciones para pasar de JumpEnd de nuevo a Idle/Walk. JumpEnd es la animación que termina el salto, ya el personaje está en el suelo pero recuperándose de la caída. Por lo que simplemente para pasar a Idle/Walk es esperar a que la animación JumpEnd esté a punto de terminar, como mismo hicimos de JumpStart a JumpLoop. Esto lo debes poder hacer por tu cuenta, así que inténtalo … 😉 te tiene que quedar así:

VisualScript de la transición de JumpEnd a Idle/Walk

VisualScript de la transición de JumpEnd a Idle/Walk

 

Muy bien, ya tenemos la maquina de estado del personaje lista, pero nos falta una cosa. En estos estados estamos usando una nueva variable IsInAir solo en modo GET, pero recuerda, como mismo hicimos para Speed, en algún punto esta variable tiene que tomar valor.

Cierra el AnimGraph y abre el EventGraph, agrega un nodo GetMovementComponent conecta el puerto de salida del TryGetPawnOwner que tenemos desde el tutorial pasado al puerto de entrada del GetMovementComponent, agrega otro nuevo nodo Is Falling, conecta el puerto de salida de GetMovementComponent al de entrada del IsFalling. Agrega la variable IsInAir en modo SET y conecta la salida de IsFalling a la entrada de IsInAir. Por último, conecta el puerto blanco de salida de SET Speed al de entrada de SET IsInAir para la continuidad del algoritmo.

EventGraph del HeroAnimBlueprint modificado para darle valor a la variable IsInAir cuando el personaje esté en el aire

EventGraph del HeroAnimBlueprint modificado para darle valor a la variable IsInAir cuando el personaje esté en el aire

 

Si has llegado hasta aquí desde el tutorial anterior no debes tener problema en entender que acabamos de hacer aquí. Obtenemos el MovementComponent del Character, este tiene un método IsFalling que retorna true/false si el usuario está en el aire o no. Este método es el que usamos para settear el valor de la variable IsInAir.

Listo, ya tenemos la nueva maquina de estado para nuestro personaje. Compila el AnimationBlueprint y corre el juego. Toca la barra espaciadora . . . ya nuestro héroe sabe saltar también :)

Personaje en el medio de un salto animándose correctamente

Personaje en el medio de un salto animándose correctamente

 

Implementando mecanismo para que el personaje corra !!

Ya tenemos a nuestro héroe que sabe caminar, reposar y saltar, pero algo que nos va faltando y es clásico en un side-scroller, la posibilidad de correr, para obtener más impulso, saltos más largos etc. ¿Quién no recuerda en uno de los últimos niveles del primer Super Mario el hueco grandísimo que solo podíamos saltar si lo hacíamos con mucho impulso ehh? :) . . . Pues bien, vamos a darle esta misma habilidad a nuestro héroe. Vamos a implementar que cuando se esté caminando con la tecla Shift presionada el personaje corra.

Primero, abre el editor y agrega en los controles una nueva entrada de tipo ActionBinding ponle el nombre de Run y selecciona al tecla LeftShift. Cierra el editor y abre la clase HeroCharacter.cpp agrega dentro del método SetupPlayerInputComponent las dos siguientes líneas:


//Le dice al motor que cuando detecte la entrada de tipo Run (Shift) estando presionada la tecla, llame al metodo ToggleRunState.
    InputComponent->BindAction("Run", IE_Pressed, this, &AHeroCharacter::ToggleRunState);
    
//Le dice al motor que cuando detecte la entrada de tipo Run (Shift) al soltar la tecla, llame al metodo ToggleRunState.
InputComponent->BindAction("Run", IE_Released, this, &AHeroCharacter::ToggleRunState);

Fíjate en un detalle, estamos llamando al BindAction para la entrada Run dos veces y pasándole el mismo método ToggleRunState (que vamos a implementar ahora) pero la diferencia entre uno y otro es que el segundo parámetro especifica exactamente cuando es que se va a llamar al método. IE_Pressed cuando se presione la tecla Shift y IE_Release cuando se suelte. Lo que queremos hacer es que si se está tocando el shift el personaje corre pero si se suelta deja de correr, algo parecido a la misma combinación que teníamos que hacer en el Super Mario para el super salto !! :)

Bien, ahora vamos a implementar el método ToggleRunState. Agrega en el .h de HeroCharacyer.cpp la declaración del método:


/** 
* Se llama cuando el motor detecta la entrada Run 
* Intercambia el estado de correr del personaje
*/
void ToggleRunState();

Pasa a la .cpp y agrega la implementación:


/**
 * Se llama cuando el motor detecta la entrada Run
 * Intercambia el estado de correr del personaje
 */
void AHeroCharacter::ToggleRunState()
{
//Si el atributo MaxWalkSpeed del CharacterMovement está en 400.f lo aumentamos a 900.f para que el personaje se mueva mas rápido
//De lo contrario lo volvemos a poner en 400.f para que regrese a su velocidad de caminar.
    if(CharacterMovement->MaxWalkSpeed == 400.0f)
        CharacterMovement->MaxWalkSpeed = 900.0f;
    else
        CharacterMovement->MaxWalkSpeed = 400.0f;
}

Muy simple, por defecto la velocidad de desplazamiento del personaje es 400 cuando este método se llama por primera vez (cuando se presiona shift) el MaxWalkSpeed está en 400 y se pasa a 900, lo que hará que el personaje de desplace más rápido, y cuando se suelte el Shift se va a llamar de nuevo y se volverá a poner la velocidad en 400, disminuyendo el desplazamiento.

Una buena tarea sería que intentes implementar este mecanismo de correr en el Blueprint del HeroCharacter. Recuerda eliminar el código C++ si vas a usar el Blueprint. Te tendría que quedar así:

Variante en el Blueprint de la funcionalidad del correr

Variante en el Blueprint de la funcionalidad del correr

 

Yo en lo personal prefiero siempre mantener toda la lógica del personaje desde C++, pero ese ejercicio te puede servir para tomar más soltura en el Blueprint Editor que tanto llama la atención :).

Otra buena tarea sería que expongas los valores máximo y mínimo del MaxWalkSpeed en el Editor para que se puedan modificar fácilmente sin necesidad de llegar al código. Ya sabes que tendrás que usar el macro UPROPERTY, pero este si no te diré como tiene que quedarte, inténtalo por tu cuenta 😉

Compila y ejecuta el juego, a medida que estas caminando deja presionada la tecla shift, verás que el personaje se desplaza mucho más rápido, pero muy feo ya que solamente se afecta su desplazamiento pero no se afecta la animación.

Agregando la animación de correr al personaje

Vamos a agregar otra animación a nuestro personaje, para que cuando esté corriendo se anime correctamente. Para esto usaremos el mismo estado Idle/Walk que ya tenemos, pero más aun, dentro de este usaremos el mismo nodo con el que hacemos blend entre las animaciones de Idle y Walk. Una súper potencialidad de este nodo es que podemos agregarle más animaciones, no solo dos. La idea será modificarlo para configurarle tres puntos de control y no dos como tenemos ahora. Uno al inicio del grafico con la animación Idle, otro en el medio con la animación Walk y otro al final con la animación Run.

Importa Run.FBX de los recursos para el proyecto. Abre el IdleWalkBlendSpace1D que creamos en el tutorial pasado. Cámbiale la propiedad X Axis Range a 900 (que es el valor que toma el personaje al correr) y da clic en el botón Apply Parameter Changes. Ahora agrega al inicio del gráfico la animación de Idle, en el medio la animación de Walk y al final la animación de Run. Asegúrate de tener marcada la opción de Enable Preview BlendSpace y mueve el cursor sobre el gráfico para que veas como se van haciendo los blend, según el valor de speed, entre reposo/caminando/corriendo. Súper genial y fácil ehh ??.

Nueva configuración del IdleWalkBlendSpace1D para Idle/Walk/Run

Nueva configuración del IdleWalkBlendSpace1D para Idle/Walk/Run

 

Guarda, ejecuta el juego y prueba correr. Ya nuestro personaje camina y corre perfectamente.

Personaje corriendo con las teclas D+Shift presionadas

Personaje corriendo con las teclas D+Shift presionadas

 

Agregando monedas para recolectar en el nivel

Ya tenemos nuestro personaje moviéndose por el escenario caminando, corriendo y saltando al estilo side-scroller. Pero ir caminando por ahí sin nada que hacer es algo aburrido, no ? Vamos a tratar de mejorar esto un poco :) . . . vamos a implementar la posibilidad de recolectar monedas por todo el escenario. En futuros tutoriales veremos que gana nuestro héroe con estas monedas.

Primero, necesitamos el modelo de la moneda. En el MarketPlace de seguro que podrás encontrarte muchísimos StaticMesh que puedas usar como moneda que nuestro personaje pueda recolectar. Te recomiendo que te tomes un tiempo para recorrer el MarketPlace, de seguro te encontrarás cosas que te encantarán… y muchas FREE ¡! 😀

De cualquier forma, puedes bajar de aquí el FBX de una moneda MUY SIMPLE Y FEA :S porque ya te he comentado que se me da muy mal el modelado 3D, pero perfecta para nuestro tutorial. Importa este FBX al proyecto (yo creé una carpeta Coin). En la ventana de importar verás que Unreal detecta que este es un StatickMesh, expande la opción avanzada y marca las opciones material y texture. Esto para que importes también el material que tiene aplicado el modelo, que igual, es extremadamente simple pero le da el look de moneda, por su color amarillo.

Bien, ya con el recurso importado vamos a crear la clase C++ que encapsulará toda la lógica de la moneda. Crea una nueva clase desde el editor de nombre Coin. En este caso que herede de Actor. Cuando el Editor te pregunte si quieres abrir la clase en el IDE dile que sí. Modifica el .h de la clase para que te quede de la siguiente forma:


/** Representa una moneda. El personaje puede capturarla colisionando con ella */
UCLASS()
class ACoin : public AActor
{
	GENERATED_UCLASS_BODY()
    
    /** 
     * USphereComponent es un componente en forma de esfera generalmente usado para detectar colisiones simples 
     * Este será el Root de la moneda y con él detectaremos las colisiones entre el personaje y la moneda
     */
     UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Coin)
    TSubobjectPtr<USphereComponent> BaseCollisionComponent;
    
    /** StaticMesh de la moneda, ya lo usamos anteriormente con el Character. En él tendrémos la instancia de el StaticMesh de la moneda */
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Coin)
    TSubobjectPtr<UStaticMeshComponent> CoinMesh;
    
    /** Simple variable booleana para activar o no la moneda */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Coin)
    bool bIsActive;
};

Préstale atención a los comentarios, como siempre, para los detalles. Básicamente lo que hacemos es definir el atributo BaseCollisionComponent que será el componente raíz de nuestra moneda, y el que usaremos para detectar colisiones con ella y un UStaticMeshComponent para poder definir el StaticMesh que representará a la moneda en el nivel. Por último el atributo bIsActive que lo usaremos como bandera para desactivar la moneda cuando se colisione con ella.

Pasa ahora a la .cpp y modifica el constructor para que te quede así:


ACoin::ACoin(const class FPostConstructInitializeProperties& PCIP)
	: Super(PCIP)
{
    //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
    CoinMesh = PCIP.CreateDefaultSubobject<UStaticMeshComponent>(this, TEXT("CoinMesh"));
    
    //Agregamos el UStaticMeshComponent como hijo del root component
    CoinMesh->AttachTo(RootComponent);
    
    //Por defecto la moneda estará activa
    bIsActive = true;
}

Aquí instanciamos un USphereComponent como el RootComponent de nuestra moneda. Creamos la instancia del UStaticMeshComponent que después configuraremos desde el editor, lo agregamos al RootComponent y por ultimo inicializamos en true bIsActive. Ya que queremos que por defecto la moneda esté active.

Agregando monedas al nivel

Vamos ahora a crear el Blueprint de la moneda, como mismo hicimos con el Character en el tutorial pasado. Entra en la carpeta Coin del ContentBrowser, clic derecho/Blueprint y selecciona Coin como clase base, dale de nombre CoinBlueprint.

Como verás, está compuesta por los mismos componentes que definimos en el constructor un USphereComponent como RootComponent y un UStaticMeshComponent. Despliega el CoinMesh y selecciona el StaticMesh que importamos para la moneda.

Sección Components del CoinBlueprint. Agregándole el StaticMesh

Sección Components del CoinBlueprint. Agregándole el StaticMesh

 

Ahora, vamos a agregar algunas monedas al escenario. Busca en el ContentBrowser el CoinBlueprint y arrástralo al nivel en lugares donde quieras que salga la moneda para que el personaje las pueda alcanzar. Una cosa importante a tener en cuenta, recuerda que en este estilo de juego el personaje siempre tendrá un Y fija por lo que tienes que asegurarte de poner las monedas en la misma Y del personaje para que cuando camine en dirección de la moneda pueda colisionar con ella. Mi nivel es muy simple, quedó así:

Nivel en edición con dos monedas agregadas.

Nivel en edición con dos monedas agregadas.

 

Ya tenemos las monedas en el nivel, pero poco se puede hacer con eso. Si caminas hacia las monedas no pasa nada, además las monedas se ven muy feas estáticas ahí sin moverse. Vamos a arreglar estas cosas

Simple mecanismo de colisión para recolectar las monedas

Como pudiste ver, ahora mismo cuando el personaje le pasa por arriba a la moneda no pasa nada, esta sigue ahí como si nada pasara. Vamos a solucionar esto implementado la lógica para detectar cuando el personaje está sobre una moneda. De momento será simple nuestra implementación, lo que haremos es incrementar un valor de “Monedas recolectadas“ que tendremos en nuestro personaje y llamar al método OnCollected() que crearemos en la moneda para ponerla inactiva, eliminarla del nivel y temporalmente imprimir un log en la pantalla para debuguear este mecanismo.

Abre la clase HeroCharacter.h ya agrega las declaraciones siguientes:


    /** Cantidad de monedas recolectadas por el personaje */
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category=Coins)
    int32 CoinsCollected;

    /** Se llama constantemente en el Tick del personaje para determinar si se está colisionando con una moneda */
    void CollectCoins();
    
    /** 
     * Se ejecuta automáticamente por el Engine en cada frame del juego
     * @param DeltaSeconds la diferencia en segundos entre el frame pasado y el actual
     */
    virtual void Tick(float DeltaSeconds) OVERRIDE; 

Vamos a comentar un poco cual es la lógica que usaremos. Todos los Actors en Unreal Engine cuentan con el método Tick. Este método es llamado automáticamente por el Engine en cada frame del juego. Si estas leyendo este tutorial probablemente estés familiarizado con el desarrollo de juegos, al menos la teoría, y sabrás de sobra la importancia de este método. Básicamente, todos lo algoritmos que queramos que estén en constante ejecución por un Actor determinado tiene que ir dentro de este método Tick. El método Tick recibe como parámetro un float que es la variación en segundos entre un frame y el anterior. Ese valor es de mucha ayuda para usarlo como multiplicador a la hora de modificar la posición del personaje o la rotación del actor para que esto se ejecute dependiente del framerate que tenga el juego en ese momento y evitar saltos en caso que la ejecución del juego baje el framerate.

Entonces, lo que haremos será tener en constante ejecución el método CollectCoins y este método lo que hará será determinar si hay alguna moneda dentro del CapsuleComponent del Character, si es así, es que está arriba de una moneda.

Abre HeroCharacter.cpp y antes de terminar la implementación del constructor agrega la línea CoinsCollected = 0. Muy simple, al crearse el Character no ha recogido ninguna moneda. Ahora agrega la implementación del método CollectCoins() y Tick


/** Se llama constantemente en el Tick del personaje para determinar si se está colisionando con una moneda */
void AHeroCharacter::CollectCoins()
{
    //Arreglo de AActors para guardar temporalmente todos los Actors que se detecten que están colisionando con el personaje
    TArray<AActor*> CollectedActors;
    
    //CapsuleComponent cuenta con el método GetOverlappingActors. Este metodo nos retorna en la llamada dentro del arreglo que le pasamos por parámetro
    //todos los objetos que estan dentro de la capsula en ese momento.
    CapsuleComponent->GetOverlappingActors(CollectedActors);
    
    //Recorremos todos los objetos dentro del CapsuleComponent
    for(int32 i = 0; i < CollectedActors.Num(); i++)
    {
        
        //Como el arreglo es de AActors tenemos que catear cada elemento a ACoin antes de usarlo
        ACoin *Coin = Cast<ACoin>(CollectedActors[i]);
        
        //Nos aseguramos que la moneda está activa y que no ha sido llamado aún el método Destroy
        if(Coin != NULL && !Coin->IsPendingKill() && Coin->bIsActive)
        {
            //Incrementamos la cantidad de momendas recolectadas
            CoinsCollected++;
            
            //Por último llamamos al OnCollected de la moneda para ejecutar toda la lógica de la moneda cuando esta es tomada por el personaje
            Coin->OnCollected();
        }
    }
}

/**
 * Se ejecuta automáticamente por el Engine en cada frame del juego
 * @param DeltaSeconds la diferencia en segundos entre el frame pasado y el actual
 */
void AHeroCharacter::Tick(float DeltaSeconds)
{
    Super::Tick(DeltaSeconds);

    //En cada update del juego llamamos al CollectCoins para estar constantemente determinando si se está colisionando con alguna moneda
    CollectCoins();
}

Este método como dijimos es el que se llamará constantemente en el Tick del player para determinar si se está colisionando con una moneda o no. Tomate unos minutos en los comentarios de cada línea para que entiendas bien el proceso para determinar si se está colisionando con una moneda. Este es un mecanismo de colisión muy simple, pero suficiente para nuestro primer trabajo con colisiones en Unreal Engine 4 :). Una cosa importante, dentro del método CollectCoins hacemos referencia a la clase ACoin que creamos, para que esto no de error ve a la parte de arriba del .ccp y debajo de la línea #include “UE4Demo.h” agrega #include “Coin.h”

Solo nos falta un detallito. Si intentas compilar ahora tendrás un error, porque cuando se detecta una colisión se llama al método OnCollected de la clase Coin, y nosotros nunca hemos implementado ese método. Pues bien, vamos a implementarlo.

Agrega la declaración del método en el Coin.h y la implementación en el .cpp


/** A llamar cuando se detecte la colision con una moneda */
void ACoin::OnCollected()
{
    //A modo de Debug ponemos un log en la pantalla
    GEngine->AddOnScreenDebugMessage(-1, 15.0f, FColor::Red, "Coin Collected !!");
    
    //Pasamos a false el flag de bIsActive
    bIsActive = false;
    
    //Y con el método Destroy de Actor, eliminamos la moneda del escenario
    Destroy();
}

Este método es el que se llamará cuando el personaje colisiona con la moneda, y de momento es muy simple. Primero usamos el método AddOnScreenDebugMessage del objeto GEngine que nos da el Framework. Este método es muy útil porque nos permite imprimir en la pantalla mensajes de un color determinado por un tiempo determinado. Para ir revisando nuestro código en tiempo de ejecución es genial. Aquí lo usamos para cuando el personaje colisione con una moneda se imprime en pantalla el texto Coin Collected !!”. Además ponemos en false la moneda y la eliminamos del escenario con la ayuda del método Destroy() de la clase Actor.

Listo, compila y ejecuta el juego. Ahora camina hacia una moneda … Bieeen !! al pasarle por arriba a la moneda esta se elimina de la escena, se incrementa la cantidad de monedas colectadas por el personaje (esto es interno, de momento visualmente no mostramos nada al respecto) y por ultimo mostramos ese log temporal en la pantalla de Coin Collected ¡!.

“Debugueando” las colisiones en el escenario

Si le prestas atención a los detalles verás un problemita que tenemos en este mecanismo de colisión. Trata de acercarte poco a poco a la moneda, verás que la colisión se detecta antes de que el personaje esté sobre la moneda. Para encontrar el problema en estos casos el Unreal Engine nos brinda un comando fenomenal que nos permite revisar en tiempo de ejecución el componente que tiene cada actor para las colisiones. Dale Play al juego nuevamente y fíjate que el editor tiene en la esquina superior derecha un campo para escribir comandos. Escribe en ese campo: show COLLISION y toca Enter. Inmediatamente en el juego se ve sobre cada actor los componentes.

Juego corriendo con el comando show COLLISION para debuguear los componentes para colisiones de cada Actor.

Juego corriendo con el comando show COLLISION para debuguear los componentes para colisiones de cada Actor.

 

Fíjate que el componente de la moneda es mucho mayor que el modelo de la moneda y probablemente sea bueno también reducir el radio de la capsula del personaje. Termina la ejecución del juego, abre el CoinBlueprint, selecciona en el modo Componentes el ROOT, en el panel Detalles busca la sección Shape que tiene la propiedad del radio de la esfera. Cambia este valor a 15, verás como en el preview la esfera que rodea a la moneda se acopla mucho más a nuestro modelo. Guarda y prueba de nuevo, verás como mejoró y ahora tiene el personaje que pegarse mucho más a la moneda para atraparla.

Modificando el radio del componente esfera para ajustar las colisiones con la moneda

Modificando el radio del componente esfera para ajustar las colisiones con la moneda

 

Puedes hacer lo mismo para la capsula del personaje si la quieres modificar un poco. Vale aclarar también que estos valores los puedes definir desde un inicio en el constructor de la clase desde C++. Prueba hacerlo como ejercicio.

Haciendo rotar las monedas constantemente en su eje.

Para darle un poco de vida a las monedas en el escenario, vamos a hacer que estas estén rotando siempre en el eje Y. Cierra el editor, abre la clase Coin.h y agrega las siguientes declaraciones:


    /** Factor de rotacion de la moneda */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Rotation")
    FRotator RotationRate;
    
    virtual void Tick(float DeltaTime) OVERRIDE;

Simplemente agregamos la declaración del método Tick que ya conocemos y un atributo nuevo RotationRate de tipo FRotator. Este será el vector de rotación que usaremos para aplicarle a la moneda constantemente en cada Tick. En realidad no tenemos que declarar un atributo para esto, pero haciéndalo así y sobre todo gracias al macro UPROPERTY podemos dejar en configuración desde el Editor el factor de rotación que tendrá la moneda.

Pasa ahora a Coin.cpp, antes de terminar la implementación del constructor agrega estas dos línea


    //Inicializa el factor de rotacion de la moneda en cada update
    RotationRate = FRotator(0.0f, 180.0f, 0.0f);
    
    //Activa para que se llame el método Tick de este actor en cada update del juego
    PrimaryActorTick.bCanEverTick = true;

La primera es la inicialización del vector de rotación que usaremos en el Tick de la moneda y la segunda línea es muy importante. Por defecto en esta clase que hereda de Actor el método Tick NO se llama, para activar en el Engine que se llame el tick de esta clase es necesario inicializar el atributo PrimaryActorTick.bCanEverTick en true.

Bien, hecho esto agrega la implementación del método Tick


void ACoin::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    //Agrega una rotacion a la moneda en cada tick para tenerlas rotando constantemente en el escenario
    AddActorLocalRotation(this->RotationRate * DeltaTime, false);
}

Como vez, muy simple. Básicamente en cada update de este Actor lo que hacemos es agregar un factor de rotación gracias al método AddActorLocalRotation.

Listo !! compila ejecuta y prueba. Super verdad !! ya las monedas se encuentran rotando en el escenario esperando a que las atrapes :)

Implementando la lógica de la moneda mediante el Blueprint.

Como ya hemos dicho, la decisión de implementar una determinada cosa en C++ o mediante VisualScripting con el Blueprint Editor es de uno. Yo en lo personal sigo las dos siguientes premisas. Primero, implementar algo siempre en un solo lugar. O sea, no tener parte de la lógica de un Actor en C++ y otra parte en el Blueprint. Trato de tenerlo todo en un mismo lugar en la medida de lo posible. Segundo, si la lógica a implementar en determinado Actor es muy simple, como es el caso de esta moneda, pues la solución ideal es el Blueprint. Independientemente de que con el Blueprint se pueden implementar algoritmos súper largos y muy complejos, yo para estos casos prefiero usar C++.

Una buen ejercicio es que intentes implementar la lógica de la moneda, todo lo que hemos hecho aquí pero en Blueprint. Te tiene que quedar así:

VisualScript de la moneda

VisualScript de la moneda

 

Fíjate que desde el Blueprint implementamos lo que va a pasar cuando se llama el método OnCollected. Para poderlo agregar aquí tienes que agregar en la declaración del método el macro UFUNCTION(BlueprintNativeEvent) o UFUNCTION(BlueprintImplementableEvent). Te explico ambos:

UFUNCTION(BlueprintImplementableEvent) es un método que NO va a tener implementación en C++, solamente será para agregarlo al Blueprint e implementarlo desde ahí. Por lo que en el .cpp no tendrás nada de este método y en el .h solo la declaración. Es en el Blueprint donde tienes que implementar lo que hará el método.

UFUNCTION(BlueprintImplementableEvent). Este es un poco más interesante. Cuando creamos un método con este macro, en la implementación tenemos que agregar en el nombre del método _Implementation. Por ejemplo, si quieres probar con OnCollected. La definición sería de la siguiente forma:


UFUNCTION(BlueprintNativeEvent)
void OnCollected();

Y la implementación quedaría así:


void ACoin::OnCollected_Implementation()
{
    //A modo de Debug ponemos un log en la pantalla
    GEngine->AddOnScreenDebugMessage(-1, 15.0f, FColor::Red, "Coin Collected !!");
    
    //Pasamos a false el flag de bIsActive
    bIsActive = false;
    
    //Y con el método Destroy de Actor, eliminamos la moneda del escenario
    Destroy();
}

Lo bueno con esto es que desde C++ tenemos una implementación, pero si queremos podemos sobreescribir esta implementación desde el Blueprint … super verdad ¡!?? cosas del UE4 :). Prueba esto implementado toda la lógica de la moneda desde VisualScripting, para que vayas ganando soltura con el Blueprint Editor, una de las maravillas del Unreal Engine 4.

Conclusión

Bueno, vamos terminando por hoy con este segundo tutorial sobre el desarrollo de juegos con Unreal Engine 4. Hoy aprendimos varias: Un mecanismo simple de detección de colisiones, la utilidad del método Tick, le agregamos las acciones de correr y saltar al personaje, configuramos la cámara de nuestro juego al estilo Side-Scroller 3D, vimos varios macros de variables y funciones de clase para la integración entre el código C++ y el Editor, y otras cosillas. Para el próximo tutorial continuaremos nuestro juego, vamos a agregar un HUD para que el usuario sepa la cantidad de monedas que ha alcanzado y el tiempo que le queda para lograr su objetivo, vamos a programar el GameMode para definir las condiciones de GameOver del juego y muchas cosas más 😉

Espero que te haya sido útil y te guste, si es así, déjame saber tus comentarios y comparte este tutorial con el resto de tus amigos también apasionados por el desarrollo de video juegos con Unreal Engine 4. Hasta la próxima !! . . . bye