Unreal 5.6 C++ – GAS Prototype


📁 Source Code on GitHub


This prototype is a focused deep-dive into the Gameplay Ability System (GAS) in Unreal Engine 5.6, implemented primarily in C++ with Blueprint subclassing for per-asset configuration. The gameplay premise is intentionally simple — survive against waves of melee and ranged enemies that spawn in increasing numbers over time — because the real subject under study is the architecture, not the game.

The project was built following the UE5 GAS Crash Course by Druid Mechanics, used as a structural foundation. The C++ systems described below reflect my own implementation, decisions, and extensions throughout the course.


Character Architecture

All characters derive from a shared abstract base class, AGP_BaseCharacter, which implements IAbilitySystemInterface and provides the common GAS initialization pipeline: GiveStartupAbilities(), InitializeAttributes(), BroadcastInitialValues(), and the OnHealthChanged delegate binding. The base class also enforces AlwaysTickPoseAndRefreshBones on the skeletal mesh, which is critical when abilities use PlayMontageAndWait — off-screen characters would otherwise stop ticking and cause ability tasks to hang indefinitely.

The two concrete subclasses implement a deliberate architectural split in where the ASC lives:

AGP_PlayerCharacter follows the Owner = PlayerState / Avatar = Character pattern. The UGP_AbilitySystemComponent and UGP_AttributeSet live on AGP_PlayerState, not on the Pawn. This means attributes, active effects, and cooldowns survive the Character’s death and destruction cycle. PossessedBy handles server-side initialization; OnRep_PlayerState handles client-side linking. The client intentionally does not grant abilities or initialize attributes — it only calls InitAbilityActorInfo and notifies the UI.

AGP_EnemyCharacter owns the ASC and AttributeSet directly on the Pawn. Since enemies are transient actors — they spawn, fight, and are destroyed — there is no need for state persistence. The ASC replication mode is set to Minimal, which replicates only GameplayTags and GameplayCues to clients. Clients need to see visual reactions (hit flashes, death), not the internal damage math.

This prototype deliberately implements both ASC ownership patterns within the same codebase, which is uncommon in beginner-level GAS projects and demonstrates a concrete understanding of when and why each pattern is appropriate.


Custom Ability System Component

UGP_AbilitySystemComponent extends the engine’s ASC to support automatic activation of passive abilities. Abilities tagged with GPTags::Abilities::ActivateOnGiven are activated automatically when granted, without requiring explicit calls from game code.

The implementation handles a non-obvious networking edge case: on a Listen Server, OnGiveAbility fires on the authority side, and OnRep_ActivateAbilities fires on the client side. Without a guard, the host would activate passives twice — once from each hook. The fix is an IsOwnerActorAuthoritative() check that restricts OnGiveAbility to the server only, and restricts OnRep_ActivateAbilities to non-authoritative clients only.

A TSet<FGameplayAbilitySpecHandle> tracks already-activated handles, providing O(1) lookup to prevent redundant activation attempts during replication bursts. The component also exposes SetAbilityLevel and AddToAbilityLevel as BlueprintCallable utilities, both server-only with authority validation.


Attribute Set

UGP_AttributeSet manages four attributes: Health, MaxHealth, Mana, and MaxMana, all replicated with REPNOTIFY_Always to support GAS prediction rollback correctly.

Clamping is implemented in two places for a specific reason. PreAttributeChange clamps the CurrentValue before it is applied — this keeps UI and gameplay queries within valid range. However, PreAttributeChange does not clamp BaseValue. A periodic Gameplay Effect using Add will accumulate on BaseValue and can push it beyond MaxHealth. When the GE expires, CurrentValue snaps back to the unclamped BaseValue, causing a health spike. PostGameplayEffectExecute handles BaseValue clamping to close this gap. Both hooks are necessary; neither alone is sufficient.

A bAttributesInitialized flag is replicated from server to client. When the first Gameplay Effect executes (always the DefaultAttributesEffect), the flag is set and OnAttributesInitialized is broadcast. UI components listen for this delegate to know when it is safe to bind to attribute change delegates. This solves the initialization race condition where widgets try to read attributes before GAS has applied any values.

Kill scoring is also handled inside PostGameplayEffectExecute: when Health reaches zero, a GPTags::Events::KillScored gameplay event is sent to the damage instigator, with the victim as the payload’s Instigator. This keeps the score tracking entirely within GAS without requiring game mode polling or custom interfaces.


Native Gameplay Tags

All tags are declared as Native Gameplay Tags in a centralized GPTags.h using C++ namespaces that mirror the tag hierarchy (GPTags::Abilities::Player::Primary, GPTags::Events::Enemy::HitReact, etc.). This eliminates unsafe string literals throughout the codebase and makes tag references type-safe and refactor-friendly.

A GPTags::None sentinel tag is used as a “no override” value for optional parameters in functions like SendDamageEventToPlayer. Rather than overloading the function or using nullable pointers, the sentinel allows callers to signal default behavior explicitly.

SetByCaller tags (GPTags::SetByCaller::Projectile, ::Melee, ::Player::Secondary) enable dynamic damage magnitudes injected at runtime into Gameplay Effect specs, avoiding hardcoded values in GE assets.


Input System

The AGP_PlayerController uses the Enhanced Input System exclusively. Input Actions are bound in SetupInputComponent using a payload pattern: each BindAction call passes its corresponding FGameplayTag directly to a single generic handler, Input_AbilityPressed. This handler calls TryActivateAbilitiesByTag on the ASC retrieved from the PlayerState — bypassing the Pawn entirely, which is safer during death and respawn when the Pawn may be null or pending destruction.

Adding a new ability slot requires exactly three changes: a new UInputAction* property, a new tag in GPTags::Abilities::Player, and one BindAction line. No per-ability handler functions needed.

All input handlers — movement, look, jump, and ability activation — guard on IsAlive() to prevent dead players from issuing any input.


Combat Utilities

UGP_BlueprintLibrary centralizes the combat math and GAS integration patterns used across multiple systems:

HitBoxOverlapTest performs a sphere overlap against the Pawn collision channel, deduplicates actors with multiple collision components, and filters out dead characters. Configurable radius, forward offset, and elevation offset allow per-ability tuning of melee reach without subclassing.

SendDamageEventToPlayer centralizes the damage application pattern — GE spec creation, SetByCallerMagnitude assignment, context setup, lethality detection, and event routing — so projectiles, traps, and area effects all deal damage consistently without duplicating logic.

ApplyKnockback scales launch force linearly from full magnitude at InnerRadius to zero at OuterRadius, with an upward rotation angle to create an arc. The enemy’s StopMovementUntilLanded registers a one-shot LandedDelegate callback that re-enables AI navigation and sends GPTags::Events::Enemy::EndAttack to signal the Behavior Tree to resume, treating knockback recovery identically to attack recovery.


Reactive UI

The UI layer is entirely event-driven. UGP_WidgetComponent discovers UGP_AttributeWidget instances in the widget tree at runtime, matches them to (Current, Max) attribute pairs configured in Blueprint via a TMap, and binds them to ASC attribute change delegates.

All bindings use TWeakObjectPtr to avoid preventing garbage collection of destroyed widgets. EndPlay explicitly unregisters all delegates to prevent dangling callbacks when the owning enemy is destroyed but the player’s ASC (on PlayerState) survives.

UGP_AttributeWidget follows a push model: it does not fetch data itself. UGP_WidgetComponent pushes (NewValue, MaxValue, OldValue) whenever an attribute changes, and Blueprint implements BP_OnAttributeChange to update progress bars or spawn floating damage numbers using the delta.

For Blueprint-authored HUD widgets, UGP_AttributeChangeTask is an async UBlueprintAsyncActionBase that exposes a persistent attribute change listener as a Blueprint node. It binds to the ASC delegate internally and re-broadcasts through a Blueprint-friendly multicast delegate, unpacking the C++ FOnAttributeChangeData struct into individual floats.


Gameplay Abilities

All abilities derive from UGP_GameplayAbility, which sets project-wide defaults: InstancedPerActor instancing policy (each character gets its own ability instance for safe state storage), LocalPredicted net execution policy (client runs immediately, server validates), and ReplicateNo replication policy (state is synchronized via tags and effects, not by replicating the ability UObject itself).

Enemy abilities override NetExecutionPolicy to ServerInitiated, since AI characters do not exist on clients and client-side prediction is not applicable.

UGP_HitReact is triggered via Gameplay Event (not activated directly), reads the Instigator from the event payload, and uses UGP_BlueprintLibrary::GetHitDirection to calculate which montage section to play based on the dot product between the avatar’s forward vector and the vector toward the attacker. BlockHitReact (GPTags::Abilities::BlockHitReact) is used as an Activation Blocked Tag on the ability, preventing flinch interruptions during attack animations.

The player’s primary attack ability (UGP_Primary) uses HitBoxOverlapTest for hit detection triggered via AnimNotify, then calls SendHitReactEventToActors to dispatch GPTags::Events::Enemy::HitReact to each hit target with the attacker as payload Instigator, allowing each target’s HitReact ability to independently calculate impact direction.


Projectile System

AGP_Projectile uses UProjectileMovementComponent for straight-line movement with zero gravity by default. Damage is applied in NotifyActorBeginOverlap with an authority check, validated target check (IsAlive()), and a call to SendDamageEventToPlayer using GPTags::SetByCaller::Projectile as the damage key. The Damage property is marked ExposeOnSpawn, allowing the spawning enemy ability to set per-instance damage values at spawn time. A 10-second InitialLifeSpan acts as a safety net against orphaned projectiles.


Tech Stack

  • Unreal Engine 5.6
  • Gameplay Ability System (GAS) plugin
  • Enhanced Input System plugin
  • C++ with Blueprint subclassing for asset configuration
  • Electronic Nodes and Blueprint Assist for Blueprint graph organization

Learning Source

Built following UE5 GAS Crash Course on Udemy.

Leave a comment

Create a website or blog at WordPress.com

Up ↑