Making Combat Feel Good: Fair, Readable, and Responsive AI


Author: Anthony Halstead

Date: 10/23/25

Combat didn’t feel good. Enemies either dog-piled the player or hung back awkwardly. Some kept re-pathing without purpose; others stopped to shoot like turrets. The animations looked disconnected from the intent. All of that adds up to “noisy” combat: hard to read, frustrating to fight, and unfun to watch. 

This post is about the concrete systems I put in to make combat feel fair, readable, and responsive and the specific choices that got me there: a token system to throttle attackers, a positioning brain to pick smart destinations, and animation layering so movement and attacks play together smoothly. 

What “feels good” actually means  

Fairness – The player should never be stun-locked or deleted by 12 enemies acting in the exact same frame. 

Readability – You should be able to glance at the field and know who’s engaging, who’s flanking, and who’s waiting. 

Responsiveness – AI reacts quickly to changing range/LOS and keeps moving while shooting; no stop-start herky-jerk. 

Those pillars guided the three main systems below. 

Attack Token Pool 

Problem: When every enemy can attack at once, you get damage spikes and visual chaos. Spamming timers or random delays doesn’t scale and still produces “unfair” moments. 

Solution: Put an attack token pool on the target. 

Implemented as a reusable UAC_AttackToken ActorComponent that lives on any attackable actor (e.g., the Player or a boss). 

Enemies request N tokens (integer) before they attack; it’s all-or-nothing. 

The pool tracks who holds how many; tokens auto-release on requestor death and are explicitly released when the attack ends/aborts. 

Why this feels good: 

Hard cap on simultaneous attackers  ->damage is spiky in a designed way, not random. 

Creates organic waves: some enemies engage while others reposition or threaten, which reads better and feels fair. 

How it plugs into behavior: 

A tiny BT service sits near the Attack task. 

It tries to reserve a configured Cost (e.g., 1 for riffraff, 3 for a boss slam), writes a TokenGranted bool, and releases on finish/abort. 

Decorate the Attack task with “TokenGranted == true” which is also influenced by    “InDesiredRange/CanSeeTarget”. 


Positioning Brain (smart movement that updates) 

Problem: My first trees used “MoveTo -> Attack.” That’s fine for melee, but not for ranged units. When I paralleled it, I ran into stale Blackboard values (range/LOS not flipping when the task wasn’t running), so attacks gated wrong and movement stalled. 

Solution: Centralize combat movement decisions in one BT Service that always ticks,

reads Player (Object) from the Blackboard. 

Computes LocationTarget (Vector) to move towards, and updates booleans: 

InDesiredRange: RangeMin ≤ dist ≤ RangeMax 

CanSeeTarget: controller line-of-sight check 

Chooses positions via one of two paths: 

Code sampling (default): sample a ring at an ideal range using  nav-project, score by ring error + travel cost, optional LOS. Styles: MaintainRing, Flank, CircleStrafe.    If an EQS asset is assigned, it can still be run with named params (inner/ideal/outer radii) and take the best. 

 I attach this service directly to the SimpleParallel node, not buried under a child. That guarantees it ticks even if a branch hasn’t started yet, keeping the Blackboard truth fresh. 

Why this feels good: 

Enemies look like they’re trying to get to a good lane (ring, flank, or strafe), not vibrating in place. 

When LOS breaks or range shifts, the booleans flip immediately and the tree responds (retargets movement or stops firing). 


Animation layering (move & shoot without fighting the state machine) 

Problem: Full-body attack animations stomp locomotion or force abrupt stops; strafing looks wrong; turning snaps fight the BlendSpace. 

Solution: Keep locomotion king; layer attacks and turns on top; 

Locomotion BlendSpace with forward/back/left/right variants across speed (idle → walk → run). 

Layered Blend Per Bone splits body: 

Lower body: cached locomotion state machine. 

Upper body: Slot “Action” for fire/reload/melee montages (or anim sequences as dynamic montages). 

Branch filter at  the   spine with a small depth so recoil/upper aim doesn’t pollute the footwork. 

Result: Enemies keep moving while shooting, flanking looks intentional, and aiming   with where the upper body is actually aiming. 

Glue that matters (little things that prevented big headaches) 

Perception ->Blackboard: Sight perception  updates the Player blackboard    key; taking damage from the player also seeds target when out of sight (however this wasnt fully tested as of this post). 

Stale flags: The service writes InDesiredRange and CanSeeTarget every tick, so decorators never get stuck. 

MoveTo observation: Observe the LocationTarget Blackboard key so MoveTo retargets smoothly when the service picks a better spot. 

How these pieces improve player experience 

Fewer “WTF” deaths. Token limits mean predictable concurrency; bosses can take more of the pool; fodder waits its turn. 

Readable intent. You can see who’s lining up a shot, who’s flanking, and who’s circling—no more amorphous blob. 

Smooth pacing. Movement continues during fire; LOS/range changes are answered immediately; the fight breathes. 

What I tried (and kept or cut) 

EQS everywhere sounded great; I kept it as an optional path in the service. For most needs, code sampling with nav-projection is faster to iterate and easy to profile. 

Custom MoveTo wasn’t necessary; the engine task works well as long as it observes your Blackboard vector and you set a sane AcceptableRadius. 

Blueprint anim graph heavy lifting stayed but all gameplay control and decisions are in C++ so behavior stays deterministic and testable. 

What’s next 

Cover & danger weights (prefer points with partial cover; avoid recent impact zones). 

Group tactics (roles that dynamically adjust token cost, or “only one heavy attack at a time”). 

Micro behaviors (burst strafe on being hit; peek-and-shoot loops for elites). 

Authoring speed (per-archetype profiles for instant drag-drop tuning). 

Closing thoughts 

Making combat feel good isn’t one silver bullet it’s a small set of systems that reinforce each other: 

Token pool keeps pressure fair. 

Positioning service keeps decisions live and legible. 

Animation layering makes it look as good as it plays. 

None of these systems are flashy on their own, but together they turn chaos into choreography and that’s where “fun” lives. 

Get Iron Reclamation

Leave a comment

Log in with itch.io to leave a comment.