Building PvE Co-op ARPG Combat with UE5 + GAS — Dungeon Wanderers Phase 2 Update

A dark stone dungeon corridor with torch light and drifting fog DevLog

The Combat Philosophy of Dungeon Wanderers

Dungeon Wanderers is a 4-player co-op PvE ARPG with procedurally generated dungeons, built for Steam with Listen Server networking.

The core of combat is the satisfaction of reading your opponent. Flashy VFX matter less than the moment when dodge, guard, and parry line up against the enemy’s attack pattern in a way that feels earned. So in Phase 2, I defined the combat core skeleton starting from that feel axis.

Phase 2 isn’t complete yet, but it’s a milestone: the four combat Gameplay Abilities are now in place. This post walks through the design calls and implementation results.

Why GameplayAbilitySystem

Unreal’s GameplayAbilitySystem (GAS) has a famously steep learning curve, but Dungeon Wanderers committed to it from Phase 1. Reasons:

  • Proven by Lyra and Fortnite at shipped-game scale
  • Multiplayer synchronization is built in from the foundation, not bolted on
  • Abilities, effects, and attributes are decoupled, so MMO/ARPG-scale combat complexity doesn’t collapse
  • Painful experience from retrofitting multiplayer onto a non-GAS project in the past

When you add multiplayer to an ARPG late, state sync, prediction, and rollback all become afterthoughts — and the feel degrades catastrophically. Looking at Lyra’s architecture, I decided to go GAS-native from day one.

The Four Combat Abilities in Phase 2

1. GA_MeleeAttack — Combo-capable Light Attack

  • Driven from Enhanced Input
  • Combo state is tracked in an FDwanComboState pure-data struct
  • Combos advance only while the input window is open
  • BP-side montages define hit frames

2. GA_Parry — Reading the Instant of Impact

  • Parry is the core mechanic of Dungeon Wanderers
  • A parry input landing in the brief window just before an incoming enemy attack triggers the reaction
  • Server-authoritative timing would destroy the feel, so parry runs LocalPredicted with lag compensation
  • Failure means a long punish window — deliberate risk/reward design

3. GA_Dodge — 8-direction + i-frames

  • Rolls in the input direction, 8 compass points
  • i-frames (invulnerability) during the early/mid phase
  • FDwanDodgeDirection isolates direction math as pure logic and is test-guarded

4. GA_Guard — Hold-to-defend

  • Reduces incoming damage while held
  • Will consume a stamina resource (planned)
  • Positioned as the safe-side choice opposite the high-risk parry

The design goal: at every moment of combat, the player is weighing “attack? — no, enemy is winding up — guard and weather it? gamble on a parry? dodge away?” Choices that meaningfully differ in risk and payoff.

The Lesson from a Past Project — LocalPredicted First

Behind this design is the painful lesson of a past ARPG project.

That project was built server-authoritative for everything. Synchronization correctness was perfect. Player experience was devastating. Even at 60ms ping, the delay between pressing a button and seeing the response killed combat feel.

From that experience, Dungeon Wanderers inverted the default:

  • Player input responses are LocalPredicted — immediate feedback, always
  • Server validation is restricted to anti-cheat and synchronizing to other players
  • Rollback on mispredict is designed carefully (a later Phase 2 task)

UE5’s AbilityActivationMode::LocalPredicted makes this technically feasible — but what mattered was committing to it as a design principle up front, before the codebase solidified otherwise.

Timing-driven mechanics like parry are the first thing to collapse when routed through server-side judgment: “I pressed the button but it didn’t register” is how feel dies. Parry absolutely has to stay local-first, and that single constraint forces consistency across the rest of the combat system.

TDD to Guard the Pure-Logic Layer

Combat logic is exactly the kind of code where silent regressions hurt the most. A parry window that silently drifted wider or tighter after a feature merge would be unacceptable in an action game — players would feel it immediately.

Phase 2 extracts combat’s numerical logic into pure-data structs, tested via Unreal’s automation framework:

  • FDwanComboState — combo state transitions
  • FDwanDamageFormula — damage calculation
  • FDwanDodgeDirection — dodge direction math
  • FDwanSkillSlotSet — 16-slot switching logic

Right now, 59+ tests pass continuously. The “numbers” of combat are protected here, even as the project grows.

Blueprint-side animation, VFX, and feel-tuning remain non-TDD — intentionally. The key design win of Phase 2 was drawing a clear line between feel code and numeric logic, and only test-guarding the latter.

Sub-agent Reviews on Critical Code

For Phase 2, I ran each GAS implementation through Claude Code’s gas-specialist and network-checker sub-agent reviews — essentially paying for a senior engineer’s pass on risky code.

Results included:

  • “This GA’s Activation Tag may conflict with another ability on simultaneous press”
  • “Cue fires before RepNotify, creating a frame where VFX and values mismatch”
  • “Cost.Stamina GE’s Instant vs Duration behavior is ambiguous, needs an explicit tag”

None of these were things I would have caught by just re-reading my own code. For solo indie dev on a system as large as GAS, having AI as a structured review pass has turned out to be a major force multiplier.

What’s Next

  • Phase 2 damage application: GE_Dwan_Damage + hit detection + lag compensation
  • Phase 2 Combat/LockOn module: enemy targeting and switching
  • A combat prototype video — the Parry feel demo as the core pitch asset
  • Phase 3 Phase System: dungeon progression management

Wrapping Up

Dungeon Wanderers Phase 2 has its combat skeleton in place. GAS-native design, LocalPredicted-first, TDD-guarded pure logic — design principles forged from past failures, now holding up as the foundation grows.

Next: damage application and lock-on. Once the parry feel demo video is recorded, I’ll share it here.

Copied title and URL