Archivo del Autor: Fernando Castillo Coello

Acerca de Fernando Castillo Coello

I've been into games development with Unreal Engine since 2014 and want to share with you some of the tips and tricks that I learned during my journey.

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

Introducción al desarrollo de video juegos con Unreal Engine 4

Unreal Engine 4, en mi opinión, es uno de los motores de juegos más potente que existe en la actualidad, y el equipo de Epic Games lo ha puesto por completo a disposición de todos bajo una licencia 19 USD mensuales.

Este primer tutorial pretende dar una introducción al UE4. Crearemos la base de nuestro juego, donde tendrás al personaje protagónico caminando por el nivel usando una cámara fija, con controles básicos. Este simple inicio nos permitirá aprender a importar los modelos 3D al proyecto. Crear la clase necesaria para controlar al personaje. Entender la filosofía que sigue el framework de Unreal en su modelo de clases. Una introducción a la programación en Unreal Engine usando C++. La comunicación entre C++ y el Editor. Los mecanismos de animación del personaje y una introducción al Visual Scripting usando el Blueprint Editor.

Obteniendo el Unreal Engine 4

El proceso para obtener el motor es súper simple. Entra en https://www.unrealengine.com/register regístrate y paga los primeros 19 USD, créeme, probablemente estos sean los 19 USD que más alegría te darán en la vida :). Ahora tendrás acceso a lo mismo con lo que trabaja el equipo de Epic Games.

El próximo paso es obtener el motor. Este lo podemos tener de dos formas, directo el ejecutable desde el Launcher que al abrirlo nos dará para bajar la última versión o compilando todo el código fuente … sip, así mismo, por si fuera poco, tenemos acceso ha todo el código fuente del motor.

En el sitio se describen bien los pasos para bajar los fuentes desde https://github.com/EpicGames/UnrealEngine/releases y los pasos para compilarlo así que nos detendremos aquí. De todas formas si tienes algún problema con el proceso puedes dejarme tus comentarios.

Requisitos antes de comenzar

Es válido aclarar en este momento que este y los próximos tutoriales asumen que tienes un dominio AVANZADO de C++ y del desarrollo de juegos.

Modelo 3D del personaje protagónico y sus animaciones

Lo primero que necesitamos para comenzar nuestro juego es el modelo 3D del personaje principal con sus animaciones. Todos los modelo 3D que conforman un juego, tanto los personajes como los objetos y las animaciones son creados por los diseñadores y animadores de nuestro equipo, con herramientas de modelado y animación 3D como Maya, 3DsMax o Blender. Al terminar el proceso de modelación y animación en estas herramientas, se exporta el modelo, el esqueleto y las animaciones en formato FBX.

Como en lo personal, el modelado y la animación 3D se me da muy mal :) vamos a partir de los recursos en FBX de uno de los proyectos de ejemplo que viene con el UE4. Esto es exactamente lo que nos daría nuestro equipo de diseño. Puedes bajar los recursos aquí: https://d26ilriwvtzlb.cloudfront.net/a/a7/ThirdPerson_FBX.zip

Descomprime el .zip, de momento solo trabajaremos con HeroTPP.FBX, Walk.FBX y Idle.FBX. Si tienes algún software de modelado 3D, como Maya por ejemplo, puedes importar estos ficheros para que les des un vistazo de cerca.

Archivo Hero.FBX cargado en Maya 2015. Vista del esqueleto.

Archivo Hero.FBX cargado en Maya 2015. Vista del esqueleto.

Archivo Hero.FBX cargado en Maya 2015. Vista del modelo.

Archivo Hero.FBX cargado en Maya 2015. Vista del modelo.

 

Hero.FBX es el modelo 3D de nuestro personaje con su esqueleto para poderlo animar. Idle.FBX y Walk.FBX son las animaciones de reposo y caminar del personaje. En estos dos últimos archivos no se encuentra el modelo ya que no es necesario, para las animaciones solamente necesitamos la información del movimiento de los huesos del esqueleto, por lo que al exportar las animaciones no hay que incluir el modelo.

Creando un nuevo proyecto en Unreal Engine 4

Ya con nuestro modelo 3D y sus animaciones en formato FBX estamos listo para comenzar. Lo primero es crear el proyecto. Crear un nuevo proyecto en Unreal Engine es súper simple. Abrimos el editor y nos muestra una ventana con dos pestañas: Project, que contiene los proyectos existentes previamente creados y New Project, que como es lógico, nos permite crear un nuevo proyecto.

Una de las primeras cosas geniales que encontramos al intentar crear un proyecto nuevo, es que ya el UE4 viene con un grupo de Proyectos “Plantilla” que podemos usar como base de nuestro juego, según el estilo que queramos crear. 3ra Persona, 1ra Persona, Top-Down o Side-Scroller.

Como el objetivo que tiene este primer tutorial, es una introducción al UE4, no vamos a usar ninguna de estas plantillas relativamente avanzadas, vamos a crear nuestro proyecto prácticamente desde cero, usaremos el Template Basic. Selecciona New Project/Basic Code y abajo en el campo nombre escribimos el nombre de nuestro proyecto, en este caso yo usaré UE4Demo. Por último da clic en Create Project.

Ventana para Crear o Abrir un proyecto en Unreal Engine 4

Ventana para Crear o Abrir un proyecto en Unreal Engine 4

Hecho esto se crea automáticamente el proyecto y se abre en el IDE correspondiente según el sistema que usemos. Para Windows es Visual Studio 2013 y para MAC OS es XCode 5.1. Este tutorial es desarrollado en MAC OS, por lo que estoy usando como IDE de programación el XCode. Una vez abierto el XCode con el proyecto, tenemos que compilarlo para poderlo abrir en el Editor. Da clic en la esquina superior izquierda para seleccionar el Scheme UE4DemoEditor – Mac y después Menú Product/Build For/Running.

El proceso de compilación demorará unos segundos. Una vez que termina podemos seleccionar desde el menú del XCode, Product/Run. Esto nos abrirá automáticamente el Editor con nuestro proyecto.

Unreal Engine 4 Editor con nuestro nuevo proyecto acabado de abrir

Unreal Engine 4 Editor con nuestro nuevo proyecto acabado de abrir

UE4 nos crea una escena con algunos objetos agregados a la misma. De momento vamos a dejarlo. Puedes dar clic en el botón Play de la barra Superior (Toolbar) para ver lo que tenemos. Por defecto tendremos el control de la cámara con el mouse y el teclado y podremos desplazarnos por la escena, pero por supuesto, este no es el objetivo, nosotros queremos que entre en este mundo nuestro personaje :)

Importando modelo 3D de nuestro personaje

Ya nuestro equipo de diseño nos entregó el modelo exportado en FBX con su esqueleto y su dos animaciones básicas :), ahora vamos a importarlo en el proyecto. En el Panel de la esquina inferior izquierda del Editor tendemos el Content Browser. En este panel es donde tendremos organizados todos los recursos de nuestro juego. Da clic en el Botón New y selecciona New Folder y dale un nombre a la carpeta, por ejemplo “Character”. Hecho esto tendremos una nueva carpeta en el Content Browser, entra en ella, selecciona Import y busca el FBX del personaje: Hero.FBX (los FBX de las animaciones los vamos a importar más tarde). Recuerda que en este FBX lo que tenemos es el modelo 3D del personaje con su esqueleto. Al dar en OK nos sale la ventana FBX Import de UE4 y ya automáticamente seleccionado Skeletal Mesh.

Ventana FBX Import del Unreal Engine 4

Ventana FBX Import del Unreal Engine 4

Vamos a tomarnos unos minutos para un poco de teoría. Como puedes ver en esta ventana de import se muestran tres tipos de recursos que se pueden importar desde FBX. Static Mesh, Skeletal Mesh y Animation.

Static Mesh: Un Static Mesh es un objeto estático de nuestro juego, por ejemplo una silla, un edificio. O sea, solamente el modelo 3D sin animación, sin esqueleto.

Skeletal Mesh: Un Skeletal Mesh, es exactamente lo que estamos importando ahora, un modelo 3D con un esqueleto asociado para ser animado. O sea, todos los personajes de nuestro juego serían Skeletal Mesh.

Animation: Un Animation, es la información de transformaciones de los huesos de un esqueleto para darle vida a las acciones como caminar, saltar, etc. Lo que tenemos en Idle.FBX y Walk.FBX que importaremos más adelante.

Automáticamente UE4 detecta que lo que estamos importando es un Skeletal Mesh no es necesario cambiar más nada, los parámetros por defecto son suficiente, da clic en el botón Import. En caso de algún warning en el proceso de importación, ignóralo. En próximos tutoriales veremos todo el proceso de Exportar/Importar y el Animation Rigging Toolset que nos da Epic para preparar los modelos y las animaciones y abordaremos en detalles este tema.

Una vez importado el modelo en el Content Browser tendremos 3 nuevos elementos: Hero (SkeletalMesh), Hero_PhysicsAsset (PhysicsAsset) y Hero_Skeleton (Skeleton).

Si haces doble clic en el SkeletalMesh puedes abrir el modelo importado en el Editor Persona de UE4. Persona es el editor de Skeleton, Skeletal Meshes, Animations Blueprints y otros elementos de animación en UE4.

El Skeletal Mesh de nuestro héroe en el Persona Editor

El Skeletal Mesh de nuestro héroe en el Persona Editor

Al abrir el SkeletalMesh en Persona a la izquierda tendremos el Skeleton Tree que es el árbol con todos los huesos que conforman el esqueleto del modelo. En el panel de abajo tenemos el Mesh Details. Este panel está compuesto por varias secciones, con los Materiales aplicados al modelo (De momento no tenemos ningún material, o mejor dicho, solamente tenemos un material por default que le da esa vista gris opaca a nuestro personaje)

El Hero_PhysicsAsset es el PhysicsAsset que se genera automáticamente al importar el Skeletal Mesh, este asset de momento no lo usaremos, en próximos tutoriales veremos para que es, pero si eres muy curioso dale doble clic, te abrirá el editor que trae UE4 para manipular este tipo de assets. En la esquina superior izquierda tiene un botón que dice Simulate, da clic en él y mira lo que pasa. Eso te dará una noción del objetivo de este recurso generado automáticamente al importar el Skeletal Mesh.

Por último el Hero_Skeleton es solamente el esqueleto del modelo que importamos, pero el esqueleto por separado. Una característica genial de UE4 es que podemos compartir el mismo esqueleto entre distintos modelos 3D que sean relativamente parecidos en su modelo. En vez de tener que importar siempre para cada uno de estos el modelo 3D y el esqueleto, solamente importamos el esqueleto una vez, y podemos asociar distintos modelos a este esqueleto.

Bien, ya tenemos en nuestro proyecto los recursos del personaje principal, vamos ahora a lo que nos gusta, el código :)

Introducción a la programación en Unreal Engine 4

Un proyecto en Unreal Engine 4 está compuesto básicamente de dos grandes piezas que trabajan en conjunto. Los niveles, que es lo que se trabaja en el Editor y el proyecto de programación, que trabajamos en el IDE de programación. Vimos como al crear un nuevo proyecto en UE4 se crean ambas partes. Ahora vamos a trabajar en la segunda parte, la parte del código.

En Unreal Engine 4 se programa en C++, al crear un nuevo proyecto, automáticamente se crea un proyecto en el XCode (o Visual Studio si usas Windows) con las clases básicas para nuestro juego. Abre tu IDE de programación con el proyecto creado. Dentro de la carpeta Source es que se encuentran los fuentes nuestros. Dentro de la carpeta Engine están todos el framework. Tener acceso a esto es genial, porque sirve de mucha ayuda para revisar como están implementadas las clases, o para que es una determinada propiedad, viendo los comentarios puestos por el propio equipo de Epic. Antes de crear nuestra primera clase vamos a comentar rápidamente la filosofía que sigue Unreal Engine en su framework.

En UE4 todos los elementos que aparecen en nuestro juego son Actors (heredan de la clase AActor). Una silla, una mesa, un enemigo o el personaje principal. Los elementos del juego que son controlados, o sea que no son estáticos, que tienen un comportamiento, son Pawns. Hay un tipo especial de Pawn que es el Character. El Character es el Pawn que representa al personaje principal y tiene implementaciones particulares que solo tendrá el Pawn que será controlado por el jugador. Por ejemplo, si en nuestro juego tenemos al personaje principal y a un enemigo. El personaje principal será un Character y el enemigo será un Pawn solamente. Ahora… todos los Pawns son controlados por una clase Controller, para el caso del Character, este es controlado por un PlayerController. El PlayerController es la clase que recibe las entradas del jugador, del ser humano, y mediante ellas controla al personaje en el juego, al Character. Básicamente el PlayerController representa al ser humano, el Character (Tipo especial de Pawn) representa al personaje dentro del juego y es controlado por el PlayerController. Mientras que los otros Pawns pueden ser controlados, por ejemplo, por AIController.

. . . sip :S, bastante enredado, pero poco a poco a medida que te familiarices con Unreal Engine dominarás esta filosofía, la jerarquía de clases y la relación entre ellas.

Volviendo al código, dentro de la carpeta Source tenemos una carpeta con el nombre que le dimos al proyecto, en mi caso UE4Demo y dentro unas pocas clases con las que comenzar nuestro juego.

La primera clase a tener en cuenta es UE4DemoGameMode esta es la clase que define el GameMode de nuestro juego. En Unreal la clase GameMode define las reglas del juego, por ejemplo, las condiciones en las que se gana, las condiciones en las que se pierde etc, además es la encargada de definir el PlayerController, el Pawn por defecto, entre otras muchas cosas. Es el núcleo del juego. Si abrimos el .h veremos que es una clase que hereda de AGameMode y de momento no tiene más nada.

//AUE4DemoGameMode.h
#pragma once

#include "GameFramework/GameMode.h"
#include "UE4DemoGameMode.generated.h"

UCLASS()
class AUE4DemoGameMode : public AGameMode
{
	GENERATED_UCLASS_BODY()
};

Como notarás de seguro, la clase tiene en su declaración dos macros que te llamarán la atención, UCLASS() y GENERATED_UCLASS_BODY

Unreal Engine posee un robusto sistema para el manejo de objetos. La clase base para los objetos en Unreal es UObject. el macro CLASS puede ser usado en clases que derivan de UObject, de esta forma el sistema manejador de UObjects es avisado de la existencia de esta clase.

Al incluir estos macros logramos que la clase a bajo nivel sea tratada por los mecanismos de Unreal como el “Recolector de basura, Serialización, Inicialización automática de las propiedades, Integración automática con el Editor etc”.

Ahora vamos a ver la implementación de nuestro GameMode. Como verás en el UE4DemoGameMode.cpp tendrás solamente la implementación del constructor.

//AUE4DemoGameMode.cpp
#include "UE4Demo.h"
#include "UE4DemoGameMode.h"
#include "UE4DemoPlayerController.h"

AUE4DemoGameMode::AUE4DemoGameMode(const class FPostConstructInitializeProperties& PCIP)
	: Super(PCIP)
{
	PlayerControllerClass = AUE4DemoPlayerController::StaticClass();
}

El constructor del GameMode de nuestro juego de momento solamente tiene la inicialización del atributo PlayerControllerClass. PlayerControllerClass es el PlayerController para nuestro juego, o sea, la clase que será la interfaz entre el ser humano y el Character que este controlará. Aquí simplemente es inicializada con una instancia estática de nuestro UE4DemoPlayerController. Por eso es que podemos movernos como un fantasma por todo el nivel cuando corremos el proyecto ahora mismo. Tenemos un PlayerController, pero como no tenemos un Character no tenemos cuerpo dentro del juego.

La otra clase que tenemos ya previamente creada es UE4DemoPlayerController. Esta es la implementación del PlayerController de nuestro juego y como verás está vacía, de momento no necesitamos nada personalizado en ella, todo lo necesario para nuestro PlayerController de momento está en la clase base APlayerController, pero dejamos esta clase por aquí para cuando necesitemos implementar algún comportamiento personalizado.

Bien, basta de teoría, vamos a la practica.

Ya tenemos importado en el Editor los recursos que conforman a nuestro personaje, pues vamos a acabar de hacerlo entrar en escena :)

Creando nuestra primera clase en Unreal Engine

Como lo primero que vamos a hacer es darle vida a nuestro personaje, la clase que vamos a crear es la clase del personaje. Como comentamos anteriormente, el personaje controlado por el jugador es un Pawn, pero es un Pawn especial, es un Character. Por lo que la clase que controle al personaje protagónico de nuestro juego tiene que heredar de Character.

Para agregar una nueva clase al proyecto la forma más cómoda de hacerlo es desde el Editor. Vuelve al Editor y mediante el menú principal selecciona File/Add Code to Project. Tendremos una ventana para seleccionar la clase base de la nueva clase que como dijimos será Character. En esa ventana puedes ver las otras clases bases comunes en Unreal Engine y una pequeña descripción de las mismas. Selecciona Character, da en el botón Next, escribe el nombre para tu clase, por ejemplo, HeroCharacter y finaliza el proceso. Al finalizar el Editor te pregunta si quieres abrir la clase en el IDE, le damos OK y ya veremos ahí nuestra clase HeroCharacter creada. La estructura de momento ya es conocida, una clase C++ normal que hereda de ACharacter y con los macros ya explicados UCLASS() y GENERATED_UCLASS_BODY().

Configurando el Character desde el Blueprint Editor.

Ya tenemos nuestra clase para representar el personaje protagónico de nuestro juego, este es un buen punto para comentar una de las cosas que en lo personal más trabajo me costó adaptarme al entrar en el mundo de Unreal Engine, sobre todo porque llegué a Unreal Engine después de trabajar mucho en el desarrollo de juegos 2D con motores como el Cocos2D. En este caso todo se hace desde código (aunque ya a estas alturas han varios Editores geniales para Cocos2D). En UE4 la filosofía de trabajo es muy distinta, aquí por supuesto que podemos hacerlo todo desde programación pero esto implica que el ritmo de producción generalmente será más lento y propenso a bug. Por este motivo al desarrollar juegos sobre Unreal Engine trabajaremos indistintamente con el Editor o directamente desde el código. Básicamente es decisión de los desarrolladores cuando usar uno u otro.

Para demostrar el engranaje entre el código en C++ y el Editor en Unreal Engine vamos a configurar nuestro Character en los dos lados, esto también nos permitirá demostrar lo genial que quedan comunicados el código C++ y el Editor.

Bien, ya tenemos desde código nuestra clase para representar al Character, vamos ahora a configurar los componentes del Character pero desde el Editor. Abre el Editor y en el Toolbar tenemos el botón Blueprints. Selecciona Blueprints/New Class Blueprint en la parte de abajo de la ventana hay una sección que dice Custom Classes, selecciona ahí y busca la clase que acabamos de crear para nuestro Character, HeroCharacter, y le ponemos un nombre, por ejemplo, HeroCharacterBlueprint y selecciona para que se cree dentro de la carpeta Game/Character. Una vez terminado el proceso se abrirá el Blueprint Editor en el modo Components. Desde aquí podemos configurar todo nuestro Character.

imagen_06

A la izquierda del editor tenemos el panel Components este panel contiene todos los componentes que conforman el Character al seleccionar uno, en el panel de abajo se muestran las propiedades de ese componente. El CharacterMovements como el nombre lo indica es el componente que contiene las propiedades que afectan el movimiento del Character, por ejemplo, aquí tenemos Max Walk Speed que es el máximo de velocidad que toma el personaje al desplazarse, hay muchísimas más propiedades, dale un vistazo a todas por arriba para que tengas una idea de todo lo que se puede configurar en el Character con respecto al movimiento.

El otro componente que tiene un Character es el CapsuleComponent. El CapsuleComponent es usado para la detección de colisiones con el personaje. Es esa capsula transparente que se ve en el Viewport del Editor y es la zona de colisión del personaje.

Por último dentro del CapsuleComponent tenemos un Mesh, que como ya te imaginarás es el Mesh que representa a nuestro personaje. Además hay un ArrowComponent que nos ayuda para saber la dirección del Character.

Bien, el primer paso será acabar de configurar el Mesh de nuestro Character. Selecciona en el panel de Componentes el componente Mesh y en el panel detalles en la sección Mesh tienes la propiedad Skeletal Mesh despliega el combobox que hay aquí y selecciona el único Skeletal Mesh que tenemos en nuestro proyecto que creamos al importar el fbx de nuestro héroe. Al hacer esto en el Viewport se verá el modelo de nuestro héroe. Usa las herramientas de traslación y rotación para colocar el Mesh dentro del CapsuleComponent y mirando en la misma dirección que el Arrow Component. Por último da clic en el botón Save en la esquina superior derecha del Editor.

Mesh del personaje en la posición correcta en el HeroCharacterBlueprint

Mesh del personaje en la posición correcta en el HeroCharacterBlueprint

Hecho esto acabamos de darle un cuerpo a nuestro Character. Vamos a probar. Cierra el Editor del Character y corre el juego a ver que tenemos.

:( … como notarás no hay ningún cambio, seguimos teniendo control gracias al PlayerController por defecto pero no tenemos nuestro personaje ni nada. Bien, el problema es que nos faltaron algunas cosillas.

Primero, asegúrate que tienes configurado bien el GameMode. Da clic en el botón World Settings del Toolbar y asegúrate tener seleccionado en la sección GameMode nuestra clase U4DemoGameMode, debajo de GameMode tendrás los elementos configurados en este GameMode en PlayerControllerClass está nuestro PlayerController (UE4DemoPlayerController) ya que en el constructor de la clase UE4DemoGameMode inicializa esa propiedad, pero como notarás Default Pawn Class dice Default Pawn. Ese es exactamente el problema que tenemos. Creamos nuestro Character y le dimos un cuerpo pero no hemos definido en el GameMode que HeroCharacter es el Character (el Default Pawn Class ) de nuestro juego.

Vamos ha hacer esto desde el código para demostrar nuevamente la comunicación entre los componentes del Editor y el código en C++. Cierra el Editor abre el proyecto C++ y busca la clase UE4DemoGameMode modifica la implementación del constructor para que quede de la siguiente forma.

AUE4DemoGameMode::AUE4DemoGameMode(const class FPostConstructInitializeProperties& PCIP)
	: Super(PCIP)
{
	PlayerControllerClass = AUE4DemoPlayerController::StaticClass();
    
    //Obtiene en PlayerPawnBPClass.Object la referencia al HeroCharacterBlueprint creado y configurado desde el Editor
	static ConstructorHelpers::FObjectFinder<UClass> PlayerPawnBPClass(TEXT("Class'/Game/Character/HeroCharacterBlueprint.HeroCharacterBlueprint_C'"));
    
    //Inicializa el atributo DefaultPawnClass con el HeroCharacterBlueprint creado y configurado desde el editor
	if (PlayerPawnBPClass.Object != NULL)
	{
		DefaultPawnClass = PlayerPawnBPClass.Object;
	}
}

Vamos a dar un stop aquí para explicar que acabamos de hacer con estas pocas líneas porque aunque tengas experiencia en C++ de seguro que esta sintaxis te parecerá algo rara. En la primera línea lo que hacemos es buscar y obtener la instancia de la clase HeroCharacter creada en el editor mediante el Blueprint Editor. Creamos una variable del tipo FObjectFinder, FObjectFinder es una estructura parametrizada publica que se encuentra dentro de otra estructura de nombre ConstructorHelpers. FObjectFinder recibe en su constructor la dirección del objeto que vamos a instanciar, si el objeto es encontrado satisfactoriamente su instancia se almacena en la propiedad Object.

Un buen consejo, como tenemos los fuentes del Framework, puedes ver la implementación de todas estas estructuras ConstructorHelpers, FObjectFinder. En XCode basta con dar clic sobre su nombre con la tecla cmd presionada. Esto te llevará a la declaración de la estructura. Tomate unos minutos y dale un vistazo por arriba para que entiendas mejor su funcionamiento. Por último notar que para definirle la ruta a FObjectFinder usamos el Macro TEXT, básicamente todos los strings que escribamos directo en el código lo tendemos que hacer con esto, para que el compilador pueda convertir el string al tipo de dato correcto, en este caso un TCHAR *.

Bien, pues en teoría tenemos en PlayerPawnBPClass.Object la instancia de nuestro Character (PlayerPawnBPClass es el nombre que le dimos a la variable que acabamos de crear de tipo FObjectFinder), lo que queda es inicializar la propiedad DefaultPawnClass con este objeto. La clase GameMode tiene la propiedad DefaultPawnClass que define el Pawn que usará el personaje.

Listo, compila y ejecuta el juego. Al arrancar el juego automáticamente se agrega al Level nuestro personaje. Esto es porque en el Level que nos crea el Unreal Editor por defecto con la plantilla que seleccionamos al crear el proyecto, tiene un Actor de tipo Player Start. Y el GameMode automáticamente busca en el Level si hay una instancia de un Player Start y agrega en esa posición el Character.

Pero que problema tenemos ahora, perdimos el control, ya no podemos desplazarnos por la escena y la cámara está como en los ojos del personaje :(. Bien, vamos a solucionar este asunto configurando temporalmente una cámara estática en nuestro juego.

Configurando una cámara estática desde C++

En el Editor da clic derecho dentro del ViewPort y selecciona del menú desplegable Place Actor/Camera. Usa las herramientas de traslación y transformación para apuntar la cámara en la dirección del Play Start, para que se vea el personaje. Así me quedó a mi:

imagen_08

Ahora vamos a decirle al Unreal que la vista del juego será desde esta cámara. Para esto tenemos que comentar algo de teoría.

Como hablamos anteriormente según la filosofía de Unreal es el PlayerController la interfaz entre el personaje protagónico del juego (el Character) y el ser humano. Por lo que es lógico que lo referente a la vista del juego sea implementado aquí. PlayerController tiene el método SetViewTargetWithBlend este método permite definir en cualquier momento a donde es que está “mirando” la cámara del juego. Lo que vamos a hacer es llamar a este método y decirle que use la cámara que pusimos en el Level como la cámara del juego.

Podemos cambiar la dirección a la que apunta la cámara de nuestro juego en cualquier momento, pero en este caso queremos que desde el inicio sea la dirección a la que apunta la cámara que agregamos al level. Para esto vamos a usar un evento muy usado en Unreal que es el evento BeginPlay. todas las clases que hereden de AActor tienen este método que se llama, como dice su nombre, cuando inicia el juego. Vamos a sobrescribir este método en nuestro PlayerController (que deriva de AActor) para en ese momento cambiar la cámara del juego.

Abre UE4DemoPlayerController.h y abajo del macro GENERATED_UCLASS_BODY() agrega la siguiente línea: virtual void BeginPlay() override; con esto hacemos visible el método BeginPlay en nuestra clase UE4DemoPlayerController para poderlo sobrescribir en el .cpp.

Abre ahora UE4DemoPlayerController.cpp y agrega el siguiente método debajo del contructor. Revisa con detenimiento los comentarios para que entiendas lo que se hace dentro del método.

/** Metodo heredado de la clase AActor se llama automaticamente por el motor cuando comienza el juego. */
void AUE4DemoPlayerController::BeginPlay()
{
    //Llamamos el Begin Play de la clase padre
    Super::BeginPlay();
    
    //Recorremos todos los Actores en el Level mediante el TActorIterator
    //TActorIterator es un iterator parametrizado que nos permite recorrer todos los actores en el level
    for (TActorIterator<ACameraActor> It(GetWorld()); It; ++It)
    {
        //Obtenemos el actor actualmente en el loop. Como solo tenemos un solo ACameraActor en el Level, el iterator solo iterará una vez
        ACameraActor* _mainCamera = *It;
        
        //Configuramos el nuevo punto de vista del juego con la camara.
        //SetViewTargetWithBlend puede recibir más parametros, pero tienen valores por defecto, y de momento no necesitamos modificarlos.
        this->SetViewTargetWithBlend(_mainCamera);
    }
}

Listo !! compila y corre. Ahora verás el juego usando la cámara que agregamos al Level y ya podrás ver nuestro Character agregado a la escena.

Configurando una cámara estática mediante el Blueprint Editor

Bien, quisiera hacer un paréntesis aquí para volver a tocar el tema del Blueprint Editor. Al comenzar en Unreal Engine muchos chocamos con la incógnita: Cómo hago esto? Mediante C+ o mediante el Blueprint. Al final la decisión es de cada cual y a medida que vayas cogiendo soltura en Unreal Engine sabrás al directo si lo que vas a hacer en C++ o en el Blueprint Editor. Quiero aprovechar este momento para demostrar esto. Vamos a hacer lo mismo que acabamos de implementar en C++, o sea, cambiar la cámara del juego, pero ahora sin escribir una línea de código, todo lo haremos mediante el Blueprint Editor.

En el Editor da clic en el botón Blueprint del Toolbar y selecciona Open Level Blueprint esto te abrirá el Blueprint Editor con el archivo Blueprint para el Level completo. Digamos que es en este Blueprint donde implementaremos las cosas generales del nivel.

Siguiendo la lógica que usamos para implementar esto desde C++. Lo primero que hicimos fue implementar el Evento BeginPlay. Pues eso mismo haremos aquí, el Blueprint Editor es un Editor de scripting visual, por lo que en este Editor lo que haremos básicamente es programar pero con gráficos (si si . . . bien complejo de asimilar y entender la primera vez :) ) … aquí podemos agregar variables, eventos del sistema, funciones de clases especificas etc. En fin, todo, o casi todo lo que haces en C++ lo podrás hacer en el Blueprint Editor.

Comenzaremos agregando el Evento Begin Play. Clic derecho en el centro de la pantalla desmarca la opción Context Sensitive y busca Event Begin Play. Acabamos de agregar a nuestro script visual un Nodo que representa al método Begin Play de nuestro juego. Ahora, según nuestra implementación en C++ lo que hicimos dentro del BeginPlay fue obtener la referencia de la cámara que tenemos en el Level y llamar al método de la clase PlayerController SetViewTargetWithBlend pasándole como parámetro la cámara. Pues eso mismo haremos aquí.

Primero, necesitamos una referencia al PlayerController, recuerda que este script es “global” a nivel del Level y la implementación de C++ la hicimos dentro del PlayerController.

Agrega un nuevo Nodo como ya sabes pero ahora será Get Player Controller. Este nodo nos retorna la referencia del Player Controller del juego. Ahora necesitamos llamar al método SetViewTargetWithBlend como mismo hicimos en C++. De nuevo agrega un nuevo Nodo de nombre SetViewTargetWithBlend. Listo, ya tenemos todos los elementos que necesitamos para nuestro algoritmo visual. Pero falta una cosa, conectarlos.

El Nodo Event Begin Play tiene como un puerto que representa la salida. O sea, lo que se va a ejecutar cuando se lance este evento en el juego y si te fijas en el nodo Set View target with Blend tiene un puerto de entrada y de salida. El de entrada es el que nos interesa. Da clic en el puerto del evento Event Begin Play y arrastras la flecha hasta el puerto de entrada de Set View Target with Blend, cuando te muestre una marquita verde suéltalo. Con esto hemos hecho la conexión entre la salida de Event Begin Play y Set View target with Blend. Que quiere decir esto, que cuando se ejecute nuestro juego, se va a disparar el evento BeginPlay y se llamará al método Set View target with Blend. Pero que pasa, SetViewtargetWithBlend vimos que es un método que pertenece al PlayerController, por lo que hay que definirle al nodo Set View Target With Blend quien es el Player Controller. El Nodo Get Player Controller tiene un puerto de salida que dice Return Value y el Nodo Set View Target with Blend tiene un puerto de entrada que dice Target. Conecta estos dos puertos y de esta forma estarás diciendo que el método SetViewTargetWithBlend que se llamará es el del PlayerController retornado por el Nodo Get Player Controller. Por último recuerda que hay que pasarle como parámetro al SetViewTargetWithBlend el Actor que usará para configurar el nuevo punto de mira. Para esto nos falta agregar un último Nodo, el nodo que representa a la cámara. Salva estos cambios, cierra el Editor y selecciona en el Level la cámara que agregamos anteriormente. Ahora abre de nuevo el Editor da clic derecho y verás que tienes un acceso directo para agregar un Nodo Camera Actor. Una vez agregado conecta el puerto de salida del camera actor al puerto New View Target del Set View Target with Blend. Listo, ya tenemos nuestro script visual completo. En la esquina superior izquierda tienes un botón que dice Compile. Da clic ahí. Por último vamos a eliminar la sobre-escritura del método Begin Play en la clase C++ ya no es necesario (si quieres coméntalo para que no pierdas el código).

Cierra el Editor, abre el proyecto C++ y comenta en el UE4DemoPlayerController.h la declaración del método BeginPlay. Ve ahora al UE4DemoPlayerController.cpp y comenta o elimina completamente la implementación del método BeginPlay.

Listo !!. Compila y ejecuta el juego. Como notarás, es idéntico el resultado :). Ya te digo, es decisión tuya implementar lo que quieras en el Blueprint Editor o en C++ tu mismo le iras encontrando las ventajas y desventajas a cada método según lo que quieras hacer.

Tomate unos minutos si quieres, que aún nos quedan varias cosas :)

Configurando las animaciones del personaje

De momento lo que tenemos es bastante poco funcional. Simplemente al abrir el juego vemos al personaje protagónico, ya visto desde una cámara fija pero está ahí quieto sin hacer nada y estático totalmente. Vamos a darle un poco de vida.

Recuerda que nuestro equipo de diseño :) nos entregó además del modelo con su esqueleto en FBX las animaciones de caminar y reposo, también en formato FBX listas para importarlas. Pues vamos a ello. Abre el Editor en el Content Browser crea una nueva carpeta, yo le pondré Animations. Aquí tendremos todas las animaciones que importemos. Entra a la carpeta e importa los dos FBX Walk.FBX e Idle.FBX. Al seleccionarlas verás que por defecto en la ventana de FBX Import sale seleccionado Animations. Más abajo tiene la opción que permite ya en el momento de la importación seleccionar el esqueleto al que están asociadas estas animaciones. Da clic ahí y selecciona Hero_Skeleton por último da clic en Import.

Ya tenemos las animaciones de nuestro personaje, si quieres puedes darle doble clic desde el Content Browser para abrirlas en el Persona Editor y ver un preview de las mismas. Desde este Editor puedes ver todos los detalles de la animación, reproducirla y muchas más cosas que veremos en próximos tutoriales.

Ya tenemos las animaciones ahora falta asociarlas con nuestro personaje.

Creando la maquina de estado y actualizando el Character con la animación de reposo y caminar mediante Animation Blueprints.

El Animation Blueprints es la herramienta que nos da Unreal Engine para implementar toda la lógica de las animaciones del personaje de una forma súper simple. A partir de máquinas de estado y visual scripting.

Vamos a crear entonces el Animation Blueprints para el personaje. Entra en el Content Browser a la carpeta Animations da clic derecho en un espacio vacío y selecciona Animation/Animation Blueprint. Esto te abrirá la ventana de creación de Animation Blueprint. En la sección Parent Class selecciona AnimInstance y abajo en la sección Target Skeleton escribe el nombre del esqueleto que usa nuestro héroe, Hero_Skeleton por último da clic en el botón OK.

Automáticamente se agrega al Content Browser un AnimBlueprint con el nombre seleccionado para que lo cambies. Ponle el nombre que prefieras, por ejemplo HeroAnimBlueprint. Ahora, antes de hacer algo en el HeroAnimBlueprint vamos a definirle en nuestro Character que será este el Blueprint que usará para sus animaciones. Para esto ve en el Content Browser a donde tienes el Blueprint del character. En mi caso Game/Character/HeroCharacterBlueprint y dale doble clic. En la esquina superior derecha, selecciona el Modo Defaults y verás que hay una sección de nombre Animation para la propiedad Animation Mode selecciona Use Animation Blueprint y para la propiedad Anim Blueprint Generated Class selecciona la clase que acabamos de crear HeroAnimBlueprint_C. Listo, guarda y cierra el Editor.

imagen_09

Ve ahora al HeroAnimBlueprint, dale doble clic y se te abrirá el Persona Editor. De inicio tienes un nodo Final Animation Pose. A este Node conectaremos la salida de lo que vamos a crear ahora, pero primero, de nuevo un poco de teoría.

En Unreal Engine hay un mecanismo súper genial e intuitivo para definir las animaciones que tienen los personajes según su estado y las condiciones que definen cada estado. O sea, si el personaje está en reposo se animará la animación de reposo, si el personaje está caminando se ejecutará la animación de caminando. Se pueden hacer ligamentos entre animaciones, para que estos cambios sean mas fluidos, además definir las condiciones de cada estado. Por ejemplo, para que el personaje esté en reposo su atributo “velocidad” tiene que estar en cero, para que esté caminando su atributo “velocidad” será distinto de cero, y así. Todo esto se puede hacer mediante una maquina de estado y un script visual en el Animation Blueprint.

Vamos a crear la maquina de estado, de momento será muy simple. Dentro del AnimBlueprint Editor da clic derecho y selecciona StateMachine/Add State Machine esto agrega un Nodo de tipo StateMachine a la escena. Cámbiale el nombre a algo lógico, como HeroStateMachine o como prefieras. Ahora, dentro de ese State Machine vamos a definir los distintos estados que tendrá el personaje. Da doble clic en el nodo State Machine para entrar a editarlo. Dentro tienes un nodo de nombre Entry, da clic derecho Add State … y ponle al nuevo nodo Idle de nombre. Ahora conecta, como mismo hicimos en el level blueprint, el puerto de salida del nodo Entry al puerto de entrada del nodo Idle que acabamos de crear.

Seguidamente vamos a definir la lógica del estado Idle. Dale doble clic al nodo Idle, da clic derecho y selecciona Animation/Play Idle. Esto agrega el Nodo que representa la animación Idle que importamos anteriormente al Editor. ahora conecta este nodo al nodo Final Animation Pose.

Listo vamos a repasar como quedan todas las conexiones en cada uno de los niveles. De adentro hacia afuera tenemos nodo Play Idle conectado al nodo Final Animation Pose. En el nivel superior (HeroStateMachine) nodo Entry conectado al nodo Idle y por último en el nivel superior (AnimGraph) el nodo HeroStateMachine conectado al nodo Final Animation Pose. Hecho esto da clic en Compile en la esquina superior derecha del Editor. En el panel de la izquierda podremos ver un preview ya del personaje con la animación del Idle

imagen_10.1

imagen_10.2

imagen_10.3

Ejecuta el juego. Ya tenemos a nuestro personaje en su estado de reposo y animándose perfectamente.

Personaje principal ya en el juego en estado de reposo animándose correctamente.

Personaje principal ya en el juego en estado de reposo animándose correctamente.

Configurando los controles del juego

Muy bien, hasta ahora hemos creado y configurado el Character, hemos configurado una cámara fija temporal para el juego y hemos configurado la primera animación del personaje, el próximo paso de seguro que sabes cual es . . . hacer caminar al chico :)

Primero vamos a definir los controles del juego. Para esto desde el Unreal Engine Editor menú Edit/Project Settings, Sección Engine/Input, Bloque Bindings. Da clic en el botón del + en Axis Mapping y despliega la flechita. En el campo para escribir, escribe MoveForward. Da clic en la flechita al lado del EditText y despliega el campo y selecciona W y en Scale deja 1.0. Da clic de nuevo en el + al lado del EditText de MoveForward selecciona del nuevo combobox la S y en Scale pon -1.0.

Ahora da clic en el + de Axis Mappings para crear otra sección al nivel de MoveForward. En el nuevo EditText escribe MoveRight. Agrega dos hijos a MoveRight: A con Scale igual a -1 y D con Scale igual a 1.

imagen_12

Lo que acabamos de hacer es definir los controles que tendrá nuestro juego. El nombre que le ponemos, por ejemplo, MoveForward es para conocer esta entrada desde programación y lo que seleccionamos en el combobox es el control que disparará esta acción. El valor de Scale es un valor numérico que llega al método desde programación, generalmente con -1 y 1 es suficiente para la mayoría de los casos. Otra cosa a notar es que gracias a este valor de Scale las acciones que son en una dirección y en su contraria las registramos con el mismo identificador (MoveForward por ejemplo) y simplemente le cambiamos el valor de Scale a 1 si es hacia delante y a -1 si es hacia atrás.

Bien, hecho esto vamos a programar en el Character la lógica para lograr el desplazamiento por la escena con estos controles, de momento será muy simple el desplazamiento del personaje en la escena, pero suficiente para entender como funciona todo. Básicamente necesitamos dos cosas. Sobrescribir el método virtual void SetupPlayerInputComponent(class UInputComponent* InputComponent) de APawn para registrar los métodos que se van a llamar cada vez que se detecte las entradas que definimos, o sea un MoveForward o un MoveRight, y por supuesto, implementar el desplazamiento del Character en dependencia de la entrada.

Abre la clase HeroCharacter.h y modifícala para que quede de la siguiente forma:


UCLASS()
class AHeroCharacter : public ACharacter
{
	GENERATED_UCLASS_BODY()
    
protected:
    
    /** 
     * Se llama cuando el motor detecta la entrada configurada para 'MoveForward'.
     * En este caso cuando el usuario toca la tecla W o S del teclado 
     */
	void MoveForward(float Value);
    
    /**
     * 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);
    
    /**
     * 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;
    
};

Nada raro aquí, simplemente definimos dos métodos en donde vamos a implementar la lógica para cuando se detecte cada entrada y agregamos aquí también la declaración del método SetupPlayerInputComponent de APawn para poderlo sobrescribirlo

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


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


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

}

void AHeroCharacter::SetupPlayerInputComponent(class UInputComponent* InputComponent)
{
    //Le dice al motor que cuando detecte las entrada de tipo MoveForward que llame al metodo AHeroCharacter::MoveForward
	InputComponent->BindAxis("MoveForward", this, &AHeroCharacter::MoveForward);
    
    //Le dice al motor que cuando detecte las entrada de tipo MoveRight que llame al metodo AHeroCharacter::MoveRight
	InputComponent->BindAxis("MoveRight", this, &AHeroCharacter::MoveRight);
}


/**
 *  Se llama cuando se detecta la entrada de tipo MoveForward (Cuando el usuario toca las teclas 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 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))
	{
		//Obtiene la rotacion actual
		const FRotator Rotation = Controller->GetControlRotation();
        
		// Crea el vector de direccion a partir de hacia donde está rotado 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 (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) )
	{
		//Determina la dirección del movimiento hacia los lados. Notar que solo nos intereza la rotacion en el eje Y
		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);
	}
}

Ya aquí si tenemos algunas cosas que comentar, pero de seguro a estas alturas tienes una idea de todo. Primero, en SetupPlayerInputComponent usamos el parámetro que recibe para configurar los métodos que se van a llamar cuando se detecte cada una de las entradas. O sea, con InputComponent->BindAxis(“MoveForward”, this, &AHeroCharacter::MoveForward); estamos diciendo que cuando el Engine detecte la entrada MoveForward, que como definimos será cuando el jugador presione las teclas W o S del teclado, se llamará el método MoveForward. Y el mismo principio para la otra entrada.

Ahora vamos a ver la implementación del método MoveForward. Este método recibe un parámetro float, que es el valor Scale que registramos en el Editor. O sea, cuando se toque la W se llamará este método con 1.0 como parámetro y cuando se toque la S se llamará este método con -1.0 como Value.

El método lo que hace es determinar la rotación que tiene el modelo y el vector en la dirección a la que está orientado y mediante el método AddMovementInput hacemos que el personaje se mueva en esa dirección. Fíjate que se calcula la dirección y como se pasa Value que será 1 o -1 entonces el personaje se moverá hacia delante o hacia atrás según Value. AddMovementInput es un método de APawn que permite aplicar un movimiento al Pawn para el caso del personaje que no sea un cuerpo físico, como el nuestro. Para el caso del MoveRight fíjate que es prácticamente lo mismo, pero al calcular la dirección lo hacemos en base al eje Y y no con el eje X.

Listo, esto es todo lo que necesitamos. Compila y ejecuta el juego, cuando abra presiona las teclas W,S,A,D del teclado para controlar al personaje. Cuidado no te caigas por los bordes de la plataforma 😉

Umm pero aún tenemos dos problemas con este movimiento. Primero, el personaje no rota en la dirección del movimiento como sería lo lógico, y el otro problema es que a pesar que se está moviendo sigue con su animación de reposo. Vamos entonces a solucionar estos dos problemas.

Primero, para solucionar el problema de la rotación es muy fácil. Abre el HeroCharacter.cpp y modifica el constructor para que te quede de la siguiente forma:

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;
}

Recuerdas que el Character tiene un CharacterMovement verdad ?. Pues aquí lo que hicimos fue modificar algunos valores para cambiar el comportamiento por defecto del Character. Puedes revisar en el Editor desde el HeroCharacterBlueprint que creamos, todas las propiedades que tiene el CharacterMovement, juega un poco con ellas para que veas todo lo que se le puede definir al movimiento del Character.

Agregando animación de caminando al personaje.

Vamos a trabajar ahora en el último problema que tenemos, hacer que el personaje cuando esté en reposo tenga su animación de reposo (como ahora) pero cuando esté caminando reproduzca la animación de caminando, con esto veremos uno de los mecanismos que nos da Unreal Engine para ligar dos animaciones de forma suavizada, los Blend Space.

Blend Space es el Nodo del Animation Blueprint que nos permite hacer blending entre dos animaciones en base a la entrada de valores. En Unreal tenemos dos tipos: el Blend Space que es para varias entradas y el Blend Space 1D que es para una sola entrada. Para este simple ejemplo usaremos el Blend Space 1D ya que necesitamos solamente una entrada para lo que queremos lograr, la velocidad del desplazamiento.

Abre el Editor y dentro de la carpeta Animations en el Content Browser da clic derecho para agregar un Animations/BlendSpace1D ponle de nombre IdleWalkBlendSpace1D y dale doble clic para abrir el Editor de este elemento. Aquí la idea es la siguiente, decirle las dos animaciones que harán blending según el valor de una variable. En el panel de las propiedades de IdleWalkBlendSpace1D en el X Axis Label escribe Speed (esto es solo para una referencia, puede ser cualquier otra palabra que te represente el valor que se tiene en cuenta para cambiar entre una animación y otra). En el rango pon 0 y 100. Ahora fíjate que más abajo tienes un espacio como de una gráfica, arrastra hacia ahí desde el panel Asset Browser la animación Idle y colócala al inicio del eje X, has lo mismo para la animación Walk y colócala al final del eje. Listo, ahora mueve el cursor sobre el eje y fíjate en el panel Preview como a medida que el valor se va modificando, el modelo va cambiando de su estado Idle a Walk. Guarda y cierra este editor.

Configuración del IdleWalkBlendSpace1D

Configuración del IdleWalkBlendSpace1D

Ahora vamos a modificar el HeroAnimBlueprint para en el State Machine al nodo Idle cambiar su comportamiento. Abre el HeroAnimBlueprint ve desde el AnimGraph hasta el nodo Idle (si quieres puedes cambiarle el nombre ahora a Idle/Walk) ya que dentro de este nodo se manejarán estos dos estados. Entra para editarlo. Elimina el nodo de Idle que estamos usando ahora y agrega el IdleWalkBlendSpace1D que acabamos de crear. Como ves este Nodo a diferencia del anterior tiene un puerto de entrada con el nombre Speed (que se lo definimos cuando lo creamos) por lo que para que funcione alguien le tiene que suministrar este valor. En el panel MyBlueprint hay un icono con un +V que dice Variable. Esto es para agregar una nueva variable al gráfico. Da clic aquí y dale el nombre de Speed a la variable, arrástrala para el espacio de trabajo y cuando te pregunte Set/Get selecciona GET. Por último conecta Speed a IdleWalkBlendSpace1D y este al Final Animation Pose.

imagen_14

Ya tenemos el Nodo Idle/Walk listo, pero te estarás preguntando … a la variable Speed, quien le da su valor ?… Esto lo tenemos que definir en el EventGraph. Vamos a implementar un algoritmo que en cada loop de la animación obtenga el Pawn (que sería nuestro personaje) después que se llame al método GetVelocity que retorna el vector de desplazamiento que lleva el personaje en ese momento. Este vector por supuesto se modifica gracias a la implementación del MoveForward y MoveRight que hicimos anteriormente en C++. Cuando tengamos la velocidad, obtenemos la distancia de ese vector y vamos a usar ese valor para darle valor a la variable Speed. Todo esto lo vamos a hacer en el HeroAnimBlueprint/EventGraph.

Ya vimos anteriormente como es el Visual Scripting en el Blueprint Editor así que no tendrás problema y será otro momento para repasar su funcionamiento. Vale la pena aclarar en este punto, que como dijimos anteriormente, todo esto se puede hacer desde programación directo. Pero por ejemplo, la lógica del comportamiento de las animaciones es uno de los casos donde es mucho mejor hacerlo en el Blueprint para que quede todo el mecanismo de animación del personaje en un solo lugar y poderlo repasar y pre visualizar fácilmente.

Abre el HeroAnimBlueprint/EventGraph, agrega y conecta los Nodos para que te quede igual que la imagen siguiente. No voy a repetir paso a paso todo el proceso, porque con la explicación que hicimos anteriormente del Blueprint Editor debes poder hacerlo por tu cuenta y entender todo lo que hace el algoritmo.

Algoritmo del HeroAnimBlueprint/EventGraph con la lógica para setear el valor de la variable Speed usada dentro del State Machine del personaje para cambiar entre las animaciones de caminando y reposo.

Algoritmo del HeroAnimBlueprint/EventGraph con la lógica para setear el valor de la variable Speed usada dentro del State Machine del personaje para cambiar entre las animaciones de caminando y reposo.

Listo, compila y guarda el Blueprint y dale Play al juego… Super verdad !! ya tenemos nuestro personaje moviéndose por el terreno correctamente según queramos y cuando está detenido se anima con su animación de reposo y cuando está caminando se anima con su animación de Walk. :)

Conclusión

Hasta aquí este tutorial de introducción al Unreal Engine 4. En el próximo tutorial modificaremos la cámara, los controles y el movimiento del personaje para hacer nuestro juego un side-scroller style. Además agregaremos algo de lógica al juego, nuestro personaje tendrá que lograr alcanzar todas las monedas que existan en el escenario antes de un tiempo determinado, para poder ganar, de lo contrario perderá y tendrá que comenzar de nuevo. Mientras… déjame saber tus comentarios :).

Compilando con Unreal FrontEnd y Configurando un arma para el Player – parte 1

Hola, aquí está la tercera entrega de esta serie de tutoriales sobre UnrealScript en esta entrega veremos dos temas completamente distintos: Compilar con Unreal FrontEnd y Configurar un arma para el Player, debido a lo extenso de este segundo tema lo dividiremos en dos partes.

Hoy veremos:
– Cómo compilar y ejecutar nuestro juego con Unreal FrontEnd (UFE)
– Cómo configurar la resolución por defecto del juego
– Cómo agregarle un arma al Player y configurarla.
– Cómo procesar el impacto del disparo del arma
– Cómo reproducir un efecto de sonido al disparar el arma.

Compilando y ejecutando con Unreal FrontEnd (UFE)

Antes de ponernos a modificar nuestros scripts, veremos como compilar y ejecutar nuestro juego desde Unreal FrontEnd (UFE).

En los dos tutoriales pasados compilamos los scripts mediante el UDK Editor o sea, modificamos los scripts, abrimos el UDK Editor, y este nos pregunta si queremos recompilarlos, le damos que sí. Cuando termina, abrimos de nuevo el UDK Editor, cargamos el Level en el que estábamos trabajando, nos aseguramos de que en el Game Type for PIE en World properties esté indicando nuestro juego y damos Play From Here. Esta variante para compilar nuestro juego es válida, pero evidentemente extremadamente tediosa, y más cuando no necesitamos para nada el Editor.

UDK trae una herramienta para compilar y empaquetar nuestro juego sin necesidad de tocar para nada el Editor. Esta herramienta es: Unreal FrontEnd (UFE), la podemos encontrar en Ruta_de_instalación_del_UDKBinariesUnrealFrontend.exe

Esta es la aplicación que se va a encargar de compilar y ejecutar nuestro juego. Como ves, su interfaz es relativamente sencilla. Hoy no vamos a ver todas las opciones que tiene, veremos solamente las que usaremos para este tutorial, y en próximos tutoriales, a medida que usemos nuevas funcionalidades las explicamos. De todas formas si desde ahora quieres saber para qué sirve cada uno de los rincones de esta herramienta, mira aquí

Antes de comenzar a trabajar con el UFE, abre el archivo DefaultGame.ini (UDKGameConfig). Verás casi al comienzo del archivo el bloque [Engine.GameInfo] sustituye todo ese bloque por el siguiente

[Engine.GameInfo]
DefaultGame=SampleGame.SampleGame
DefaultServerGame=SampleGame.SampleGame
PlayerControllerClassName=SampleGame.SamplePlayerController
DefaultGameType="SampleGame.SampleGame"

Con estos cambios estamos configurando el GameType por defecto para el Engine.

Bien, ahora sí vamos a configurar el UFE para compilar nuestro juego. Lo primero que debemos hacer es asegurarnos de que en el panel de Perfiles (a la izquierda de la ventana) tenga seleccionado UDKGame on PC. Este es nuestro juego, si entras a la raíz de la instalación del UDK verás que hay una carpeta llamada UDKGame. Esa es la raíz de nuestro juego, dentro están todos los paquetes de sonido, armas, mapas, etc (Content). Los archivos de configuración (Config), etc. En conclusión, todo lo que conforma nuestro juego. Aquí vamos a estar trabajando indistintamente en el transcurso de los tutoriales, por ese motivo no me voy a detener en explicar cada una de las carpetas y su contenido en este momento, aunque por sus nombres te puedes llevar una buena idea de su objetivo.

Después de garantizar que tengamos seleccionado el perfil UDK Game on PC podemos compilar a ver que tenemos. Da clic sobre el botón Start. Como verás, el resultado será que se compilará y ejecutará el juego de ejemplo que trae el UDK. Como es lógico esa no es la idea, lo que queremos es compilar nuestro juego. En este punto tenemos dos variantes. Una es modificar los archivos de configuración del UDK Game para que cargue también nuestro GameType (y lo podamos seleccionar en el menú GameMode) o más directo, indicar una URL a compilar. En este caso vamos a usar la segunda variante, ya que queremos independizarnos totalmente de este juego de ejemplo y desarrollar nuestro juego desde “cero”.

Si te fijas el UFE tiene un Panel llamado Maps to Cook. En este panel podemos indicar los Mapas para “cocinar” y empaquetar con nuestro juego. Da clic en el botón Add, verás un listado de Mapas, pero… aquí no está el Mapa con el que hemos estado trabajando (la habitación sencilla). El motivo de esto es que aquí salen los Mapas que están en la carpeta .UDKGameContentMaps. Sabiendo esto lo único que tenemos que hacer es copiar nuestro Mapa aquí, pero antes debemos indicar en el Mapa un PlayerStart. Este será el punto donde aparezca el Player cuando comience el juego, cuando compilábamos y ejecutábamos desde el Editor no lo necesitábamos porque dábamos Play From Here :-).

Agregando el PlayerStart a nuestro Mapa

Hacer esto es muy sencillo, abre el UDK Editor, carga el mapa con el que hemos estado trabajando y en el punto donde quieras que aparezca el Player da clic derecho – Add Actor – Add PlayerStart. Listo, con esto tenemos, puedes aprovechar y desde aquí guardar directo en UDKGameContentMaps. En caso que los tutoriales los estés siguiendo con otro Mapa, no hay problema, solamente asegúrate que tenga un PlayerStart y esté en la carpeta UDKGameContentMaps.

Agregando nuestro Mapa al listado de mapas a cocinar por el UFE

Ahora solo nos queda agregar nuestro mapa al listado de mapas a “cocinar” por el UFE. Para esto da clic sobre el botón Add del panel Maps to Cook selecciona el Mapa en la lista y da clic en el botón Add Selected Maps (recuerda que para que aparezca aquí debes haberlo copiado en la carpeta UDKGameContentMaps).

Ahora nuestro mapa estará en el listado de Maps to Cook, solo nos queda configurar la URL. Esta URL es una forma más rápida de indicar el GameType y el mapa por defecto que queremos compilar y ejecutar. Debajo del panel Maps to Cook hay dos pestañas Launch Map y URL, selecciona la pestaña URL y en el campo de texto escribe:

MG-SampleLevel?game=SampleGame.SampleGame

Es bastante sencillo entender qué significa cada elemento de la URL, la raíz es el Mapa que queremos cocinar (cambia el MG-SampleLevel por el nombre de archivo de tu mapa) y mediante la variable game indicamos el GameInfo

Ahora da clic sobre el botón Start y el Unreal FrontEnd hará el resto ;-).

En el último panel del UFE tenemos la consola de salida ésta te va mostrando el progreso de la compilación. En caso de algún error, aquí te mostrará los detalles, si tienes algún error repasa de nuevo los pasos indicados hasta aquí, si aun así no compilas, coméntame tu error a ver si puedo ayudarte, espero no tengas problemas.

Bien!. Ya sabemos cómo compilar y ejecutar nuestro juego desde el UFE. Esto nos hará el proceso de desarrollo mucho más fácil y rápido.

Un pequeño detalle, los archivos de configuración del juego traen como resolución por defecto para el juego: 1024×768. Con los niveles de detalles que alcanza el Unreal Engine, esta resolución es mucho para los que no tenemos una tarjeta de video profesional :-S. por lo que el juego se debe ver bastante lento. Podemos bajar esta resolución a 800×600 para evitar este problema.

Configurando la resolución de pantalla inicial para nuestro juego

Para hacer esto abre el archivo UDKEngine.ini que se encuentra en .UDKGameConfig y con la funcionalidad de Buscar del Editor de Texto que estés usando busca: ResX. Es evidente lo que debes hacer ahora no? :-). Cambia el 1024 por 800 y el 768 de la siguiente línea por 600. Cierra el archivo y guarda los cambios. Ahora haces exactamente lo mismo con el archivo DefaultGame.ini. Cierra el archivo, guarda y compila de nuevo. Mucho mejor para nuestro caso verdad ? :-)

Cerrando el juego mediante el comando “exit”

Si recuerdas, al compilar nuestro juego mediante el Play From Here del Editor teníamos la ventaja que al presionar Esc se cerraba el juego. En este caso esto no va a funcionar, recuerda que esto sería lo mismo que tendría el usuario final, el proceso normal es que salga una ventana preguntando si quieres salir etc. En el proceso de desarrollo, esto es bastante tedioso.

Para salir rápido del juego podemos usar la consola. Ya con el juego abierto y corriendo, presiona la tecla Tab. Se te abrirá en la parte inferior de la pantalla la consola de comandos. Poco a poco iremos viendo varios comandos sumamente útiles, aquí está el primero. Escribe exit en la consola y presiona Enter.

Bien, ahora si podemos volver a nuestros scripts y modificarlos, ya que compilarlos y ejecutar vimos que es cosa de unos pocos clics gracias al UFE.

Continuando con el proyecto que dejamos del tutorial pasado, vamos a seguir trabajando sobre el Player. En este caso vamos a agregarle un arma con la que pueda defender el universo :-)

Agregando un arma al Player

En UnrealScript los “pasos” para adjuntarle un arma al Player son generalmente los siguientes:

1. Crear un InventoryManager y decirle al Pawn del Player que lo use. Por el nombre de esta clase se deduce su objetivo no?.

2. Crear el arma (Weapon) y adjuntarla al mesh del Player. Como es lógico, debemos crear también la clase que encerrará toda la lógica del arma. O sea, donde se procesa lo que pasa cuando se dispara, cuando el tiro le da a alguien o algo, etc.

3. Agregar el arma al InventoryManager

Bien, ya que sabemos esto vamos a comenzar a armar a nuestro Player.

Dentro de la carpeta Classes de SampleGame crea un nuevo archivo llamado SampleInventoryManager.uc y dentro cópiale el siguiente código.

class SampleInventoryManager extends InventoryManager;

defaultproperties
{
	PendingFire(0)=0
}

Como hemos hecho hasta ahora, lo primero es indicar de qué clase vamos a heredar, y para este objetivo tenemos la clase InventoryManager. Por otro lado, en cuanto a las propiedades a configurar en defaultproperties lo único que necesitamos por el momento es inicializar el PendingFire. Este atributo es un arreglo para definir el estado de fuego de nuestra arma, por lo general lo inicializaremos en 0 (más adelante cuando veamos el comando showdebug weapon podrás ver en la práctica como cambia este atributo). Seguro te llama la atención que el atributo es un arreglo, esto es para poder configurar el PendingFire en cada uno de los modos de fuego que tendrá nuestra arma. Por ejemplo, podemos tener un arma que tenga el disparo normal, pero además tenga un lanza cohetes. En este caso, el valor de PendingFire en el índice 0 es para el disparo estándar, y para el índice 1 es para el lanza cohetes. La mayoría de los atributos relacionados con nuestra arma siguen este mismo principio. En este ejemplo, tendremos un solo modo de fuego, por lo que solamente tenemos que configurar el índice 0.

Ya teniendo listo nuestro InventoryManager, debemos indicarle al Pawn del Player que lo use. Abre el archivo SamplePawn.uc y en el bloque defaultproperties agrega la siguiente línea

InventoryManagerClass=class'SampleGame.SampleInventoryManager'

Con esta línea a estas alturas no debes tener ningún problema si has seguido los tutoriales anteriores.

Bien, pero con esto no es suficiente, siguiendo la estructura lógica del UDK el GameInfo de nuestro juego llama al método AddDefaultInventory de nuestro Pawn. Si buscas este método en InventoryManager (la clase de la que hereda SampleInventoryManager) verás que no tiene implementación, por lo tanto la debemos implementar nosotros (con una línea tendremos). En esa misma clase SamplePawn agrega el siguiente método (entre las líneas var DynamicLightEnvironmentComponent … y defaultproperties):

function AddDefaultInventory()
{
	InvManager.CreateInventory(class'SampleGame.SampleWeapon');
}

Esta es la primera vez que creamos un método en una clase, pero como vez, en este caso no hay mucha explicación que dar en cuanto a sintaxis. :-). El cuerpo del método es una sola línea y lo que hace es crear un nuevo Item y agregárselo al inventario del Pawn. InvManager es una referencia al inventario. Como el ítem que queremos crear es el arma, pues lo que pasamos como parámetro es la clase que define nuestra arma, que aún no la tenemos hecha, así que no compiles todavía 😉

Creado la clase Weapon

Dentro de la carpeta Classes de SampleGame crea un nuevo archivo llamado SampleWeapon.uc y agrégale las siguientes líneas

class SampleWeapon extends Weapon;

defaultproperties
{
	Begin Object Class=SkeletalMeshComponent Name=WeaponMesh
		SkeletalMesh=SkeletalMesh'WP_LinkGun.Mesh.SK_WP_LinkGun_3P'
	End Object
	Mesh=WeaponMesh
	Components.Add(WeaponMesh)

	FiringStatesArray(0)=WeaponFiring
	WeaponFireTypes(0)=EWFT_InstantHit
	FireInterval(0)=1.0
}

Sencillamente estamos creando una clase para encerrar la lógica del arma de nuestro Player. Como siempre, el UDK nos da una clase de la que podemos heredar para facilitarnos las cosas. Como vez, en el bloque defaultproperties configuramos los aspectos básicos del arma. El primer bloque ya no te debe asustar, estamos indicando cual será el mesh del arma. Puedes darle un look en el AnimSet al kGun.Mesh.SK_WP_LinkGun_3P para que veas con detalles el arma que usaremos, o puedes usar cualquier otra que quieras.

El siguiente atributo que inicializamos es el arreglo FiringStatesArray. En este arreglo se almacenará el firing state de los distintos modos de fuego. Sé que lo que acabo de decir no es muy explicativo :-S pero espera al siguiente tutorial para comprender mejor este atributo. Tiene que ver con un tema bien importante en la programación de juegos, y que dicho sea de paso UnrealScript lo maneja muy bien, los States. Así que no te preocupes, en el siguiente tutorial veremos detalladamente los states y comprenderás a la perfección el objetivo de este atributo.

Con la línea: WeaponFireTypes(0)=EWFT_InstantHit lo que hacemos es indicar el tipo de disparo que usará esta arma. El atributo WeaponFireTypes es un arreglo de EWeaponFireType y este es una enumerada que define los distintos tipos de disparo (EWFT_InstantHit, EWFT_Projectile, EWFT_Custom, EWFT_None). Para este tutorial usaremos el primero. Este atributo define el comportamiento del disparo, por ejemplo, con EWFT_InstantHit estamos haciendo que el impacto del tiro sea al momento de presionar el clic, para nuestro primer juego con esto tenemos, Por otro lado, EWFT_Projectile nos permite trabajar con algo más parecido a la realidad o sea, con este modo de disparo nuestra arma se comportará más como en la vida real, tiene un alcance determinado, la bala va modificando su trayectoria de acuerdo a la distancia de desplazamiento etc. El tema de Proyectiles lo veremos en próximos tutoriales. Como dije, por el momento tendremos una puntería certera: “donde pongas el ojo, o mejor dicho el cursor :-) ahí pondrás la bala”.

Otro comportamiento interesante que podemos ajustar es el intervalo entre disparos, para esto constamos con el atributo FireInterval. Bueno, si nos ponemos a definir cada uno de los atributos con los que puedes trabajar no terminamos ni en un año :-) Así que te dejo a ti para que juegues con el resto, revisa la clase Weapon y verás todo lo que puedes configurar, los comentarios son bastante descriptivos. Recuerda algo, cada atributo de esto es un arreglo, para poder configurar el comportamiento en cada uno de los modos de disparo que pueda tener nuestra arma.

Ya casi estamos listos. El siguiente paso es adjuntar el arma al Player

Para esto necesitamos sobrescribir el método AttachWeaponTo, que heredamos de Weapon. Y en él debemos adjuntar el mesh del Arma al mesh del Player. Dentro de la clase SampleWeapon agrega los siguientes métodos

simulated function TimeWeaponEquipping()
{
	AttachWeaponTo( Instigator.Mesh,'WeaponPoint' );
	super.TimeWeaponEquipping();
}

simulated function AttachWeaponTo(SkeletalMeshComponent MeshCpnt, optional Name SocketName)
{

	MeshCpnt.AttachComponentToSocket(Mesh, SocketName);
}

Como vez, aquí tenemos dos métodos, uno con el que adjuntaremos el arma al Player (AttachWeaponTo) y el otro para indicar dónde se va a llamar este método, y para esto un buen lugar es en el método TimeWeaponEquipping. Por tanto, lo que hacemos es sobrescribir este método para agregarle la llamada a AttachWeaponTo pasándole como primer parámetro el mesh del player, que mediante Instigator.Mesh lo podemos obtener y el socket donde vamos a adjuntar el arma.

En AttachWeaponTo el proceso de adjuntar los mesh se hace exactamente en la línea MeshCpnt.AttachComponentToSocket(Mesh, SocketName); . Lo que se le debe indicar al método AttachComponentToSocket es el mesh del arma y el socket. Este último parámetro es el punto físico del mesh donde será adjuntada el arma. Por supuesto, este punto debe estar definido. Si revisas en el AnimSet del UDK Editor el mesh que usamos para nuestro Player, podrás ver los Sockets que tiene definido, y como es lógico, el WeaponPoint está en las manos (Por defecto los Sockets están ocultos, para verlos, selecciona en el menú principal del AnimSet View – Show Sockets).

Notarás que estos dos métodos tienen la palabra clave simulated delante. Por el momento no te preocupes por su objetivo, tiene que ver con temas de red, en su momento tocaremos detalladamente el tema.

Bien, con esto tendríamos, si en este punto ejecutas el juego, verás que ya nuestro Player tendrá el arma pero aparece muy oscura. Por tanto, vamos a arreglar esto usando el LightEnvironment que configuramos para el Pawn del Player para iluminar también el arma. Sustituye el método AttachWeaponTo que acabamos de crear por el siguiente:

simulated function AttachWeaponTo(SkeletalMeshComponent MeshCpnt, optional Name SocketName)
{
	local SamplePawn P;
	P = SamplePawn(Instigator);
	MeshCpnt.AttachComponentToSocket(Mesh, SocketName);
	Mesh.SetLightEnvironment(P.LightEnvironment);
}

Primero creamos una variable local para obtener una referencia al Pawn de nuestro Player. Para esto nos ayuda Instigator, pero recuerda que el miembro LightEnvironment está definido únicamente como miembro de SamplePawn por lo que debemos hacer una especie de casting al Instigator para poder acceder a este miembro, y precisamente eso es lo que hacemos con la línea: P = SamplePawn(Instigator); Por último, usamos el método SetLightEnvironment del mesh de nuestra arma y le pasamos el LightEnvironment de nuestro Pawn.

Listo!!, Compila y Ejecuta. Tendrás el siguiente resultado

Ya nuestro robot está armado y casi listo para salvar al mundo :-) … Digo casi listo porque si tratamos de disparar (por defecto es el clic izquierdo del mouse) no tendremos ningún resultado. Pero podemos probar lo siguiente: con el juego corriendo abre la consola de comandos (tecla Tab) y entra el siguiente comando: showdebug weapon. En las últimas líneas verás la información acerca del arma y si disparas verás cómo cambian los valores, demostrando que todo el proceso se está llevando a cabo correctamente, el único problema que tenemos es que en ningún momento hemos definido la lógica para el impacto. Que como ya mencionamos, al usar el modo EWFT_InstantHit, este momento es en cuanto presionamos el clic. Vamos a resolver este problema.

Mostrando la “mirilla” de nuestra escopeta

Con este sistema de impacto (EWFT_InstantHit) como dijimos, “donde pongamos el ojo ponemos la bala” pero si no tenemos una mirilla para nuestra arma el proceso de “poner el ojo” exactamente donde queremos será bastante difícil :-) Por tanto, antes de trabajar en el impacto del disparo, vamos a dibujar en el HUD una pequeña cruz blanca que nos servirá de mirilla, para asegurarnos de dar exactamente donde queremos.

Una de las formas que tenemos para hacer esto es sobrescribir el método DrawHUD de InventoryManager. Abre el archivo SampleInventoryManager.uc y agrega el siguiente método

simulated function DrawHUD( HUD H )
{
	local float CrosshairSize;
	super.DrawHUD(H);

	H.Canvas.SetDrawColor(255,255,255,255);

	CrosshairSize = 4;

	H.Canvas.SetPos(H.CenterX - CrosshairSize, H.CenterY);
	H.Canvas.DrawRect(2*CrosshairSize + 1, 1);

	H.Canvas.SetPos(H.CenterX, H.CenterY - CrosshairSize);
	H.Canvas.DrawRect(1, 2*CrosshairSize + 1);

}

Este método no lo explicaremos en este tutorial, para no salirnos del tema, ya que el tema del HUD y Canvas en UDK es bastante extenso, por eso también le tenemos dedicado un tutorial. Por el momento, basta con saber que lo que hace es dibujar en la pantalla una cruz blanca en la posición donde esté mirando el Player.

Bien, puedes compilar y ejecutar, tendrás el siguiente resultado.

Ahora sí podemos encargarnos de procesar el impacto del disparo.

Procesando el momento de impacto del disparo

Para indicar la lógica de impacto del disparo para el modo InstantHit debemos implementar el método ProcessInstantHit de la clase Weapon. Abre la clase SampleWeapon y agrega en ella el siguiente método

simulated function ProcessInstantHit(byte FiringMode, ImpactInfo Impact, optional int NumHits)
{
	local SamplePlayerController PC;
	PC = SamplePlayerController(Instigator.Controller);
	PC.ClientMessage("Material: "$Impact.HitInfo.Material);
}

¿Qué hemos hecho aquí?. Bueno, como aún no tenemos enemigos, vamos a definir el momento del impacto muy sencillo. Mostraremos en pantalla el nombre del material aplicado sobre el elemento donde impactó nuestro disparo. Como vez, uno de los elementos que le es pasado a este método es ImpactInfo, mediante esta variable (la puedes buscar con el UnCodeX) obtendremos muchísima información del objeto al que le dimos el tiro. Para nuestro ejemplo solo usaremos el valor Impact.HitInfo.Material. Que contiene el material de ese elemento. Ahora, para mostrar esto podemos usar un método definido en PlayerController que nos sirve para mostrar mensajes en la pantalla: ClientMessage. Por tanto, necesitamos desde el arma obtener una referencia a nuestro PlayerController y para esto nos ayuda el miembro Controller de Instigator. Ya parados en el controller llamamos a su método ClientMessage que recibe simplemente el string que se mostrará en pantalla. Notar el uso del operador $ con el objetivo de concatenar dos cadenas de caracteres.

Ya tenemos lista la lógica de procesamiento del momento del impacto de nuestros disparos. Si has estado siguiendo los tutoriales con el mapa de la habitación que hicimos en el primero, no podrás ver ningún cambio porque en esta habitación no tenemos ningún otro elemento al no ser la propia habitación. Además, a ésta no le aplicamos ningún material. Por tanto, para ver en funcionamiento el ejemplo bien claro, abre con el UDK Editor nuestro Mapa, el que está en UDKGameContentMaps y agrégale a la habitación cualquier Static Mesh. Supongo que esto lo sabes hacer, si no es así, antes de seguir con estos tutoriales, debes darle un vistazo al tema del Diseño de Niveles con UDK, de esto si hay mucha información en internet.

Hecho esto, compila, ejecuta y dispara a distintos elementos en el nivel, para que veas que en el momento del impacto, se muestra un mensaje en la pantalla con el nombre del material aplicado a ese objeto.

Este sistema de mensajes también es muy útil para depurar nuestro juego en busca de errores.

Bien, aunque con este sistema para procesar el Impacto del disparo será muy difícil eliminar a los enemigos :-) nos sirve a la perfección para comprender el proceso, y como aún no tenemos enemigos entonces podemos respirar….

Ya tenemos a nuestro robot armado y listo para atacar al enemigo :-) pero como notarás, tenemos algunos problemitas. El primero, por mucho que disparemos no se escucha nada. Vamos a solucionar esto reproduciendo un efecto de sonido en el instante del disparo para darle más vida al juego.

Reproduciendo el efecto de sonido para el disparo del arma

Reproducir un efecto de sonido cuando el arma se dispara es bastante sencillo. Solamente tenemos que indicar el sonido (SoundCue) a reproducir y reproducirlo en el momento del disparo. Abre la clase SampleWeapon y después de la línea class SampleWeapon extends Weapon; agrega lo siguiente

var	array<SoundCue>	WeaponFireSnd;

A continuación, en el bloque defaultproperties agrega:

WeaponFireSnd(0)=SoundCue'A_Weapon_Link.Cue.A_Weapon_Link_FireCue'

Como ves, lo que hacemos aquí es crear un array para almacenar el efecto de sonido (SoundCue) del disparo para los distintos modos de nuestra arma (aunque para el ejemplo recuerda que estamos usando un solo modo) y en el bloque defaultproperties indicamos que efecto vamos a usar.

Hecho esto, solo nos queda indicar en qué punto se debe reproducir el sonido. Un buen lugar para esto es el método FireAmmunition ya que aquí es donde comienza la lógica del disparo. Vamos entonces a sobrescribir este método para agregar la reproducción del efecto. En la clase SampleWeapon agrega lo siguiente:

simulated function FireAmmunition()
{
	Super.FireAmmunition();
	Instigator.PlaySound(WeaponFireSnd[0]);
}

Lo único que hacemos aquí es garantizar que el FireAmmunition de la clase base se ejecute y además se reproduzca el efecto de sonido que indicamos. Para reproducir el efecto nos ayuda el método PlaySound de Instigator. También puedes notar que para acceder al índice deseado en WeaponFireSnd indicamos directamente 0, como nuestra arma solo tiene un modo de fuego con esto no tenemos problema, pero si quisiéramos usar la misma línea para reproducir el sonido de acuerdo al modo de fuego activado podemos usar el atributo CurrentFireMode.

En este punto es válido aclarar una cosa, lo más aconsejable es separar la lógica de la reproducción del efecto en un método independiente donde además agreguemos algunas comparaciones como: if(Instigator != None), por ejemplo, para evitar errores en tiempo de ejecución. Con el objetivo de hacer estos tutoriales lo más claros y concisos posible este tipo de cosas las omitiré de vez en cuando, espero que tu si las tengas en cuenta para tu juego 😉

Compila, ejecuta y comienza a disparar… :-)

¿Qué nos va quedando?…

¿Te has fijado en el momento en el que el robot dispara su arma? Que dicho sea de paso, no se ve nada liviana. Ni se inmuta tan siquiera :-S ¿No crees que deba hacer algún movimiento por el retroceso del arma en el momento del disparo? Además, ¿No crees también, que se deba ver un pequeño efecto en la punta del cañón cuando se dispara el arma? Bien, estas y otras cosas las veremos en el siguiente tutorial 😉

Terminando por hoy…

Las clases que conforman nuestro juego van siendo cada vez más grandes, estoy buscando la manera de subirlas junto con el Mapa que estamos usando, para una referencia rápida. Por el momento, si las necesitas puedes escribir a mi correo que te las mando sin ningún problema.
Mi correo: nan2castillocoello@gmail.com

Si has seguido los tutoriales, en el pasado anuncié que para este veríamos una introducción a Actors, pero me pareció mejor armar a nuestro robot primero para que haya más acción ;-). Por tanto, el tema de Actors lo veremos después del siguiente tutorial ya que aún nos queda terminar de afinar algunos detalles en cuanto al disparo del arma de nuestro robot. También veremos que cosa son los states en la programación de juegos y cómo implementarlos en UnrealScript.

Puedes estar al tanto de los próximos tutoriales también siguiendo mi twitter: nan2cc

Hasta la próxima… chao 😉

Me gustaría mucho oír tus comentarios.

Nota: Este proyecto fue hecho con la versión del UDK de Abril de 2011 (UDKInstall-2011-04-BETA)

Introducción a Unreal Script – parte 2

En este tutorial continuamos con el proyecto que dejamos en el pasado tutorial. Ahora se comienzan a complicar un poco las cosas.

  • Configuraremos en el Pawn del player un grupo de propiedades para ajustar su comportamiento físico con el mundo y le agregaremos un mesh para que luzca mucho más cool :-).
  • Crearemos una clase para configurar la cámara principal del juego, creando un estilo de juego 3ra persona.
  • Jugaremos con las propiedades del Pawn para modificar su comportamiento físico

Comencemos!!

Agregándole un mesh al Player

Del proyecto que dejamos en el tutorial pasado, cuando ejecutábamos nuestro juego lo que teníamos era una especie de estilo de juego 1ra persona donde podíamos controlar al player pero no tenía ninguna forma. Vamos a cambiar esto dándole un cuerpo a nuestro personaje.

Evidentemente lo primero que tenemos que tener es el mesh que le vamos a aplicar al player. Para este ejemplo usaremos uno de los mesh que trae el UDK, vamos a darle un vistazo.

Abre el UDK Editor y localiza el Content Browser (View – Browser Windows – Content Browser) y en la sección Packages, en el campo para filtrar, escribe: CH_LIAM_Cathode. Al momento aparecerá este paquete en los resultados de la búsqueda, despliégalo y aparecerá un Mesh, lo seleccionas y veras el preview del SkeletalMesh SK_CH_LIAM_Cathode.

Da doble clic sobre el SK_CH_LIAM_Cathode y se abrirá el AnimSet Editor del UDK con este mesh. Puedes pasar unos instantes admirándolo (para acercar la cámara desplaza hacia arriba el mouse con el clic derecho presionado).

Bien, cierra el AnimSet Editor, ya que sabemos el nombre del mesh que vamos a usar y en que paquete está, vamos a indicárselo al Pawn de nuestro player. Abre el archivo SamplePawn.uc que dejamos en el proyecto pasado. Vamos a configurar uno de sus miembros, el Mesh.

Actualmente nuestro SamplePawn consta de una sola línea (class SamplePawn extends Pawn;) ahora vamos a agregarle un bloque defaultproperties que del tutorial pasado sabemos que es para darle valor por defecto a las variables miembro que queremos. En este caso lo que buscamos es agregarle a nuestro player el mesh que acabamos de ver. Pues para esto, lo que debemos es inicializar el objeto Mesh que tiene nuestro SamplePawn (lo hereda de Pawn) Este atributo es una instancia de SkeletalMeshComponent.

Agrega el siguiente bloque defaultproperties a SamplePawn (a continuación de: class SamplePawn extends Pawn;)

defaultproperties
{
	Begin Object Class=SkeletalMeshComponent Name=WPawnSkeletalMeshComponent
		SkeletalMesh=SkeletalMesh'CH_LIAM_Cathode.Mesh.SK_CH_LIAM_Cathode'
	End Object
	Mesh=WPawnSkeletalMeshComponent
	Components.Add(WPawnSkeletalMeshComponent)
}

Como ya hemos mencionado, la sintaxis para las inicializaciones en los bloques deafultproperties son distintas a la sintaxis normal de UnrealScript. En este caso lo que hacemos es crear una instancia de SkeletalMeshComponent, inicializar su miembro SkeletalMesh y asignársela al atributo Mesh de SamplePawn. Por último lo agregamos como componente del Pawn.

Bien, realmente con esto tenemos para lograr lo que buscamos, que es agregarle un mesh al player. Pero antes debemos configurar el atributo bDelayedStart de nuestro GameInfo. Este atributo es para demorar el inicio del juego, y es usado generalmente en juegos multiplayers, por lo que en nuestro caso no lo necesitamos.

Abre el SampleGame.uc y dentro del bloque defaultproperties agrega la siguiente línea:

bDelayedStart=false

Listo, hora de ver que hemos logrado con todo esto. Guardamos los cambios y abrimos el UDK Editor, nos preguntará, si queremos recompilar los scripts, le decimos que sí. Cuando termine cargamos el level que hicimos en el tutorial pasado y damos clic derecho – Play from Here.

Uuuufff :-( Que pasa??, sigo viendo lo mismo… Exacto, lo que pasa es lo siguiente, nuestro player ya tiene cuerpo, pero la cámara por defecto de nuestro juego es estilo 1ra persona, por lo que teóricamente estamos detrás de la cámara. Vamos a solucionar este problema configurando la cámara principal de nuestro juego para lograr un estilo 3ra persona.

Configurando la cámara principal del juego para tener un estilo 3ra persona:

Este método que usaremos para configurar la cámara no es el único, pero al hacerlo en una clase independiente nos permitirá tener toda la lógica de la cámara centralizada. Para esto, necesitamos crear una nueva clase, por suerte para nosotros el UDK nos brinda una clase llamada Camera que nos ayudará con nuestro objetivo. Si buscamos esta clase en el UnCodeX, veremos que como comentario dice: Camera: defines the Point of View of a player in world space. (UDKInstall-2011-04-BETA)(No es necesario acudir a Google Translate para saber qué significa esto no ?? :-D)

Para crear nuestra cámara agregaremos una nueva clase a nuestro SampleGame que herede de Camera. Dentro de la carpeta Classes del proyecto agrega el siguiente archivo: SamplePlayerCamera.uc y copia dentro el siguiente código.

class SamplePlayerCamera extends Camera;

var Vector CamOffset;
var float CameraZOffset;
var float CameraScale, CurrentCameraScale; /** multiplier to default camera distance */
var float CameraScaleMin, CameraScaleMax;

function UpdateViewTarget(out TViewTarget OutVT, float DeltaTime)
{
   local vector      HitLocation, HitNormal;
   local CameraActor   CamActor;
   local Pawn          TPawn;

   local vector CamStart, CamDirX, CamDirY, CamDirZ, CurrentCamOffset;
   local float DesiredCameraZOffset;

   // Don't update outgoing viewtarget during an interpolation
   if( PendingViewTarget.Target != None && OutVT == ViewTarget && BlendParams.bLockOutgoing )
   {
      return;
   }

   // Default FOV on viewtarget
   OutVT.POV.FOV = DefaultFOV;

   // Viewing through a camera actor.
   CamActor = CameraActor(OutVT.Target);
   if( CamActor != None )
   {
      CamActor.GetCameraView(DeltaTime, OutVT.POV);

      // Grab aspect ratio from the CameraActor.
      bConstrainAspectRatio   = bConstrainAspectRatio || CamActor.bConstrainAspectRatio;
      OutVT.AspectRatio      = CamActor.AspectRatio;

      // See if the CameraActor wants to override the PostProcess settings used.
      CamOverridePostProcessAlpha = CamActor.CamOverridePostProcessAlpha;
      CamPostProcessSettings = CamActor.CamOverridePostProcess;
   }
   else
   {
      TPawn = Pawn(OutVT.Target);
      // Give Pawn Viewtarget a chance to dictate the camera position.
      // If Pawn doesn't override the camera view, then we proceed with our own defaults
      if( TPawn == None || !TPawn.CalcCamera(DeltaTime, OutVT.POV.Location, OutVT.POV.Rotation, OutVT.POV.FOV) )
      {
         /**************************************
          * Calculate third-person perspective
          * Borrowed from UTPawn implementation
          **************************************/
         OutVT.POV.Rotation = PCOwner.Rotation;
         CamStart = TPawn.Location;
         CurrentCamOffset = CamOffset;

         DesiredCameraZOffset = 1.2 * TPawn.GetCollisionHeight() + TPawn.Mesh.Translation.Z;
         CameraZOffset = (DeltaTime < 0.2) ? DesiredCameraZOffset * 5 * DeltaTime + (1 - 5*DeltaTime) * CameraZOffset : DesiredCameraZOffset;

         CamStart.Z += CameraZOffset;
         GetAxes(OutVT.POV.Rotation, CamDirX, CamDirY, CamDirZ);
         CamDirX *= CurrentCameraScale;

         TPawn.FindSpot(Tpawn.GetCollisionExtent(),CamStart);
         if (CurrentCameraScale < CameraScale)          {             CurrentCameraScale = FMin(CameraScale, CurrentCameraScale + 5 * FMax(CameraScale - CurrentCameraScale, 0.3)*DeltaTime);          }          else if (CurrentCameraScale > CameraScale)
         {
            CurrentCameraScale = FMax(CameraScale, CurrentCameraScale - 5 * FMax(CameraScale - CurrentCameraScale, 0.3)*DeltaTime);
         }
         if (CamDirX.Z > TPawn.GetCollisionHeight())
         {
            CamDirX *= square(cos(OutVT.POV.Rotation.Pitch * 0.0000958738)); // 0.0000958738 = 2*PI/65536
         }
         OutVT.POV.Location = CamStart - CamDirX*CurrentCamOffset.X + CurrentCamOffset.Y*CamDirY + CurrentCamOffset.Z*CamDirZ;
         if (Trace(HitLocation, HitNormal, OutVT.POV.Location, CamStart, false, vect(12,12,12)) != None)
         {
            OutVT.POV.Location = HitLocation;
         }
      }
   }

   // Apply camera modifiers at the end (view shakes for example)
   ApplyCameraModifiers(DeltaTime, OutVT.POV);
}

defaultproperties
{
   CamOffset=(X=12.0,Y=0.0,Z=-13.0)
   CurrentCameraScale=1.0
   CameraScale=9.0
   CameraScaleMin=3.0
   CameraScaleMax=40.0
}

Tomado de: udn.epicgames.com

😮 ¡!! Como vez, para lograr la cámara que queremos no es un par de líneas solamente. El tema de cámaras en UnrealScript es bastante extenso por lo que será un tutorial completo de esta serie. Por el momento es suficiente con saber que básicamente lo que hacemos con esta clase, es sobrescribir el método UpdateViewTarget para desplazar la cámara del player un poco hacia atrás y hacia arriba logrando el estilo 3ra persona. UpdateViewTarget es llamada en cada frame de nuestro juego para actualizar la orientación y rotación de la cámara de acuerdo a la posición del Player.

Te repito, no te preocupes si no entiendes cada una de las líneas de esta clase, en el tutorial de cámaras de esta serie abarcaremos detalladamente este tema.

Solamente queda indicarle al PlayerController que use esta cámara. Abre la clase SamplePlayerController y a continuación de la línea: class SamplePlayerController extends PlayerController; agrega el siguiente bloque.

defaultproperties
{
   CameraClass=class'SampleGame.SamplePlayerCamera'
}

Ya a estas alturas, supongo que no tengas problemas con esto, no?? :-)

Listo!!!. Compila y prueba.

EEEHHH!! Ya el player tiene forma (lo que te debe dar una buena alegría) pero tenemos algunos problemas que resolver: primero, a pesar de tener una luz en la habitación, el mesh está muy oscuro, segundo, si nos tratamos de mover nos desplazamos pero sin mover ni una articulación :-S Bien…, vamos a arreglar esto.

Lo primero, es adjuntarle al Mesh del Player, además de su “esqueleto”, la animación de sus movimientos, para esto la clase SkeletalMeshComponent (de la que es instancia Mesh) tiene dos miembros AnimTreeTemplate y el array AnimSets. El primero (AnimTreeTemplate) debe hacer referencia a un AnimTree. Para nuestro ejemplo usaremos uno de los que trae el UDK en este caso CH_AnimHuman_Tree.AT_CH_Human. Puedes buscarlo en el Content Browser del UDK Editor como mismo hicimos para el esqueleto. Y el segundo es un arreglo donde podemos almacenar las distintas secuencias de animación del personaje. En este caso, usaremos CH_AnimHuman.Anims.K_AnimHuman_BaseMale, puedes buscar también ésta con el Content Browser, da doble clic y en la ventana Unreal AnimSet Editor, en el panel Browser de la izquierda, selecciona la pestaña Anim y verás un ListBox con todas las Secuencias de Animaciones para los distintos movimientos del personaje.

Por suerte para nosotros!!!, todo esto es trabajo de los diseñadores de nuestro equipo 😀 .

Entonces, en la clase SamplePawn agregamos a continuación de la línea SkeletalMesh=SkeletalMesh’CH_LIAM_Cathode.Mesh.SK_CH_LIAM_Cathode’, las siguientes líneas:

AnimTreeTemplate=AnimTree'CH_AnimHuman_Tree.AT_CH_Human'
AnimSets(0)=AnimSet'CH_AnimHuman.Anims.K_AnimHuman_BaseMale'

Aquí lo único nuevo que tenemos, en cuanto a sintaxis de UnrealScript es la inicialización de un index del arreglo AnimSets, nada raro, sencillamente entre paréntesis indicamos el index del array.

Listo!!!. Compila y prueba.

😀 Ahora sí tenemos a nuestro robot, moviéndose correctamente animado por nuestra habitación, pero aún nos queda un problemita…., la luz.

Para solucionar esto debemos agregar a nuestro Pawn una instancia de DynamicLightEnvironmentComponent, esta clase es usada para iluminar los componentes y/o los actores durante el juego.

Abre el archivo SamplePawn.uc, e inmediatamente después de la línea class SamplePawn extends Pawn; y antes del bloque defaultproperties agrega la siguiente línea:

var DynamicLightEnvironmentComponent LightEnvironment;

Con esta línea estamos agregando un nuevo miembro a SamplePawn de tipo DynamicLightEnvironmentComponent. En UnrealScript las variables de instancia de una clase son declaradas con la palabra clave var.

Bien, hecho esto ahora debemos inicializar este miembro y agregarlo como componente a nuestro Pawn. Dentro del bloque defaultproperties agrega el siguiente bloque (antes de: Begin Object Class=SkeletalMeshComponent Name=WPawnSkeletalMeshComponent)

Begin Object Class=DynamicLightEnvironmentComponent Name=MyLightEnvironment
      // Configuración de los atributos
End Object
LightEnvironment=MyLightEnvironment
Components.Add(MyLightEnvironment)

Bien, a esta altura tampoco te debe sorprender la sintaxis de aquí. Dentro del bloque Begin Object podemos poner la inicialización de los atributos de DynamicLightEnvironmentComponent, para lograr objetivos específicos con las luces. En futuros tutoriales ampliaremos el tema de las luces con UnrealScript, por el momento con los valores por defecto tenemos.

Ya configurado MyLightEnvironment debemos indicarlo en el LightEnvironment del SkeletalMeshComponent. Para esto dentro del bloque Begin Object Name=WPawnSkeletalMeshComponent agrega la siguiente línea:

LightEnvironment=MyLightEnvironment

Listo!!!. Compila y prueba.

Very cool ehh?? 😀

Jugando con las propiedades de Pawn:

Bien, ya tenemos un personaje para nuestro juego. Ahora, podemos jugar un poco con las propiedades de Pawn para modificar las características de nuestro robot, por ejemplo, si queremos modificar la velocidad máxima de desplazamiento de nuestro robot, podemos ajustar el valor del atributo GroundSpeed. Para esto, en la clase SamplePawn dentro del bloque defaultproperties agregamos la siguiente línea:

GroundSpeed=440.0

Si revisas con el UnCodeX la clase Pawn verás tooooodoooosss 😮 los atributos que podemos ajustar, te dejo a ti para que juegues un poco con ellos 😉 con los comentarios que traen es suficiente para tener una noción de lo que hacen.

Conclusión

Hasta aquí esta segunda parte (y final) de Introducción a UnrealScript. Con lo que hemos hecho hasta ahora ya estamos listos para jugar un poco con los atributos que tienen las distintas clases de las que hemos heredado.

En el próximo tutorial veremos una introducción a Actors (casi todo en el mundo UDK hereda de Actor). Veremos como nuestro robot puede interactuar con otros elementos del juego. Aprenderemos a compilar y ejecutar nuestro juego desde el Unreal FrontEnd, entre otras cosas.

Hasta entonces… chao!!! 😉

PD: Aquí te dejo el contenido de todas las clases de nuestro proyecto para una referencia rápida.

SampleGame.uc

class SampleGame extends GameInfo;

defaultproperties
{
   PlayerControllerClass=class'SampleGame.SamplePlayerController'
   DefaultPawnClass=class'SampleGame.SamplePawn'
   bDelayedStart=false
}

SamplePawn.uc

class SamplePawn extends Pawn;

var DynamicLightEnvironmentComponent LightEnvironment;

defaultproperties
{
	GroundSpeed=440.0

	Begin Object Class=DynamicLightEnvironmentComponent Name=MyLightEnvironment
		//...
	End Object
	Components.Add(MyLightEnvironment)
	LightEnvironment=MyLightEnvironment
   
	Begin Object Class=SkeletalMeshComponent Name=WPawnSkeletalMeshComponent
		SkeletalMesh=SkeletalMesh'CH_LIAM_Cathode.Mesh.SK_CH_LIAM_Cathode'
		AnimTreeTemplate=AnimTree'CH_AnimHuman_Tree.AT_CH_Human'
		AnimSets(0)=AnimSet'CH_AnimHuman.Anims.K_AnimHuman_BaseMale'
		LightEnvironment=MyLightEnvironment
	End Object
	Mesh=WPawnSkeletalMeshComponent
	Components.Add(WPawnSkeletalMeshComponent)
} 

SamplePlayerCamera.uc

class SamplePlayerCamera extends Camera;

var Vector CamOffset;
var float CameraZOffset;
var float CameraScale, CurrentCameraScale; /** multiplier to default camera distance */
var float CameraScaleMin, CameraScaleMax;

function UpdateViewTarget(out TViewTarget OutVT, float DeltaTime)
{
   local vector      HitLocation, HitNormal;
   local CameraActor   CamActor;
   local Pawn          TPawn;
   
   local vector CamStart, CamDirX, CamDirY, CamDirZ, CurrentCamOffset;
   local float DesiredCameraZOffset;

   // Don't update outgoing viewtarget during an interpolation 
   if( PendingViewTarget.Target != None && OutVT == ViewTarget && BlendParams.bLockOutgoing )
   {
      return;
   }

   // Default FOV on viewtarget
   OutVT.POV.FOV = DefaultFOV;

   // Viewing through a camera actor.
   CamActor = CameraActor(OutVT.Target);
   if( CamActor != None )
   {
      CamActor.GetCameraView(DeltaTime, OutVT.POV);

      // Grab aspect ratio from the CameraActor.
      bConstrainAspectRatio   = bConstrainAspectRatio || CamActor.bConstrainAspectRatio;
      OutVT.AspectRatio      = CamActor.AspectRatio;

      // See if the CameraActor wants to override the PostProcess settings used.
      CamOverridePostProcessAlpha = CamActor.CamOverridePostProcessAlpha;
      CamPostProcessSettings = CamActor.CamOverridePostProcess;
   }
   else
   {
      TPawn = Pawn(OutVT.Target);
      // Give Pawn Viewtarget a chance to dictate the camera position.
      // If Pawn doesn't override the camera view, then we proceed with our own defaults
      if( TPawn == None || !TPawn.CalcCamera(DeltaTime, OutVT.POV.Location, OutVT.POV.Rotation, OutVT.POV.FOV) )
      {   
         /**************************************
          * Calculate third-person perspective
          * Borrowed from UTPawn implementation
          **************************************/
         OutVT.POV.Rotation = PCOwner.Rotation;                                                   
         CamStart = TPawn.Location;
         CurrentCamOffset = CamOffset;
         
         DesiredCameraZOffset = 1.2 * TPawn.GetCollisionHeight() + TPawn.Mesh.Translation.Z;
         CameraZOffset = (DeltaTime < 0.2) ? DesiredCameraZOffset * 5 * DeltaTime + (1 - 5*DeltaTime) * CameraZOffset : DesiredCameraZOffset;
         
         CamStart.Z += CameraZOffset;
         GetAxes(OutVT.POV.Rotation, CamDirX, CamDirY, CamDirZ);
         CamDirX *= CurrentCameraScale;
      
         TPawn.FindSpot(Tpawn.GetCollisionExtent(),CamStart);
         if (CurrentCameraScale < CameraScale)
         {
            CurrentCameraScale = FMin(CameraScale, CurrentCameraScale + 5 * FMax(CameraScale - CurrentCameraScale, 0.3)*DeltaTime);
         }
         else if (CurrentCameraScale > CameraScale)
         {
            CurrentCameraScale = FMax(CameraScale, CurrentCameraScale - 5 * FMax(CameraScale - CurrentCameraScale, 0.3)*DeltaTime);
         }                              
         if (CamDirX.Z > TPawn.GetCollisionHeight())
         {
            CamDirX *= square(cos(OutVT.POV.Rotation.Pitch * 0.0000958738)); // 0.0000958738 = 2*PI/65536
         }
         OutVT.POV.Location = CamStart - CamDirX*CurrentCamOffset.X + CurrentCamOffset.Y*CamDirY + CurrentCamOffset.Z*CamDirZ;
         if (Trace(HitLocation, HitNormal, OutVT.POV.Location, CamStart, false, vect(12,12,12)) != None)
         {
            OutVT.POV.Location = HitLocation;
         }
      }
   }

   // Apply camera modifiers at the end (view shakes for example)
   ApplyCameraModifiers(DeltaTime, OutVT.POV);
}

defaultproperties
{
   CamOffset=(X=12.0,Y=0.0,Z=-13.0)
   CurrentCameraScale=1.0
   CameraScale=9.0
   CameraScaleMin=3.0
   CameraScaleMax=40.0
}

SamplePlayerController.uc

class SamplePlayerController extends PlayerController;

defaultproperties
{
   CameraClass=class'SampleGame.SamplePlayerCamera'
}

Nota: Este proyecto fue hecho con la versión del UDK de Abril de 2011 (UDKInstall-2011-04-BETA)

Introducción a UnrealScript – parte 1

Este es el primer tutorial de una serie de tutoriales relacionados con UnrealScript, el lenguaje de programación del MAGNIFÍCO motor de juegos Unreal Engine 3.

En este primer tutorial (dividido en dos partes) veremos:

  • Cómo instalar y configurar el UnCodeX para poder revisar todas las clases del UDK organizadas jerárquicamente
  • Cómo preparar un nuevo proyecto para comenzar a desarrollar nuestro juego
  • Crearemos las clases necesarias para permitirle al jugador controlar el personaje del juego mediante el teclado y el mouse.
  • Configuraremos las clases del player para indicarle un mesh y propiedades físicas que le permitan interactuar con el mundo de forma correcta.
  • Crearemos las clases necesarias para configurar la cámara principal de nuestro juego, creando un estilo de juego 3ra persona.

Instalando UnCodeX

Antes de comenzar a preparar el proyecto, vamos a instalar y configurar UnCodeX, una excelente aplicación que nos servirá para explorar de forma mucho más cómoda, toda la jerarquía de clases del UDK. Para instalar y configurar el UnCodeX , primero lo descargamos desde aquí.

Una vez descargado he instalado vamos a Tree -> Settings en su menú principal. Seleccionamos Source Paths del panel de la izquierda y agregamos la ruta:

Ruta_hacia_la instalación_del_UDKDevelopmentSrc

Damos OK, y de nuevo en el menú principal seleccionamos Tree -> Rebuild and Analyse

Listo!!!. Ya tenemos todas las clases del UDK organizadas jerárquicamente.

Ya instalado y listo el UnCodeX vamos a comenzar con nuestro proyecto. Lo primero que vamos hacer es preparar el directorio donde estarán los scripts de nuestro juego.

Nota: Tener en cuenta que Unreal Engine 3 está diseñado para trabajar con un solo proyecto a la vez.

Configurando el directorio del proyecto

Para crear nuestro primer proyecto en UnrealScript vamos a movernos hasta la raíz de la instalación del UDK, una vez aquí localizamos la carpeta DevelopmentSrc. Dentro de esta carpeta creamos una carpeta con el nombre que le querramos dar a nuestro proyecto. Para este ejemplo la nombraremos SampleGame.

Dentro de esta carpeta SampleGame creamos otra carpeta y la nombramos: Classes

Ya estamos listos para comenzar a chocar con las primeras líneas en UnrealScript. 😉

UDK tiene toda una biblioteca de clases que nos servirán para heredar las funcionalidades “básicas” para cada elemento del juego. Conociendo esto vamos a crear las primeras clases para nuestro juego, las clases que representarán al Player.

El Player es controlado por dos elementos: PlayerController y Pawn.

PlayerController

PlayerController es el responsable de determinar como el jugador controla el personaje mediante sus dispositivos de entrada (teclado, mouse o lo que sea)

Si vamos al UnCodeX y buscamos la clase Controller, veremos que de ella heredan dos clases AIController y PlayerController. Para nuestro ejemplo, por el momento, es suficiente con crear una clase que herede de PlayerController. A grandes rasgos con heredar de esta clase tendremos lo necesario para lograr que el jugador pueda controlar los movimientos del personaje usando el teclado y el mouse.

Sabiendo esto, vamos a crear nuestra primera clase UnrealScript, muy sencilla, pero por algo se empieza no? :-)

Dentro de la carpeta Classes que creamos para nuestro juego vamos a crear el archivo SamplePlayerController.uc Lo editamos con cualquier editor de texto (en futuros tutoriales veremos como configurar Visual Studio y nFringe, para tener un IDE más potente para nuestro trabajo en UnrealScript, para este ejemplo con el notepad de Windows tenemos) y le agregamos la siguiente línea

class SamplePlayerController extends PlayerController;

Bien, nuestra primera línea en UnrealScript ;-). Lo que hemos hecho aquí es crear una nueva clase llamada SamplePlayerController que hereda de la clase del UDK PlayerController. Como mencionamos anteriormente, con heredar de PlayerController es suficiente para tener lo básico para el player.

Ya teniendo listo el PlayerController para nuestro personaje vamos a pasar a Pawn.

Pawn

Mientras PlayerController es la encargada de determinar cómo las entradas (mediante el teclado, mouse o cualquier otro sistema para comunicarse con el juego) del jugador son usadas para controlar el personaje principal. La representación visual del personaje y la lógica para determinar cómo interactuar con el mundo físico es encapsulada en Pawn.

Vamos a hacer el Pawn que controlará a nuestro personaje.

Dentro de la carpeta Classes de nuestro proyecto creamos un nuevo archivo llamado SamplePawn.uc y le agregamos la siguiente línea

class SamplePawn extends Pawn;

Como vemos, lo que hacemos aquí es muy parecido a lo que hacemos para crear nuestro PlayerController, simplemente heredamos de una clase base del UDK que ya tiene implementado el principio básico de todo Pawn. Por el momento con esto tenemos.

GameInfo

Ahora sólo nos queda crear el GameInfo de nuestro juego. Esta clase, a grandes rasgos, es la que determina las reglas del juego, las condiciones bajo las cuales el juego avanza o se termina, entre otras muchas cosas que iremos viendo poco a poco. Además GameInfo es la encargada de decirle al motor cuales clases debe usar como PlayerController, Pawn.

Sabiendo esto, entonces vamos a crear el GameInfo

En la carpeta Classes del proyecto creamos un nuevo archivo llamado SampleGame.uc y le agregamos las siguiente líneas

 class SampleGame extends GameInfo;

 defaultproperties{
 	PlayerControllerClass=class'SampleGame.SamplePlayerController'
 	DefaultPawnClass=class'SampleGame.SamplePawn'
 }

Bien aquí ya tenemos nuevas cosas, la primera línea a esta altura no nos debe asustar sencillamente heredamos de la clase GameInfo que nos brinda el UDK para facilitarnos un poco las cosas. Pero en este caso tenemos que inicializar dos variables miembro de esta clase que son PlayerControllerClass y DefaultPawnClass.

En UnrealScript el bloque defaultproperties dentro de una clase se usa para especificar valores por defectos a variables miembros de dicha clase. En este bloque la sintaxis de las asignaciones es ligeramente distinta al estándar del UnrealScript. La sintaxis para inicializar objetos (como es el caso) es la siguiente:

ObjectProp=ObjectClass’ObjectName’

Listo!!! Ya tenemos preparada todas las clases necesarias para nuestro primer ejemplo.

Compilando…

Antes de compilar necesitamos informarle al motor de la existencia de un nuevo proyecto. Para hacerlo vamos al archivo DefaultEngine.ini que se encuentra en Raiz_del_UDKUDKGameConfig, abrirlo con cualquier editor de texto y buscar la línea [UnrealEd.EditorEngine] inmediatamente debajo de esta línea habrán varias líneas que comienzan con +EditPackages .. . Pues aquí es donde tenemos que indicar la existencia de nuestro proyecto. Al final de este grupo agregamos nuestro proyecto, en nuestro caso, quedaría el bloque completo [UnrealEd.EditorEngine] de la siguiente forma:

 [UnrealEd.EditorEngine]
 +EditPackages=UTGame
 +EditPackages=UTGameContent
 +EditPackages=UDNGame
 +EditPackages=SampleGame

Listo, ahora si podemos compilar. Para compilar los scripts, podemos usar el UnrealFrontend o abrir el editor. En próximos tutoriales hablaremos del UnrealFrontend, pero en este caso lo haremos mediante el UDK Editor.

Abrimos el UDK Editor y nos saldrá un ventana preguntándonos: si queremos recompilar los scripts, le decimos que sí y esperamos que termine. Finalmente nos debe salir que todo el proceso terminó sin problemas

Creando un nivel sencillo para nuestro juego

Ya compilados los scripts y abierto el UDK Editor, vamos a crear un muy simple nivel para probar nuestro juego. Antes quiero aclarar que el diseño de niveles en UDK Editor es toda una profesión, pero evidentemente ese es el trabajo de los diseñadores de nuestro equipo 😉 En el caso que quieras aprender sobre este tema, te recomiendo libros como Sams.Mastering.Unreal.Technology Volumen I y II (el Volumen III que era de UnrealScript por desgracia fue cancelado). Por suerte, información para aprender a diseñar niveles con UDK Editor si hay bastante, con un poco de Google tendrás ;-).

Para nuestro ejemplo crearemos un nivel muy simple. O si prefieres, puedes cargar cualquiera de los mapas que trae el UDK de ejemplo.

Para crear una sencilla habitación donde probar lo que tenemos hasta ahora, con un Proyecto nuevo en el UDK Editor damos clic derecho sobre el Cubo en el panel de herramientas (en la izquierda del editor) En los valores de X, Y, Z de la ventana Brush Builder – Cube que se abre indicamos 1024, 1024 y 512 respectivamente. Damos Build y Close.

Seguidamente hacemos clic sobre el botón CSG Add del mismo panel de herramientas en la sección CSG. Se crea un cubo con las dimensiones indicadas.

Ahora volvemos a dar clic derecho sobre el Cube en el panel de herramientas y en los valores de X, Y, Z indicamos 1000, 1000 y 500 respectivamente. Build y Close.

A continuación hacemos clic sobre el botón CSG Substract de la sección CSG en el panel de herramientas. Con esto sustraemos del cubo grande prácticamente todo su interior dejando solo una pequeña pared que lo hace una habitación.

Ahora nos desplazamos en el ViewPort Perspectiva hacia dentro de esta habitación (podemos usar la rueda del mouse para movernos hacia adelante y hacia atrás en el ViewPort). Una vez dentro de la habitación creamos una luz. Para esto mantenemos presionada la tecla L y hacemos clic en el centro de la habitación. Ajustamos la posición de la luz hacia el techo de la habitación y listo.

Ya tenemos nuestro nivel diseñador, extremadamente sencillo pero perfecto para probar nuestro proyecto 😉

Ejecutando nuestro primer juego con UnrealScript

Bien, ya tenemos nuestro nivel creado, en este punto tenemos dos métodos para ejecutar nuestro proyecto sobre ese nivel.

1 – Indicar nuestro SampleGame en el Game Type For PIE del World Info.

2 – Indicar nuestro SampleGame como default game por defecto para el motor en el DefaultGame.ini.

Para este ejemplo usaremos la primera variante, en otros tutoriales veremos la segunda.

Ya con el UDK Editor abierto en el nivel que acabamos de diseñar y los scripts compilados, vamos a View en el menú principal del UDK Editor y seleccionamos World Properties. En la ventana que se nos abre desplegamos la sección World Info nos movemos hasta la propiedad Game Type For PIE y del combo box de su derecha seleccionamos nuestro proyecto (SampleGame). Cerramos y listo.

En el menú principal del UDK Editor seleccionamos Build – Build All. Cuando termine la compilación en cualquiera de los viewport seleccionamos clic derecho Play from Here.

BOOOM!!! 😀 Ya tenemos corriendo nuestro primer proyecto. Como es lógico lo logrado no es muy llamativo, pero independientemente de esto…., es bastante excitante, no es así? 😉

En este punto, tenemos un estilo de juego 1ra persona, con un Player sin Mesh, ni afectado por la gravedad (mira hacia arriba y avanza y veras a lo que me refiero :-) ), pero que se mueve por todo el nivel reaccionando perfectamente a las teclas que presione el jugador o a como mueva el mouse.

Para el próximo tutorial

Aquí terminamos esta introducción a UnrealScript. En el próximo tutorial seguiremos trabajando con este proyecto, vamos a agregarle un mesh al player para darle forma, vamos a agregarle propiedades físicas, vamos a agregar una cámara a nuestro juego para crearlo estilo 3ra persona, entre otras cosas.

Hasta entonces. . . 😉

Nota: Este proyecto fue hecho con la versión del UDK de Abril de 2011 (UDKInstall-2011-04-BETA)

ComboBox MultiColumna en ASP.NET

Hace poco me hizo falta implementar un componente estilo ComboBox que al desplegarse muestre dos columnas.

Por ejemplo, en una tabla de países se pueden almacenar las iniciales del país en un campo y el nombre del país en otro. La idea es que el componente cuando se despliegue muestre dos columnas con estos datos (Columna 1 -> Iniciales del país. Columna 2 -> Nombre completo del país). Como en la siguiente imagen:

ASP.NET no brinda ningún componente con esta característica. Comparto aquí como solucioné este problema para que lo utilice todo el que quiera, agradecería mucho cualquier comentario.

Antes de comenzar a inventar busqué en internet con esperanzas de que existiera algo que solucionara mi problema, lo mejor que encontré fue este post , MAGNÍFICO!!!,solo que no se acomodaba a lo que exactamente yo necesitaba, pero me sirvió de inspiración al ver el uso que Thomas le dió al componente DropDownExtender del AJAX Toolkit. Gracias Thomas.

Primeros pasos.

Lo primero que haremos será prepara nuestro archivo .aspx. Para esto usaremos varios componentes de ASP.NET y un componente del AJAX Toolkit. Si todavía no lo tienes, descargarlo!!!

Instalando el AJAX Toolkit

Para el funcionamiento de nuestro ComboBoxMulticolumna necesitamos usar el DropDownExtender del AJAX Toolkit. Si aún no tienes en el Toolbox los componentes del AJAX Toolkit sigue los siguientes pasos:

– Descarga el último paquete del AJAX Toolkit

– Una vez descargado, descomprímelo.

– Con el Visual Studio abierto en nuestro proyecto muévete hasta el ToolBox. Ahí en algún espacio vacío da clic derecho y selecciona Add Tab, dale un nombre, por ejemplo, AJAX Toolkit.

– En el nuevo Tab que aparece da clic derecho y selecciona Choose ítem. En la ventana que te saldrá da en el botón Browser, muévete hasta la carpeta descomprimida del AJAX Toolkit, entra en la carpeta de ejemplo (AjaxControlToolkitSampleSite), una vez dentro, entra en la carpeta bin y selecciona la dll del AJAX Toolkit. Da clic en OK y listo, al momento te saldrán todos los componentes del AJAX toolkit en el Toolbox del Visual Studio, listos para usarlos en tu proyecto.

Una vez que tenemos todos los componentes del AJAX Toolkit en el Toolbox seleccionamos y arrastramos para nuestro .aspx el componente DropDownExtender.

El Visual Studio nos debe agregar el siguiente código:

En el lugar donde soltamos el componente:


<asp:DropDownExtender ID="DropDownExtender1" runat="server"> </asp:DropDownExtender> 

En la cabecera del archivo (para registrar el AJAX Toolkit)


<%@ Register Assembly="AjaxControlToolkit" Namespace="AjaxControlToolkit" TagPrefix="asp" %> 

Y tenemos listo el DropDownExtender ahora nos queda agregar a nuestro .aspx los componentes de ASP.NET que usará el ComboBoxMulticolumna. Para esto agregamos al archivo desde el Toolbox un Panel y un Label (estos los colocamos dentro de un UpdatePanel, agregamos también al UpdatePanel el DropDownExtender que ya tenemos en nuestra página) y un ScriptManager (Este último para que funcione el DropDownExtender).


<asp:ScriptManager ID="ScriptManager1" runat="server"> </asp:ScriptManager> 
 
<asp:UpdatePanel ID="UPnlComboBoxMultiColumna" runat="server"> 
<ContentTemplate> 
 
        <asp:Panel ID="PnlComboBoxMultiColumna" runat="server"> 
        </asp:Panel> 
 
        <asp:Label ID="LblComboBoxMultiColumna" runat="server" Text=""></asp:Label> 
 
        <asp:DropDownExtender ID="DDEComboBoxMultiColumna" runat="server"> 
        </asp:DropDownExtender> 
 
</ContentTemplate>  
</asp:UpdatePanel> 

Ahora vamos a indicarle al DropDownExtender los dos componentes que usará para su funcionamiento. (Un Label que funciona como base del ComboBox y un Panel que contiene todos los ítems que se muestran al desplegarse el ComboBox) Para esto le agregamos al DropDownExtender los siguientes pares de atributo – valor:

TargetControlID="LblComboBoxMultiColumna"
DropDownControlID="PnlComboBoxMultiColumna"

Ahora el DropDownExtender quedará de la siguiente forma:


<asp:DropDownExtender ID="DDEComboBoxMultiColumna" runat="server" TargetControlID="LblComboBoxMultiColumna" DropDownControlID="PnlComboBoxMultiColumna"> 
</asp:DropDownExtender> 
 

También debemos indicarle algunas reglas de estilo al Panel para que aparezca inicialmente oculto.


<asp:Panel ID="PnlComboBoxMultiColumna" runat="server" Style="display: none; visibility: hidden;"></asp:Panel> 
 

Vamos a dejar momentáneamente el aspx aquí y vamos a pasar al aspx.cs

Implementación del aspx.cs (código en el servidor)

Lo primero que haremos será preparar la fuente de datos para poblar el ComboBoxMultiColumna. Lo más probable es que la fuente de datos de este componente sea una tabla de la base de datos de nuestra aplicación pero para el ejemplo usaremos una lista de datos sencilla.

Vamos a crear la clase para almacenar los datos.

public class Country
{
    public string id { get; set; }
    public string value { get; set; }

    public Country(string id, string value)
    {
        this.id = id;
        this.value = value;
    }
}

Esta es un clase muy sencilla que nos servirá para almacenar id y valor de cada País. El ComboBoxMultiColumna lo que mostrará será: en la primera columna el id del país y en la segunda columna el valor (nombre del país).

Bien, teniendo esto, en el Page_Load de nuestro .aspx.cs agregamos el siguiente código

List<Country> list = this.getDataSource();

foreach (Country country in list)
{
    if (LblComboBoxMultiColumna.Text == "")
    {
        LblComboBoxMultiColumna.Text = country.id;
    }

    LinkButton lb = new LinkButton();
    lb.ID = "Lbl" + country.id + "ComboBoxMultiColumna";

    lb.Text = "<div>";
    lb.Text += "<span style="width: 40px;">" + 'u00A0' + country.id + 'u00A0' + "</span>";
    lb.Text += "" + 'u00A0' + country.value + 'u00A0' + "";
    lb.Text += "</div>";

    PnlComboBoxMultiColumna.Controls.Add(lb);
}

Explico detalladamente que estamos haciendo aquí. El objetivo de estas líneas es poblar el ComboBoxMultiColumna. Para esto, primero obtenemos el listado de países que funcionará como fuente de datos para nuestro ComboBoxMultiColumna. Seguidamente está el código del método getDataSource()

private List<Country> getDataSource()
{
    List<Country> list = new List();

    list.Add(new Country("ABW","Aruba"));
    list.Add(new Country("AFG","Afghanistan"));
    list.Add(new Country("AGO","Angola"));
    list.Add(new Country("BHS","Bahamas"));
    list.Add(new Country("BMU","Bermudas"));
    list.Add(new Country("BOL","Bolivia"));
    list.Add(new Country("BRA","Brazil"));
    list.Add(new Country("COG","Congo"));
    list.Add(new Country("COL","Colombia"));
    list.Add(new Country("CUB","Cuba"));
    list.Add(new Country("CYM","Cayman"));
    list.Add(new Country("EGY","Egypt"));
    list.Add(new Country("EST","Estonia"));

    return list;
}

Después de obtener los países, recorremos el listado con el objetivo de crear un LinkButton con los datos del país (id y valor). Este es el punto clave de nuestro ComboBoxMultiColumna. Lo que haremos será obtener id y valor y colocarlos cada uno dentro de un que estos a su vez estarán dentro de un

y este será el valor del atributo Text del LinkButton. El if que está al comenzar el foreach su objetivo es configurar el valor que se ve en el ComboBoxMultiColumna la primera vez que carga la página.

Por último, una vez configurado nuestro LinkButton lo agregamos al Panel que se despliega al hacer clic sobre el Labelsimulando el comportamiento de un ComboBoxque no está desplegado, gracias al DropDownExtender.

Vamos a pararnos aquí y ver cómo está quedando todo. Si ejecutamos nuestra página en el navegador veremos que nos sale un Label con el ID del primer elemento que agregamos a nuestra fuente de datos (ABW) si damos clic sobre este elemento aparece el resto de los elementos … pero … pasa algo …. Todos los elementos aparecen uno sobre otro por lo que no se entiende nada :-S. No hay problema, vamos a arreglar este lío agregando un poco de estilo a nuestros elementos.

Vamos a nuestro archivo aspx y agregamos los siguientes estilos.


    <style type="text/css">
        
     .MultiColumnTextBoxStyle
        {            
            border: 1px solid #99bbe8;
	    padding: 1px 1px 0px 3px;	        
	    font-size:12px;  
            font-family:"Calibri","sans-serif"; 
            background-color:#ffffe1;    
        }
     
        .MultiColumnContextMenuPanel 
        {
            height:150px;            
            overflow:scroll;            
            overflow-x: hidden;
	    border: 1px solid #868686;
	    z-index: 1000;
	    background: url(menu-bg.gif) repeat-y 0 0 #FAFAFA;
	    background-color:#FFF;	        
	    cursor: default;
	    padding: 1px 1px 0px 1px;
	    font-size:12px; 
            font-family:"Calibri","sans-serif"; 
        }
 
        a.MultiColumnContextMenuItem
        {            
	    margin: 1px 0 1px 0;
	    display: block;
	    color: #003399;
	    text-decoration: none;
	    cursor: pointer;	
	    padding: 4px 19px 4px 4px;
	    white-space: nowrap;
        }

        a.MultiColumnContextMenuItem-Selected
        {
	    font-weight: bold;
        }

        a.MultiColumnContextMenuItem:hover
        {
	    background-color: #FFE6A0;
	    color: #003399;
	    border: 1px solid #D2B47A;
	    padding: 3px 18px 3px 3px;
	    text-decoration:none;    
        }
        
    </style>

Notar que MultiColumnContextMenuPanel usa una imagen de fondo. Esta imagen la podemos obtener de la carpeta AjaxControlToolkitSampleSiteApp_ThemesSampleSiteThemeImages del AJAX Toolkit.

IMPORTANTE: Debemos cambiarle el tamaño agrandándola a 40 pixeles de largo y mantener la altura igual, para que ocupe todo el espacio de la primera columna del ComboBoxMultiColumna.

Ya con las reglas de estilo en nuestro aspx (o css) vamos a indicar los atributos CssClass a cada componente. Quedarán de la siguiente forma:


<asp:Panel ID="PnlComboBoxMultiColumna" runat="server"  Width="200px" CssClass="MultiColumnContextMenuPanel" Style="display: none; visibility: hidden;"></asp:Panel> 
 
<asp:Label ID="LblComboBoxMultiColumna" runat="server" Text="" Width="65px" CssClass="MultiColumnTextBoxStyle"></asp:Label> 

Si vemos ahora nuestra página en el navegador notaremos el cambio. Ya tenemos un ComboBoxMultiColumna. 😉

Pero faltan algunos detalles. Si pasamos el mouse sobre cualquier ítem del ComboBoxMultiColumna no hay ningún cambio visual, pues para solucionar esto ya tenemos una regla CSS definida solo debemos agregársela a cada uno de los LinkButton cuando los creamos. Para esto vamos al aspx.cs y en el bloque de código dentro del foreach donde creamos los LinkButton agregamos la siguiente línea

lb.CssClass = "MultiColumnContextMenuItem";

Ahora volvemos a ejecutar nuestra aplicación en el navegador y veremos el cambio. Mucha mejor verdad 😉

Bien, ya tenemos casi todo el trabajo terminado pero … ¿Qué falta? … pues lo más importante, hasta ahora todo muy bonito pero si damos clic en cualquier Ítem de los que se despliegan en el ComboBoxMultiColumna no obtenemos ningún resultado. Bien, pues para esto haremos lo siguiente:

Cuando seleccionemos un Ítem del ComboBoxMultiColumna actualizaremos un trozo de nuestra página mediante un UpdatePanel mostrando el elemento seleccionado. Y también actualizaremos el elemento que se muestra cuando el ComboBoxMultiColumna no está desplegado.

Para esto vamos a agregar a nuestro archivo aspx un Label dentro del UpdatePanel así debe quedar:


        <asp:UpdatePanel ID="UPnlComboBoxMultiColumna" runat="server"> 
            <ContentTemplate> 
 
                <asp:Panel ID="PnlComboBoxMultiColumna" runat="server" Width="200px" CssClass="MultiColumnContextMenuPanel" Style="display: none; visibility: hidden;"> 
                </asp:Panel> 
 
                <asp:Label ID="LblComboBoxMultiColumna" runat="server" Text="" Width="65px" CssClass="MultiColumnTextBoxStyle"></asp:Label> 
 
                <asp:DropDownExtender ID="DDEComboBoxMultiColumna" runat="server" TargetControlID="LblComboBoxMultiColumna" DropDownControlID="PnlComboBoxMultiColumna"> 
                </asp:DropDownExtender> 
                 
                <asp:Label ID="LblSelectedItemComboBoxMultiColumna" runat="server" Text=""></asp:Label> 
             
            </ContentTemplate>  
        </asp:UpdatePanel>   

Ya teniendo listo el aspx pasamos a nuestro aspx.cs para agregar algunos nuevos detalles.

Primero, vamos a configurar el evento Clic de cada LinkButton (Ítem del ComboBoxMultiColumna) para que cuando lo seleccionemos se dispare la acción que deseamos. Para esto en el bloque de código donde creamos cada LinkButton dentro del foreach agregamos la siguiente línea

lb.Click += new EventHandler(ComboBoxMultiColumnaItem_onClick);

Evidentemente tenemos también que crear el método ComboBoxMultiColumnaItem_onClick. Aquí está

    protected void ComboBoxMultiColumnaItem_onClick(object sender, EventArgs e)
    {
        string linkButtonText = ((LinkButton)sender).Text;
        string ID = linkButtonText.Split('u00A0')[1];
        string VALUE = linkButtonText.Split('u00A0')[3];
        LblComboBoxMultiColumna.Text = ID;
        LblSelectedItemComboBoxMultiColumna.Text = " Elemento seleccionado: " + VALUE;
    }

Si nos fijamos en la línea donde configuramos el atributo Text de cada LinkButton al principio y al final de cada dato (id y valor) colocamos un carácter invisible (u00A0). Esto lo hacemos con el objetivo de tener un carácter para picar el Text y poder separar cada componente (id y valor) sin afectar visualmente los Ítems del ComboBoxMultiColumna.

Bien, pues entonces lo que hacemos en el método que se dispara con el onClick de cada LinkButton es eso,separamos ese string mediante el carácter vacío y tomamos id y valor. Una vez que tenemos estos podemos hacer con ello lo que queramos. En nuestro caso lo que hacemos es mostrar el valor en un Label y actualizar el ID seleccionado.

Nos faltó un pequeño detalle. Cuando abrimos la página por primera vez el ComboBoxMultiColumna nos muestra el id del primer Ítem pero el Label que nos indica el elemento seleccionado no muestra nada. Para arreglar este pequeño detalle en nuestro archivo aspx.cs dentro del foreach, dentro del if agregamos la siguiente línea:

LblSelectedItemComboBoxMultiColumna.Text = " Elemento seleccionado: " + country.value;

Vamos hacia el navegador para ver que hemos logrado…. EEEHHHHH ¡!!!! Ya tenemos un ComboBoxMultiColumna completamente funcional ;-).

Espero que le sea de ayuda a alguien, agradecería cualquier comentario. A continuación todo el código del ejemplo para una referencia más rápida.

Archivo aspx


<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" Culture="es-ES"%>

<%@ Register Assembly="AjaxControlToolkit" Namespace="AjaxControlToolkit" TagPrefix="asp" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<%--
// ----------------------------------------------------------
//  Creado por Fernando Castillo Coello el 04/07/2011 
// ----------------------------------------------------------
//  http://nan2cc.wordpress.com 
//  nan2castillocoello@gmail.com
// ----------------------------------------------------------
//  Puede usar y modificar este código sin ninguna limitación
// ----------------------------------------------------------
--%>

<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
    <title></title>

    <style type="text/css">
        
     .MultiColumnTextBoxStyle
        {            
            border: 1px solid #99bbe8;
	        padding: 1px 1px 0px 3px;	        
	        font-size:12px;  
            font-family:"Calibri","sans-serif"; 
            background-color:#ffffe1;    
        }
     
        .MultiColumnContextMenuPanel 
        {
            height:150px;            
            overflow:scroll;            
            overflow-x: hidden;
	        border: 1px solid #868686;
	        z-index: 1000;
	        background: url(menu-bg.gif) repeat-y 0 0 #FAFAFA;
	        background-color:#FFF;	        
	        cursor: default;
	        padding: 1px 1px 0px 1px;
	        font-size:12px; 
            font-family:"Calibri","sans-serif"; 
        }
 
        a.MultiColumnContextMenuItem
        {            
	        margin: 1px 0 1px 0;
	        display: block;
	        color: #003399;
	        text-decoration: none;
	        cursor: pointer;	
	        padding: 4px 19px 4px 4px;
	        white-space: nowrap;
        }

        a.MultiColumnContextMenuItem-Selected
        {
	        font-weight: bold;
        }

        a.MultiColumnContextMenuItem:hover
        {
	        background-color: #FFE6A0;
	        color: #003399;
	        border: 1px solid #D2B47A;
	        padding: 3px 18px 3px 3px;
	        text-decoration:none;
	        
        }
        
    </style>

</head>
<body>
    <form id="form1" runat="server">
    <div>

        <asp:ScriptManager ID="ScriptManager1" runat="server">
        </asp:ScriptManager>

        <asp:UpdatePanel ID="UPnlComboBoxMultiColumna" runat="server">
            <ContentTemplate>

                <asp:Panel ID="PnlComboBoxMultiColumna" runat="server"  Width="200px" CssClass="MultiColumnContextMenuPanel"
                Style="display: none; visibility: hidden;">
                </asp:Panel>

                <asp:Label ID="LblComboBoxMultiColumna" runat="server" Text="" Width="65px" CssClass="MultiColumnTextBoxStyle"></asp:Label>

                <asp:DropDownExtender ID="DDEComboBoxMultiColumna" runat="server"  
                TargetControlID="LblComboBoxMultiColumna" DropDownControlID="PnlComboBoxMultiColumna">
                </asp:DropDownExtender>
                
                <asp:Label ID="LblSelectedItemComboBoxMultiColumna" runat="server" Text=""></asp:Label>
            
            </ContentTemplate> 
        </asp:UpdatePanel>

    </div>
    </form>
</body>
</html>


Archivo aspx.cs


// ----------------------------------------------------------
//  Creado por Fernando Castillo Coello el 04/07/2011 
// ----------------------------------------------------------
//  http://nan2cc.wordpress.com 
//  nan2castillocoello@gmail.com
// ----------------------------------------------------------
//  Puede usar y modificar este código sin ninguna limitación
// ----------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

/// <summary>
/// Clase para almacenar los valores de cada Items del ComboBoxMultiColumna
/// </summary>
public class Country
{
    public string id { get; set; }
    public string value { get; set; }

    public Country(string id, string value)
    {
        this.id = id;
        this.value = value;
    }
}

public partial class _Default : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        List<Country> list = this.getDataSource();

        foreach (Country country in list)
        {
            if (LblComboBoxMultiColumna.Text == "")
            {
                LblComboBoxMultiColumna.Text = country.id;
                LblSelectedItemComboBoxMultiColumna.Text = " Elemento seleccionado: " + country.value;
            }

            LinkButton lb = new LinkButton();
            lb.ID = "Lbl" + country.id + "ComboBoxMultiColumna";
            lb.CssClass = "MultiColumnContextMenuItem";
            lb.Click += new EventHandler(ComboBoxMultiColumnaItem_onClick);

            lb.Text = "<div style=" height:15px; ">";
            lb.Text += "<span style="width:40px; float:left; margin:0px; padding:0px;">" + 'u00A0' + country.id + 'u00A0' + "</span>";
            lb.Text += "<span>" + 'u00A0' + country.value + 'u00A0' + "</span>";
            lb.Text += "</div>";

            PnlComboBoxMultiColumna.Controls.Add(lb);
        }
    }

    /// <summary>
    /// Se dispara con el onClick de cada Item del ComboBoxMultiColumna.
    /// </summary>
    protected void ComboBoxMultiColumnaItem_onClick(object sender, EventArgs e)
    {
        string linkButtonText = ((LinkButton)sender).Text;
        string ID = linkButtonText.Split('u00A0')[1];
        string VALUE = linkButtonText.Split('u00A0')[3];
        LblComboBoxMultiColumna.Text = ID;
        LblSelectedItemComboBoxMultiColumna.Text = " Elemento seleccionado: " + VALUE;
    }

    /// <summary>
    /// Retorna el listado de paises que funcionará como fuente de datos para el ComboBoxMultiColumna.
    /// </summary>
    private List<Country> getDataSource()
    {
        List<Country> list = new List<Country>();

        list.Add(new Country("ABW", "Aruba"));
        list.Add(new Country("AFG", "Afghanistan"));
        list.Add(new Country("AGO", "Angola"));
        list.Add(new Country("BHS", "Bahamas"));
        list.Add(new Country("BMU", "Bermudas"));
        list.Add(new Country("BOL", "Bolivia"));
        list.Add(new Country("BRA", "Brazil"));
        list.Add(new Country("COG", "Congo"));
        list.Add(new Country("COL", "Colombia"));
        list.Add(new Country("CUB", "Cuba"));
        list.Add(new Country("CYM", "Cayman"));
        list.Add(new Country("EGY", "Egypt"));
        list.Add(new Country("EST", "Estonia"));

        return list;
    }
}