Designing Combat AI That Stays Readable Under Pressure
Enemies choose targets by faction, share reusable state machines, and take turns attacking so group fights feel busy without turning into a pile-on.
Build enemies that feel different, pick sensible targets and fight in groups without all attacking at once, using reusable Godot components instead of a separate AI for every class.
Working in Unreal taught me to split AI into clear jobs. State machines show what an enemy is doing now. Behavior Trees choose what should happen next. Tasks handle the actual action and know when it's done.
When I built Zero Protocol's AI in Godot, I didn't try to copy Unreal's tools node for node. I kept the useful part: each piece has one clear job, and the state machine pulls those pieces together.
More than chase and attack
I didn't want enemies standing around switched off until the player walked into a circle. They can patrol or wander, notice an ally being attacked, investigate noise, search the last place they saw a target, then go back to what they were doing.
The world also contains civilians and neutral animals. Enemies can attack them, the player can intervene to rescue them, and different factions can become involved in the same fight. This meant the AI couldn't just assume that the player was every actor's only possible target.
The roster also needed to give the player genuinely different problems. Ghouls rush into melee. Ranged casters keep their distance. Summoners add more threats. Grunts swap between ranged and melee attacks depending on the space around them.
I wanted those differences without building every enemy from scratch.
Who to fight, what to do, when to commit
I split the AI into three questions.
The target manager decides who an enemy wants to fight. It removes anything their faction won't attack, then scores the rest by visibility, distance, faction preference and recent damage.
That means two enemy types can look at the same fight and make different choices. A nearby civilian might tempt a mindless ghoul, while a disciplined Raid enemy strongly prefers the player. Taking damage can pull attention for a while, but it fades instead of replacing the enemy's normal priorities forever.
Each enemy also gets a small, stable preference of its own. If several civilians are equally good targets, enemies spread between them instead of all picking the same mathematically perfect choice. Because that preference doesn't keep changing, their targets don't flicker back and forth.
Once an enemy has a target, the state machine chooses what it actually does. Patrol, Alert, Chase, Hold, Attack, Recovery, Reposition, Search and Reaction are clear states, each with its own start, update and exit rules.
Those states work a lot like tasks in an Unreal Behavior Tree. Attack handles the windup and hit. Reposition handles where to move. Search takes the enemy to the last-known position and looks around. Each state also cleans up after itself, shutting down hitboxes and releasing attack permission even if a stun, launch or death interrupts it.
The Combat Director answers the last question: should this enemy be allowed to attack right now?
Fairness without passivity
The Combat Director allows up to three melee attacks and two ranged attacks against the player at once. It doesn't pick targets or tell enemies where to move. It only controls how much pressure can hit the player at the same time.
Enemies ask for a slot right before they attack, not while they're moving in. If one doesn't get a slot, it doesn't freeze in a visible queue. Melee enemies move around a wide ring while facing the player, and ranged enemies keep aiming or finding a better position. They stay active without everybody attacking at once.
Fights between non-player factions ignore this limit. Enemies attacking civilians can stay messy and dangerous, while the pressure aimed at the player stays under control.
Same foundation, different enemies
Most enemies start from one of two shared families: melee chasers or ranged skirmishers. They use the same targeting, movement, senses, reactions and death handling. Each class only replaces the choices that make it feel different in a fight.
A Raid Grunt uses the standard melee pursuit loop, but can branch into a fireball attack beyond ten metres when its cooldown, range and ranged attack permission allow it. Closing the distance returns it to melee behaviour.
A ranged Minion attempts to hold its preferred distance, but entering danger-close range triggers a separate decision between committing to melee or fleeing. At range it can choose between ordinary projectiles, a triple attack or summoning more enemies.
A Skeletal Hound replaces the normal melee arrival with a deliberate circle and pounce sequence. It closes quickly, orbits while waiting for the correct range and attack permission, then locks its landing point during a telegraphed leap.
The same basic rhythm stays readable even when the choices change. Alert, approach, attack, recovery, reaction and death still mean the same thing across the roster.
Pursuit without cheating
When an enemy can see its target, it saves their position. If it loses sight, it goes to that saved spot instead of tracking the target through walls.
Different enemies remember for different lengths of time. Mindless ghouls reach the remembered point and quickly give up. Search-capable enemies can investigate the location, look around and sweep nearby navigation points before returning to their previous activity.
Arena fights can widen detection and memory. That lets the same enemy handle a hunt in an open level or keep the pressure up inside a sealed arena without needing a second version of the AI.
I'm not trying to make AI clever for the sake of it. I'm choosing where enemies can make their own decisions and where the game needs to keep them in check. Targets can change, but not every second. Enemies can search, but not forever. They choose their own actions, but the Combat Director controls how many attacks hit the player at once.
That gives every enemy the same clear combat rhythm, while still leaving room for each class to feel different.