Archivo de la etiqueta: Blueprints

Desarrollando un Third Person Multiplayer Shooter en U4 – Parte 1

En este tutorial vamos a preparar la base de nuestro juego. Un personaje con su pistola en tercera persona. Podrá apuntar y disparar proyectiles en la dirección a la que apunta. Esto nos va a permitir, además de preparar la base de nuestro juego, ver varias cosas que nos ofrece el Engine y que no hemos visto en tutoriales anteriores, como los AimOffset, para implementar el movimiento de un personaje al apuntar. El ProjectileMovementComponent, que nos permite afectar en el Tick de un actor su posición a partir de una velocidad y gravedad, simulando el desplazamiento de un proyectil. Y justamente basados en este componente veremos como implementar un sistema de disparo con proyectiles. Así que sin más, manos a la obra !!

Requisitos previos: Para no extender innecesariamente cada tutorial, las cosas que ya hemos visto en anteriores entregas no las volvemos a abordar en detalles. Si recién comienzas tu aventura en el desarrollo de videojuegos con Unreal Engine 4, antes de continuar con este tutorial te recomiendo que le des un vistazo a los anteriores.

NOTA: Este tutorial ha sido desarrollado con Unreal Engine 4.8, si estás trabajando con otra versión puede que encuentres algunas diferencias ya que el Engine está en constante actualización. De ser así, déjame tus comentarios al final del post y buscamos juntos la solución.

Preparando los recursos para el proyecto.

Antes de comenzar a implementar cualquier cosa necesitamos los recursos de nuestro juego (modelos y animaciones) y si hay un lugar bueno para encontrar recursos para nuestras pruebas, estudios y tutoriales, ese es el Marketplace :).

Vamos a necesitar dos paquetes que hay en el Marketplace, que sí, son de pago :( pero con el precio que tienen para lo que nos brindan, son prácticamente un regalo. Me refiero al Pistol Anim Set Pro y el Military Weapon Silver

Captura de pantalla del Marketplace de los paquetes Pistol Anim Set Pro y Military Weapon Silver

Captura de pantalla del Marketplace de los paquetes Pistol Anim Set Pro y Military Weapon Silver

Con estos recursos a la mano ya podemos comenzar. Vamos iniciar nuestro proyecto a partir de la plantilla Third Person en C++. Los amantes a los blueprints se sentirán un poco mal, pero la realidad es que muchas de las funcionalidades orientadas al multiplayer en Unreal aún no están expuestas a Blueprints, por eso lo más probable es que si tu objetivo es desarrollar un juego multiplayer y quieras usar características avanzadas, vas a necesitar C++. Pero no te preocupes, que veremos también las opciones que tenemos desde blueprint, que la realidad es que para cosas simples sigue siendo una opción.

Configurando las animaciones básicas para el personaje.

Después de crear el proyecto a partir de la plantilla “Third Person en C++“ y agregar los recursos del Pistol Anim Set Pro y Military Weapon Silver Abre el Blueprint del Character y vamos a cambiarle el esqueleto para que use el Epic_Skeleton_Template_Skeleton que viene con el PistolAnimsetPro. En realidad es el mismo esqueleto que viene con Unreal pero todos los assets de animaciones del PistolAnimsetPro están hechos para esta copia del esqueleto. Haciendo esto nos evitamos el retarget de cada una de las animaciones que queramos usar. También con esto ya adelantamos la creación del socket en la mano del personaje para anclar el arma, este esqueleto que viene en el PistolAnimsetPro ya lo tiene creado.

Además de esto agrega los siguientes atributos a la clase del Character:

/** true cuando el personaje está apuntando su arma */
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category=Character)
bool bIsAiming;

/** Animación de disparo con la pistola */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Animation")
UAnimMontage *ShotPistolAnimation;

En el constructor inicializa bIsAiming en true; ya que temporalmente, desde el inicio del juego el personaje estará apuntando y bUseControllerRotationYaw también ponlo en true;

Ahora vuelve al Editor y crea un BlendSpace1D con las animaciones Pistol_Idle, Pistol_WalkFwdLoop y Pistol_RunFwdLoop (la versión InPlace). Después ve al AnimationBlueprint del personaje y modifica el nodo Idle/Run en el StateMachine para que use este BlendSpace que acabas de crear. Si tienes duda de como hacer esto dale un vistazo a los primeros tutoriales donde se explica el proceso detalladamente.

Captura del BlendSpace en modo de edición. Los assets Pistol_WalkFwdLoop y Pistol_RunFwdLoop puedes colocarlos en el valor que prefieras según el MaxWalkSpeed de tu personaje al caminar y correr.

Captura del BlendSpace en modo de edición. Los assets Pistol_WalkFwdLoop y Pistol_RunFwdLoop puedes colocarlos en el valor que prefieras según el MaxWalkSpeed de tu personaje al caminar y correr.

En este punto tenemos la animación de reposo y caminando mientras está apuntando, pero tenemos un pequeño detalle que usaremos como pretexto para ver un Asset de animación que nos brinda Unreal y que no hemos visto en tutoriales anteriores. Hablo del AnimOffset.

Introducción al AimOffset en Unreal Engine 4

Si te detienes un segundo por algún Third Person Shooter que tengas a mano, como el Max Payne 3 por ejemplo, y analizas su comportamiento, verás que el personaje tiene dos estados (centrándonos solo en lo que se refiere a apuntar/usar su arma). Puedes estar apuntando el arma, listo para disparar (generalmente con el clic secundario se pasa a este estado) y con su arma abajo, que la tiene equipada pero no está apuntando. En el primer estado verás que por lo general el personaje rota su Yaw en dirección del Controller pero si apuntas hacia arriba o hacia abajo lo que hace es rotar el torso. Pues bien, es precisamente este segundo movimiento lo que podemos implementar gracias a los AimOffset.

El AnimOffset es un asset de animación que almacena una serie de poses que son “blendeados“ a partir de los parámetros de entrada, que por lo general son la rotación en los ejes Yaw y Pitch. De esta forma hacemos que nuestro character pueda apuntar su arma sin tener que rotar todo el cuerpo, ya sea horizontalmente o verticalmente.

Imagen tomada de https://docs.unrealengine.com/latest/INT/Engine/Animation/AnimHowTo/AimOffset/index.html donde se ve al personaje apuntando su arma en distintas direcciones sin rotar todo el cuerpo, gracias al AnimOffset.

Imagen tomada de https://docs.unrealengine.com/latest/INT/Engine/Animation/AnimHowTo/AimOffset/index.html donde se ve al personaje apuntando su arma en distintas direcciones sin rotar todo el cuerpo, gracias al AnimOffset.

Para simplificar un poco la implementación de nuestro mecanismo de apuntar haremos que en la horizontal el personaje simplemente rote en la dirección del controller, por esto fue que pusimos en true la propiedad del Character Use Controller Rotation Yaw. Más adelante vamos a “afinar” un poco más esto, para evitar que cuando el character esté apuntando en la horizontal rote patinando en el suelo, pero de momento es suficiente. Para apuntar en la vertical si vamos a hacer uso del AimOffset.

Creando un AimOffset

En el Content Browser selecciona Add New/Animation y verás que tenemos dos opciones AimOffset y AimOffset 1D. Tal como tenemos con los BlendSpace, en el AimOffset pasa lo mismo. Podemos crearlo para que tenga una sola entrada, por ejemplo, la rotación solo en un eje (AimOffset 1D), o para que tenga dos entradas. Aunque de momento vamos apuntar en la horizontal con el controller, vamos a crear el AimOffset de dos entradas (Aim Offset) para ver el proceso más complejo y ya dejarlo listo para cuando vayamos, más adelante, a mejorar el apuntar en la horizontal.

Después de crear el Aim Offset, dale un nombre y ábrelo para editarlo. Como verás su modo de edición es prácticamente idéntico a los BlendSpace (de hecho, los AimOffset son una especie de BlendSpace). Como ya ha estas alturas debes entender bien como funcionan los BlendSpace simplemente te dejo como tienes que configurar el AimOffset con las animaciones que nos brinda el PistolAnimsetPro. La característica que tienen los AimOffset a diferencia de los BlendSpace es que los primeros trabajan con animaciones de un solo frame, o sea, con poses, y es el propio Engine el que se encarga de hacer el blend entre los distintos poses según los valores de entrada para que el movimiento se vea fluido.

Captura del AimOffset en modo de edición. Los parámetros de entrada son la rotación que tendrá el controller en los ejes Yaw y Pitch y el rango de valores entre -90 y 90 es para evitar la torsión total del cuerpo.

Captura del AimOffset en modo de edición. Los parámetros de entrada son la rotación que tendrá el controller en los ejes Yaw y Pitch y el rango de valores entre -90 y 90 es para evitar la torsión total del cuerpo.

Primero configura los parámetros de entrada y el rango de valores como se muestra en la imagen, después arrastra los siguientes assets de animación al gráfico del AnimOffset comenzando de arriba hacia abajo y yendo de izquierda a derecha (recuerda usar la versión InPlace)

Pistol_AimOffset_LU
Pistol_AimOffset_CU
Pistol_AimOffset_RU
Pistol_AimOffset_LC
Pistol_AimOffset_CC
Pistol_AimOffset_RC
Pistol_AimOffset_LD
Pistol_AimOffset_CD
Pistol_AimOffset_RD

Abre cada uno de estos assets para que veas que son una animación de un solo frame y que representan el pose máximo en cada una de las direcciones en las que podrá apuntar el character. Hecho esto puedes mover el cursor sobre el gráfico, simulando la entrada de los valores, para que veas en el Preview como se comporta la animación. Salva y cierra el editor del AimOffset.

Gracias al PistolAnimsetPro los Assets necesarios para crear el AnimOffset ya estaban listos, pero te recomiendo que profundices un poco más con la documentación oficial de Unreal en las secciones Aim Offset y Creating an Aim Offset porque los Animation Sequence que se necesitan para cada pose tienen varios detalles importantes de configuración a tener en cuenta para que después estos assets los puedas usar en el AimOffset. También puedes revisar los ejemplos del Content Examples.

Bien, con estos dos assets listos podemos pasar a implementar la lógica en el EventGrapth del AnimationBlueprint para poder darle valor a las variables que servirán como valores de entrada a estos dos assets. Abre el AnimBlueprint del Character y crea las siguientes variables:

Speed (float): Para el BlendSpace del Idle/Run, será la velocidad del personaje.

PlayerAimYaw (float) y PlayerAimPitch (float): Serán la dirección de apuntar en cada eje, estos son los parámetros de entrada para el AimOffset.

Delta (float): Variable temporal para almacenar el DeltaTime y poderlo usar cómodamente en el script.

IsAiming (bool). Variable para saber si el personaje está apuntando o no, la inicializamos con la variable de mismo nombre que agregamos en el Character.

Una vez creada cada una de estas variables, implementa el siguiente algoritmo:

EventGraph del AnimBlueprint del Character para obtener los valores necesarios para pasar como parámetro al BlendSpace del estado Idle/Run y el AimOffset

EventGraph del AnimBlueprint del Character para obtener los valores necesarios para pasar como parámetro al BlendSpace del estado Idle/Run y el AimOffset

En este algoritmo no hacemos nada complejo, la velocidad la obtenemos a partir del Length del vector velocity del Character, como hemos hecho muchas veces ya y los valores para el Aim los obtenemos calculando la diferencia entre la rotación del Controller (la cámara) y la rotación del Character (mediante el nodo Delta (rotator)), suavizamos un poco el valor resultante mediante el nodo RInterp To para evitar saltos bruscos, y por último, limitamos esos valores entre -90 y 90 ya que son los valores extremos de entrada para el AimOffset para evitar la torsión total del cuerpo.

Solo un detalle curioso que seguro notarás y es ese 2.5 que restamos al Pitch y el 10 que sumamos al Yaw de la rotación del Controller. Esto lo hago para “forzar“ un poco la pose inicial del Character. Por defecto la pose inicial es apuntando totalmente hacia delante, pero si te fijas en la mayoría de los shooters el personaje está un poco hacia la izquierda de la pantalla y su pistola queda apuntando hacia el centro, lo que implica que tenga que estar un poco rotada hacia la derecha.
Después que terminemos, prueba quitar esos dos valores para que notes la diferencia en el pose del personaje con respecto a la mira (el centro de la pantalla).

Ahora crea un Montage a partir de la animación Pistol_ShootOnce. Este Montage lo usaremos para reproducirlo en el momento del disparo. Settea desde el editor la propiedad ShotPistolAnimation que creamos en el Character con este Montage. Por último pasa al AnimGraph y modifícalo para que te quede de la siguiente forma.

Captura del AnimGraph del AnimBlueprint del personaje después de agregar el Slot para poder reproducir el Montage del disparo, el Layered blend per bone, para blendear ese montage y solo reproducir esa animación en la parte de arriba del esqueleto y por último, antes de terminar en el Final Animation Pose, tenemos el AimOffset que creamos para lograr el movimiento al apuntar.

Captura del AnimGraph del AnimBlueprint del personaje después de agregar el Slot para poder reproducir el Montage del disparo, el Layered blend per bone, para blendear ese montage y solo reproducir esa animación en la parte de arriba del esqueleto y por último, antes de terminar en el Final Animation Pose, tenemos el AimOffset que creamos para lograr el movimiento al apuntar.

Aquí lo único nuevo que tenemos es que antes de terminar en el Final Animation Pose tenemos el nodo que representa el AimOffset que creamos anteriormente. Esto es lo último necesario para tener nuestro mecanismo de apuntar listo.

Guarda todo, compila y dale Play al juego. El personaje ya inicia en su pose de apuntar, si mueves el mouse en la vertical para girar el personaje rota en esa dirección y si mueves el mouse hacia arriba y hacia abajo verás como entra en juego el AimOffset haciendo que solamente rote el torso para apuntar en esa dirección, tal como haríamos en la vida real. Puedes probar poner en false la propiedad bUseControllerRotationYaw para que veas como se comporta el AimOffset al apuntar en la horizontal.

Captura de pantalla del juego donde se ve el personaje en su pose de apuntar.

Captura de pantalla del juego donde se ve el personaje en su pose de apuntar.

Bien, ya terminamos con el movimiento básico para nuestro personaje. Hay un detalle que seguro notarás, el Third Person Sample no tiene ese punto de mira en el centro de la pantalla. En realidad lo pedimos prestado del First Person Sample :) es muy simple. Crea un blueprint que herede de HUD y agrega lo siguiente (copia la textura del First Person Sample). No olvides modificar el GameMode para usar este Blueprint como HUD.

Simple método del HUD para dibujar una mira en el centro de la pantalla (Tomado del First Person Template)

Simple método del HUD para dibujar una mira en el centro de la pantalla (Tomado del First Person Template)

Solo nos va quedando agregarle un arma a nuestro personaje, verdad ?. Pero antes de implementar el arma vamos a implementar el proyectil que usará esa arma.

Anteriormente implementamos un mecanismo para disparar un arma y detectar la colisión del disparo con el enemigo, pero en ese caso lo hicimos un poco ficticio. Lo que hicimos fue que en el momento del disparo se haga un Line Trace para simular la trayectoria de la bala, y el primer Actor con el que colisione ese Line Trace es el actor que recibe el disparo. Este modo de disparo generalmente se conoce como “Instant Hit“ porque inmediatamente que se hace clic, se hace el trace y se detecta el impacto. Esa solución es totalmente válida y para nuestro caso se ajustaba perfectamente, pero no siempre esa es la solución que estamos buscando para este tipo de situación. En muchos casos necesitamos una implementación que se ajuste más a la realidad. Cuando se dispare el arma salga de esta un proyectil con una velocidad determinada, que sea afectado por la gravedad, que tenga cuerpo etc. Pues para esto tenemos un fenomenal componente que nos brinda Unreal Engine 4: el ProyectileMovementComponent.

Introducción al uso del ProjectileMovementComponent

El ProjectileMovementComponent es un componente que nos permite actualizar la posición del Actor en cada Tick simulando la trayectoria de un proyectil a partir de su velocidad y gravedad. También permite activar si queremos que rebote al impactar. Es muy útil cuando queremos implementar por ejemplo: un lanza granadas, el disparo de una pelota, o el disparo de una bala. Aunque no debemos abusar de su uso, ten en cuenta que sería un actor más en el escenario calculando física, render etc. Por ejemplo, los disparos de una ametralladora no son aconsejados hacerlos por esta vía, mejor usar el Instant Hit, por la cantidad de proyectiles que se tendrían que crear en runtime y todo el calculo de la física. En realidad, una pistola como la que estamos implementando ahora, también por lo general su disparo se implementa como instant hit, pero vamos a hacer la excepción aquí para poder ver como funciona este genial componente. Después, un buen ejercicio es que intentes hacer por tu cuenta la implementación Instant Hit para esta arma, y si tienes alguna duda puedes dejarme un comentario y vemos como solucionarlo 😉

Bien, lo primero que vamos a crear es el Actor que usaremos como proyectil de nuestra arma. Crea una clase en C++ de nombre AProjectile que herede de Actor. Agrégale un componente de colisión, un StaticMesh y un ProjectileMovementComponent. A continuación te dejo como te debe quedar, ve detenidamente por los comentarios, sobre todo los relacionados al ProjectileMovementComponent para que veas como es su inicialización.

//-------------------------------------------
// Projectile.h
//-------------------------------------------

#pragma once

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

UCLASS()
class THIRDPERSONSHOOTER_API AProjectile : public AActor
{
	GENERATED_BODY()

    /** Componente de colisión */
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
    class UBoxComponent* CollisionComponent;

    /** Mesh del proyectil */
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
    class UStaticMeshComponent* MeshComponent;

    /** Componente para lograr el movimiento del proyectil */
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
    class UProjectileMovementComponent* MovementComponent;

public:
	// Sets default values for this actor's properties
	AProjectile();

	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

	// Called every frame
	virtual void Tick( float DeltaSeconds ) override;

    FORCEINLINE class UBoxComponent* GetCollisionComponent() const { return CollisionComponent; }

    FORCEINLINE class UStaticMeshComponent* GetMeshComponent() const { return MeshComponent; }

    FORCEINLINE class UProjectileMovementComponent* GetMovementComponent() const { return MovementComponent; }
};

//-------------------------------------------
// Projectile.cpp
//-------------------------------------------

#include "ThirdPersonShooter.h"
#include "Projectile.h"

AProjectile::AProjectile()
{
 	//Settea este actor para que se llame el método Tick() en cada frame
	PrimaryActorTick.bCanEverTick = true;

    //Crea el componente de colision como el RootComponent de este Actor
    CollisionComponent = CreateDefaultSubobject<UBoxComponent>(TEXT("CollisionComponent"));
    RootComponent = CollisionComponent;

    //Crea el componente para el Mesh del proyectil y lo ancla al RootComponent
    MeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MeshComponent"));
    MeshComponent->AttachTo(RootComponent);

    //Crea el UProjectileMovementComponent para el movimiento del proyectil
    MovementComponent = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("MovementComponent"));

    //Fíjate que el MovementComponent no se ancla a ningun otro componente, en cambio lo que se hace es definir cual será el componente
    //que se moverá a partir de la configuración de este, en este caso es el rootcomponent el que queremos afectar.
    MovementComponent->UpdatedComponent = CollisionComponent;

    //Velocidad iniciar que tendrá el proyectil al ser agregado a la escena
    //En realidad un proyectil se movería mucho más rápido, pero vamos a quedarnos de momento con este valor para poder ver su recorrido
    MovementComponent->InitialSpeed = 2000.0f;

    //Máxima velocidad que podrá alcanzar
    MovementComponent->MaxSpeed = 2000.0f;

    //Con bRotationFollowsVelocity en true, hacemos que la rotación sea actualizada en cada tick para q empareje con vector de velocidad
    MovementComponent->bRotationFollowsVelocity = true;

    //El valor de gravedad que afectará al proyectil, en este caso no queremos que sea afectado por la gravedad.
    MovementComponent->ProjectileGravityScale = 0.f;
}

// Called when the game starts or when spawned
void AProjectile::BeginPlay()
{
	Super::BeginPlay();

}

// Called every frame
void AProjectile::Tick( float DeltaTime )
{
	Super::Tick( DeltaTime );

}

Compila y abre el editor, crea un nuevo Blueprint que herede de la clase AProjectile que acabamos de crear, nómbralo BP_Projectile. Ábrelo para editarlo y en el StaticMeshComponent settea un mesh que será el cuerpo del proyectil, como no tenemos ningún recurso en forma de bala, puedes usar una esfera escalada o cualquier otro mesh semejante. Ajusta las propiedades del Collision Component y la posición del Mesh dentro de este.

Selecciona el ProjectileMovementComponent y dale un vistazo a las propiedades que puedes modificar desde acá, verás que por defecto los valores de velocidad y gravedad ya están configurados con los números que le dimos en C++.

Pudiera darse el caso que quisiéramos que nuestro proyectil se comportara más como una pelota, por ejemplo, como sucede en el Template First Person. En este caso solo tienes que disminuir el valor de la propiedad InitialSpeed y MaxSpeed para que salga disparado a menos velocidad y aumentar el valor de la propiedad GravityScale, para que la gravedad lo haga caer al suelo mucho más rápido. Puedes crear un proyecto basado en la platilla del First Person y revisar los parámetros del blueprint del proyectil en ese proyecto para que veas su comportamiento en el juego.

Finalmente el BP_Proyectile te debe quedar así.

BP_Projectile. En este caso el Mesh del proyectil brilla tanto por el material que tiene aplicado, lo hice así para poderlo ver mejor al ser disparo. En tu caso, te repito, puedes usar cualquier Mesh incluso sin material.

BP_Projectile. En este caso el Mesh del proyectil brilla tanto por el material que tiene aplicado, lo hice así para poderlo ver mejor al ser disparo. En tu caso, te repito, puedes usar cualquier Mesh incluso sin material.

Pues bien, ya con el proyectil listo, solo nos queda el arma.

Implementando la pistola que usará nuestro personaje

Primero vamos a crear la clase base de todas las armas que tendremos en nuestro juego.

//-------------------------------------------
// Weapon.h
//-------------------------------------------

/** Clase base de todas las armas */
UCLASS()
class THIRDPERSONSHOOTER_API AWeapon : public AActor
{
	GENERATED_BODY()

private:

    /** Mesh del arma */
    UPROPERTY(VisibleDefaultsOnly, Category=Mesh)
    USkeletalMeshComponent* Mesh;

protected:

    /** Get del Mesh **/
    FORCEINLINE USkeletalMeshComponent* GetWeaponMesh() const { return Mesh; }

public:

	/** Constructor */
    AWeapon(const FObjectInitializer& ObjectInitializer);

	/** Es llamado por el engine cuando este actor es agregado al juego */
	virtual void BeginPlay() override;

	/** Se llama en cada frame */
	virtual void Tick( float DeltaSeconds ) override;

    /** Retorna el posición del socket MuzzleFlash. La punta del cannos del arma */
    FVector GetMuzzleLocation() const;

    /**
     * Método virtual puro, nunca tendrá implementación en esta clase, se tiene que implementar en las clases base
     * Tendrá la lógica de disparo de cada arma
     */
    virtual void FireWeapon() PURE_VIRTUAL(AShooterWeapon::FireWeapon,);

};

//-------------------------------------------
// Weapon.cpp
//-------------------------------------------

#include "ThirdPersonShooter.h"
#include "Weapon.h"

AWeapon::AWeapon(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
    // Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    PrimaryActorTick.bCanEverTick = true;

    //Inicialización del Mesh como RootComponent de este actor
    //Fíjate que le desactivamos la colisión a este mesh, de esta forma evitamos que un disparo pueda colisionar con el arma
    //a menos que quieras implementar algún mecanismo en el que si esto pasa, el personaje suelte el arma 😉
    Mesh = ObjectInitializer.CreateDefaultSubobject<USkeletalMeshComponent>(this, TEXT("WeaponMesh"));
    Mesh->SetCollisionObjectType(ECC_WorldDynamic);
    Mesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
    Mesh->SetCollisionResponseToAllChannels(ECR_Ignore);
    RootComponent = Mesh;
}

FVector AWeapon::GetMuzzleLocation() const
{
    USkeletalMeshComponent* UseMesh = GetWeaponMesh();
    return UseMesh->GetSocketLocation(FName(TEXT("MuzzleFlash")));
}

// Called when the game starts or when spawned
void AWeapon::BeginPlay()
{
	Super::BeginPlay();
}

// Called every frame
void AWeapon::Tick( float DeltaTime )
{
	Super::Tick( DeltaTime );
}

Como ves, es una clase muy simple que hereda de Actor y de extra solo le agregamos el método GetMuzzleLocation que nos devuelve la posición del socket en la punta del cañón del arma.

Ahora crea otra clase que herede de esta clase AWeapon y vamos a llamarla APistol. Esta clase APistol será la clase para la pistola que usará nuestro personaje. Tendrá un atributo que será una estructura que crearemos para almacenar en ella la información del tipo de proyectil que usará. De esta forma nos quedará muy fácil si en el futuro queremos cambiar el tipo de proyectil que usa el arma. Además de esto, tendrá la implementación del método FireWeapon con toda la lógica necesaria para calcular la dirección en la que se tiene que hacer el Spawn del proyectil para que salga disparado en la dirección exacta a la que estamos apuntando.

//-------------------------------------------
// Pistol.h
//-------------------------------------------

#pragma once

#include "Weapon.h"
#include "Projectile.h"
#include "Pistol.generated.h"

/**
 * Estructura para encapsular la información del projectile que usa cada arma
 * Cada arma tendrá una instancia de esta estructura, que se podrá editar desde el Editor para facilmente configurar la información de su proyectil
 */
USTRUCT()
struct FProjectileData
{
    GENERATED_USTRUCT_BODY()

    /** Clase del proyectil. Necesario para el momento de hacer el Spawn */
    UPROPERTY(EditDefaultsOnly, Category="Defaults")
    TSubclassOf<class AProjectile> ProjectileClass;

    /** Damage que causará este proyectil al impactar */
    UPROPERTY(EditDefaultsOnly, Category="Defaults")
    float Damage;

    /** Constructor por default de la estructura */
    FProjectileData()
    {
        ProjectileClass = NULL;
        Damage = 100;
    }
};

UCLASS()
class THIRDPERSONSHOOTER_API APistol : public AWeapon
{
	GENERATED_BODY()

protected:

    /** Info del proyectil que usará esta arma */
    UPROPERTY(EditDefaultsOnly, Category=Config)
    FProjectileData ProjectileConfig;

public:

    /**
     * Dispara el arma
     * Calcula la posición y rotación en la que debe de hacerse el Spawn del proyectil para que salga en la dirección a la que estamos apuntando
     */
	virtual void FireWeapon() override;
};

//-------------------------------------------
// Pistol.cpp
//-------------------------------------------

#include "ThirdPersonShooter.h"
#include "Pistol.h"

void APistol::FireWeapon()
{
    //TODO: Implementar el disparo
}

Con estas clases creadas, compila, abre el editor y crea un blueprint a partir de esta clase, yo le llamé BP_Pistol. Como SkeletalMesh selecciona la pistola que viene en el Military Weapon Silver y fíjate que gracias al atributo Projectile Config que creamos en C++ como EditDefaultsOnly. Desde acá podemos definir el proyectil que usará esta pistola, junto con el Damage que causará.

BP_Pistol configurado con el Mesh de la pistola que viene en el Military Weapon Silver Pack y para usar como proyectil el BP_Projectile que creamos previamente

BP_Pistol configurado con el Mesh de la pistola que viene en el Military Weapon Silver Pack y para usar como proyectil el BP_Projectile que creamos previamente

Ahora, para comprobar que todo está bien, vamos desde el BeginPlay ha equiparle el arma a nuestro personaje. Abre el blueprint del character, en el BeginPlay usa el nodo Spawn Actor From Class (que nos permite agregar un actor a la escena en tiempo de ejecución) para hacer un Spawn de la pistola y anclarla a la mano del personaje. Fíjate que en este caso es importante settear como el Instigator del arma al Character (self) . . . Ups y casi se me olvida :), debemos agregar en la clase del Character un atributo de tipo AWeapon para tener en todo momento una referencia al arma que está usando. Abre la .h del Character y agrégale el siguiente atributo (Recuerda incluir la .h de AWeapon):

/** Arma actualmente equipada */
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category="Weapon")
class AWeapon* CurrentWeapon;

Listo !! ahora si podemos equiparle el arma a nuestro Character en el Begin Play

BeginPlay del Character, donde se hace un Spawn del arma, se inicializa el CurrentWeapon del Character y se ancla la pistola a la mano.

BeginPlay del Character, donde se hace un Spawn del arma, se inicializa el CurrentWeapon del Character y se ancla la pistola a la mano.

Muy bien, con esto ya tenemos nuestro personaje caminando por el escenario con la pistola en la mano, solo nos queda implementar el mecanismo para disparar el arma. Ve al Pistol.cpp y sustituye el método FireWeapon que dejamos pendiente por implementar por el siguiente:

void APistol::FireWeapon()
{
    //El Origen del disparo es la punta del cannon de la pistola (la posición del socket "MuzzleFlash")
    FVector Origin = GetMuzzleLocation();

    //Ahora vamos a determinar la rotación inicial que le daremos al proyectil para que salga disparado hacia donde estamos apuntando.
    //Para determinar esta rotación, primero tenemos que hacer un Trace hacia donde está "mirando" la camara
    //para obtener la posición exacta del objeto al que estamos apuntando (el objeto con el que colisione ese Trace)
    //y con esta posición, podemos calcular la rotación que le tenemos que dar al proyectil para que vaya en esa dirección.

    //Primero vamos a obtener el punto de inicio del Trace

    FVector ShootDir = FVector::ZeroVector;
    FVector CamLocModified = FVector::ZeroVector;

    //El Instigator de un actor es el responsable del Damage causado por este actor. Por lo general se settea al hacer el Spawn del actor
    //A partir del Instigator de la pistola, que será nuestro Character, obtenemos el Controller para poder obtener el punto de mira de la camara
    APlayerController* const PlayerController = Instigator ? Cast<APlayerController>(Instigator->Controller) : NULL;

    if (PlayerController)
    {
        FVector CamLoc;
        FRotator CamRot;

        //GetPlayerViewPoint del PlayerController retorna en CamLoc la posición de la cámara y en CamRot la rotación
        PlayerController->GetPlayerViewPoint(CamLoc, CamRot);
        ShootDir = CamRot.Vector();

        //Ahora vamos a desplazar el vector CamLoc para que quede a partir del character y en la dirección a la que estamos mirando
        //para poder hacer un Trace en dirección a donde estamos mirando sin que nuestro Character bloquee ese Trace por estar en el medio

        FVector Diff = ((Instigator->GetActorLocation()) - CamLoc);

        float DotProductResult = FVector::DotProduct(Diff, ShootDir);

        CamLocModified = CamLoc + (ShootDir * DotProductResult);
    }

    //Ahora tenemos que hacer un LineTrace desde la posición que acabamos de calcular, en dirección a donde estamos apuntando,
    //para predecir el punto de impacto que tendrá el proyectil y poder calcular la rotación que le tenemos que dar para que vaya en esa dirección.

    const float WeaponRange = 10000.0f; // Un valor grande que básicamente sería el alcance del proyectil

    const FVector EndTrace = CamLocModified + ShootDir * WeaponRange;

    FCollisionQueryParams TraceParams;
    FHitResult Impact(ForceInit);
    GetWorld()->LineTraceSingleByChannel(Impact, CamLocModified, EndTrace, ECC_GameTraceChannel1, TraceParams);

    //Finalmente el ajuste de rotación del proyectil para que al hacerle el spawn vaya en esa dirección
    //El vector resultante de la resta entre el punto de impacto y el origen, normalizado, nos da ese vector de dirección que buscamos
    if (Impact.bBlockingHit)
    {
        ShootDir = (Impact.ImpactPoint - Origin).GetSafeNormal();
    }

    //Listo !! ... ya tenemos los dos vectores que necesitamos, origen y rotación, ahora solo queda hacer el Spawn del proyectil

    //Creamos el FTransform para el Spawn del proyectil
    FTransform SpawnTransform(ShootDir.Rotation(), Origin);

    //Iniciamos el Spawn. Fíjate que la Class que le pasamos es lo que setteamos en ProjectileConfig.ProjectileClass
    //Esta clase la vamos a cargar desde el Editor en las propiedades del Blueprint de la pistola y será el BP_Projectile que creamos a partir de la clase AProjectile
    AProjectile* Projectile = Cast<AProjectile>(UGameplayStatics::BeginSpawningActorFromClass(this, ProjectileConfig.ProjectileClass, SpawnTransform));
    if (Projectile)
    {
        UGameplayStatics::FinishSpawningActor(Projectile, SpawnTransform);
    }
}

Es muy importante que vayas detenidamente por los comentarios del método porque para poder garantizar que al hacer el spawn del proyectil, este vaya exactamente en dirección a donde estamos apuntando, necesitamos de un poco de matemáticas y Traces :).

Lo que hacemos en el método es determinar la rotación inicial que le daremos al proyectil para que salga disparado hacia donde estamos apuntando. Para determinar esta rotación, primero tenemos que hacer un Trace hacia donde está “mirando” la cámara para obtener la posición exacta del objeto al que estamos apuntando (el objeto con el que colisione ese Trace) y con esta posición, podemos calcular la rotación que le tenemos que dar al proyectil para que vaya en esa dirección. Una vez que tenemos la posición y la rotación del proyectil solo tenemos que hacerle un Spawn en el nivel y del resto se encarga el ProjectileMovementComponent 😉

La línea Roja es una línea recta desde la punta de la pistola, como vez esa no puede ser la dirección que siga el proyectil. La línea azul es el Trace que hacemos para determinar la posición del objeto al que estamos mirando y por último, la línea verde representa la trayectoria que seguirá el proyectil después de ajustar su rotación inicial.

La línea Roja es una línea recta desde la punta de la pistola, como vez esa no puede ser la dirección que siga el proyectil. La línea azul es el Trace que hacemos para determinar la posición del objeto al que estamos mirando y por último, la línea verde representa la trayectoria que seguirá el proyectil después de ajustar su rotación inicial.

Por último, abre la clase del Character y agrega el método que disparará el arma que tengamos equipada. Recuerda llamar este método desde algún InputAction.

void AUnrealMannequin::FireWeapon()
{
	if(CurrentWeapon)
    {
        //Play a la animación de disparo
        PlayAnimMontage(ShotPistolAnimation);

        //Fire del arma
        CurrentWeapon->FireWeapon();
    }
}

Listo !! Compila y dale Play al juego. Dispara el arma y verás que en el momento en el que tocamos la tecla de Fire, además de la animación del personaje, sale disparado el proyectil desde la punta del cañón y exactamente hacia la dirección en la que estamos apuntando, justo como si se tratara de un proyectil real 😉

Captura del juego en ejecución donde se ve después de hacer el disparo, el proyectil “spawneado “ en la escena desde la punta del cañón y moviéndose en dirección a donde estamos apuntando.

Captura del juego en ejecución donde se ve después de hacer el disparo, el proyectil “spawneado “ en la escena desde la punta del cañón y moviéndose en dirección a donde estamos apuntando.

Terminado por hoy . . .

Vamos a dejar este tutorial hasta aquí. Verdad que aún no hemos visto nada de multiplayer, que es el plato fuerte de la serie, pero antes necesitábamos de todo este mecanismo inicial. En el próximo tutorial comenzaremos ya con el multiplayer. Este mismo sistema de disparar el arma vamos a hacerlo funcionar en multiplayer, así que no te vayas muy lejos . . . mientras, como siempre, me encantaría escuchar tus comentarios 😉