I've created numerous enemy AI patterns. My first tries were not the brightest. Because I wanted to challenge myself to get better at coding, I challenged myself on this subject.
That was a great training for me, but it took a lot of time and energy. Event though I learned a lot from that challenges, I always wanted to get a job in game development, so I needed to research the common technics that were used.
Coroutines
In my researches, I encountered the use of coroutines in simple behaviour scripts. The logic was simple: Using a while loop inside of the coroutines created a mini update function for our use.
So I decided to use it in my melee enemy AI script. Enemy's behaviour logic was simple:
1.a) Idle mode.
1.b) If it has a patrol point designated, "Patrol Coroutine" is controlling the movement back and forth.
2) If player is in detection range, "Patrol Coroutine" is stopped and "Combat Coroutine" is initiated.
3) If player is out of max range, "Combat Coroutine" is stopped and "Reset Coroutine" is initiated. "Reset Coroutine" moves the enemy back to the starting position and upon arrival, "Reset Coroutine" is stopped. Then the behaviour loops back to the First Logic (Idle or Patrol).
With these three coroutines, we have a general concept of a basic enemy. But what's happening inside of these coroutines?
Macro Behaviours
1) Patrol Coroutine:
We have an enemy object which includes a navigation mesh agent component that is waiting for a destination, so that it can move the object there.
Our coroutine's job is simple: give the coordinates of the patrol point to the agent component and wait for arrival. When arrived to the patrol point, give a new destination coordinate (which is the starting point) to the agent. Wait for arrival. Now we are at the beginning.
Sounds amazing to create a behaviour pattern when you think this is all you need to do. But here was the harsh truth for me: Even though the enemy object was "technically" patrolling, it was actually ping ponging back and forth. No waiting, no time for turning, just constant movement that looked ugly.
In my opinion, that's why the hard part of writing a behaviour script is not the implementation of "Macro Behaviours". They are easy to plan, and write. But "Micro Behaviours" are the hard and the important parts. And in our case, they are the factors that save your enemy object from looking like a ping pong ball.
For example, after arriving the destination, "Micro Behaviour" can tell the agent to wait for couple seconds before moving to the other point. After some time, enemy object can initiate a rotation before starting to move. And all these little details makes our behaviour script more convincing.
Of course there are tons of micro behaviours in these coroutines and I won't be talking about how every one of them work.
2) Combat Coroutine:
Just like the patrol coroutine, combat coroutine has micro and macro behaviours. The macros are:
A) Strafe State:
Waiting and walking to sides around player between attack combos.
B) Gap Close State:
When strafe state ends and player is not in the attack range, enemy object starts to run towards player. If player close enough, attack state initiates.
C) Attack State:
And BAM! Attack the player. Can attack multiple times if player is in range.
These macro behaviours are the pillars of our combat logic. But without the micro behaviours, it is again, ugly and unconvincing. And if we want an AI that has adjustable complex behaviours, we need to add a lot of micro behaviour logic to our code.
Micro Behaviours
1) Turn, then Move Logic:
Just like in the patrol coroutine, we have an unconvincing enemy movement where it's trying to move to the player without even facing them.
So I use dot product values to determine if enemy is looking too far away. If true, stop moving and just continue to turn. If enemy's forward gets into the specified dot product range again, then continue the movement.
2) Aggression Percentage:
Aggression Percentage: 30%
Aggression Percentage: 100%
When combat initiates, enemy has two possible state options to start with. Attack state or strafe state. For example with an 30% aggression percentage, enemy chooses "Attack State" over "Strafe State" with 30% possibility. It's pretty simple.
One of the reasons we implement this is: when there are eight or nine enemies at the same field, you can give them lower aggression, so that they would not be all attacking at the same time. Or when there are less enemies, we can give them higher aggression.
By the way, this system does not work for one time. This aggression system is working in every situation that the enemy gets out of combat coroutine like getting poise broken and guard downed.
3) Poise System:
A real basic poise system. Enemy has a poise bar. When enemy is damaged, its current health is reduced and at the same time its current poise is reduced. If it reaches 0, enemy's poise is broken and guard down animation plays out. During this waiting time, combat routine stopped.
After the guard down time has ended or enemy has been damaged, the combat routine is started and current poise is full again. If enemy is not taking damage for a specified time, poise starts to regenerate until bar is full. This poise regeneration logic is handled in a seperate coroutine.
4) Attack Step Window:
When enemy is attacking, movement and rotation of the object is controlled by methods. They can slow or completely stop the speed of movement and rotation. That can give us more convincing attack movement for enemies.
At the start of the attack animation, enemy's movement is stopped and rotation speed is slowed. Rotation speed is back to full for a quick turn to the player at the last few frames before swinging the attack.
Rotation is stopped and movement is turned on during the attack swing. When swing ends, the movement ends too. When animation ends, movement and rotation speed is back to default. This attack movement and rotation rule set is called attack step window.
5) Attack Selection and Heavy Attack Modifier:
Heavy Chance: 20%
Heavy Chance Modifier: 5%
Heavy Chance: 100%
Heavy Chance Modifier: 0%
This micro behaviour basically chooses an attack when available. Our enemy has 2 attack types. Basic and heavy attack. Attack selection method makes a decision about doing heavy or basic attack. And just like the aggression percentage, our enemies have a heavy attack chance. Let's say it is 20%.
With that kind of probability, enemy will most likely choose the basic attack. But every time the basic attack is chosen, the heavy attack chance modifier is added to the heavy attack chance. Let's say it is 5%. If heavy attack is chosen in any point, the heavy attack chance is back to the default value. That gives us a great opportunity to create more complex enemies.
6) Damage Tolerance:
When enemies take damage, they are usually staggered a bit. Stagger will cancels their attack in midway. But this mechanic is really harsh for our poor enemy AI. Player can beat enemy without a sweat. That's why I added Damage Tolerance micro behaviour which counts all the damages taken.
When current damage count is reached to the damage tolerance limit, enemy gets a buff called "Stagger Block" for specified seconds. In this window, enemy cannot be staggered. And immediately, enemy's heavy attack chance is raised up to 100%, so that it can make an uncontested comeback against the player.
After all these buffs, enemy's damage counter is back to zero. This micro behaviour gives us a great opportunity to create enemies that needs to be dealt with caution.
7) Attack Limit:
When player is in the attack range of the enemy, attack state gets activated and enemy attacks until the player is out of range. But that's a big problem for player. This enemy needs to stop sometimes. So I added attack limit behaviour. It is simple, when enemy attacks, current attack count incremented. If attack limit is reached, enemy stops attacking and gets to the strafe state even though the player is still in the attack range.
Heavy attacks increment the attack count to the attack limit. Let's say that heavy attack tires the enemy out and they need some breathing time. That micro behaviour gives us the opportunity to adjust enemy's combat aggressiveness.
8) Min-Max Wait Timer:
While enemy is waiting in strafe state, there is a timer that counts. Until minimum wait time is reached, enemy is not capable of attacking even when player is in the attack range. When timer is reached to the minimum wait time, enemy will get to the attack state if player is in range. If not, enemy will continue to wait until the maximum wait time.
After that, enemy will no longer wait for player and gap close state will be activated for attacking. This micro behaviour gives us the opportunity to adjust enemy's wait time between attacks and close range aggressiveness.
Great, now I have talked about all the important behaviours of my enemy AI script. This macro and micro behaviour model gives us a lot of opportunities. Just think about it, with adjusting all the behaviour parameters, you can create various enemy types.
Agile but fragile enemy example
Slow but tough enemy example
For example: a less aggressive, slow but hard hitting brute enemy. Or an agile, highly aggressive but fragile enemy. And all these with the same AI behaviour. All thanks to the coroutines, I've written this more efficiently and quickly.
Prototype's GitHub Link: github.com/OktayBaysal/Enemy-AI-with-Adjustable-Aggression-System-with-Coroutines