A Gameplay Framework with GAS based on Risk of Rain 2

Empowering designers to create complex skills and items with Unreal's GameplayAbilitySystem.

Posted by Vitor Cantão on Sunday, January 29, 2023

Introduction

Crafting RPG-like systems and new skills is always a fun and enjoyable process. It offers creative freedom while also requiring careful game balance consideration. It’s often the meat of the gameplay loop, after all. As the primary inspiration for this project, Risk of Rain 2 does an incredible job of tackling complex synergies and well-thought skill kits, with fast-paced and intuitive gameplay.

To support this process, designers need systems that are flexible and enable frequent iteration, providing an optimal environment for emergent design. This project is a study on this topic and aims to present a gameplay framework with tools to create complex skills, items, and systems. It builds on Unreal’s GameplayAbilitySystem (GAS) and focuses on flexibility, scalability, and extensibility. Some features include the Damage Pipeline, Ability Stacking, Proc chains, and a Melee System.

This article also seeks to expand the conversation about GAS, as there are limited resources, especially on the covered topics. The first chapters are focused on the specifics of the Framework, providing the necessary information for the later chapter to discuss more practical examples.

Note

This article assumes familiarity with Unreal and a basic understanding of the GAS framework.

Characters

Attributes

This section describes some of the character’s attributes cited throughout the article.

Attribute Description
Base Damage Base Damage that scales with Level. Every Damage is a multiplier of this attribute.
Total Damage1 Final Damage output. This attribute is modified by effects and other abilities (such as on-hit items). See Damage Pipeline.
Critical Strike Chance Chance to land a critical strike, doubling the damage dealt (also affects proc-chains).

1.Total Damage is a Meta Attribute. (See GAS Documentation)

Attribute Description
Health/Max Health Scales with level. If it reaches 0 or less, the character dies and activates on-kill effects.
Health Regen The amount of health regenerated every second.
Shield/Max Shield Similar to health, but with special rules. Shields are emptied before health. See the Personal Shield Generator item.
Armor Reduces (or increases if negative) the damage that a character takes.

Characters in Risk of Rain (including monsters) have four active skills slots: Primary, Secondary, Utility, and Special. A skill can have multiple charges, with effects being able to increase and decreasie it. While prototyping various solutions, I find using attributes to track the ability charges the most intuitive approach. Of course, if you don’t have a small, fixed number of skill slots, this approach might be unsustainable. An alternative is to use tags instead. Skill stacks and recharge are covered in detail in the Ability Stack section.

Attribute Description
Primary/Max Primary Number of charges the primary skill has.
Secondary/ Max Secondary Number of charges the secondary skill has.
Utility/Max Utility Number of charges the utility skill has.
Special/Max Special Number of charges the special skill has.

Creating Characters

Every new character derives from the ASCharacterBase class and has an IAbilitySystemInterface. Player controlled characters have their ASC on the PlayerState with a Mixed replication mode, while AI characters have their ASC on the AvatarActor with a Minimal replication mode.

When creating new characters, designers need to fill in a few parameters on the character’s blueprint. The initial stats are granted by an instant GE that overrides the fundamental attributes. To assign skills to each slot a Skill Set (see below) is used. Designers can also add passive skills and assign any permanent tags.

Bandit Character

Skill Set

Designers can easily assign skills to slots by populating a Skill Set. This means that the skills themselves don’t have an inherent concept of slots, but are later assigned to them within a Skill Set.

Skill Set

Bandit’s Skill Set example.

The Skill Set is a simple DataAsset that features the GiveSkills method. Given an ASC, it grants the ability to the correct slot using an InputID.

class AS_API UGameplaySkillSet : public UDataAsset
{
  // ...
	UPROPERTY(EditAnywhere, DisplayName = "Skill", Category = "Primary (M1)")
	TSubclassOf<UGameplaySkill> PrimarySkill;
  // ...
}



void UGameplaySkillSet::GiveAbilities(UAbilitySystemComponent* ASC) const
{
  // ...
  ASC->GiveAbility(FGameplayAbilitySpec(
          PrimarySkill, 
          DefaultLevel, 
          static_cast<int32>(EASAbilityInputID::Primary))
  );

  ASC->GiveAbility(FGameplayAbilitySpec(
          SecondarySkill, 
          DefaultLevel, 
          static_cast<int32>(EASAbilityInputID::Secondary))
  );
  // ...
}

Ability Stack

Ability stacking is a common feature in MOBAs and similar genres. As opposed to firing abilities and waiting for their cooldown, like in traditional RPGs, you can use them right after as long as there are charges/stacks left. Charges are generated after a certain period up to a maximum limit.

Overwatch 2 - Ability Stack

Overwatch 2 has many examples of ability stack. Tracer’s “Blink” ability can have up to 3 charges.

While GAS has support for stacking GameplayEffects, there’s currently no support for Ability stacking. So, instead of using the built-in Cost and Cooldown, I came up with a system that utilizes a GameplayStackRecharger. Designers can set up the max stacks, the recharge duration and the recharge Ability for each GameplaySkill.

Skill Config

After the skill has been granted through a Skill Set, the stack attributes are automatically changed to reflect the skill’s Max Stacks. This is possible using a dynamic GE created at runtime:

void UGameplaySkill::ApplyStackChangeGameplayEffect(
  const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilitySpec& Spec)
{
	UGameplayEffect* GEStackChange = 
    	NewObject<UGameplayEffect>(GetTransientPackage(), FName(TEXT("StackChange")));
	
	GEStackChange->DurationPolicy = EGameplayEffectDurationType::Instant;

	const int32 Index = GEStackChange->Modifiers.Num();
	GEStackChange->Modifiers.SetNum(Index + 2);

	FGameplayModifierInfo& MaxStackInfo = GEStackChange->Modifiers[Index];
	MaxStackInfo.ModifierMagnitude = FScalableFloat(MaxStacks);
	MaxStackInfo.ModifierOp = EGameplayModOp::Override;

	FGameplayModifierInfo& CurrentStackInfo = GEStackChange->Modifiers[Index + 1];
	CurrentStackInfo.ModifierMagnitude = FScalableFloat(MaxStacks);
	CurrentStackInfo.ModifierOp = EGameplayModOp::Override;

	MaxStackInfo.Attribute = UAbilitiesStackSet::GetMaxAttributeByInputID(Spec.InputID);
	CurrentStackInfo.Attribute = UAbilitiesStackSet::GetCurrentAttributeByInputID(Spec.InputID);

	ASC->ApplyGameplayEffectToSelf(GEStackChange, 1.0f, ASC->MakeEffectContext());
}

Gameplay Stack Recharger

The GameplayStackRecharger (GSR) is a GameplayAbility that is responsible for applying a recharge GE. As a GameplayAbility, it offers the flexibility to create complex logic in order to apply the recharge effect. However, in most cases, skills only require the basic method of recharging, which is to begin recharging as soon as the current stack count is below the maximum. The image below demonstrates the default recharge ability.

Default Blueprint Example

The DefaultStackRecharge ability waits for the stack attribute to change and applies the Recharge Effect if the current stack is below the maximum.

Recharge Gameplay Effect

The RGE is an archetype GE that increases the stack attribute associated with the skill by one. It has both duration and period equal to the desired recharge duration. Additionally, the “Execute periodic effect on application” option is set to false. In other words, the period acts as a delay, just like a cooldown. By setting the GE duration to the same value as the period, it becomes easy to calculate how much time is left for the modifier to apply.

Info

An Archetype Gameplay Effect is a standard blueprint Gameplay Effect created through the Editor that is later modified with C++ in run-time. This is necessary as, in C++, only Instant GameplayEffects can be created at runtime.

// We modify the GE before applying it. 
void UGameplayStackRecharger::ApplyRechargeEffect()
{
	// ...
	RechargeGE->DurationMagnitude = FScalableFloat(StackRechargeDuration);
	RechargeGE->Period = StackRechargeDuration;
	RechargeGE->bExecutePeriodicEffectOnApplication = false;
	// ...
}

Damage

Gameplay Damage Container

Inspired by Epic’s Action RPG Sample FGameplayEffectContainer, Damage Containers are the primary way to do damage using Skills. It saves the effort of manually creating the required GameplayEffectExecutionCalculations and setting the base damage multiplier, while also creating an easy way to pass damage to be applied on projectiles.

Most important, however, is handling the Damage Pipeline and abstracting it from game designers. As explained in more detail in the next subsection, the Damage Pipeline needs a DamageCalculationEffect and a DamageApplicationEffect, which is obtained by either passing both classes as a parameter when creating a DamageContainerSpec or simply using the ones assigned to the Skill.

Damage Container BP Functions

Damage Pipeline

The Damage Pipeline is the sequence of steps necessary to inflict damage. The need for a more complex system, in contrast to how Unreal’s Lyra Sample Project handles damage, comes from the nature of the flexible modifiers in Risk of Rain.

While the BaseDamage attribute is associated with the character and only ever changes when leveling up, the TotalDamage attribute is the final value used to inflict damage and is altered by an arbitrary number of items and/or passive skills. For this reason, unlike Lyra, the modifiers are not hardcoded in the GameplayEffectExecutionCalculation and are instead added in response to the Event.BeforeDamage event, after the DamageCalculationEffect is applied.

This enables designers to modify the TotalDamage attribute before the final calculation, based on the target’s information, such as its position, health percentage, animation state, and so on. See Bandit’s Passive for a practical example. The following is a chart to illustrate how the Damage Pipeline works:

Damage Pipeline

In summary:

  1. The Damage Pipeline begins when a DamageContainer is applied.
  2. The DamageCalculationEffect is applied to the instigator. Its job is to calculate the TotalDamage based on the BaseDamage, compute whether the damage is a critical hit, and fire the BeforeDamage event.
  3. Passive skills and items react to the BeforeDamage event, modifying the TotalDamage.
  4. The DamageExecutionffect is applied to the target. Its job is to modify the target’s shield and health based on the instigator’s TotalDamage and the target’s armor. Also fires the Hit event.
  5. On-hit Passive skills and items react to the On-Hit event.

The Skills section contains lots of examples of how to use the Damage Container, while the Item proccing section demonstrates some on-hit reactions.

Items

Items are collectibles that grant special GameplayEffects to characters. In this project, items are DataAssets with a name, description, icon, and most importantly, a GameplayAbility. Creating new items is as easy as creating an Item Data Asset and filling in its fields.

Ukelele Data Asset

Inventory

For players and monsters to keep track of and receive the effect of items they must possess an InventoryComponent. The inventory system for this project is pretty standard. Every controller has an InventoryComponent that can be interacted with by an Interactable interface. A pickup object is a common example of this.

Imgur

Example of an Item Pickup.

Every item added to the inventory becomes an ItemEntry, which keeps a reference to the data asset, the GameplayAbilitySpec and the stack count. This is important, as we’ll see in the next subsection. Other systems, such as the UI, can subscribe to inventory events containing all the information available by ItemEntries.

Inventory UI

Inventory UI

Item Stacking

If a player acquires a copy of a previously owned item, that item count increases in their inventory. This effect is known as stacking, and most items have one or two variables that increase with stacking. So, instead of adding multiple ItemEntries of the same item, we only need to change the stack field.

Item Example

Item stacking example from RoR2 Wiki. Each stack increases the chance to apply.

Since increasing the item power by stack is essentially leveling it up, I decide to use the built-in ability level in GAS. By default, you can change the level of abilities by either assigning a new level to the GameplayAbilitySpec or by removing the ability and re-granting it at the desired level. To streamline this process, the method SetAbilityLevel is available on the custom ASC class.

void UASAbilitySystemComponent::SetAbilityLevel(FGameplayAbilitySpec& Spec, int32 NewLevel)
{
	if (!HasAuthorityOrPredictionKey(&Spec.ActivationInfo))
	{
		return;
	}
	
	Spec.Level = NewLevel;
	// Dirty specs will be replicated to clients
	MarkAbilitySpecDirty(Spec);
	
	// If we're dealing with a passive ability, cancel it and activate it again with the new level
	const UASGameplayAbility* AbilityDef = Cast<UASGameplayAbility>(Spec.Ability);
	if (AbilityDef && AbilityDef->ActivateAbilityOnGranted)
	{
		CancelAbility(Spec.Ability);
		TryActivateAbility(Spec.Handle, true);
	}
}

Info

A Passive Ability is automatically activated when granted and runs continuously. Read more on GASDoc Passive Abilities.

By retrieving the ability level on an ability blueprint, designers can use it for a wide range of possibilities, like increasing the radius of an elemental explosion, the number of enemies affected by a debuff, or the damage dealt.

Stack Scaling

AtG Missile Mk. 1 total damage scaling.

Proccing

Many items in Risk of Rain have a “Chance to Trigger” that normally occurs whenever a player hits or kills an enemy. This can be easily modeled by reacting to an event and checking if the random probability is successful.

However, as a balance mechanism, each skill has a Proc Coefficient. The Proc coefficient scales an item’s trigger chance. So, for a fast-hitting skill, the proc coefficient will usually be low. As an example, if an item with a 20% trigger chance is activated by a skill with a proc coefficient of 0.5, the actual trigger chance is only 10%.

Fields

Example of an item that triggers on Event.HitEnemy with a 20% trigger chance and a proc coefficient of 1.0.

Chains and Mask

Proc coefficients are also present on items. The reason for this is that items can also proc other items. When one or more items are triggered by one item, this is known as proc chain.

Note that an item can proc other items, which in turn can proc other items. This can lead to an extensively large chain. To combat that, each item can only be activated once by chain. This is known as proc mask.

Big Chain

Proc chain with mask example.

Imgur

Illustration of multiple proc chains with mask.

By using a DamageExecutionffect, items can do damage and trigger events such as Event.HitEnemy and Event.Kill. This means that items can activate other items, making proc chains work out of the box. However, to factor in the proc coefficients, we need a way to pass it as event data. And if we want to mask proc chains, we need to remember all the previous triggered items.

A solution to these problems is to create a custom GameplayEffectContext containing a list of GameplayAbilities. Here is an excellent article on creating and using your own GameplayEffectContext by KaosSpectrum.

/* ASGameplayEffectTypes.h */

// List of all previous triggered Abilities CDOs.
TArray<TWeakObjectPtr<UGameplayAbility>> AbilityCDOChain;

// Some of the relevant functions for proc chain and mask
TArray<UGameplayAbility*> GetAbilityChain() const;

virtual void AddAbilityToChain(UGameplayAbility* Ability);
virtual void AppendAbilityChain(const TArray<UGameplayAbility*> Abilities);

The GameplayAbility ShouldAbilityRespondToEvent function can now be overridden to check for both proc chance and proc mask with the new ability chain. The proc chance is a multiplication of the trigger chance with the last ability proc coefficient. The proc mask check fails if the current ability (item) is already on the ability chain.

Should Ability Respond to Event

Every item that uses proc chains and masks must be responsible to change its own context. Below is the SetContextAbilityChain function. It modifies the current GameplayEffectContext by adding the previous chain and the current ability to the ability chain list. The AppendPreviousChain and AddAbilityToChain are custom blueprint functions that utilize our custom GameplayEffectContext.

Set Chain

Item Abilities Breakdown

This section presents two distinct items, a passive shield and a proc damage missile, to show the general idea behind their abilities.

AtG Missile Mk. 1

“10% chance to fire a missile that deals 300% (+300% per stack) TOTAL damage.”

Demo

The AtG Missile Mk. 1 is an On-hit item that activates on an Event.HitEnemy trigger. The missile deals damage scaled by the total damage dealt, so it does massive damage if part of a big proc chain. The GE, which is a generic DamageExecutionffect with a Set by Caller magnitude, is passed to the missile projectile and applied when the actor hits an enemy.

Blueprint

Personal Shield Generator

“Gain a shield equal to 8% (+8% per stack) of your maximum health. Recharges outside of danger.”

Demo

The Personal Shield Generator is a passive item that grants the character two GameplayEffects. An instant one that overrides the Shield and MaxShield attributes, and an infinite one that slowly increases the Shield attribute over time.

Blueprint

The shield should only recharge when out of danger. This can be achieved by granting the character a “Recently Damaged” tag when hit, and adding an ongoing tag requirement of not having that tag to the shield regen GE.

Imgur

Melee System

There are several melee abilities in Risk of Rain. Its melee system is not designed to be precise animation-wide, but reliable and lenient. For this reason, the melee system presented here (inspired by Epic´s A Guided Tour of Gameplay Abilities) is designed around pre-defined hitboxes that are activated during an animation window.

In order to perform melee attacks, every character needs to implement the IMeleeAbilityInterface and possess a Melee Component. The Melee Component is responsible for knowing which hitboxes are active and performing the collision checks on them. If a check is successful, it sends a GameplayEvent so that Melee Skills can react.

void UMeleeComponent::PerformMelee()
{
	for (FMeleeCollisionData* HitBox : HitBoxes)
	{
		TArray<FHitResult> HitResults;
		SweepMultiMelee(HitBox, HitResults);
		
		for (const FHitResult& HitResult : HitResults)
		{
			AActor* ActorHit = HitResult.GetActor();

			// Use HitActors as a TSet for O(log N) retrieval
			if (!HitActors.Contains(ActorHit))
			{
				HitActors.Add(ActorHit);
				
				SendMeleeHitEvent(HitResult, ActorHit);
			}
		}
	}
}

Melee Component

The Melee Component needs a DataTable containing the available hitboxes and a Hit Tag to send the hit GameplayEvent.

The hitboxes are stored in a DataTable, with each entry containing the Row Name, the Location Offset from the character’s position, and the hitbox sphere Radius.

Hitbox DataTable

As all melee skills derive from GS_MeleeBase, designers only need to specify the animation montage and the damage dealt by the skill. The idea is to play the animation montage and wait for melee hit events, extract the Hit information and apply a Damage Container. See the Serrated Dagger skill for an example.

Base Melee GA

GS_MeleeBase blueprint.

In the animation montage view, designers can represent the hitboxes' active frames using the NotifyStateHitboxWindow. Each NotifyStateHitboxWindow has a list of names as a parameter, so multiple hitboxes can be active at a time.

Melee Animation

Animation

Active hitboxes during Bandit’s Serrated Dagger melee skill.

The NotifyStateHitboxWindow is a NotifyState that calls the melee interface on the owner with the hitbox names. It registers when it begins and unregisters when it ends.

Hitbox Start

Enemies Hit

Skills Breakdown

This section presents a quick breakdown of all skills of the Bandit and Engineer heroes.

Bandit

Passive - Backstab

“All attacks from behind are Critical Strikes.”

The backstab skill is a great example of using the flexibility of the Damage Pipeline. By reacting to the BeforeDamage event, designers can freely modify the EffectContext and character attributes before the damage is actually applied. If just a GameplayEffectExecution was used instead, this behavior would need to be hardcoded.

Imgur

Primary - Burst

“Fire a burst for 5x100% damage. Can hold up to 4 shells.”

Demo

The Burst skill is a simple hitscan ability that fires multiple rounds. What set it apart is its unique Reload mechanic.

Imgur

Instead of using the default GSR, it uses a custom blueprint that listens for stack changes and applies a GE_BurstReload when the stack decreases. However, after firing, there is a delay to start reloading again that depends on the number of bullets left.

Delay Reload Blueprint

The Bandit can’t reload while using other abilities (with the Smoke Bomb being an exception). This behavior is achieved by using Ongoing Tag Requirements.

Fire Delay Tags

Shoot Blueprint

Secondary - Serrated Dagger

“Slash for 360% damage. Critical Strikes also cause hemorrhaging”

Imgur

Hemorrhage: Deal 2000% base damage over 15s. Can stack.”

The Serrated Dagger is a melee skill that makes use of the Melee System. To create it, one has to create a new blueprint with the GS_MeleeBase as a parent and override the parameters appropriately:

Parameters

This is enough for most melee abilities, however, the skill also applies Hemorrhage if the hit is critical. To do this, we can listen to the HitEnemy event before calling the base ActivateAbility, and apply the effect by checking the Custom Context.

Blueprint

The hemorrhage GE uses a simpler version of the DamageExecutionffect that doesn’t consider the target’s armor. It does 2000% base damage over 15s.

Hemorrhage

Utility - Smoke Bomb

“Deal 200% damage, become invisible, then deal 200% damage again.”

Area Damage

The Smoke Bomb is a skill that does area damage, turns the character invisible and increases its speed momentarily. The area damage works just like the previous skills, but a sphere trace is used instead and multiple hits are passed into the Damage Container.

Blueprint

There are three steps involved to become invisible. First, a GE that grants the Invisible tag is applied. The character then unregisters itself from the AIPerceptionSystem. Finally, the skill spawns an invisible dummy character that acts as a decoy, following the Bandit every 2s.

Invisibility

Invisibility: Prevents enemies from targeting you. Pings your location every 2s.”

Special - Lights Out

“Fire a revolver shot for 600% damage. Kills reset all your cooldowns.”

Imgur

The Lights Out is a single-bullet hitscan ability with a unique mechanic. If it kills an enemy on hit, all Bandit’s skills have their cooldown (stacks) reset. There are a couple of ways of achieving this mechanic. An easy one is to listen for the Kill event just before applying the Damage Container, then apply a reset GE.

Blueprint

Engineer

Primary - Bouncing Grenades

“Charge up to 8 grenades that deal 100% damage each.”

Demo

The Bouncing Grenades is a projectile-based skill with a small area of damage. The skill blueprint largely consists of the spawning logic, similar to the AtG Missile Mk 1. However, as a skill, the Damage Container needs to be passed to the projectile. Below is an example of applying an external Damage Container.

Imgur

Secondary - Pressure Mines

“Place a two-stage mine that deals 300% damage, or 900% damage if fully armed. Can place up to 4.”

Demo

The Pressure Mines is a projectile-based skill similar to Bouncing Grenades. The big difference is in the projectile logic and, as we’ll discuss, the number of projectiles that can be active at a time.

To limit the number of active mines we first need to store all the mines the skill spawned. For every last mine that exceeds the limit, we call a generic Deactivate function from a GameplayAbilitySpawnable interface and remove it from the data structure. Essentially a LIFO (Last In First Out).

Limit Blueprint

This is not sufficient, however, as the mines can explode before having a chance to leave the data structure. We can safely remove it by listening to OnDestroy delegate. All of this can be abstracted into a base blueprint GS_BaseProjectileLimit so designers can easily create skills with this mechanic.

Remove on Destroy

Utility - Bubble Shield

“Place an impenetrable shield that blocks all incoming damage.”

The Bubble Shield is a simple spawn skill. It spawns an actor with a sphere mesh that uses complex collision, thus allowing for the collision to be one-sided.

Demo

Special - TR12 Gauss Auto-Turret

“Place a turret that inherits all your items. Fires a cannon for 100% damage. Can place up to 2.”

Demo

The TR12 Gauss Auto-Turret skill spawns an AI character that inherits the owner’s inventory. It uses the WaitTargetData AbilityTask with a custom TargetActor to determine whether the placement is valid or not.

The custom TargetActor is a GATargetActor_Placement C++ class, which does a couple of traces based on parameters such as the collision height, radius, distance from the actor and max slope angle. By using the confirmation type User Confirmed, we can run the collision check on Tick and use the result on the IsConfirmTargetingAllowed method. If it is successful, the TargetActor waits for the confirmation input from the user.

Imgur

Below is an example of targeting. The user can only confirm the action if the turret is green.

Placement

The turret actor is a character just like the Engineer, with its own set of attributes, skill set, abilities and tags. As the turret has an inventory too, the skill passes the owner inventory as a spawn parameter.

Turret

AI

Designers can use the flexibility of GameplayAbilities together with Behavior Trees to create complex tasks for AI characters. GameplayTags are particularly useful for triggering context-specific abilities. One straightforward example is to activate skills based on their slot (Primary, Secondary, etc.).

BT

This BehaviorTreeTask retrieves the ASC from the AI character and tries to activate an ability using the skill slot selected by the designer:

Skill by Slot Blueprint

BTT_UseSkillByType blueprint.

It’s also easy to activate skills based on other properties such as distance, like triggering a melee skill if close or a random ranged one if far away. If the AI gets a debuff, using a skill that clears it is possible by using GameplayTags.

While some skills are partially character specific, many are not. This means that AI can use skills created for player controlled characters. AI characters can even use items without any necessary adjustments. Here’s an example of an AI character using Engineer’s Bouncing Grenades, as well as the Atg Missile Mk 1 item.

Grenade with Item


This is it! If you have questions, need help figuring something out, or have a suggestion, please feel free to comment below 😃. Unreal’s official Discord Server, Unreal Slackers, has an amazing channel focused on GAS. Check it out!


M ↓   Markdown Help?
T
Tom Looman
2 points
2 years ago

I love this! I've been wanting to upgrade my public source code to be like RoR2. Concrete examples like this can really help people finally understand GAS.

permalink
M
Mike
2 points
2 years ago

Terrific article! The information is to the point and laid out perfectly! Do you have any plans to release a public project for this like Tranek has as resource?

permalink
Vitor Cantão
3 points
2 years ago

Thanks Mike! Appreciate it!

I didn't plan to release the project initially, but after receiving so many requests, I'm starting to consider it. If I find the time, I'll make it happen!

permalink
N
Nick
2 points
2 years ago

Great write up, seconded on the project files! I don’t have any plans to make a game like risk of rain but this system on top of GAS seems extremely powerful and I’d love to see the full implementation.

permalink
E
Eliza
2 points
2 years ago

Thank you for this absolutely amazing article!!

Also chiming in to request project files as it would be a great learning resource ^^

permalink
Matthew Windley
2 points
2 years ago

This is an probably the best use case for gas, with many advanced examples of how to utilize it well. Every other project out there only touches the surface with the basic fundamentals and I would love to see this released to examine whats going on under the hood at a more advanced level. I'd be willing to donate via patreon, UE Marketplace or privately to access the project files. I just want to learn more, and reward you for your labor.

permalink
B
Bernardo Reis
1 point
21 months ago

Congratulations on the excellent article! I found it while I was looking for ways to implement Abilities with more than 1 charges similar to what you've done here. One question for you: The approach I thought of taking is very similar to what you have and I thought of using effects to add/consume charges from a skill. The challenge I'm facing though is that I couldn't find a way to create an effect in C++ and dynamically set the attribute it impacts. Did you find a way to do that? Or do you have a different recharger effect for each of your attributes?

permalink
Vitor Cantão
0 points
21 months ago

Thank you! Yes, I did look for something like you described. What I found is what you can only create Instant GE's at runtime in C++, but we want a perioric one, right? I found a way around this with what you can call an Archetype Gameplay Effect. This is essentially a blueprint GE created in the Editor that serve only to be modified in C++ before the application. That way you can dynamically set attributes and other fields. Hopes this helps!

permalink
B
Bernardo Reis
0 points
21 months ago

Hi Vitor, thank you for the response! So just to check if I got your approach correctly, you create a regular GE asset using the editor, and then you load it in the code using something like

static ConstructorHelpers::FObjectFinder YourBPOb(TEXT(“Blueprint’/Game/Characters/YourBP.YourBP’”));

and then you instantiate it and modify it in C++? So far what I tried was creating a UGameplayEffect class in C++, then create a GameplayEffectSpec from it and modify it, something like

FGameplayEffectSpecHandle SpecHandle = MakeOutgoingGameplayEffectSpec(UDecreaseSkillChargeEffect::StaticClass(), GetAbilityLevel()); SpecHandle.Data.Get()->ModifiedAttributes.Add(MyAttribute);

But it doesn't seem to work

permalink
Vitor Cantão
0 points
21 months ago

No problem! Yes exactly, or you could grab it by using TSublassOf. With that you can modify the GameplayEffectSpec instance to your liking.

Let me know if this works for you

permalink
YING-GH
0 points
8 months ago

Is it possible to modify the Attribute of the AttributeSet targeted by Effect in this way?

permalink
M
Matt
1 point
19 months ago

Hey! First and foremost, awesome article! I have been trying to create something similar myself. I have a question though, your ApplyStackChangeGameplayEffect function calls a class UAbilitiesStackSet which I can't seem to figure out what it should be. Can you help me out to understand where do you create this and what it should be?

permalink
Vitor Cantão
0 points
19 months ago

Hey Matt! The UAbilitiesStackSet is an UAttributeSetcontaining Current and Max attributes for each skill type (Primary, Secondary, Utility and Special). Hope it's clear now!

permalink
M
Matt
0 points
19 months ago

Thanks for the reply! Oh that makes sense, I was thinking it was an AbilitySet. Do you perhaps have a project uploaded somewhere where we can take a look at it?

permalink
H
ho efron
0 points
9 months ago

Hello ?Some Picture Lose?

permalink
?
Anonymous
0 points
2 years ago

I have so many questions - as long as this blog post is, it handwaves so much implementation that it's hard to use as a learning resource and instead acts as a bit of a teaser that makes me REALLY wish there was more to dig into. The one way sphere for instance - I take it that's just a standard mesh with complex collision and it just blocks certain channels - not pawns for instance. The events (event.onkill, event.ondamage, ect.), the proccing, the damage system. It's all so close to being immensely helpful.

I have this system for non-GAS that uses a monolithic class to handle interactions between all of my systems by just brute force calling events on everything when something relevant happens - but I'm trying to use GAS because of the multiplayer benefits. It feels.. so close to being usable for me. I don't know if I could use my monolithic system but inside of GAS in the same way your event.onkill system works because I don't know how that system really operates. I'm looking at the blog post you linked to and it seems promising, but I wanted to share my experience so far just in case it's helpful? But yeah the other blog looks like it'll fill in a lot of those gaps for what it's worth.

But yeah if you don't want to make the source available I understand but just a bit more C++ or specificity would go a long way to help explain what's going on beyond the high-mid level overview. I get to see how you're using the building blocks most of the time but wish I could see how the blocks were built so to speak.

Nonetheless, huge thanks for the post - it's excellent and incredibly presented and I hope that my desire for more doesn't take away from the fact that what's here is fantastic.

permalink
Vitor Cantão
0 points
2 years ago

Hi! Thank you very much for your feedback. I appreciate you sharing your thoughts!

I agree with you, and I would love to share more. My first idea was to show what I've been working on as a portfolio project, but as resources on GAS are a little scarce, I thought of sharing even more. However, I believe this post also requires some GAS knowledge to be more useful (see GAS Bible. GAS is not hard to understand, but you have to delve into many concepts, which can take a while.

The events, for example, are GameplayEvents that actors that implement the IAbilitySystemInterface can interact with by specifying a GameplayTag.

Still, if you want to know more about something specific, just let me know! Thank you again for taking your time to write this comment and for the compliment!

permalink
?
Anonymous
0 points
2 years ago

This'll be not the best as I'm on my phone so scrolling up and down is not very feasible but I'm mostly struggling with the base implementations (not how they're used once they exist, just the how they exist) of:

Items and Item Inventories The Damage Pipeline Damage Containers Damage Effect Containers Proccing

I understand at a higher level what all of these do but am struggling to put it all together at a low level. The Damage Pipeline, for instance, is what I'd say I'm struggling with the most. Before your post I thought that the Damage Execution and Attribute Post and Pre modified events were how I could do something like what you've done but I don't even know about Gameplay Effect Context / Containers.

I understood the vast majority of the rest of it, save for not knowing if Event.OnHit and the like are baked into the damage systems you're overriding / deriving from or if they're custom.. and how exactly those are called such that every item receives the callback.

But yeah, I've been researching GAS for approaching a week now and have just shy of reached the point where I feel confident in my understanding of the system enough to really get started. I'm hoping that understanding how to build your system which is so flexible and modular while still being.. what's the word... basically, items and effects and characters are interacting with one another in ways that are more conducive to what I'm making which, prior to GAS, worked under the hood like a Slay The Spire style game - events would trigger effects which would trigger events. I had a brute force option and a "better" system which used delegates.. and a "best" system which was a big undertaking and ultimately led me to this.

But yeah, think Dead Cells where an ability might make a frozen target leak oil and then if the target dies while frozen they explode fire and set the oil on fire. And you might have an ability that says "every time you kill a frozen enemy gain 1 maximum health". And maybe an enemy has an ability which says "No nearby allies can become frozen or chilled" because they're outputting a heat aura and maybe another enemy could have an ability which says "If I thaw from being frozen I become enraged".

They're not immensely complicated but once you start stacking them it gets to be that way. Ability Slots hold items, items have passive and active effects, enemies respond to things happening to them or near them.. it's certainly a lot. But I'm hoping that by starting with your system I can convert mine into GAS.

permalink
?
Anonymous
0 points
2 years ago

I don't have an edit button but just wanted to make it clear that I understand how to use GA and GE and so on to implement what I've described so far - for the heat aura you could apply an Infinite Duration GE that applies the Tag Immune.Frozen and when the character leaves the aura or the Aura / character providing it dies we -1 the Tag from anyone with it in the radius of the aura. There might be a slightly better way to do that but outside of a duration reapplication it's probably alright. Not ideal, but fine enough.

An Onkill + HasTag frozen could then apply fire explosion GA with an instant GC that applies the VFX and SFX.. I think?

But once we introduce proccing and chaining and preventing infinites (or even just repeat chains we don't want) and items like RoR 2's that you can just stack forever.. I'm really just trying to sort out the damage pipeline above all else, except the delegates. The how I set that up to call on.kill and for it to send the actor and so on. I'm pretty sure I'm going to feel dumb when it's just standard stuff but with GAS I keep learning that there's usually a GAS specific way to do everything so I'm trying not to make any assumptions which.. kinda also makes me seem really naive 😂

permalink
?
Anonymous
0 points
2 years ago

Alright, at my computer going to see what I can add for specifics: Ability Stacking (GSR) seems weirdly explained. I've always considered it a Charge or Ammo system and as a result I don't really understand why the solution isn't to just have a GA that, similar to Regenerating mana, stamina, or health, just replenishes an amount to to max and then adds 1 to a Gameplay Tag or Attribute via a GE which also resets the Attribute we're charging to 100%? Or waiting for a delay? The system just feels a bit wrong. That said, I don't understand GAS well enough to say that it is - but it is something that confuses me. Like even if the goal was to just say "every 5 seconds give me another stack of dynamite, and if I have Dynamite Tags equal to MaxDynamite then wait until the count changes and check if I have less than max and if I do start counting again" isn't that.. supported in a less complicated way? If not that's completely okay - I'm just trying to learn. I do understand that for displaying a number rather than a progress bar you'd want to know how much longer it'll be before you get another stack of, say, dynamite. So duration based (and being able to see how much time is left) is better than calculating it constantly using a percentage and total cooldown but then there's also the question of what happens when you suddenly get 75% cooldown reduction.

The Damage Pipeline is more complicated so I'll try to comment this as I go as summarizing is more difficult here. Damage Containers - "It saves the effort of manually creating the required GameplayEffectExecutionCalculations" confuses me - I still need to brush up on these as I'm still learning so that's probably just a "read the docs" thing right now. I've been looking at the GameplayEffectExecutionCalculations for GasShooter and ARPG example and the 3.5 hour video (still have an hour left) example so I haven't encountered the GameplayEffectContainers yet. The second paragraph explains well how you get the DamageCalculationEffect and DamageApplicationEffect and while I'm confused as to what those are I'm once again pretty sure it's a "read the docs" as they're standard.

Total damage being a meta attribute makes sense to me but it's still not clear, even in the docs, how you actually use this. I haven't seen anyone ever use a meta attribute in fact - they just use PostAttributeChange, PreAttributeChange, and the DamageExecutionCalculation.

Then we introduce Event.BeforeDamage and Event.Hit sort of out of nowhere, as is the idea of adding modifiers in as a response (how exactly?) by the items or characters via Abilities / GEs I take it? But.. it's an event? Probably a Delegate? I'm very confused about this unfortunately.

Damage containers are applied which was made clear earlier. Apply External Damage Container. It takes in targets which are given via hit events like Overlap or a line trace or similar. I'm not entirely sure what Applying does but I imagine it's basically "start hurting things"

Still no idea about the DamageCalculationEffect but at least now I know that its' role is to calculate TotalDamage based on BaseDamage (and I presume Damage attempting to be dealt) - does it do anything beyond more or less BaseDamage * Damage = TotalDamage?

Yeah I see the Trigger Abilities which comes with Target Data attached and is caused by Event.BeforeDamage but.. I'll guess? TotalDamageCalculationEffect returns TotalDamage which then maybe is sort of affected by Trigger Abilities (this is very vague to me right now) and then we use the final total damage to apply damage? Which counts as a hit so we call Event.Hit and trigger On-Hit abilities like the Uke which does a chain lightning effect to nearby enemies - this one I do understand. Albeit I would prefer that the abilities had more context about why they were being triggered?

And then damage is dealt? How do the On-Hit abilities work into this? How does Total Damage Application Effect Execution?

Back to the list.

The DamageCalculationEffect is applied to the instigator - meaning the character that fired the ability to attack. It calculates TotalDamage based on Damage and Base Damage (right?) and determines whether the ability Crits - then fires the BeforeDamage event.

Okay so now I understand how the event is called. Awesome - it's just called here.

Passive skills and items (meaning Passive Gameplay Abilities) react to the BeforeDamage event (do we just do a ForEach and call the event on them? How does that work here specifically?) and modify the TotalDamage - okay perfect, that answers the question about how the damage was being modified earlier - TotalDamage must be passed in with the Trigger Data somehow? Or they just.. grab it off of the ASC? Confused here for sure.

The DamageExecutionEffect is applied to the target - meaning thing(s) getting hit. It modifies the target's shield and health based on the instigator's (aka Source Character's) TotalDamage and the target's armor. Also fires the Hit event. That last sentence confuses me. It just has an event called "hit" ? what is it firing it for? Who receives it? How?

Then On-Hit passive skills and items react to the On-Hit event. I take it it's the Instigator's items in this case, but what if the enemy needed an "On I've been hit" event to fire here?

I'll continue in another post but don't want this to go on for too long in a single reply.

permalink
?
Anonymous
0 points
2 years ago

Items are DataAssets that apply GEs to characters and have a Name, Description, Icon, GA to grant, and a max stack amount. I take it that the GA applies the GE? It's a little vague here.

To keep track of and receive the effects of items (Gameplay Effects? Gameplay Abilities?) the Controller (Ai or Player) needs an InventoryComponent which has an interface - I presume it just adds and removes items? Queries how many it has? Why only a Controller, could a Character or Actor have one?

Every item added to the inventory becomes an ItemEntry - not terribly clear but I presume this is an array of structs held in the InventoryComponent given that it references the Data Asset (Class?), the GA Spec (Handle? - I take it this is to remove the GA later or?), and the stack count (just an int I take it?). UI and other systems can subscribe to Inventory Events (using Binding / Delegates I take it?) - Inventory Events include all information available by ItemEntries (aka the GA Spec, Data Asset Reference, and Stack Count)

We use the stack count and some designer driven math to increase the power of an item as the stack count increases. This makes sense and I understand it all easily.

Proc Coefficient is easily understood. The image isn't difficult to understand but Trigger chance, Ability Triggers, and so on aren't labels I'm aware of so I take it they're custom and the logic is as well. Nonetheless, I take it that we wait for a Gameplay Event (which based on my notes can be either a GA or a GE?) is applied with the Event.HitEnemy tag before checking if it succeeds or fails based on more or less Trigger Chance * Proc Coefficient for the odds and if it succeeds we apply the GE? And all of this happens in a GA?

I'm not sure why the proc mask image has Sticky Bomb going into 2 separate AtG Missile Mk. 1's as an outcome but neither could go into a Ukelele.

By using a DamageExecutionEffect, items can do damage and trigger events such as Event.HitEnemy and Event.Kill - makes sense, because if an item does damage it does so by hitting an enemy (with missiles, an electric zap, whatever) and can kill an enemy. As a result they can cause other items to activate.

However, to factor in the proc coeff. we need to pass it in as Event Data. And if we want to mask out proc chains, we need to remember all of the previously triggered items. For the implementation at hand, this makes sense enough to me.

To solve this we create a custom GameplayEffectContext - which it's now been several hours as I had to leave and come back but I think I figured out what that was in my last post? Currently I have no idea but I think I figured it out? Maybe? Anyway, we create a GameplayEffectContext and it contains a list of GA and a link to an article where I can solve my own problem .-. whoops. To be fair I read this much earlier and oh boy was I way ahead of myself on that one - didn't understand GAS well enough so I carried on learning about GAS.

Anyway so we uh.. make some context for the GameplayEffect and the context is that we've already activated a bunch of Abilities and here they are, don't do it again. We do this by using the GameplayAbility function ShouldAbilityRespondToEvent - which we override - to check for proc chance and proc mask using the Ability Chain stored inside of the Context Handle of the GameplayEventData which I anticipate to be the GameplayEffectContext just.. slightly named differently. I also presume the Macro is self evident and so long as every ability in the proc chain ISN'T the.. actually I'm confused, I'll keep reading but I understand the proc mask being a list that this Ability shouldn't be on yet (hence IsUnique) but why is the Proc Chance Check an array? That seems odd.

Every item that uses Proc Chains and Masks is responsible for.. read and wrote out this whole paragraph, checks out, can see the C++ code, definitely understand.

The On-Hit missiles are easy to understand but there isn't much here. The screenshot could use a little more visible space for better understanding for me but I get it. I like using BPUE (er, Pastebin but for Blueprints is how they describe it so google that?) because screenshots often are too big for this to work well otherwise.

Personal Shield Generator only confuses me as to why the nodes used are different and why only one handle is saved but that's about it.

Melee system I just watched the video on that so it makes perfect sense to me.

Looking at the screenshot shows the APplyDamageContainer node but that's about it for anything relevant to this unique project.

The backstab is interesting in that it shows you can set a variable of an EffectContext using the EventData passed in via Event ActivateAbilityFromEvent - which is labeled Event.BeforeDamage which means I do get to see what that looks like. Very nice :D

Burst shows how to check for an Attribute which may or may not exist so that's awesome, I don't fully understand AssignTagSetByCallerMagnitude but I do completely see what's happening - if the primary ability (ammo remaining?) is <= 0 then we reload really quickly, else we reload a bit slower? And that's.. Data.Length? Data Tag? And we apply this as a GameplayEffectSpec? I'm.. definitely confused on some stuff here.

Ongoing Tag Requirements I understand for sure. Shooting makes sense to me.

Hemorrhage we setup a Wait GE Ability Task and when we hit something we check if it was a Crit and if so apply GE Hem. which makes sense to me - didn't realize you could reliably do this but makes sense and I think it's cool (my mind is always looking for weird bugs like what if you timed a dynamite toss such that it explodes and triggers this instead? What if it crit idk?)

The smoke bomb makes sense.

Other than the thing I mentioned earlier which seems like it's just not a problem, Light's Out makes sense to me.

Grenades make sense to me. Mines make sense. Their Bind Event ot OnDestroyed and OnSpawnedDestroyed (though the name is weird) makes sense to me.. sort of. The GameplayAbilitySpawnable Interface has a Deactivate Function - I presume this exists for all spawnables, and Mines are a spawnable. So that makes sense. Is On Destroyed the built in Unreal Engine function which tells an Actor they've been Destroy()'d ? If so, I understand now.

Turret makes enough sense to me. Don't really need it explained to me how to duplicaet an array of structs in an InventoryComponent thankfully.

I haven't yet watched the YouTube video about Behavior Trees and GAS yet so I don't want to ask questions I might know the answer to after that.

And finally, as a bonus question:

I'd like a character who can possess an enemy character and an enemy which can possess the player character - at the moment it seems like the best way to do this is by storing the ASC on the Characters rather than the Player State so that way Controllers can just Pawn hop? And for the enemy controlling the player I could do some stuff to make the camera controllable by the player controller even if the player character is being controlled by an Ai Controller. Is there anything I'd need to do to make this work?

permalink
?
Anonymous
0 points
2 years ago

You could sell the project on Patreon or you could create a course. I loved the Video of A Gameplay Framework based on Risk of Rain 2 - Unreal GAS

permalink
O
Oliver
0 points
2 years ago

You could sell the project on Patreon or you could create a course. I loved the Video of A Gameplay Framework based on Risk of Rain 2 - Unreal GAS https://www.youtube.com/watch?v=wphkYjRpIq8&ab_channel=PurpleSkyD

permalink
A
Anon
0 points
18 months ago

Hello, I'm curious where/when you clear/reset the TotalDamage attribute?

permalink
?
Anonymous
0 points
18 months ago

I'm referencing how you use the TotalDamage attribute between the two GameplayEffects in the DamagePipeline. But how do you clear the attribute so it doesn't just keep adding every time you use an ability? It seems like you would have to know when all abilities have finished running and then wait until it's run the second effect in the damage pipeline. Then clear the TotalDamage attribute, but that doesn't make sense to me.

Thank you!

permalink
Vitor Cantão
0 points
18 months ago

Hello! As soon as an damage ability starts it sets the TotalDamage attribute to be equal the BaseDamage attribute. This way the attribute won't keep getting incremented.

Hope this helps!

permalink
?
Anonymous
0 points
18 months ago

It does a lot, thank you. I was scratching my head with that one. I assume you did this through another GameplayEffect?

permalink
Vitor Cantão
0 points
18 months ago

Awesome!

Not really, this is done inside the DamageCalculation (which is a GameplayEffectExecutionCalculation class)

permalink
?
Anonymous
0 points
18 months ago

Ah, I was confused if you were even using a UGameplayEffectExecutionCalculation class at all. But I think I understand, you're basically using the calculation class for just the basic calculations that aren't modular and are okay to be hard-coded. Things that every ability's damage will effect?

permalink
Vitor Cantão
0 points
18 months ago

Yes, exactly!

permalink
U
UncleTron
0 points
18 months ago

Can you show how your Skill class is defined? What is the difference between a GameplayAbility and a GameplaySkill?

permalink
J
Jackson from Slightly Esoteric Games
0 points
13 months ago

This is EXCELLENT

permalink
?
Anonymous
0 points
17 months ago

Hello, I'm confused about this part "However, after firing, there is a delay to start reloading again that depends on the number of bullets left.".

How are you using that GameplayEffect to introduce a delay? And also how are you triggering the stack recharger after applying that GE and waiting some delay? Thank you!

permalink
?
Anonymous
0 points
16 months ago

Great article ! AFAIK net serializers will change with Iris so maybe this will be irrelevant in the future, but in the context of a multiplayer game, do you think your custom GameplayEffectContext would create bandwidth issues (passing a gameplay abilities Tarray instead of the I think default uint8)?

permalink
N
Nic
0 points
14 months ago

Hi, Love your article! I was wondering how you did you damage numbers since they are really crisp! I myself use widgets and It doesn't look as good. Do you use particles?

permalink
M
Minh
0 points
13 months ago

Hi, how do you set is crit data into FGameplayEffectContext in bandit backstab ability?

permalink
something more-something
0 points
13 months ago

Hi! by any chance do you have the project for download? I want to give a more in deep check to the damage pipeline if possible.

Thanks in advance!

permalink
Alexander M. Owens-Richards (AMOR)
0 points
12 months ago

Hi Vitor Cantão,

First of all, fantastic job on your demonstration of the Gameplay Ability System (GAS) based on Risk of Rain 2! The clarity and depth of your presentation were truly impressive.

I am particularly interested in making a more melee-focused game. Could you provide some guidance or suggestions on how to adapt your framework to emphasize melee combat mechanics? Any tips on structuring abilities or animations for melee attacks within the GAS framework would be greatly appreciated.

Thanks again for your excellent work and support!

Best regards, Alexander M. Owens-Richards

permalink
J
Julius
0 points
8 months ago

Super cool! I am wondering currently how do you trigger the Recharge Gameplay Effect?

permalink