Tutorial GameplayAbilitySystem

Después de estudiar en qué consiste el framework GameplayAbilitySystem y sus conceptos veamoslo en acción en estos tres ejemplos:

  1. Ataque melee
  2. Regenerar salud usando maná
  3. Seleccionar objetivo y confirmar

Para ilustrar los tres ejemplos he creado un proyecto llamado AbilityTut usando la plantilla ThirdPerson en C++.

Puedes usar cualquier proyecto que quieras.

Empecemos:

Setup

Antes de nada hay que preparar UE4 para usar GameplayAbilitySystem.

Así que lo primero es activar el Plugin: GameplayAbilities (Edit > Plugins).

Dado que vamos a programar en C++ hay que añadir las librerías del plugin al código. Para ello procedemos como habitualmente, editando el fichero xxxx.Build.cs.

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

El siguiente paso será añadir el componente AbilitySystemComponent al actor/es que necesite interactuar con el Game Abilities Framework.

En mi caso el actor principal se llama AAbilityTutCharacter:

UCLASS()
class AAbilityTutCharacter {
  // ...
  UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera)
  class UAbilitySystemComponent* AbilitySystemComponent;
  // ...
};

AAbilityTutCharacter::AAbilityTutCharacter()
{
    // ...
    AbilitySystemComponent = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("AbilitySystem"));
    // ...
}

Una parte importante de la implementación es indicar al motor que este actor interactúa de algún modo con el Gameplay Ability framework.

Aunque técnicamente el motor podría saberlo mirando simplemente si el actor tiene como componente un UAbilitySystemComponent, la forma más eficiente y explícita de hacerlo es implementando una interfaz:

UCLASS()
class AAbilityTutCharacter : public ACharacter, public IAbilitySystemInterface
{
  // ...
  virtual class UAbilitySystemComponent* GetAbilitySystemComponent() const override
  {
    return AbilitySystemComponent;
  }
  // ...
};

Una vez creado el componente lo inicializamos en BeginPlay:

void AAbilityTutCharacter::BeginPlay() {
  Super::BeginPlay();
  if (AbilitySystemComponent) {
    AbilitySystemComponent->InitAbilityActorInfo(this, this);    
  }  
}

En la llamada a InitAbilityActorInfo le pasas el actor que proveerá del AbilitySystemComponent y el avatar (el actor que lo representa gráficamente). En nuestro caso es el mismo actor.

Esta llamada permite que el AbilitySystemComponent registre eventos internos, busque los componentes SkeletalMesh y MoveComponent del avatar si los tuviera, y otras cosas internas que podrá usar potencialmente en el futuro.

Por último, necesitamos darle una oportunidad al componente AbilitySystemComponent para que se mantenga internamente actualizado cada vez que el actor es poseído por un controlador:

void AAbilityTutCharacter::PossessedBy(AController* NewController)
{
       Super::PossessedBy(NewController);
       if (AbilitySystemComponent)
       {
              AbilitySystemComponent->RefreshAbilityActorInfo();
       }
}

Preparando el Actor

Antes de empezar a crear habilidades (creando nuestras propias clases que hereden de UGameplayAbility) vamos a preparar al Actor para registrar dichas habilidades y usarlas.

En primer lugar vamos a declarar un array de habilidades:

UCLASS()
class AAbilityTutCharacter : public ACharacter
{
  // ...
  UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "TutCharacter")
  TArray<TSubclassOf<UGameplayAbility>> AbilityToAcquires;  
  // ...
};

Ahora, en su versión en Blueprint (ThirdPersonCharacter) puedes añadir las habilidades que vayamos creando:

Abilities Property

Lo veremos más adelante en cuanto creemos las primeras habilidades.

El siguiente punto a tratar es, ¿qué hacemos con esta lista de habilidades?

Necesitamos registrarlas para poder usarlas. Para ello se usa el método UAbilitySystemComponent::GiveAbility(...).

Desde el BeginPlay:

void AAbilityTutCharacter::BeginPlay()
{
  Super::BeginPlay();
  if (AbilitySystemComponent) {
    if (AbilityToAcquires.Num() > 0) {
      for (TSubclassOf<UGameplayAbility> AbilityClass : AbilityToAcquires) {
        FGameplayAbilitySpec Spec(AbilityClass);
        AbilitySystemComponent->GiveAbility(Spec);
      }
    }
    AbilitySystemComponent->InitAbilityActorInfo(this, this);
  }
}

Por último vamos a crear un método para ejecutar las distintas Gameplay Abilities. Por ejemplo cuando el jugador pulsa la tecla 1 lanza la primera habilidad, con la tecla 2 la segunda y con la tecla 3 la tercera.

void AAbilityTutCharacter::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent)
{
  // ...
	PlayerInputComponent->BindAction("Ability1", IE_Pressed, this, &AAbilityTutCharacter::TryActivateFirstAbility);
	PlayerInputComponent->BindAction("Ability2", IE_Pressed, this, &AAbilityTutCharacter::TryActivateSecondAbility);
	PlayerInputComponent->BindAction("Ability3", IE_Pressed, this, &AAbilityTutCharacter::TryActivateThirdAbility);
  // ...
}

void AAbilityTutCharacter::TryActivateFirstAbility()
{
	TryActivateAbility(0);
}

void AAbilityTutCharacter::TryActivateSecondAbility()
{
	TryActivateAbility(1);
}

void AAbilityTutCharacter::TryActivateThirdAbility()
{
	TryActivateAbility(2);
}

void AAbilityTutCharacter::TryActivateAbility(int AbilityIndex)
{
	check(AbilityIndex >= 0 && AbilityIndex < AbilityToAcquires.Num());
		    AbilitySystemComponent->TryActivateAbilityByClass(AbilityToAcquires[AbilityIndex]);
}

Recuerda añadir las nuevas acciones en project settings:

Player Input

Gameplay Ability básico

Vamos a crear un Gameplay Ability básico para entender el proceso.

Primero crea una nueva clase que herede de GameplayAbility, también puedes usar Add New > Gameplay > Gameplay Ability Blueprint.

New Gameplay Ability BP

Llámala GA_Basic. Esta habilidad simplemente imprimirá por pantalla un "Hola k ase":

GA_Basic

Ahora añádela al actor:

Añadir la habilidad GA_Basic

Si ejecutas el juego y pulsas el botón 1 deberías ver por pantalla "Hola k ase". ¡Enhorabuena! Has creado tu primera habilidad.

Aunque funciona lo cierto es que no está bien implementada.

Lo que ocurre es que en este caso tan básico no importa. Pero cuando ejecutamos el gameplay ability (en nuestro caso imprimir por pantalla) debemos hacer un par de llamadas internas.

En concreto, antes de nada tenemos que llamar a un método interno para que se cobre el coste (quizás maná o lo que fuera, lo veremos más adelante) y al final debemos llamar al método EndAbility:

Gameplay Ability Basic completo

El método CommitAbility devuelve true si se ha podido cobrar el coste.

Ataque Melee

Ahora vamos a crear una habilidad: Ataque Melee.

Para ello creamos un nuevo Blueprint derivado de  GameplayAbility:

New Gameplay Ability BP

Dos puntos importantes:

  1. El ataque melee debe ejecutar una animación y si le da a algún actor, restarle salud a dicho actor.
  2. El ataque melee no se puede ejecutar continuamente, debe pasar un tiempo entre usos.

Vamos con el punto 2. Que deba pasar un tiempo entre usos se le llama cooldown. Ve al campo Cooldown Gameplay Effect del Gameplay Ability recién creado:

Melee Gameplay Ability

Este campo define la duración entre usos, para ello necesitamos un Gameplay Effect que defina la duración.

Crea un nuevo Gameplay Effect:

New Gameplay Effect

Y configúralo tal que así:

Cooldown Gameplay Effect

Para que un Gameplay Effect funcione como cooldown, necesita marcar el actor con algún tag para saber si ya está bajo algún cooldown no compatible.

Para ello se usa el campo Granted Tags que sirve para añadir tags al actor:

Granted Tags

Asígnalo como cooldown ¡y ya está!

Assign cooldown

Si haces un pequeño prototipo del ataque melee podrás comprobar como funciona el cooldown:

Prototipo de la habilidad ataque melee

Cada vez que ejecutes el ataque (no olvides asignarlo al actor) podrás comprobar como tendrás que esperar el tiempo requerido por cooldown.

¿Qué ocurriría si olvidas incluir la llamada interna CommitAbility? Efectivamente, no se cobraría el coste y por tanto no se ejecutaría el cooldown.

La siguiente parada es que en vez de imprimir por pantalla, ejecute una animación.

Recuerda que toda la ejecución de un GameplayAbility no se hace mediante el método Tick, si no lanzando tareas (GameplayTask) asíncronas.

Así que la pregunta es: ¿existe una tarea que ejecute una animación? Sí, PlayMontageAndWait:

PlayMontageAndWait

Ahora cuando pulses la tecla correspondiente a la habilidad se ejecutará la animación hasta completarse y entonces concluirá la habilidad. Como puedes comprobar las tareas son asíncronas.

En mi caso he usado el personaje Shinbi gratuito de la Epic Store:

PlayMontageAndWait con el personaje Shinbi

Ahora nos gustaría hacer daño con el ataque melee. Para ello necesitamos tener, al menos, un atributo salud.

Salud y maná

Vamos a añadir los atributos a nuestro heroe.

Crea una nueva clase que herede de UAttributeSet, llámalo UTutAttributeSet:

UCLASS()
class ABILITYTUT_API UTutAttributeSet : public UAttributeSet
{
	GENERATED_BODY()
public:
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "TutAttributeSet")
	FGameplayAttributeData Health;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "TutAttributeSet")
	FGameplayAttributeData Mana;

	virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) override;
};

void UTutAttributeSet::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
{
	Super::PreAttributeChange(Attribute, NewValue);
	if (Attribute.GetUProperty() == FindFieldChecked<UProperty>(UTutAttributeSet::StaticClass(), GET_MEMBER_NAME_CHECKED(UTutAttributeSet, Health))) {
		NewValue = FMath::Clamp(NewValue, 0.0f, 100.0f);
	}
}

Ahora bastaría con instanciar dicha clase como miembro en el actor principal para usarla:

UCLASS()
class AAbilityTutCharacter : public ACharacter, public IAbilitySystemInterface
{
  // ...
  UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Ability)
	class UTutAttributeSet* TutAttributeSet;
  // ...
}

AAbilityTutCharacter::AAbilityTutCharacter()
{
  // ...
  TutAttributeSet = CreateDefaultSubobject<UTutAttributeSet>(TEXT("TutAttributeSet"));
  // ...
}

Ataque Melee con daño

Para modificar el atributo salud necesitamos un GameplayEffect.

Creamos un nuevo Blueprint llamado GE_Damage que herede de GameplayEffect y lo configuramos tal que así:

GameplayEffect Damage

Como puedes comprobar tiene efecto instantáneo y añade -25 al atributo Health.
Para aplicar un GameplayEffect a cualquier actor.

Ahora tenemos que detectar cuando la espada choca contra alguien.

Podríamos hacerlo usando un CapsuleColliderComponent adjuntado a la espada y escuchar para colisiones. No vamos a entrar en los detalles de la implementación de la colisión pero tienes toda la información relevante aquí.

Ahora que detectamos la colisión, fíjate por un momento en como tenemos definido la habilidad ataque melee:

Inmediatamente después de ejecutar la tarea (la animación) podemos mantenernos a la espera de recibir algún evento. En este caso, la colisión. ¿Existe una tarea que nos permite esperar un evento? Sí, Wait for Gameplay Event.

WaitForGameplayEvent

Ahora tenemos que decidir que evento esperar. A los eventos le ponemos un nombre dándole un tag.

WaitForGameplayEvent Tag

Cuando colisionemos la espada con algo, enviaremos dicho evento. ¿Qué hacemos cuando recibamos el evento? Debemos aplicar un GameplayEffect que realice el daño.

Aquí la parte que detecta la colisión y envía el gameplay event (nota como enviamos un payload que recibiremos cuando detectemos el envío del evento):

Send Gameplay Event

Y aquí dentro del gameplay ability detectando el evento y aplicando el gameplay effect:

Receive Gameplay Event

Regenerar salud usando maná

Parar crear una habilidad que cueste maná y regenere salud necesitamos:

  1. Un AttributeSet que contenga los atributos salud y mana
  2. Crear un gameplay effect que modique la salud
  3. Crear un gameplay effect que describa el coste en maná
  4. (Opcional) Crear un gameplay effect que haga de cooldown.

Aquí el Attribute Set:

DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FAttributeChangedDelegate, float, Health, float, MaxHealth);

UCLASS()
class ABILITYTUT_API UMyAttributeSet : public UAttributeSet
{
    GENERATED_BODY()

public:

    UMyAttributeSet();

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attribute")
    FGameplayAttributeData Health;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attribute")
    FGameplayAttributeData MaxHealth;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attribute")
    FGameplayAttributeData Mana;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attribute")
    FGameplayAttributeData MaxMana;

    FAttributeChangedDelegate OnHealthChangeEvent;

    FAttributeChangedDelegate OnManaChangeEvent;

    virtual void PostGameplayEffectExecute(const struct FGameplayEffectModCallbackData &Data) override;
};

UMyAttributeSet::UMyAttributeSet():
    Health(200.0f), MaxHealth(200.0f),
    Mana(200.0f), MaxMana(200.0f)
{

}

void PostGameplayEffectExecute_SingleAttribute(const struct FGameplayEffectModCallbackData& Data, FName MemberName, FGameplayAttributeData& Attrib, FGameplayAttributeData& MaxAttrib, FAttributeChangedDelegate& ChangeEvent)
{
    if (Data.EvaluatedData.Attribute.GetUProperty() == FindFieldChecked<UProperty>(UAttributeBaseSet::StaticClass(), MemberName))
    {
        // en verdad el clampeo debería estar en PreAttributeChange
        Attrib.SetCurrentValue(FMath::Clamp(Attrib.GetCurrentValue(), 0.0f, MaxAttrib.GetCurrentValue()));
        Attrib.SetBaseValue(FMath::Clamp(Attrib.GetBaseValue(), 0.0f, MaxAttrib.GetBaseValue()));

        ChangeEvent.Broadcast(Attrib.GetCurrentValue(), MaxAttrib.GetCurrentValue());
    }
}

void UMyAttributeSet::PostGameplayEffectExecute(const struct FGameplayEffectModCallbackData &Data)
{
    Super::PostGameplayEffectExecute(Data);

    PostGameplayEffectExecute_SingleAttribute(Data, GET_MEMBER_NAME_CHECKED(UMyAttributeSet, Health), Health, MaxHealth, OnHealthChangeEvent);
    PostGameplayEffectExecute_SingleAttribute(Data, GET_MEMBER_NAME_CHECKED(UMyAttributeSet, Mana), Mana, MaxMana, OnManaChangeEvent);
}

Aquí el Gameplay Effect que añade SALUD:

Gameplay Effect: Add Health

Aquí el Gameplay Effect que describe el COSTE:

Gameplay Effect: Mana Cost

Y aquí el Gameplay Effect que describe el COOLDOWN:

Gameplay Effect: Maná Cooldown – Effect
Gameplay Effect: Maná Cooldown – Tags

Nótese como en el cooldown necesitas taguear el actor para saber si tiene aplicado el cooldown o no.

Ahora toca crear el Gameplay Ability. Crea un Gameplay Ability llamado GA_HealthRegen.

Modifica sus atributos para añadir los gameplay effect sobre el coste y cooldown:

Gameplay Ability: Health Regen Set

Sobreescribir el método ActivateAbility, vamos a hacerlo fácil y solo vamos a hacer lo justo para aplicar el coste y el efecto para añadir salud:

HealthRegen Activate

Varios puntos para aclarar:

  1. El método CommitAbility ejecuta el coste del efecto previamente seteado en el atributo.
  2. Lo lógico sería usar algunas GameplayTasks como PlayMontageAndWait tal y como se hace en el tutorial previo sobre ataques melees.

Recuerda que para usar la habilidad antes hay que añadirla al AbilitySystemComponent.

Y, como siempre, usar el método TryActivateAbility para lanzar la habilidad:

Try Activate

Seleccionar objetivo y confirmar

¿Cómo implementar una habilidad como el VATS del Fallout? ¿o hablando de manera más genérica debes seleccionar un objetivo o conjunto de objetivos?

La tarea Wait Target Data junto con los actores derivados de GameplayAbilityTargetActor son perfectos para habilidades dónde debes seleccionar objetivos.

Entre las tareas que se pueden ejecutar en una habilidad está la tarea Wait Target Data. Esta tarea spawnea un actor especial derivado de GameplayAbilityTargetActor.

Este actor, GameplayAbilityTargetActor, es responsable de mostrar algún tipo de feedback visual al usuario a modo de selección y una vez confirmada la selección construye la estructura FGameplayAbilityTargetDataHandle como resultado de la selección.

El actor GameplayAbilityTargetActor sirve, precisamente, para seleccionar el/los objetivo/s. Como hemos dicho, la tarea Wait Target Data spawnea dicho actor y se queda a la espera de que se confirme la selección.

Este actor es responsable de:

  1. Mostrar algún tipo de feedback (efecto, sonido, representación visual, etc.,) al usuario del objetivo/s que está seleccionando
  2. Una vez confirmado el objetivo construir un array de TargetDataReadyDelegate. Esta estructura almacena los datos relevantes de la selección (por ejemplo el actor objetivo, o una posición, etc.,)
  3. Llamar al delegado correspondiente (TargetDataReadyDelegate) para hacerle ver a la tarea que los datos ya están listos.

Dentro de la GameplayAbility, la tarea Wait Target Data tiene una pinta como ésta:

Wait Target Data

Los campos importantes son Confirmation Type, Class y del otro lado Valid Data y Data.

El campo Confirmation Type define cómo se confirma la selección ¿será inmediata, esperará a que el usuario confirme, personalizada?. Entraremos en más detalles más abajo.

En el campo Class debes seleccionar una subclase de tipo GameplayAbilityTargetActor que es la que spawneará inmediatamente al ejecutar la tarea.

Y, por el otro lado, tienes el evento Valid Data que será llamado cuando en GameplayAbilityTargetActor se haga broadcast del delegado TargetDataReadyDelegate.

Y en Data tendrás una estructura de tipo FGameplayAbilityTargetDataHandle como resultado de la selección. Esta estructura es en verdad un wrapper de un array de estructuras FGameplayAbilityTargetDataHandle.

En FGameplayAbilityTargetData y las subclases de esta estructura puede almacenar una referencia a un actor, una posición, un FHitResult, etc.,

Diagrama de clases de Gameplay Ability Target Data

En defnitiva, la estructura FGameplayAbilityTargetDataHandle representa el resultado de la selección de objetivos como un array de FGameplayAbilityTargetData dónde esta estructura, a su vez, puede almacenar actor, transform, FHitResult o cualquier otro dato relevante.

Cuando seleccionas la subclase GameplayAbilityTargetActor en el campo Class entonces la tarea amplia con pines específicos para dicha subclase:

Wait Target Data Class

En este caso las propiedades: Start Location, Reticle Params, etc., viene de la clase padre GameplayAbilityTargetActor mientras que Radius es propio de la subclase:

UCLASS()
class MIRLUK_API AGATargetActor_GroundBlast : public AGameplayAbilityTargetActor
{
       GENERATED_BODY()

public:
       AGATargetActor_GroundBlast();

       UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GroundBlast", meta  = (ExposeOnSpawn = true))
       float Radius;

       virtual void StartTargeting(UGameplayAbility* Ability) override;
       virtual void ConfirmTargetingAndContinue() override;

       virtual void Tick(float DeltaSeconds) override;

       TArray<TWeakObjectPtr<AActor>> GetOverlappedActors(const FVector& Origin)  const;

       bool GetPointTarget(FVector& PointTarget) const;
};

Como puedes ver la propiedad Radius tiene ExposeOnSpawn=true por eso aparece como pin.

Los métodos importantes que debes sobreescribir de AGameplayAbilityTargetActor: StartTargeting y ConfirmTargetingAndContinue.

El método StartTargeting es llamado inmediatamente al spawnear el actor (recuerda que el spawn lo hace automáticamente la tarea Wait Target Data) y es un momento perfecto para almacenar la GameplayAbility de dónde la llaman, el PlayerController y el Pawn para su usos posteriores. También es el momento perfecto para inicializar algunos temas visuales.

void AGATargetActor_GroundBlast::StartTargeting(UGameplayAbility* Ability)
{
       Super::StartTargeting(Ability); // <-- OwningAbility = Ability;

       MasterPC = Ability->GetCurrentActorInfo()->PlayerController.Get();
       SourceActor = Ability->GetCurrentActorInfo()->AvatarActor.Get();

       // actualizar alguna info para UI, color o el tamaño del decal o lo que sea
}

Nota como MasterPC y SourceActor son propiedades de la clase AGameplayAbilityTargetActor pero que no son usadas, así que es perfecto para que sean usadas en la subclase.

El método ConfirmTargetingAndContinue es llamado cuando el jugador confirma. La confirmación puede ser instantánea ó a la espera del usuario ó personalizada.

La forma de confirmar se establece en el dropdown del nodo de la tarea:

Confirmation Type

En modo de confirmación instantáneo llama a ConfirmTargetingAndContinue inmediatamente después de StartTargeting (y el usuario no interactúa en nada).

Cuando el modo de confirmación es a la espera del usuario entonces la tarea se queda a la espera de que en algún punto del código (quizás cuando el usuario pulse una tecla) se llame al método TargetConfirm de AbilitySystemComponent.

Por ejemplo en la propia clase del personaje:

Input Action confirm

Dónde "InputAction Confirm" puede estar bindeada a la tecla "E" como en muchos juegos para confirmar la selección.

Sea cuál sea el modo de confirmación, una vez se haga la confirmación se llama al método ConfirmTargetingAndContinue que es el responsable de construir el payload que aparece como pin Data en la tarea Wait Target Data.

El pin Data es una array de estructuras FGameplayAbilityTargetDataHandle. Por último, el método deberá hacer un broadcast del delegado TargetDataReadyDelegate para avisar que los datos están listos.

Si vas a devolver una lista de actores, la forma más rápida de construir el array de FGameplayAbilityTargetDataHandle es usando el método MakeTargetDataHandleFromActors del atributo StartLocation.

Una implementación de ejemplo para ConfirmTargetingAndContinue dónde se selecciona los actores alrededor de un radio de explosión sería:

void AGATargetActor_GroundBlast::ConfirmTargetingAndContinue() // override
{
       FVector PointTarget;
       if (GetPointTarget(PointTarget))
       {
              TArray<TWeakObjectPtr<AActor>> Overlaps =  GetOverlappedActors(PointTarget);
              if (Overlaps.Num() > 0)
              {                    
                     FGameplayAbilityTargetDataHandle DataHandle =  StartLocation.MakeTargetDataHandleFromActors(Overlaps, false);
                     TargetDataReadyDelegate.Broadcast(DataHandle);
              }
              else
              {
                     TargetDataReadyDelegate.Broadcast(FGameplayAbilityTargetDataHandle());
              }
       }
}

bool AGATargetActor_GroundBlast::GetPointTarget(FVector& PointTarget) const
{
       // Usar:
        // 1) MasterPC->GetPlayerViewPoint(Start, EyeRotation);
        // 2) y GetWorld()->LineTraceSingleByChannel(...)
       // para obtener el punto dónde está mirando el jugador
}

TArray<TWeakObjectPtr<AActor>>  AGATargetActor_GroundBlast::GetOverlappedActors(const FVector& Origin) const
{
    // devolver en un array los actores dentro de una esfera de radio "Radius" con centro en "Origin" usando:
    // GetWorld()->OverlapMultiByObjectType(...)
}

Por último el GameplayAbility quedaría así:

Apply GameplayAbility

Dónde el bucle for podría lanzar los actores aplicándoles una fuerza.

Nota como el nodo "Get Actors from Target Data" tiene como parámetro un index. Esto es así porque, recuerda, Data es un array.

El atributo StartLocation, de tipo FGameplayAbilityTargetingLocationInfo, tiene una serie de métodos muy útiles para construir la estructura FGameplayAbilityTargetDataHandle:

FGameplayAbilityTargetDataHandle MakeTargetDataHandleFromHitResult(TWeakObjectPtr<UGameplayAbility> Ability, const  FHitResult& HitResult) const;

FGameplayAbilityTargetDataHandle MakeTargetDataHandleFromHitResults(TWeakObjectPtr<UGameplayAbility> Ability, const  TArray<FHitResult>& HitResults) const;

FGameplayAbilityTargetDataHandle MakeTargetDataHandleFromActors(const  TArray<TWeakObjectPtr<AActor>>& TargetActors, bool OneActorPerHandle = false)  const;

Por ejemplo si quisieras devolver además de un array de actores la posición central de la esfera podríamos hacer:

FGameplayAbilityTargetDataHandle DataHandle =  StartLocation.MakeTargetDataHandleFromActors(Overlaps, false);

// Note: This is cleaned up by the FGameplayAbilityTargetDataHandle (via an  internal TSharedPtr)
FGameplayAbilityTargetData_LocationInfo* CenterLocation = new  FGameplayAbilityTargetData_LocationInfo();

CenterLocation->TargetLocation.LiteralTransform = FTransform(PointTarget);
CenterLocation->TargetLocation.LocationType =  EGameplayAbilityTargetingLocationType::LiteralTransform;

DataHandle.Add(CenterLocation);

TargetDataReadyDelegate.Broadcast(DataHandle);
Jorge Moreno Aguilera

Jorge Moreno Aguilera