/ project log

GAS Melee Combat System

A character-action melee combat system on Unreal Engine 5's Gameplay Ability System — combo chains, a 0.4-second perfect parry, motion-warping magnetism, and Behavior Tree enemy AI on one attribute pipeline.

April — June 2026

Personal Project • Gameplay Programmer • Unreal Engine 5 / GAS / C++ & Blueprints

What is the GAS Melee Combat System?

This is a solo-built, character-action melee combat prototype in Unreal Engine 5, built entirely on the Gameplay Ability System (GAS) using both Blueprints and C++. Locomotion comes from Epic's Game Animation Sample, extended with the community GASPALS plugin for weapon-pose overlay layering; everything on top of that - the combat, the enemy AI, and the UI - is my own architecture and implementation. The goal was not a collection of isolated feature demos, but one integrated system where combat, AI, and UI all react to a single attribute and event pipeline.

The shipped feature set: 6-hit light-attack combo chains with escalating stamina costs, hold-to-charge heavy attacks with a sprint-triggered slam-dunk variant, hold-to-block with a 0.4-second perfect-parry window that rewards counter-damage and slow motion, directional root-motion hit reactions, motion-warping combat magnetism that keeps combos connected, a five-state enemy AI driven by Behavior Trees and EQS, and a fully event-driven reactive HUD - health and stamina bars with a delayed-drain "ghost bar" damage indicator, and an ability bar that builds itself from granted abilities.

Design Principles

1. One shared pipeline, not isolated features: Combat, AI, and UI all react to the same attribute and event pipeline instead of being built as three separate systems that happen to coexist.

2. Damage is a decision, not a modifier: Damage never touches Health directly. It's a meta-attribute evaluated once, centrally, before anything else happens, so every consumer (block-checking, hit reactions, AI perception) reads from the same decision instead of re-deciding it independently.

3. Reuse over duplication: The same arrow components that drive hit-reaction direction also drive motion-warping targets. The same animation-overlay state machine that handles idle and ready poses handles the block pose. When two systems need the same kind of spatial or state data, they share the component instead of each getting their own.

4. Architecture follows what's actually built: Systems get scrapped mid-design the moment the authored content stops matching the assumption they were built on. The shipped architecture reflects what the content actually supports, not a hypothetical future version of it.

Mechanics and Gameplay

Light Attacks - 6-Hit Combo Chain
Heavy Attacks - Charged & Slam-Dunk Variant
Block & Perfect Parry - The 0.4s Window
Enemy AI - Behavior Tree Debug View
Motion Warping - Combat Magnetism

How GAS Powers This

Gameplay Ability System at the core. Every attack, block, and dodge is a Gameplay Ability, granted and activated through the Ability System Component.

Attribute Sets & Gameplay Effects. Health, stamina, and the Damage meta-attribute live in a custom Attribute Set, modified only through Gameplay Effects rather than direct value changes, so state stays clean and replication-ready.

Gameplay Tags for state. Blocking, cooldowns, and ability-activation gating all run through Gameplay Tags rather than boolean flags scattered across classes.

Gameplay Cues for feedback. Hit VFX, sound, and camera shake fire through Gameplay Cues, keeping presentation fully decoupled from gameplay logic.

C++ where it counts. The damage interception point, the attribute set, and the AI perception reporting hook are C++. Everything that benefits from visual iteration (combo sequencing, montage timing, Behavior Tree logic) is Blueprint.

Summary of Contributions

Centralized Damage Pipeline (C++): Damage is a meta-attribute, not a direct health modifier - every damage source in the game funnels through one C++ function (PostGameplayEffectExecute) before Health ever changes. That single interception point handles block-checking (a blocked hit early-returns into the block pipeline and never touches health), dispatches hit reactions via payload-carrying Gameplay Events, and reports damage to AI perception. Because the logic is centralized, the perfect-parry counter-hit staggers enemies correctly with zero special-casing - it's just another damage event flowing through the same pipe.

PostGameplayEffectExecute - Damage Interception
PostGameplayEffectExecute damage interception
PostGameplayEffectExecute - Hit Reaction Dispatch
PostGameplayEffectExecute hit reaction dispatch
Gameplay Tag Taxonomy
Gameplay tag taxonomy

Combo & Ability Architecture: All attacks share a single parent ability that owns the combo state machine - children only configure data. Light attacks cycle a montage array with an escalating stamina cost per hit in the chain, so the basic attack is itself a resource-management decision. Sprint attack variations (a full alternate combo set, plus a slam-dunk heavy) were added with zero new abilities, effects, or tags: one Sprinting boolean cached at ability activation drives a Select node between montage arrays. Because abilities are instanced per execution, the sprint state is naturally snapshotted - a combo chain never switches movesets mid-swing.

GA_MeleeAttack_Base - Shared Combo Logic
GA_MeleeAttack_Base shared combo logic
GA_MeleeAttack_Base - Sprint Bool Caching
GA_MeleeAttack_Base sprint bool caching
GA_Sword_1 - 6-Hit Combo Array
GA_Sword_1 six-hit combo array
GA_Sword_1 - Sprint Variant Select
GA_Sword_1 sprint variant montage selection

Perfect Parry as a Timestamp, Not a State Machine: The parry is a timing variant of blocking, so it lives inside the block ability instead of being its own system: the block ability records the game time when it activates, and a hit arriving within 0.4 seconds branches into counter-damage plus a slow-motion window. No separate ability, no extra tags, no desync risk between block and parry state. The block pose itself reuses the character's existing animation-overlay state machine rather than a dedicated montage - the overlay system already knew how to blend an upper-body pose over locomotion, so building a parallel system would have been pure duplication.

GA_Block - Parry Window Branch
GA_Block parry window branch logic
Block Break & Stamina Gating
Block break and stamina gating logic

Enemy AI - Behavior Trees, Perception, and EQS: Enemies run a five-state machine (Passive, Attacking, Investigating, Stunned, Dead) on a Behavior Tree and Blackboard, fed by three perception senses - sight, hearing, and damage. EQS-driven strafing scores positions around the player by path validity and distance, which keeps multiple enemies from clustering on the same spot. Enemy attacks synchronize with the tree through a gameplay-event handshake - the ability fires an event on every exit path so the tree task knows exactly when to resume. A dedicated Stunned state halts navigation the instant a hit reaction plays, so root-motion staggers and pathfinding never fight over the capsule.

BT_Enemy_Base - Full Tree Structure
BT_Enemy_Base behavior tree structure
EQS Strafe Query - Anti-Clustering
EQS strafe query anti-clustering logic

Reactive UI with Zero Tick Polling: Every UI element binds to GAS attribute-change and tag-count-change events instead of polling on Tick - the HUD only does work when something actually changes. The health bar snaps immediately on damage while an orange "ghost bar" holds the previous value and drains down after a beat, visually showing exactly how much damage was taken. The ability bar builds itself from whatever abilities are currently granted, with per-ability cooldown overlays driven by cooldown-tag events. The same directional arrow components on each enemy serve two systems at once - motion-warping landing positions and hit-reaction direction detection - one set of spatial math, two consumers.

Process

The project ran on a decision log - over fifty architecture decisions recorded with reasoning and reversibility, which kept the system coherent as it grew. The most valuable discipline turned out to be scope control: a fully-designed aerial juggle-lock system (new ability, new gameplay effect, enemy-side tick logic) was scrapped mid-design once it became clear the actual authored animation was a ground slam, not an airborne combo - the shipped version achieves the same visual payoff with a single Select node on existing infrastructure. The same principle drove the block system (reuse the existing overlay state machine instead of building a montage pipeline) and the dual-purpose arrow components. Architecture should follow the content that actually exists, not assumptions about what it might be.

Reflection

GAS has a reputation for being opaque, and this project taught me why - and how to work with it anyway. The lessons that cost the most hours are now standing rules: Set By Caller magnitudes silently read as zero during cost checks, so cost effects must use baked values; tag-based ability activation carries no event payload, so anything that needs to know who hit it or how hard must be triggered through a Gameplay Event with populated data; Execute and Add GameplayCue are not interchangeable. The single best debugging story was an editor that froze on every second play session - the root cause, found by copying the log file out of a frozen process and reading the last lines before silence, was duplicate MetaSound assets sharing an internal GUID from being file-copied instead of editor-duplicated. Building combat, AI, and UI against one shared pipeline - rather than as three separate features - is the thing I'd carry into any production codebase.

My first two shipped projects, Storge and Slow & Steady, both worked, but each system - movement, AI, audio, UI - was built and wired up on its own. That was fine at university-project scope, but it meant every new feature meant new one-off glue code connecting things that had never been designed to talk to each other. This project was built the other way around: architecture first, one shared pipeline, before a single ability existed. The setup cost more time in week one than either of those earlier projects took to get moving at all. But every system after that - a new enemy type, a new UI element, a new ability - plugged into existing infrastructure instead of fighting it. That's the tradeoff I'd make again on anything bigger than a weekend prototype.