Enemy Combat System

After finally workging through the rest of my enemy navigation bugs, I decided to begin working on the real enemy combat system. I decided that I wanted something that was flexible, easily extendable, and fully data driven. I began by drafting up a design document describing each layer and how the system would work.

  1. EnemyAttackData a scriptable object defining the attack configuration such as animation, range, cool down, damage values, and damageVolumeId.
  2. EnemyAttackSequence an abstract class that exposes EnemyAttackData GetNextAttack().
  3. RandomAttackSequence a class that extends EnemyAttackSequence. It deals with randomly selecting the next available attack (that’s not on a cool down).
  4. EnemyCombatController a component that exposes attackSequence via [SubclassSelector] in which allows me to select any class that extends EnemyAttackSequence. It also handles a dictionary of DamageVolumes keyed by the volumeId which exposes a HitDetected event, as well as applying damage via the DamageProcessor.
  5. EnemyAttackState a simple FSM state that drives the attack behavior, using the combat controller (and EnemyContext). It listens to the animation events including AttackFinished, DamageStart, and DamageEnd. By defualt this state remains active until the player is no longer within attack range or something interrupts it such as stun or death.

That’s the basic system. There’s of course a lot of internal logic I’m not explaining here, but the beauty of this system is that it’s extremely generic and fully data-driven. In theory, any enemy, including ranged and melee attackers can use the exact same logic. Where it’ll really shine later is when I combine my custom utility AI system with it.

Bosses and Utility AI

Eventually I’ll extract some of the logic from the existing EnemyAttackController and create two new subclasses (or I’ll convert it into an abstract class or interface) - something like BasicEnemyCombatController and BossCombatController. For basic enemies, sequences are always pre-configured. Basic enemies normally only have 1-3 attacks they can use. Instead of RandomAttackSequence I’ll create a SequentialAttackSequence or RangedAttackSequence or whatever..

The primary difference between basic enemies and bosses will be that bosses can have multiple sequences which will be selected via the utility AI system. How that’ll work is essentially by assigning each action one or more conditions and then selecting the action in which has the highest condition score. These conditions can be anything from LastDamagePercentOfMaxHealth, TargetMovedAway, TargetHealed, TargetStunned - and the list can go on and on..

Each action then will be configured to select a specific attack sequence. Once committed, this new sequence will play out until interrupted or it finishes, upon which time, we’ll ask the utility system for the next sequence.

Design Decision

This design was my original architecture. While I ran it past a few different LLMs which generally stated it was a solid plan, I ultimately decided on it because it makes sense for my game. It’s not overly complex structurally speaking, but is highly configurable and allows the much of the same enemy logic to be reused without modification. Perhaps all basic enemies will always use a random attack sequence - and in fact, if I only ever assigned one attack configuration to it, there’s no functional difference to a random selection or a single “sequence” but the magic is, it’s all editor friendly.

Using [Serializable] on each EnemyAttackSequence class, I can configure their properties in the Unity inspector. There’s zero need for them to be full monobehaviors. I can swap them out whenever I want in the editor without touching a single LOC.

Conclusion

I may find some shortcomings down the road, but it was one of those systems where all of the pieces just fell into place with zero pain. In my experience, when this happens, it’s typically been a pretty good sign that the design is solid. Critical hits? Increase the critical hit chance value. Want knockbacks? Increase the knockback force. Want 3 points of physical damage and 2 points of necrotic damage? Select the damage attributes and assign them a value.

The only thing I’ve left out for now was integration with the status effect system, but that’s literally just adding a status effect configuration to the EnemyAttackData SO.