V4 is one game that has to feel like six different ones. A tactical-FPS operator, a social-stealth assassin, a real-time-tactics specialist, a Wukong-class action- RPG hero, an RTS unit-commander, and a 2D run-and-gun arcade pilot all run on the same actor and the same Ability System Component — they differ only in which stat block, which verbs, and which effects are mounted at spawn. That is the job of V4's GAS layer: give every "cell" a clean, mutually exclusive slice of Unreal's Gameplay Ability System while sharing one base for attributes, one base for abilities, one base for effects, and one gameplay-tag namespace. Unlike V2's combat core, this layer is not rollback-deterministic or frame-data-driven; it is a conventional, replicated, server-authoritative GAS stack whose value is in how cleanly it partitions six rulesets behind shared machinery. This page is the architecture-side companion for V4's GAS, part of the Combat, Character & Input Systems group; the section hub is ../V4_ARCHITECTURE.md.
The source of truth is Unreal C++ under V4/ue/Source/V4Gameplay (the shared
base), the per-cell ability modules (V4Tactical, V4Stealth, V4Tactics,
V4ActionRPG, V4RTS, V4Arcade), and the native tag table at
V4/ue/Config/DefaultGameplayTags.ini. There is no TypeScript contract package
for any of it.
What ships, honestly#
The GAS layer is real, substantive, and tested — not a skeleton. Six
concrete attribute sets, a thirty-two-class ability family, twenty-three
gameplay-effect classes, a resolve-and-apply library, the operator pawn that
wires it together, and five automation specs all exist as working C++ with
domain-specific clamps, armor mitigation, and periodic decay. The
V4.Gameplay.* automation specs spawn a real AV4Pawn_Operator into a
UWorld, switch cells, apply effects, and assert computed attribute values
(armor-mitigated health of 85.0, suppression decaying 40 → 20 → 0 over two
seconds, RTS unit health stacking 100 → 65 → 45) — these would fail against
placeholders.
Five honest qualifications, so the rest of the page can be read at face value:
- The abilities and effects are C++ classes, not authored Blueprint assets.
The monolith says "GameplayAbility blueprints implement the verb library"; in
the repo the verbs are
UCLASS(Blueprintable)C++ types (UGA_Fire,UGA_LightAttack, …) that tests instantiate directly viaStaticClass(). They are Blueprintable — designers can subclass them — but V4 ships zero binary.uassetability or effect assets. The C++ classes are canonical. - Effects are thin SetByCaller appliers, not MMC/ExecCalc math. V4 has no
UGameplayModMagnitudeCalculation/UGameplayEffectExecutionCalculationpairs like V2's combat layer. Every effect is a single additive modifier fed aSetByCallermagnitude. The one piece of real "execution math" — armor mitigation — is hand-coded inPostGameplayEffectExecute, not in an exec calc. - Armor mitigation is Tactical-only. The mitigation hook keys specifically
on
UV4Attr_Tactical::GetHealthAttribute(). The RTS set has aUnitArmorattribute, but nothing in the effect path consumes it yet — it is a stored stat, not a damage reducer. - The ASC is a thin cell-tracking subclass.
UV4AbilitySystemComponentadds anActiveCellfield and a cell-event delegate and nothing else. There is no integerSimulationFrame, no rollback snapshot, no input-tag queue — V4's determinism/netcode lives inV4Netcode, separate from this layer. - The doc under-counts. The monolith lists six Stealth and three Tactics abilities; the code has seven and four (each adds a takedown ability). Counts below come from the headers, not the prose.
The shared spine#
Every controllable character is an AV4Pawn_Operator
(V4Gameplay/Public/V4Pawn_Operator.h:13), which implements
IAbilitySystemInterface and owns exactly one UV4AbilitySystemComponent
created as a default subobject, replicated in
EGameplayEffectReplicationMode::Mixed (V4Pawn_Operator.cpp:10). On
BeginPlay it calls InitializeForCell(DefaultCell) with
DefaultCell = Tactical (V4Pawn_Operator.h:34). That single method is the
whole cell-switching mechanism: it NewObjects the one attribute set for the
requested cell, hands it to the ASC via SetSpawnedAttributes, calls
InitAbilityActorInfo, and records the cell on the ASC
(V4Pawn_Operator.cpp:27-67). Because ActiveAttributeSet is a single pointer
that is overwritten on each switch, a pawn is either a Tactical operator
or an ARPG hero, never both at once — the "mutually exclusive at run-time"
contract the monolith promises is enforced by construction.
The cell identity itself is EV4RulesetCell (V4Core/Public/V4CellState.h:7):
None, Tactical, Stealth, Tactics, ActionRPG, RTS, Arcade. It is the join key
for the entire layer — attribute sets, abilities, and effects all carry it, and
the ASC stores the live one (UV4AbilitySystemComponent::ActiveCell,
V4AbilitySystemComponent.h:31).
Attribute sets — one base, six stat blocks#
All attribute sets derive from UV4AttributeSetBase
(V4Gameplay/Public/V4AttributeSets.h:16), an Abstract set that holds the
replicated OwningCell, overrides PreAttributeChange and
PostGameplayEffectExecute, and exposes a virtual GetClampForAttribute that
subclasses fill in. The clamp machinery is genuinely defensive: V4Clamp
(V4AttributeSets.cpp:26) treats any non-finite value as the floor before
FMath::Clamp, so an effect can never push an attribute to NaN.
PreAttributeChange clamps the incoming value; PostGameplayEffectExecute
re-clamps the resulting value after an instant effect, and runs the armor
mitigation pass first (V4AttributeSets.cpp:80-94).
The six concrete sets, with their replicated defaults (each field uses
ReplicatedUsing = OnRep_* with REPNOTIFY_Always):
| Set | Cell | Attributes (default) |
|---|---|---|
UV4Attr_Tactical (:36) |
Tactical | Health 100, Armor 0, Suppression 0, StaminaSprint 100, AmmoCurrent 30, AmmoReserve 90 |
UV4Attr_Stealth (:92) |
Stealth | Health 100, Detection 0, DisguiseClass 0, Noise 0 |
UV4Attr_Tactics (:134) |
Tactics | Health 100, ActionPoints 2, Vision 1 |
UV4Attr_ARPG (:169) |
ActionRPG | Health 100, Stamina 100, Posture 0, Will 100, Mana 50 |
UV4Attr_RTS (:218) |
RTS | UnitHealth 100, UnitArmor 0, UnitAttack 10, UnitRange 600, UnitMovementSpeed 400, UnitSightRange 1200 |
UV4Attr_Arcade (:274) |
Arcade | Health 1, Lives 3, ScoreMultiplier 1 |
That is 27 replicated FGameplayAttributeData fields across six sets, each
exposed through the V4_ATTRIBUTE_ACCESSORS macro (:9) that synthesizes the
standard GAS getter/setter/initter quartet. The clamps are domain-specific, not
boilerplate: Tactical Health is 0–100 but Armor is 0–200
(V4AttributeSets.cpp:137), ammo is 0–999, Tactics ActionPoints cap at 12
and Vision at 10, RTS unit attributes share a wide 0–100000 ceiling, and
Arcade Health caps at 9, Lives at 99, and ScoreMultiplier has a floor of
1.0 (V4AttributeSets.cpp:429) so a score multiplier can never drop below ×1.
The V4.Gameplay.Attributes.Defaults spec (AttributeSetSpec.cpp:13) asserts
every one of these defaults and owning-cell values.
Abilities — one gated base, thirty-two verbs#
UV4GameplayAbilityBase (V4Gameplay/Public/V4GameplayAbilities.h:9) extends
UGameplayAbility with three things a multi-cell roster needs. First, a cell
gate: RequiredCell (:52) plus IsAbilityCellCompatible
(V4GameplayAbilities.cpp:80), which returns true for None but otherwise
casts the actor's ASC to UV4AbilitySystemComponent and checks
GetActiveCell() == RequiredCell. This is folded into CanActivateAbility
(:12) so a Tactical GA_Fire simply cannot fire on a pawn currently mounted
as an ARPG hero. Second, a fail-loud activation seam: ActivateAbility
(:23) checks the cell, calls CommitAbility, then dispatches to a protected
ExecuteV4Ability(..., FString& OutFailureReason), recording
bLastActivationSuccessful and a human-readable LastActivationReason
("wrong-cell", "commit-failed", or the verb's own reason) before
EndAbility. Third, sane defaults: the constructor sets
InstancedPerActor, LocalPredicted net execution, and ReplicateYes
(V4GameplayAbilities.cpp:5-10), so every cell inherits client-predicted,
replicated activation without restating it.
Each cell module subclasses that base into its verb library. The full count is
32 concrete UGA_* classes:
| Cell module | Abilities | Count |
|---|---|---|
V4Tactical |
Fire, Reload, ADS, Lean, ThrowGrenade, BreachStack, Revive, Heal | 8 |
V4Stealth |
Crouch, BodyDrag, LightKill, StealthTakedown, StickyCam, MarkAndExecute, Disguise | 7 |
V4Tactics |
PlanQueueWaypoint, FireQueued, TacticsTakedown, ShowdownToggle | 4 |
V4ActionRPG |
LightAttack, HeavyAttack, Dodge, Parry, Transform, CastCharm | 6 |
V4RTS |
BuildStructure, TrainUnit, ResearchTech, AttackMove | 4 |
V4Arcade |
Shoot, Jump, DropPlatform | 3 |
The verbs are not stubs: each ExecuteV4Ability delegates to a real domain
component on the avatar and reports honest failure. UGA_Fire
(V4Tactical/Private/V4TacticalAbilities.cpp:44) pulls the Tactical attribute
set and calls Weapon.ConsumeRound, failing with "no-ammo"; UGA_Heal
(:206) routes through the effect library and fails with "heal-failed";
UGA_LightAttack (V4ActionRPG/Private/V4ActionRPGAbilities.cpp:30) advances a
UV4HeroComboComponent, and UGA_Transform (:122) defaults to the
ARPG.Form.Tiger form (300 HP, 12 s, WillCost = 1) and activates it through a
UV4TransformationComponent. The V4.Gameplay.Abilities.ActivationClasses spec
(AbilityClassSpec.cpp:170) spawns an operator per cell, attaches the needed
components, gives-and-activates each ability, and asserts the domain side
effect — ammo 30 → 29, reload back to 30, Tactical heal 50 → 85, ARPG
combo step, dodge i-frames, RTS build/train/tech queues, arcade jump raising the
pawn. A companion V4.Gameplay.Abilities.Classes spec (:124) pins every
ability's RequiredCell, and V4.Gameplay.Ability.Activation
(AbilitySpec.cpp:28) verifies the base activation path end-to-end through
TryActivateAbility.
How input reaches these is deliberately out of scope here: named input actions
map to the GA.Activation.Tag.* namespace and are dispatched by the input
router. See ./input-pipeline.md. The montages, combo
state machines, and dodge/parry frame windows the verbs drive are in
./animation-pipeline.md, and the cell-specific
component backbones (V4HeroComboComponent, V4CommandQueueComponent, the
posture system) are detailed in
./per-cell-deep-dives.md.
Gameplay effects — a typed kind, a resolve table, a scaling applier#
Effects are organized by a single enum, EV4AttributeEffect
(V4Gameplay/Public/V4GameplayEffects.h:12), with 16 kinds — Damage,
Heal, SuppressionApply/SuppressionDecay, StaminaCost, DetectionApply,
NoiseApply, ActionPointCost, PostureDamage/PostureDecay, ManaCost,
UnitDamage/UnitRepair, ArcadeDamage, ExtraLife, ScoreMultiplierBoost.
The base UV4GameplayEffectBase (:33) carries Cell, EffectKind, and a
MagnitudePerLevel, and offers two configurators — ConfigureInstantAdditive
and ConfigurePeriodicAdditive (V4GameplayEffects.cpp:41, :62) — both of
which build a single additive FGameplayModifierInfo whose magnitude is a
SetByCaller keyed "V4.GameplayEffect.Magnitude" (:103). From that base
descend 23 concrete UGE_V4_* classes (5 Tactical, 4 Stealth, 3 Tactics, 6
ARPG, 2 RTS, 3 Arcade), each a one-line constructor binding a cell, a kind, an
attribute, and a per-level rate — e.g. UGE_V4_TacticalDamage is
(Tactical, Damage, Health, -1.0) (:109) and UGE_V4_TacticalHeal is
(Tactical, Heal, Health, +1.0) (:118).
The runtime entry point is UV4GameplayEffectLibrary
(V4GameplayEffects.h:286). ResolveAttributeEffectClass(Cell, Kind) (:320)
is an explicit nested switch that returns the right UGE_V4_* class or
nullptr for any combination a cell doesn't support — so a Tactical request
for NoiseApply resolves to null, which the V4.Gameplay.Attributes.Effects
spec asserts as a rejected cross-cell effect (AttributeSetSpec.cpp:115).
ApplyAttributeEffect (:413) is where the one genuinely subtle design
decision lives. A bare FScalableFloat magnitude would be constant regardless
of the requested amount; instead the library reads the class's
MagnitudePerLevel and sets the SetByCaller magnitude to
MagnitudePerLevel * RequestedMagnitude (:451-455), so the same Damage
class scales linearly with the caller's number. That comment-as-rationale
(V4GameplayEffects.h:44-50) is the kind of domain-specific reasoning that
distinguishes real code from a generic wrapper.
Armor mitigation, and the decay-on-apply trick#
Two pieces of behaviour are worth tracing because they are where the layer earns its keep.
Armor mitigation (V4AttributeSets.cpp:37) runs inside
PostGameplayEffectExecute. After an instant Damage effect has already
subtracted its full magnitude from Tactical Health, the mitigation pass
recomputes RawDamage = -Magnitude, finds
MitigatedDamage = max(0, RawDamage - Armor), and credits the prevented
portion back to Health. With Armor = 10 and a 25 damage request, Health
lands at 100 − 25 + 10 = 85 — exactly what the effects spec asserts
(AttributeSetSpec.cpp:188). Flat armor as damage subtraction, applied through
the standard GAS execution path, with a final clamp behind it.
Decay-on-apply is how V4 implements "suppression decays over 2 s" and
"posture decays over 3 s" without a per-frame tick. When ApplyAttributeEffect
lands a SuppressionApply or PostureDamage, it measures how much the tracked
attribute actually rose, then recursively applies the matching periodic decay
effect for exactly that measured amount (V4GameplayEffects.cpp:431-473,
V4GetStackedAttributeForDecay at :13). The decay effect is a HasDuration
periodic modifier whose per-period rate is fractional and negative —
-(1.0/2.0) per 1 s over 2.01 s for suppression (:136), -(1.0/3.0) per 1
s over 3.01 s for posture (:255) — so a 40-point suppression bleeds off
20/s and is gone in two seconds, and a 30-point posture bleeds 10/s over
three. The spec drives the world's timer manager directly and asserts
40 → 20 → 0 and 30 → 20 → 10 → 0 (AttributeSetSpec.cpp:211-265). The
0.01 s duration padding exists precisely so the engine's strict
InternalTime > ExpireTime period check fires the expected number of times — a
real timing nuance, documented in the spec.
How cells share and specialize#
The whole layer is a deliberate split between one shared mechanism and six narrow specializations:
- Shared: the operator pawn, the
UV4AbilitySystemComponentand itsActiveCell, the abstractUV4AttributeSetBase(clamp + finite-guard + replication + armor hook), the abstractUV4GameplayAbilityBase(cell gate + fail-loud reason + predicted/replicated defaults), the abstractUV4GameplayEffectBaseand the resolve-and-apply library, and oneGA.Activation.Tag.*input namespace. - Specialized per cell: exactly one attribute set, one
UGA_*verb family, one slice ofUGE_V4_*effects, and one gameplay-tag subtree. Adding a seventh cell means a newEV4RulesetCellvalue, one attribute set, a verb module, its effects, aResolveAttributeEffectClassarm, and a tag root — with no change to the shared base.
The tag side mirrors this. V4/ue/Config/DefaultGameplayTags.ini defines 595
native tags under FastReplication, including all seven cell roots
(Tactical, Stealth, Tactics, RTST, ARPG, RTS, Arcade), a shared
Operator.* roster family (48 tags), and a flat GA.Activation.Tag.* namespace
of 47 input-to-ability activation tags (GA.Activation.Tag.Fire,
.PlanWaypoint, .Parry, .BuildStructure, …). The cell-specific state tags
the monolith cites are present and correctly rooted —
Tactical.State.Stance.{Stand, Crouch,Prone}, Tactical.Cover.{Left,Right},
Stealth.Detection.{Hidden, Suspicious,Compromised}, Stealth.Disguise.Waiter,
Tactics.Order.{Move,Attack, Sync}, ARPG.Combat.Parry.Window,
ARPG.Form.{Tiger,Wolf,Bird,Stone,Wind}, RTS.Faction.*, RTS.Tech.Tier2, and
Arcade.Weapon.* / Arcade.PowerUp.Active.
One last connecting note for the reader: the UV4MatchStateSubsystem
(V4Gameplay/Public/V4MatchStateSubsystem.h) that lives in the same module is a
lobby/flow machine (MainMenu → ModeSelect → … → MatchResult), not a combat
state machine — it sequences match setup, not in-fight phases, and is unrelated
to the per-hit GAS path described above. V4's combat "state" is the per-cell
component state (combo, posture, cover, detection), reached through the
abilities here.
How it connects#
GAS is the seam where input, animation, and per-cell systems meet. Input actions
activate these abilities through the GA.Activation.Tag.* namespace
(./input-pipeline.md); the abilities drive montages,
combo machines, and the dodge/parry/posture frame windows
(./animation-pipeline.md); and the cell-specific
component backbones each verb delegates to — the ARPG combo and posture systems,
the tactical cover/grenade/revive components, the RTS build/train/tech
components — are unpacked cell by cell in
./per-cell-deep-dives.md. The attribute defaults and
effect rates are the numbers the balance and live-ops layers tune. For where
V4Gameplay and the per-cell modules sit in the overall build, and the full GAS
section in prose, return to the hub:
../V4_ARCHITECTURE.md.
Related#
- Animation Pipeline — AnimBlueprints, motion matching, and the frame windows V4 abilities drive
- Input Pipeline — EnhancedInput IMCs and the
GA.Activation.Tag.*mapping that fires these abilities - Per-Cell Deep-Dives — the domain components each
UGA_*verb delegates to - The section hub: ../V4_ARCHITECTURE.md