Gameplay Ability System

El Gameplay Ability System es un framework altamente flexible que permite construir los atributos y habilidades que puedes encontrar en cualquier RPG o MOBA. Es fácilmente replicable y es usado ampliamente en los juegos de Epic como framework fundamental en el que vertebra sus juegos.

Veamos algunos conceptos importantes del sistema para inmediatamente después hacer varios ejemplos de uso. Entre los ejemplos implementaremos un ataque melee con cooldown, regenerar salud consumiendo puntos de maná o, incluso, seleccionar un objeto para un ataque a distancia.

Motivación

Imagina que quieres implementar la siguiente característica: tu personaje principal (implementado en la clase AHero) posee puntos de magia que puede consumir para lanzar bolas de fuego o de nieve a enemigos.

Por un lado necesitarías implementar algún método en AHero para lanzar bolas (de nieve o fuego). Este método debería restar puntos de maná. Si la bola impacta sobre un enemigo debería restarle puntos de salud. Hasta aquí fácil.

Ahora hay enemigos de fuego o de nieve y las bolas de su tipo no le hacen daño. Habría que implementar dicha característica.

Los enemigos de nieve podrán lanzar bolas de nieve consumiento sus propios puntos de magia y hacerte daño de salud a ti si te impacta. Habría que copiar&pegar el mismo código que hiciste para tu héroe en la clase enemigo. O mejor aún, implementar una clase intermedia "LanzarBola".

Quizás haya un súper enemigo mitad hielo y mitad nieve que no le haga daño ninguna bola y además pueda lanzarte ambas.

Sería interesante añadir algún tipo de powerup que recuperaras salud usando magia. Algunos enemigos también podrían tener esta habilidad.

Como ves un ejemplo muy simple pero que se va complicando, ¿no sería genial hacerlo de un modo más fácil?

Por ejemplo, definimos dos atributos: Mana y Salud.

Ahora definimos dos efectos: restar mana y restar salud.

Por último definimos una serie de habilidades: lanzar bola de nieve y lanazr bola de fuego.

Estas habilidades usan los efectos anteriores: restan mana al lanzador y restan salud al que recibe la bola.

Además podemos marcar (tags) los actores como "Inmune al hielo" ó "Inmune al fuego" para que estas habilidades los tengan en cuenta.

Por último asociamos estas habilidades a los actores (hero y/o enemigo) que queramos.

Pues esto es precisamente es lo que hace Gameplay Ability System.

Te abstrae de todos los detalles de implementación y eleva el nivel de abstracción; con Gameplay Ability System piensas en atributos, efectos, tags y habilidades en vez de invertir tiempo en crear clases intermedias para evitar duplicidad de código.

El componente

Todas las característica que ofrece UE4 están encapsuladas en componentes. El framework Gameplay Ability System no iba a ser una excepción. El componente que se usa como eje central para interaccionar con el framework se llama UAbilitySystemComponent.

Entre los métodos destacados están registrar las habilidades de las que dispone dicho actor y ejecutar dichas habilidades.

Veremos como configurarlo y los métodos que ofrece en detalle cuando implementemos los ejemplos. Pero antes de eso, debemos comprender los fundamentos en los que se divide el framework.

Quizás en una primera lectura no termines de comprender del todo algún concepto, pero quedarán mucho más claros cuando implementemos algunos ejemplos.

Gameplay Attributes

Los actores que interaccionan con Gameplay Ability System generalmente necesitan un conjunto de propiedades numéricas (salud, karma, estamina, armadura, resistencia, puntos de acción, etc.,). Estas propiedades numéricas se conocen como Gameplay Attributes.

Cada Gameplay Attributes, es decir, la salud, la estamina, etc., está representado en UE4 por la estructura FGameplayAttributeData.

FGameplayAttributeData es una estructura que almacena un valor numérico (en concreto, un float).

La estructura está formada principalmente por dos campos: BaseValue y  CurrentValue .

El CurrentValue, como su propio nombre indica, almacena el valor más reciente del atributo. El valor base, en cambio, almacenada el último valor actualizado.

El 99% del tiempo ambos campos serán iguales. ¿Dónde está la diferencia? Por ejemplo, en un efecto de buffs (2 puntos de daño por segundo durante 5 segundos), el valor base será el valor antes del daño mientras que CurrentValue se irá modificando en tiempo real, cuando pasen los 5 segundos el valor base será actualizado con el valor actual.

Cada actor puede poseer uno o más FGameplayAttributeData. La clase que representa esta colección es UAttributeSet. Un UAttributeSet está formado, por tanto, por un conjunto de FGameplayAttributeData.

En código:

UCLASS()
class UMiListaDeAtributos : public UAttributeSet
{
  GENERATED_BODY()
public:
  UMiListaDeAtributos();

  UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attributes")
  FGameplayAttributeData Salud;

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

  UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attributes")
  FGameplayAttributeData PuntosDeAccion;
};

UCLASS()
class AMiActorDeEjemplo : public AActor
{
  // ... aqui el Actor se asociará a la clase UMiListaDeAtributos y la usará de algún modo
};

La clase UAttributeSet tiene algunos métodos muy útiles para sobreescribir. Por ejemplo, el método PostGameplayEffectExecute se ejecuta cuando algún atributo (FGameplayAttributeData) cambia su valor:

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

       FName HealthMemberName = GET_MEMBER_NAME_CHECKED(USimpleAttributeSet,  Salud);
       // Podrías hacer:     FName HealthMemberName = FName(TEXT("Salud"));
      // pero con la macro GET_MEMBER_NAME_CHECKED es más seguro porque lanzas
      // un error en tiempo de compilación si no existe el campo

        // El método FindFieldChecked está en UnrealTypes y sirve, precisamente, para obtener la UProperty* declarada en la clase en cuestión
       if (Data.EvaluatedData.Attribute.GetUProperty() == FindFieldChecked<UProperty>(UMiListaDeAtributos::StaticClass(), HealthMemberName))
       {
              UE_LOG(LogTemp, Warning, TEXT("¡Me hicieron pupa! Ahora mi salud es de: %f"), Health.GetCurrentValue());
       }

        // Se recomienda el código anterior poruqe es más robusto a errores, pero se podría haber simplificado con:
        //    if (Data.EvaluatedData.Attribute.GetName() == "Salud") { ... }
}

También es interesante sobreescribir el método PreAttributeChange si necesitas clampear o modificar de algún modo los valores de los atributos antes de modificarlos:

void UMiListaDeAtributos::PreAttributeChange(const FGameplayAttribute& Attribute,  float& NewValue)
{
       Super::PreAttributeChange(Attribute, NewValue);
       if (Attribute.GetUProperty() ==  FindFieldChecked<UProperty>(UMiListaDeAtributos::StaticClass(),  GET_MEMBER_NAME_CHECKED(UMiListaDeAtributos, Salud)))
       {
         // Si vamos a modificar la salud, nos aseguramos que esta entre 0 y 100
         NewValue = FMath::Clamp(NewValue, 0.0f, 100.0f);
       }
}

Hasta ahora hemos presentado el concepto de atributo (representado por FGameplayAttributeData). Que un actor puede tener asociado un conjunto de atributos (representado por la clase UAttributeSet). Y que la clase UAttributeSet posee algunos métodos interesantes para sobreescribir. Sin embargo, ¿cómo modificamos estos atributos?.

Los Gameplay Attributes son modificados por Gameplay Effects.

Los Gameplay Effects son la forma que proporciona el framework Gameplay Ability System para cambiar los atributos (FGameplayAttributeData).
Cambiar los atributos incluye:

  • Cambios directos a los atributos, como restar cierta cantidad de puntos de salud.
  • Cambios temporales (a menudo llamados "buffs" ó "debuffs"), como un boost a la velocidad o un powerup.
  • Cambios persistentes que son aplicaddos durante el tiempo, como regenerar puntos mágicos.

Los Gameplay Effects son implementados como data-only Blueprint (clase base UGameplayEffect). Esto es, son clases declarativas, sin métodos, simplemente almacenan datos.

Entre los datos principales de un Gameplay Effect se encuentran:

  • la duración,
  • el modificador (por ejemplo, incrementar un 5% la resistencia)
  • y los requisitos (esto es, los Gameplay Tags que son requeridos -o prohibidos- para que se aplique el gameplay effect).
GameplayEffect

Tiene mucho sentido, piensa por un instante ¿qué datos necesita almacenar una clase que modifique un atributo?.

Diagrama de clases GameplayEffect

Sobre el modificador, ¿qué pasa si no me basta con indicar un porcentaje o un valor concreto? Si como modificador necesitas algo más que un simple número puedes usar UGameplayEffectExecutionCalculation para definir ecuaciones más complejas.

Siguiente parada, ¿cómo aplicamos estos GameplayEffect a los FGameplayAttributeData de un actor/es concreto?

Gameplay Ability

Los Gameplay Ability describen una habilidad.

Por ejemplo: "Beber una poción de salud", "Ataque melee", "Lanzar magia de fuego", "Regenerar salud usando maná", "Seleccionar objetivos para bombardear", etc.,

Técnicamente los Gameplay Ability derivan de la clase UGameplayAbility y definen lo que una habilidad hace, si cuesta algo usarla, cuando o bajo qué condiciones pueden ser usada y demás. Como sin duda estarás deduciendo, los Gameplay Ability están intimimamente relacionados con los GameplayEffect.

En general, podemos decir que una habilidad (UGameplayAbility) sirve para aplicar uno o varios GameplayEffect que, en última instancia, modificarán uno o varios atributos (FGameplayAttributeData).

Para que un actor pueda usar una habilidad debe de:

  • Hemos dicho que una habilidad está representada por la clase UGameplayAbility, así que lo primero es implementar dicha habilidad en una clase derivada de Gameplay Ability.
  • El actor debe poder interactuar con el framework, esto es, debe tener un componente AbilitySystemComponent entre su lista de componentes.
  • Para usar la habilidad, ésta debe estar registrada previamente en el componente AbilitySystemComponent usando el método UAbilitySystemComponent::GiveAbility.
  • Una vez registrada, puedes usar los métodos AbilitySystemComponent::TryActivateAbilityByClass(...) ó AbilitySystemComponent::TryActivateAbilitiesByTag(...)

Para usar el método AbilitySystemComponent::TryActivateAbilitiesByTag(...) el Gameplay Ability debe estar propiamente tagueado (los gameplay ability tienen la propiedad Ability Tags precisamente para ello).

Cuando se llama al método AbilitySystemComponent::TryActivateAbilitiesByXXX(...) se comprueba si se cumplen los requisitos para activar la habilidad (coste, cooldown, tags requeridos) internamente el método que se encarga de las comprobaciones es CanActivateAbility.

Entre los requisitos que se pueden usar están el coste, cooldown, tags requeridos y otros. Es tan fácil como setear las propiedades correspondientes del Gameplay Ability.

Por ejemplo para el coste está la popiedad "Cost Gameplay Effect Class" (el coste se describe usando un gameplay effect), para el cooldown la propiedad "Cooldown Gameplay Effect Class".

Si se cumplen los requisitos entonces internamente se llama al método ActivateAbility. El desarrollador (nosotros) sobreescribe la función ActivateAbility. Y llama eventualmente a la función CommitAbility que aplicará el coste (en stamina, puntos de magia o lo que sea) y/o el cooldown.

El desarrollador también puede sobreescribir CanActivateAbility aunque el que trae por defecto ya hace todas las comprobaciones importantes: coste, cooldown, tags requeridos, etc.,

El trabajo de un Gameplay Ability no se hace en un método Tick si no lanzando tareas (Gameplay Ability Tasks) asíncronas y entonces gestionar la salida de dichas tareas a través de delegados.

Por último, cuando la habilidad ha concluído hay que llamar a EndAbility.

Los Gameplay Tags son etiquetas que le puedes poner a cualquier actor y se usan extensivamente para definir el comportamiento de las habilidades.

Los Gameplay Tags pueden ayudar a determinar como los Gameplay Abilities interaccionan con otros Gameplay Abilities o con actores.

Cada Ability posee un conjunto de Tags que los identifica y categoriza en distintas maneras que pueden afectar a su comportamiento.

Por ejemplo, "Cancel Abilities With Tag" cancela cualquier Ability que ya se esté ejecutando cuyos tags se encuentren en esta lista mientras la habilidad se esté ejecutando.

Por ejemplo la habilidad "Ataque melee" puede contener en "Cancel Abilities With Tag" el tag: "Magic", de manera que se cancelen todas las habilidades relacionadas con la magia mientres se esté haciendo el ataque melee.

Además de "Cancel Abilities With Tag" existen "Block Abilities With Tag", "Activation Requited Tags", y otros muchos.

Aquí un ejemplo de la habilidad: "Ataque Melee". Fíjate que "Play Montage And Wait For Event" es un Ability Task:

Ejemplo de uso de Gameplay Ability Task: Play Montage and Wait for Event

Gameplay Cue

Los Gameplay Cue son efectos (partículas, sonidos, etc.,) que se despliegan cuando se aplica algún GameplayEffect.

El desarrollador debe implementar algunas de las funciones sobreescribibles. La principal: OnExecute.

Hasta aquí la teoría, vamos a ver en la práctica.

Jorge Moreno Aguilera

Jorge Moreno Aguilera