Enemy AI in Godot

15-08-2022

🌱

6 minutes

Breadcrumbs

This is the first solution I encountered where you allow the player to drop objects that disappear overtime so if the entity loses sight of the player, it decides to look for you breadcrumbs instead. I tried out this solution but I didn’t really enjoy, I wanted something more flexible and smarter. So I searched onwards. But learning this wasn’t a waste of time because through following the tutorial suggested in this article I grasped fsm much better.

Nav2dServer get_map_path()

Next I tried a new method from nav2d node. The Navigation Node provides navigation and pathfinding within a 2d area, all you do is add it as the parent of ur tilemap, add nav collisions to the tilemap that the entity can navigate on and ur gucci, you then have access to the get_map_path function, which returns path to the entity your following.

The problem with the path that was generated was that it skimmed the collider and the entity would get stuck, i put the problem on reddit and the only solution was adding non navable tile “padding” where the entity cannot go. But i didn’t like this solution because u can go to a padding tile ull be safe because the entity cannot walk on that tile, also if ur entity is bigger than a tile, it would still get stuck

So i scraped all my work again and searched on


Context Steering

So while surfing the web to get ideas for a really smart AI I encountered a video by Game Endevor. I can’t recommend this video more but to give you the gist of it, he made an AI that used Context Steering behaviors, which keeps a record of desirable and undesirable directions and makes a decision based off those parameters. He didn’t provide code for the implementation so I really needed understand the system before tackling it myself.

Thankfully he suggested a chapter from an AI book which was the perfect place to start. The chapter was 11 pages and after skimming it, I felt enlightened. It outlines all the obscurities with implementing this context steering, check it out if ur coo, btw I wanna give a shoutout to mah boi Craig Reynolds for developing the system, he’s my dawg. After reading the chapter I was so stoked, I broke it down and thought about how I would implement the system in godot.

Firstly I knew I had to have an interest vector. In my case my entity would be interested in chasing us, but this system is so flexible, you can make multiple behaviors interesting, for instance circling around a player, running away from the player, moving towards certain items

But for the first implementation i chose 1 variable, moving toward the player

So in the get_map_path function I’m getting the first index in that path which is vector in the direction of the player

But I had to convert this vector into a single number to represent interest.

So I added 8 raycasts around the entity representing the 8 directions the entity can move. I also have an array which converts these raycasts as a vector, 01 is down, 10 is to the right ect.

Now I have a vector pointing toward the direction of the player, what I do next is get the DOT product of the player direction vector, against the array of vectors representing each direction the entity CAN move. The result is a new vector with values between -1 and 1, the higher the value, the more interest the entity has in moving in that direction. If we have 2 areas of interest we can add these 2 dot products together, but beware, this can mess up the results, but I will explain why in a minute.

Next we need an array of danger, so we added those 8 raycasts around out entity and these raycasts can be used to detect if something is interesting and we want to move toward it, but it can also be used if something is bad for out entity and we should move away, for instance,

A wall Another entity Something that can damage us A distance too far away from us

So what we do if we detect a wall, is we take that direction and we add a value a positive value representing the danger. Then we will have an array of areas our entity DOES NOT want to move. What we do with this array is we subtract it with the interest array but BEWARE, remember I said the dot product is between -1 and 1 and that we can add dot products together, if we choose a small number representing danger like 1 and in the previous section we had an interest of 1,5 in a particular direction because we added an interest of 1 and .5, it would leave us with ,5 in a position thats dangerous so 1 to represent danger is probably not enough in certain situations so keep it in mind, I also have padded out the danger so that the directions adjacent to the danger are also a bit dangerous.

Once we get this array of numbers representing the interest in each direction we convert the highest number into a direction and that is the direction our entity should take!

Now this actually did work, but I was still getting some stutters, because the entity wanted to move in a direction, and then that direction detected a danger to it moved away no danger so then it moved back danger and the process went on into infinity. So this is where steering force came in, because before I was just plugging the best direction straight into the move and slide function and calling it a day, but this is not the correct method.

Now remember our homie Craig Reynolds well he talked about idealized vehicles, as a series of three layers, action selection, steering and locomotion, so we already selected the action, and now we need to steer the damn thing. We can ignore the locomotion because our entity is simple, we tell it a direction and it moves, we don’t have to control limbs here.

So there is actually an equation that delivers realistic steering and the best part is (its simple)

steering_force = desired_velocity - current_velocity

So instead of plugging in the interest directly into our move in slide, we will instead make the interest the Acceleration, and we will add the acceleration to our velocity, the really cool thing about doing it this way is I can control how fast the entity turns. A good way to think about this is by imagining an elephant chasing a target and a cheetah chasing a target, if that target makes a sharp turn the elephant will make a much rounder turn than the cheetah which will turn very sharp. And when I get the acceleration I can multiply it by a number, the higher the number the more sharp the turn, the lower the number, the more rounded the turn and when depending on the speed of ur entity, you must tweek this value if you’re still getting stuck on colliders.

. And when this was all complete the ai worked like a charm and I combined this solution with state machines to get some really cool results, you see because of this interest, we can tell the entity to favor whatever direction we want. So in the game endeavor video he had cool combat behaviors where the entity circles around and comes in for the attack and then goes back out and I wanted to replicate this just to see how sharp my implementation was, when it worked it made me feel SOOO good!

But I wanted to program my own unique boss, so i came up with a boss area, and I prototyped the level using Tiled which made the level design process so easy and i thought of different behaviors for the boss. And my mind was amazed by the potential of the system. You see in phase 1 I favored movement away from the player because Silenus has a ranged attack, but I was having an issue where he was hugging the walls too much, so I favored running away and staying in the middle of the arena, and it worked so splendidly I wanted to cry.

Now I want to take some time to discuss the finite state machines I’m using, because in conjunction with context steering, it just makes the entity so much smarter.

So the important rule of thumb is that the state SHOULD never make the decision of changing states, each state should simply emit various signals and the entity should interpret that signal and decide which state to change to. For instance lets talk about the circling behavior from before, we have a generic chase state that runs toward our player, and once the entity gets within a particular distance it can emit a signal and our entity can start circling the player, but with that same chase state another entity can use it and once it gets to that same particular distance and it receives that signal it can ignore it a keep chasing our player and get right next to it get a signal that its within range and then change to an attack state, when you set up the state machines like these it makes it very flexible and it makes it so much easier to have many enemies with different behaviors while reusing the states.