So I decided to do a short post on implementing AI in Blueprints.
AI can mean so many things - but in this context I mean creating the simple brains behind an enemy character in a game. The same principles generally apply to most games.
In this first post I am NOT going to use either Behavior Trees or the Nav Mesh. Both of those tools are great, but this post is meant to focus on basic ai concepts and less on the tooling. See here for a post on Behavior Trees that I did in the past.
As I continued to work on my game jam game post jam, I wanted more enemies. I was envisioning having various skeleton enemies - eg. a swordsmen, mage, and archer. I wanted these enemies to pursue, attack, and retreat as if they were sentient beings.
To implement this, I went with a simple hierarchy: A base EnemyCharacterBP class based on the standard Character and a set of derived specialized child classes like EnemySwordsmenBP or EnemyMageBP. Thus, the hierarchy has a very shallow depth of 1. Initially I was planning to prototype in blueprints, and possibly moving stuff into c++ as needed. Prototyping in blueprints is orders of magnitude faster. The only downside is that blueprints are slower. So once the design has stabilized, it often makes sense to move performance critical pieces into code.
One thing to consider with this approach, I can easily create a base c++ enemy class between Character and EnemyCharacter since Character is a c++ class. But it is not possible to create a c++ class parent class for the specialized bp class, since both EnemyCharacterBP and EnemySwordsmenBP are blueprint classes. Thus, I went with:
EnemyCharacter (c++) -> EnemyCharacterBP -> EnemySwordsmenBP
SIDE NOTE - ANIMATION
Another thing to keep in mind is that all animation is handled by our AnimationBP. The general pattern here is for the AnimBP to pull the info it needs from the owning pawn during UpdateAnimation. The current EnemyState is an example of information the AnimBP might use to transition.
Locomotion and movement
Every enemy needs to move. In this case, I am using root motion to move the character.
With Root Motion, the animator adds movement to the root bone, and this movement is applied directly to the character. See here for a prior post on root motion.
Thus, the core of character locomotion simply consists of determining what direction to face and how fast to play the blendspace.
Without root motion, we would simply need to use something like ApplyMovement to our pawn.
A critical task for most enemies is to select a target. I chose to have the enemy select the closest visible hero. This will work, even for coop. Basically, get all actors of the hero class. If they are withing sensing range, then we do a LineTrace test for visibility. See this for a quick side note about GetAllActorsOfClass performance. We only do a visibility Line Trace if a hero is in range.
Rotating towards an enemy is actually very easy to do:
FindLookAtRotation - This gives to rotation required to rotate a direction Vector A to a Target Vector B.
The speed of rotation can be controlled by lerping to the target rotation
SIDE NOTE - LERPING
The Lerp family of nodes are a critical addition to your gamedev toolbox. To review, general lerp will return a value between A and B based on an alpah value of 0 to 1. There are a couple of variants - we will use RInterp To to rotate.
BTW -A common gamedev idiom involves lerping toward a target value based on delta time. This is slightly non-intuitive from the typical geometric interpretation, since you eventually arrive at the target even though alpha is always a small delta time value close to zero and never close to 1. This works because the value being lerped toward the target is continually updated every tick as well.
Another backbone of our ai is just a simple StateMachine. A StateMachine is another core gamedev pattern. It is basically a list of unique states coupled with a set of rules that govern transitioning from one state to another. (Again - the entity can only be in one state at a time). For example, the enemy swordsmen might pursue our hero until he is in range and then attack.
Pursue (Locomotion forward)
Strafe (Locomotion sideways)
Retreat (Locomotion backward)
Flee (Turn and Run)
Attack (Attack Animation)
These states can be implemented as an Enumeration. An enumeration is a type defined by "custom list of states".
The core of the state machine enables you to define behavior when changing state. To accomplish this, we define a simple interface:
These functions define the interface for a basic state machine.
As long as CurrentState is only changed through ChangeState and never modified directly, we can be sure the appropriate EnterState and LeaveState will be called in derived classes. EnterState and LeaveState provide a place to centralize logic that must be done.
It is surprising how such a simple approach can help you define behavior that might otherwise result in fairly complex code. It really helps to centralize code around transitioning to and responding to state changes.
One note regarding a StateMachine and multiplayer. Only the server should change state - let a repnotify handle firing the OnEnter and OnLeave on the clients.
Another thing for multiplayer - clients should only process "cosmetic" changes. Any events that change state need to only occur on the server.
Periodically, our enemy needs to evaluate the world and decide what to do. I am calling this the BrainTick. The brain evaluates the rules that determine what state to be in. These rules typically involve evaluating factors - things like Range, AttackCooldown, Health. This tick does not necessarily have to happen every frame. If we have a lot of enemies on screen, it may help to only evaluate this every couple of frames.
"Brain Factors" : factors that the ai must consider to determine state.
TargetRange - The distance to the current target. The target is typically the hero, but could also be a skeleton to resurrect or another npc to attack.
AttackEnabled, AttackCooldown - Enemies need time to recharge their attack. During this window their behavior may change to a retreating or avoiding stance.
Health - Weak enemies close to death may need to flee.
Aggro - More aggresive enemies may run and attack like a banshee.
NearbyDeadCompanions - The Necromancer needs to be aware of any dead skeletons in range in order to move and resurrect them.
Since our behavior is so dependent on range, it is a primary driving force for determining changes to state and it is critical to define core distance thresholds for state changes. Think of these as concentric circles around our entity. As the target moves between rings, we re-evaluate core state.
Most of this can boil down to determining what state the enemy should be in based on the current factors. Remember, these states are pulled by the AnimationBP to determine what animation to play. Since the animation contains root motion, this is all that is needed for our enemy to pursue or attack the hero.
For example, here are a couple of rules to illustrate:
If we have no target in SenseRange, then set state to Unaware, otherwise rotate towards target.
Else if target is inside Sense but outside Pursue, set state to Aware
Else if target is inside Pursue but outside Attack, set to Pursue
Else if the target is inside Attack range and AttackEnabled, then Attack
Else if the target is inside Attack range and AttackDisabled, then Evade (eg Retreat or Strafe or Flee)
As you can see, this is a fairly simple enemy. At a high level, during each BrainTick. the ai simply evaluates a few core factors to determine what state to be in.
Then the AnimBP, HUD, and other systems can correctly responds to these changes in enemy state.
By playing the correct animation, root motion drives the character to move, evade, attack, etc...