8. Enemies & AI

In this section, we will add simple AI-controlled enemies to the game.

These enemies will patrol their platform randomly until the player comes nearby. When that happens, they will chase the player to inflict damage on collision. If the player escapes, they will go back to patrolling.


Fair warning: this section is the hardest part of the tutorial and will require the combined use of every skill we've learned so far: flow graphs, state graphs, super units, transitions, macros, custom events and variables. Make sure you understand every previous section before starting, because now that the concepts have been introduced, we will go a bit faster than usual. 

When we're done, the final set of graphs will look like this:



Take a short break, stretch your legs, and when you're ready, let's get started!

1. Root state machine

The first thing we'll do is create a root state machine on our enemy. It will have two states:

  • Alive: When the enemy is alive most of the logic happens, like patrolling, chasing and damaging. For the first time, this state will be a super state, meaning it will itself be contain a state graph. Previously, we only used flow states, where the child graph was a flow graph.
  • Dead: When the enemy is killed, it should slowly spin downwards then disappear. This will be a simple flow state that we will implement in the next section, when we add projectiles to kill enemies.

To create the root state machine:

  1. Open the Level3 scene
  2. Select the Enemy object
  3. Add a new State Machine component
  4. Create a new macro for it called Enemy
  5. Apply the changes to the prefab

In the root state machine:

  1. Delete the default Start state, because it's a flow state and we need a super state
  2. Create a new Super State, open it and change its title to Alive
  3. Right click the Alive state and choose Toggle Start
  4. Create a new Flow State, open it and change its title to Dead
  5. Add a transition from Alive to Dead.

At this point, root state graph should look like this:

2. Alive state

Inside the Alive state graph, we will need three child states:

  • Patrol: When the player is away and the enemy is patrolling the platform randomly. This will be a Super State, because the patrol will include different states as well. Does this feel like inception already? :)
  • Chase: When the player is nearby, the enemy chases it. This will be a regular flow state.
  • Damage: In parallel to patrol and chase, the enemy can inflict damage when colliding with the player. This will be a regular flow state as well.

Parallel here means that there will be two "systems" of states in the alive state graph that will run at the same time: one for movement (Patrol or Chase), and one for damage (just one state). To do that, we only have to define multiple start states: a powerful feature unique to Bolt. Our two start states will be Patrol and Chase.

To recap:

  1. Add a super state called Patrol
  2. Add a flow state called Chase
  3. Add a flow state called Damage
  4. Toggle start on Patrol
  5. Toggle start on Damage
  6. Add transitions back and forth between Patrol and Chase

The graph in alive should then look like this:

3. Damage

Inflicting damage to the player is really easy now that we have already created all the health system in the previous section. As a reminder, we added a Damage custom event to the PlayerHealth state graph that took one argument: the amount of damage inflicted. 

Open the Damage state. 

Now, all we have to do is use our On Collision With macro to trigger that event and inflict 1 heart of damage:

That's it! If you test your game now, the enemy should inflict damage to the player, which in turn should become temporarily invulnerable:

Notice how keeping our graphs DRY and organized made this very simple: the collision code is fully handled on its own, and the player is responsible for its own health and damage system. The enemy, a separate entity, only has to trigger a single event. This is an example of how nesting and events in Bolt allow you to create a robust game architecture.

4. Walking

Let's take a moment to plan ahead: we know our enemy has to walk in multiple places. It has to walk left and right when patrolling a platform, and walk towards a player when chasing. That's 3 places already we're we'll need a walking graph. At this point, you should see where this is going: we'll keep everything DRY and create a reusable macro! Create a new flow macro asset called EnemyWalk. This graph will be similar to movement on the player, but not exactly the same.

4.1 Direction & movement

Add a new float input value called direction:


This direction input represents a X axis value. 

  • If it is above zero, the enemy should walk right
  • If it is below zero, the enemy should walk left
  • If it is equal to zero, the enemy should stay idle

Note that this direction isn't a speed: for example, if the direction is -5, the enemy should go left exactly as fast as if the direction had been -1. To do that, we will first normalize the direction before multiplying it by the speed. Normalizing keeps the sign (+ / -), but with an value of 1, unless the value was zero, in which case it leaves it as is. In practice, it means we calculate movement like this:


Notice we use a Speed object variable again. Because we want the player to be a bit faster than the enemies, we will add a speed variable with a value of 2 to our enemy game object and apply the changes to the prefab:

4.2 Flip

Like we did for the player earlier, we'll flip the enemy sprite by setting the X axis of the scale equal to the direction, but only if the direction isn't zero. If it is zero, meaning the enemy is idle, we'll skip flipping altogether and keep the last scale.

You'll notice that this time the Equal unit takes numbers and allows for an inline value for B. This is because Numeric is checked in the graph inspector for the unit:


It works is exactly the same as using the following nodes, but a bit more conveniently:

4.3 Velocity

Like we did for the player, we'll set the enemy rigidbody's X velocity to match the calculated movement variable:


4.4 Animation

Finally, we pass the speed to the enemy's animator controller:


At this point, the final EnemyWalk macro should look like this when zoomed out:

5. Patrol

Now that we have our walking macro ready, we can implement patrol.

Open the patrol state under Enemy > Alive > Patrol:


In this nested state graph, we will need three states:

  • Idle: When the enemy is idle and waiting. It adds a bit of realism to have it "sit and think"!
  • Walk Left: When the enemy decides to walk left
  • Walk Right: When the enemy decides to walk right

By default, our enemy will be idle. Setup your graph like so:

  1. Rename the default Start state to Idle
  2. Add a new flow state called Walk Left
  3. Add a new flow state called Walk Right
  4. Arrange the states in a pyramid with Idle on top


We will now add use our EnemyWalk macro as a super unit in each of these states. Delete every event in each state and drag & drop our macro to create the super unit.

  • In Idle, the direction should be 0
  • In Walk Left, the direction should be -1
  • In Walk Right, the direction should be +1

When you're done, each state should have a single node:

IdleWalk LeftWalk Right

Next up, we will create the transitions between these states.

5.1 Change mind transitions

Sometimes, the enemy should randomly change its mind about where it's going. It might be idle and decide to walk, or change direction, or stop walking. We will call this the "change mind" transition, and we'll create a single reusable macro to handle all of them.

Create a new flow macro called EnemyChangeMind, and set it up like this:

  • Random Range is located under Unity Engine > Codebase > Random
  • On Timer Elapsed is located under Events > Time
  • Trigger State Transition is located under Nesting

This simple transition randomly waits between 1 and 3 seconds to trigger. That's it!

Remember to give it a title so it's easier to understand what it does:

Next step: add it to the graph. An enemy can change its mind from any state of a patrol to any other, so we need back and forth transitions between each state in our triangle:

Then:

  1. Select each of these transitions
  2. Set its source to Macro
  3. Choose the EnemyChangeMind macro

The inspector for each transition should then look like this:

And the state graph should look like this:

If you test now, the enemy should start patrolling randomly... but it won't stop when it reaches the edge of its platform!

Let's fix that by adding another type of transition when the enemy reaches the edge of its platform.

5.2 Reach edge transitions

To determine whether the enemy has reached the end of its platform and should switch direction, we'll use a technique similar to how we detected whether the player was grounded earlier: we will use a downwards circle cast. However, this time, we'll add a small offset in the direction of the enemy, so that our circle cast predicts ahead of time if the end of the platform will be reached:


If that ground check with an offset returns false, we'll know that the enemy soon won't be grounded, and therefore has reached the edge of its platform.

5.2.1 Add parameters to the ground check

Since we already have a GroundCheck macro, we will only modify it to add some parameters. Open the GroundCheck macro we created earlier for the player controller:


Add a new Input unit with three parameters:

  • Offset (Vector 2, default 0,0): the offset to add to the origin of the circle cast from the object's position
  • Radius (float, default 0.3): the width of the circle cast
  • Distance (float, default 1.1): the distance to check downwards

We could leave radius and distance at constants, but while we're at it, why not turn them into arguments to make our macro really flexible?


Then, connect the new input ports to the circle cast node, using an Add to apply the offset:

5.2.2 Create a reach edge transition macro

Create a new flow macro called EnemyReachEdge for our transition. Set it up so that the transition triggers if the enemy is not grounded:


Then, we'll need to calculate the offset parameter so that it points in the direction the enemy is facing. We can use the X axis of the scale to know that, because we flip the enemy in our movement code. We then multiply it by 0.5 units forward, which is a small offset of about half the enemy's width.


Then, just connect the offset vector to the offset port of our super unit:

5.2.2 Add the reach edge transitions to the patrol state

Finally, we just need to add this transition to our patrol graph. Logically, an enemy should only ever reach an edge while walking either left or right, and when it does, it should immediately go in the opposite direction. Therefore, we'll only add our new transition back and forth between the Walk Left and Walk Right states:


If you test your game now, the enemy should properly avoid falling off the platform:

6. Chase

Now that our patrol state is complete, we'll implement the Chase state:

When chasing, the enemy should walk towards the player. Thanks to our EnemyWalk macro, this is fairly easy: we only need to calculate the direction from the enemy to the player and pass it to the super unit. 

Using simple vector maths, we subtract Player Position - Enemy Position to get our direction vector, and take the X component from it. Because the enemy walk macro takes care of normalizing the value to +1 / -1, we can pass that directly:

6.1 Chase transitions

The enemy should start chasing the player when it gets nearby, and stop when it escapes.

Add a Detection variable to the enemy object to indicate how far it starts seeing the player, and apply the changes to the prefab:

Open the transition going from Patrol to Chase:

Here, we will calculate the distance between the player and the enemy, and transition if it is lower than the detection radius:

Rename the transition to Player Nearby:

Back to the alive state, open the transition from Chase to Patrol:

Here we'll implement the opposite check: if the player is farther than the detection range, trigger the transition:

Tip: You can copy-paste everything from the Player Nearby transition, right-click Less and choose Replace, then pick Greater Or Equal instead.

Rename the transition to Player Away and get back to the alive state:

If you test your game now, you'll see that the enemy does chase the player when it gets near... but even if that means falling off the ledge!


Fortunately, this is an easy fix: just use the Reach Edge transition to transition back to patrol when chasing:


Conclusion

You got through the hardest part! Congratulations!

At this point, your enemy should patrol the platform, chase the player when it gets near, and inflict damage on collision.

Here's a full HD screenshot of all the nested structure of our enemy graph (Full Resolution Link). We used the full power of nesting to reuse our code and make a robust AI:


This article was helpful for 23 people. Is this article helpful for you?

Hi just one small point - in 2. Alive State the recap should be "2. Add a flow state called Chase"

+2

Hi again - on recapping this section I there's something I don't understand. In the Patrol logic, how does Bolt decide which state to transition to if the Change Mind random timers pick the same value? E.g. Enemy is in Idle state, so there's two possible transitions >Walk Left or >Walk Right, each controlled by the same logic. If both timers pick the same value at the same time which transition takes precedence? Or, do we presume that because they're floats, the chances are too small to worry about? Or am I missing something else...?

Also, in the very last stage, adding the Reach Edge transition from Chase back to Patrol doesn't work in practice because if the player is still within the detection radius, the enemy goes back to chasing and we get a transition loop - and it falls off the edge anyway! :)

The value of the timer gets picked when the parent state is entered. The lowest value of all transitions will be triggered first, and all other transitions will be ignored after.

If they both pick the exact same value (which is highly improbable), there is unspecified behaviour, meaning either of them could get traversed, and Bolt doesn't define or guarantee an order. Transitions have no notion of priority, they're all checked simultaneously. However in this case it doesn't matter at all, because both decisions would be adequate.

As for the last transition, I'm puzzled because it worked when I tested it (but yes, there's a transition loop; we're just being lazy to be honest!). I'll give it another look when I have a minute.

+7

I tweaked the PlayerNearby transition to include a ground check.  This worked reasonably well.



Yeah, that worked for around 10 tries without making enemy stuck on the edge. Before this fix it wasn't that good :D 
Thanks

That indeed help to improve it. Not ideal but definitely better. 

I extended the graph that checks for the ground to also return the object that triggered the collision. Then I added an object variable to store the output of that as the CurrentPlatform when that ground check is being made. Then before I let the enemy chase, I check to see if they are on the same platform. This seemed to work and then they also should continue patrolling and not get stuck on the edge.

I also tested it.

Set the enemy's damage value to 0, and make the player stand on the edge of the foundation.
Then the enemy falls off the edge.

I believe that two transitions "Player Away" and "ReachEdge" will not be evaluated at the same time.
It is dangerous to set up multiple transitions.

As it is inappropriate as a tutorial, please correct it to the ideal structure.  I hope. > Mr.Lazlo

Thank you

PS.
I am using Google Translate.

Even just standing at the edge, the enemy dropped.

On 4.2 Flip, my enemy flip is scaling the object by the speed variable. if i set it to 2, enemy will scale by 2, if speed variable is 3; enemy scale X will be 3.

I've checked all the nodes and nothing seems wrong.

Are you sure you used the Normalize unit?

Yeah, the Normalize ("Returns the unit length version of a scalar")

I get the error "state transition trigger cannot be used outside a state transition." when I try to play now. (once)  I've been following this to the T and I don't see a rogue state transition trigger anywhere. Any ideas? It highlights the enemy when I select the error in console.

Having the same issue here. Did you find what was wrong?

+2

I found a fix. It's not pretty, but it works. It appears that triggering a state transition from within a super unit is what causes the error. What I did was copy/paste of the units from the EnemyReachEdge macro into both Reach Edge transitions. Again, it's not DRY, but it works...

You need to add the transition the same way as under "5.1 Change mind transitions".

+2

You need to use the Graph Inspector to change the source of the transition to "Macro" and then choose "EnemyReachEdge". Same way as under "5.1 Change mind transitions".

+1

If you added the EnemyChangeMind and EnemyWalk macros as components of the Enemy you will get errors like "state transition trigger cannot be used outside a state transition." Remove those components and your errors should disappear.

I had the same problem! I noticed I accidentally put EnemyWalk Super Unit into each transition instead of using the correct macro, "EnemyChangeMind".

Can't find check box to enable Numeric on Equal unit. Bolt v.1.2.3 Alpha 3

"You'll notice that this time the Equal unit takes numbers and allows for an inline value for B. This is because Numeric is checked in the graph inspector for the unit:"

This should be in the graph inspector, it isn't on the unit itself (for some reason, not sure)

Yes, it's not there:)

Ok having trouble not sure what going on,..   I think it may be the enemy Direction


the enemy is flipping like crazy due the the value in coming from the Get X node  is flipping ?   the distance values going into the node don;t add up they should be much different since the player is so far away from the Enemy ?

I moved the player variable to the application to get the hud to work everything i think is the same as the tutorial

Also tried 3 vectors nodes same problem


I'm missing something simple please help





OK fixed my issue by adding a New Scene Variable for the player ( I already had one as a application Variable ) change the enemy to uses that Variable and all is working

Suggestion.

When you add a Super Unit to a Flow Graph or Super State it should show this. EG if I add a Fixed Update to Damage (Damage being a Flow Graph it shows but not any other objects like Super Unit. It would also be great you could right click on jump to a Macro or State Machine file. As the project gets bigger you want to be able to do this. But like Right click in Visual Studio and say jump to a function that could live in another class file.

Type-O Its Codebase>Unity Engine>Random. Not Unity Engine>Codebase>Random

I had to declare a new scene variable in Level 3 otherwise the the Get Variable > Player for Player Nearby threw an error - it could not find the variable declared in Level 1. I think this is a change in the latest version of Bolt which cannot reference scene variables from other scenes. Is the better solution to create an application variable for all scenes?

Sounds right. Application is across everything while scene is for that scene only and does not five when new scene is loaded

I am having trouble at 6.1 - I have created the 2 transitions 'Player Away' and 'Player Nearby'. I made sure to use the get Application variable (and not scene as shown in tut) for both transition. If I play the scene, and watch the transition graph, it works well (finds the player and its position, compares it to the self position of the enemy... but as soon as 'Less' comparison returns true, I get the following error: "InvalidOperationException: Variable not found: 'Player'. Bolt.VariableDeclarations.Get (System.String variable)."

I cannot figure out where it comes from... any ideas? :)

Never mind... I had kept the Player var as a scene variable in the Chase state... my bad.

I had problems too. If you look in the Chase (6.1) state up in the tutorial, he had put Get Variable: Scene (Player). This should probably be fixed. :)

I honestly feel stupid for this, but the collision between enemy and player isn't triggering for me in the end of Step 3, am I missing something here? It's triggering the Damage event in the enemies State Flow, but not actually taking health from the player, am I missing a piece somewhere?

hi please be sure that your Player is in Player layer and Enemy in Enemy layer, when the collusion occurs. Because you may change the layer of the player to Playerinvincible by code before and because of the bug in the code it doesn't return to Player layer again.

take care

Hello. I have a little problem. When i'm at the edge of one platform, and the monster is at the edge of the other, (the chase program is enable) the enemy is terribly flipping, and ignore the edge. I looked all my codes, and all is good. How can i fix that ? Thanks for your help !

I don't understand why my EnemyWalk  macro don't turn Super unit. 

Hi jussi, how did you solve the EnemyWalk graph input cannot be used outside super input problem?

Your final quick-fix works but in a kinda weird way.

This will result in endless loop and a flickering enemy, which will still find his way to his "death" (lul) and fall from his platform. How would a solution look like in which the enemy won't fall apart a platform? I think I would try to edit the "Player Nearby" and add a "Reach Edge" check in there and only if a player is nearby AND the enemy still don't reach the end of the platform THEN I'll start the transition. Am I wrong / missing something? Btw pretty nice tool and tutorial anyways! :)


Yes I understood why the enemy is flickering, but in Player Nearby there is already a "Reach Edge" with the Ground Check we put into. So I don't know how we could fix that...

when my character hits the enemy nothing happens?

+4

Just in case your enemy is flipping like crazy and doesn't really change directions at step 5.2.2 of this tutorial - change the "Update" event to "Fixed Update" if you have a fast system - both events will trigger on the same frame, before the character will be able to flip :)

I had this issue, and it solved it. Fun debugging experience anyways.

Thanks for this fix, it solved my problem. This makes sense too. Correct me if I’m wrong. Running on my i9 workstation the framerate is fast enough that it processes the commands multiple times in a single frame. The fixed event forces the command to instantiate only once per frame.

At first, I couldn't get why, once the enemy flips and its relative ray cast is also flipped, the branch value still returns false, which creates our loop. But if the false event still exists because you are in the same frame then you do have a potential loop.

This raises more questions in terms of best practices with Bolt, given optimal efficiency within the range of older/slower systems and more powerful setups. Much more to learn, but you do have an amazing product sir.

Thanks for this fix, it solved my problem. This makes sense too. Correct
me if I’m wrong. Running on my i9 workstation the framerate system is fast
enough that it processes the commands multiple times in a single frame.

Thanks for that ! My character kept falling and something was up and this fixed it. Thx.

+1

Please tell me how to implement "EnemyChangeMind" without using "On Timer Elapsed"...

I have an issue, which I assume everyone else doing this tutorial are having too.

Since the 1.4 update the EnemyChangeMind flow macro in section 5.1 has to be changed to this:


However, using a coroutine means that this transition is still executed even after the state has already exited. This results in the states transitioning when they shouldn't, which accumulates very quickly making the enemy "Change its mind" very quickly. See here:

Is there some alternative way to replace the now obsolete On Timer Elapsed unit?

Or some way to stop the On Enter State coroutine when the state it transitions from is exited through another transition? I noticed there are some units to stop coroutines but no way to reference the On Enter State coroutine.

Hope that is clear and would welcome any suggestions. Thanks.

+3

This is how I managed to get it working:


Hope that helps some people.
+4

Hello, there is my fix in EnemyChangeMind to prevent several output transitions. I create graph bool variable which tells if transitions was already triggered

+1

Hello! mat
Your sample is very useful.

I was testing with "start" and "enable".
but "On Enter State" is don't use 

https://support.ludiq.io/communities/5/topics/1989-14-upgrade-guide-coroutines-wait-and-on-timer-elapsed
> Flow Machines: Start or On Enable
> States and State Transitions: On Enter State

As you pointed out, there is a coroutine problem.
I am a beginner, so try improving by referring to your sample.

Thank you!

I am poor at English, so I use Google translation.
I am sorry if it was a funny sentence.

Hi there,
I get an error in step 3. When I hit an enemy the error "MissingValuePortInputException: Missing input value for 'a'." occurs. 

On collision a heart disappears and the health variable of the player decreases by 1. But there is no hurt animation.

In play mode after collison, the Alive, Damage and the Custon Event Trigger turn red in the flow graph. Besides there is no change in the layer visible (Player layer to PlayerInvincible) when I touch the enemy.


Any suggestions would be appreciated, thanks!

+1

I ended up using the On Enter State unit (as a coroutine) for starting the Wait For Seconds timer, and an On Exit State unit connected to Stop All Coroutines. It works but I'm not sure if it's the best way to do this.

Hey, i´v got a problem at 4.1 .
I´m using tthis tutorial to script my own game. I scaled my enemy to 0.14 instead of 1. So when i hit Play, my enemy scales up when i use the script of the tutorial. I fixed it on the Y Axis, so it only scale up on the X Axis.. But i don´t know how and where i can fix it.

As you can see, the X node of Vector3 is connected with "normalize" and "input". 

Is there a possibility to change the X Axis from 1 to 0.14?

Another problem. i´ve got an error in my console and it says : state transition trigger cannot be used outside a state transition. I can´t find the mistake.