Archivo de la etiqueta: Tasks

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.