Gameplay Ability System
An item can grant gameplay effects, abilities and attribute sets while it sits somewhere, and take them back when it leaves. Equip a helmet and the armour bonus applies; drop it in a bag and it does not.
What a grant is conditioned on
Every grant — effect, ability or attribute set — answers the same three questions, and any it leaves empty it stops asking:
| Filter | Answers |
|---|---|
| Container Filter | Which containers. Where the item is kept |
| Slot Filter | Which slots within them. Which peg it hangs on |
| State Filter | Which equipment states. What it is doing there |
The third is usually the one you want. A container says a sword is in your hotbar; only the state says it is the one in your hand. Without it, a weapon's abilities are yours while it sits in a bag — which is almost never the intent.
StateFilter: Inventory.Equipment.State.Held // only while actually holding it
StateFilter: (empty) // a passive charm, wherever it is
The state is resolved the same way the visuals resolve it — the item's own tag, else the one its container declares, else Stored — so a grant and a mesh can never disagree about whether a helmet in a paperdoll counts as equipped.
What level it comes out at
A level typed into the asset means a +5 sword and a +50 sword are two assets. They should be one item with a counter.
| Level Source | Reads |
|---|---|
| Fixed | The number on the grant. Every copy grants the same thing |
| Item State | A per-instance counter on the item, named by Level State Tag |
Item state replicates, saves, and can be changed by a transaction like anything else, so an enchantment, a charge count or a durability tier can drive the grant with no second asset.
Falling back to the design-time number would make every item that was never rolled come out at whatever the designer happened to leave in the field — which is how an unrolled sword becomes a legendary one.
And Stack Mode decides whether a pile of several grants more than a pile of one: Once for an amulet you only benefit from carrying one of, Per Unit for five torches that burn brighter than one. Per Unit scales the level, so the effect has to be built to read one.
Setup
GAS is a dependency of the plugin, so there is no module to enable. Your character needs the usual one-time setup:
AMyCharacter::AMyCharacter()
{
AbilitySystem = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("AbilitySystem"));
AbilitySystem->SetIsReplicated(true);
AbilitySystem->SetReplicationMode(EGameplayEffectReplicationMode::Mixed);
Attributes = CreateDefaultSubobject<UMyAttributeSet>(TEXT("Attributes"));
}
void AMyCharacter::BeginPlay()
{
Super::BeginPlay();
AbilitySystem->InitAbilityActorInfo(this, this);
}
Implement IAbilitySystemInterface so the inventory can find the component. Then add
UEDGASLinkComponent to the actor — that is what watches containers and reconciles grants.
The Grants fragment
FEDGrantsFragment holds three lists, each entry carrying its own conditions:
| Grant | Gives |
|---|---|
| Effect Grants | A gameplay effect applied while the condition holds. |
| Ability Grants | An ability granted while the condition holds. |
| Attribute Set Grants | An attribute set added while the condition holds. |
Every grant shares one FEDGrantCondition:
| Field | Meaning |
|---|---|
| Container Filter | Which container the item must be in. |
| Slot Filter | Which slot, matched against the slot tag and the entry's dynamic tags. |
Empty filters mean "wherever it is carried", which suits a passive charm. A helmet narrows the container filter to equipment.
The same vocabulary as visuals
Grants and equipment visuals read the same tags: the container's type, the slot's tag, and the
entry's DynamicTags. One item cannot be worn and stowed at once, so one answer serves both.
That is why drawing a sword can change its model and its buffs in one step:
FEDSetDynamicTagsTransaction Draw;
Draw.Container = Container;
Draw.Handle = SwordHandle;
Draw.Tags = FGameplayTagContainer(DrawnTag);
Container->RequestTransaction(FInstancedStruct::Make(Draw)); // server-only
A visual state filtered on Drawn and an effect grant filtered on Drawn both react. Nothing wires
them together.
Reconciliation, not events
UEDGASLinkComponent does not apply grants on an "equipped" event. It reconciles: on any change
it works out what should be granted now and makes reality match, adding what is missing and removing
what no longer qualifies.
That matters because events can be missed, arrive out of order, or fire twice — on a client, on a respawn, on seamless travel. Reconciling from state cannot drift.
It also means teardown is free: the component undoes everything it granted on EndPlay, so a pawn
being reused after a respawn does not keep the last life's effects.
What a granted ability can ask about its item
An ability wants to know which item it is acting for — which weapon is firing, which armour is
absorbing. GAS answers that with the spec's source object: GetCurrentSourceObject() on an
ability, GetSourceObject() on an effect context. The link component fills it in through one
overridable:
UObject* UMyGASLink::ResolveGrantSourceObject_Implementation(const FEDItemEntry& Item) const;
| Case | What the ability gets |
|---|---|
| The item has an Instance Class | Its UEDItemInstance. Cast to your subclass and you have the item. |
| The item is on the struct fast-path | The link component. There is no per-item object to hand out — that is what makes two hundred arrows cost zero UObjects — so nothing about the item can be recovered from it. |
| Your game has its own per-item object | Whatever you return. Override the function on a subclass of the link component. |
That third row is the one to reach for when abilities were written against an equipment instance of your own, which is the usual shape in Lyra-derived projects:
UObject* UMyGASLink::ResolveGrantSourceObject_Implementation(const FEDItemEntry& Item) const
{
if (UObject* Mine = MyEquipment->FindInstanceForItem(Item.Handle))
{
return Mine;
}
return Super::ResolveGrantSourceObject_Implementation(Item);
}
The same object is used for ability specs and for the effect contexts of that item's effects, so an ability and the effects it applies never disagree about what granted them.
Keeping the inventory across a respawn
Containers on the PlayerState or the Controller survive a respawn; containers on the Pawn die with it. Which is right is a game decision — a shooter loadout wants to survive, a corpse's pockets do not — and the surviving arrangement has consequences worth meeting on purpose.
A respawn is not an inventory event. Nothing moved, nothing changed, so no OnInventoryChanged
fires. Anything waiting for one waits forever.
Nothing is cleared for you. The framework will not empty a dead player's bag, because it cannot know whether that is what dying means in your game. Decide on the server at the moment you decide someone died:
| Rule | What to do |
|---|---|
| Keep everything | Nothing. This is the default. |
| Drop everything | Extract the entries on death and spawn them in the world. |
| Keep some | Query by tag or category and extract the rest. |
| Grant a starting loadout on spawn | Check first. A blind TryAddItem on every spawn is how a player ends up with the rifle they picked up and the starting pistol on top of it. |
Anything applied to the pawn has to be re-derived. Meshes, anim layers, an actor in the hand,
anything your own UEDContainerObserverComponent subclass sets up: it belonged to the pawn and died
with it. The entries still say the item is held; the new pawn's hands are empty. Two steps fix it:
-
Gate
CanObserveon the avatar, so the observer applies nothing — and drops nothing silently — while there is no pawn to act through:bool UMyBridge::CanObserve_Implementation() const
{
return Super::CanObserve_Implementation() && GetPawnToActThrough() != nullptr;
} -
Call
NotifyObservationContextChanged()fromOnPossessedPawnChanged, or wherever the new pawn becomes known. The observer re-derives every item it holds against the pawn that exists now. Without the call it still recovers, but only on the next safety-net poll — half a second of empty hands on every respawn.
UEDEquipmentManagerComponent looks for its avatar mesh on its own actor, so equipment visuals
belong on the Pawn even when the containers do not. What an actor OWNS can outlive the body; what is
ATTACHED to a body cannot.
Attribute-driven capacity
A container's Slot Count Mode can be Attribute, reading a GAS attribute for its capacity:
Slot Count Mode: Attribute
Capacity Source: Capacity From Attribute (GAS) -> MyAttributeSet.CarryingCapacity
A Strength perk then grows the bag with no inventory code at all. Editor validation refuses Attribute mode with no attribute set, since that failure is otherwise silent.
The same applies to FEDWeightLimitRule, which can read a weight cap from an attribute.
Core does not depend on GAS
The bridge is its own module, EDInventoryGAS. Core does not link GameplayAbilities, so a game
that never grants an ability does not pay for the ability system.
What Core keeps is the seam. Capacity from an attribute is a FEDCapacitySource picked in a
layout, and FEDCapacityProviderRegistry is what resolves one — Core defines the question, and the
GAS module registers the implementation that answers it with an attribute. Add your own source the
same way and Core never learns what it reads.
Grants work the same way round: fragments describe them, UEDGASLinkComponent applies them, and an
item with a Grants fragment on an actor with no ability system is inert rather than broken.
FEDGrantsFragment and its friends are now /Script/EDInventoryGAS.*. An asset saved before that
finds them through the [CoreRedirects] block the plugin ships in Config/DefaultEngine.ini —
copy those lines into your project's Config/DefaultEngine.ini, because core redirects are read
before plugin configs merge.
Where next
- Equipment — the visual half of the same vocabulary
- Usable items — the other GAS seam