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.
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.
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 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
.
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.
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 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:
In summary:
- The Damage Pipeline begins when a DamageContainer is applied.
- The
DamageCalculationEffect
is applied to the instigator. Its job is to calculate theTotalDamage
based on theBaseDamage
, compute whether the damage is a critical hit, and fire the BeforeDamage event. - Passive skills and items react to the
BeforeDamage
event, modifying theTotalDamage
. - 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. - 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.
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.
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
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 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.
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%.
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.
Proc chain with mask example.
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.
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
.
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.”
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.
Personal Shield Generator
“Gain a shield equal to 8% (+8% per stack) of your maximum health. Recharges outside of danger.”
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.
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
.
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);
}
}
}
}
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.
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.
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.
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.
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.
Primary - Burst
“Fire a burst for 5x100% damage. Can hold up to 4 shells.”
The Burst skill is a simple hitscan ability that fires multiple rounds. What set it apart is its unique Reload mechanic.
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.
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.
Secondary - Serrated Dagger
“Slash for 360% damage. Critical Strikes also cause hemorrhaging”
“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:
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.
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.
Utility - Smoke Bomb
“Deal 200% damage, become invisible, then deal 200% damage again.”
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.
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: 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.”
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
.
Engineer
Primary - Bouncing Grenades
“Charge up to 8 grenades that deal 100% damage each.”
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.
Secondary - Pressure Mines
“Place a two-stage mine that deals 300% damage, or 900% damage if fully armed. Can place up to 4.”
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).
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.
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.
Special - TR12 Gauss Auto-Turret
“Place a turret that inherits all your items. Fires a cannon for 100% damage. Can place up to 2.”
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.
Below is an example of targeting. The user can only confirm the action if the turret is green.
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.
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.).
This BehaviorTreeTask
retrieves the ASC
from the AI character and tries to activate an ability using the skill slot selected by the designer:
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.
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!
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.
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?
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!
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.
Thank you for this absolutely amazing article!!
Also chiming in to request project files as it would be a great learning resource ^^
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.
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?
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!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
No problem! Yes exactly, or you could grab it by using
TSublassOf
. With that you can modify theGameplayEffectSpec
instance to your liking.Let me know if this works for you
Is it possible to modify the Attribute of the AttributeSet targeted by Effect in this way?
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?
Hey Matt! The UAbilitiesStackSet is an
UAttributeSet
containing Current and Max attributes for each skill type (Primary, Secondary, Utility and Special). Hope it's clear now!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?
Hello ?Some Picture Lose?
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.
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 aGameplayTag
.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!
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.
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 😂
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.
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?
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
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
Hello, I'm curious where/when you clear/reset the TotalDamage attribute?
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!
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!
It does a lot, thank you. I was scratching my head with that one. I assume you did this through another GameplayEffect?
Awesome!
Not really, this is done inside the DamageCalculation (which is a
GameplayEffectExecutionCalculation
class)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?
Yes, exactly!
Can you show how your Skill class is defined? What is the difference between a GameplayAbility and a GameplaySkill?
This is EXCELLENT
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!
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)?
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?
Hi, how do you set is crit data into FGameplayEffectContext in bandit backstab ability?
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!
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
Super cool! I am wondering currently how do you trigger the Recharge Gameplay Effect?