Character AI behavior
Last updated
Last updated
In this tutorial, we will introduce AI as a way to control non-player characters in games using standard techniques such as navigation meshes and behavior trees. This tutorial is built on top of the tutorial Character movement programming. We will have one AI controlled character that can detect when the player gets close and start chasing the player.
The complete code for this tutorial can be found on GitHub using the link below.
You can also play the finished demo with this link.
https://hologyengine.github.io/ai-tutorial/
You can download the code from the project above and go through the tutorial to understand how it is set up and how everything is working.
You can also build on top of what you created in the Character movement programming tutorial.
In this tutorial, we will use something called a behavior tree. This is a hierarchical structure used to model the decision-making process of AI entities. It consists of nodes representing tasks or behaviors, organized in a tree-like structure.
Root Node: The starting point of the tree.
Composite Nodes: Manage child nodes and determine their execution order (e.g., Sequence or Selector).
Decorator Nodes: Modify the behavior of child nodes (e.g., add conditions or loops).
Leaf Nodes: Represent individual actions or checks, such as moving to a location or detecting an enemy.
Behavior trees are flexible and modular, making them popular for creating complex, dynamic AI behaviors in games.
For our AI controlled character to be able to know what path to walk to reach a certain point, we will use a navigation mesh.
A navigation mesh (nav mesh) is a simplified 3D representation of a game environment used for pathfinding and AI movement. It consists of interconnected polygons that define areas where characters can walk, avoiding obstacles and inaccessible terrain.
You can read more about navigation meshes and how to use them in Navigation.
To start, we will create a new Actor class which extends the Character actor class. Create a new file called ai-character.ts in the folder src/actors/. By extending the Character actor, we inherit all existing functionality such as setting up the character model and animations.
The first thing we do in the code below is to override the property modelName and change it to another model asset that we have imported to be able to more easily tell the characters apart.
Next, we inject a couple of services that will be used for navigating and querying the world for the player character. We declare a property that will hold our behavior tree that we will implement in the next steps. The behavior tree is created in the onBeginPlay method which is only called when the game is playing and not in the editor as we don't want the character running around while we try to edit our scene. To ensure smooth behavior, we update the behavior tree every frame with the time since last update using the tick function on the root behavior tree node.
You also need to export this class in the file src/actors/index.ts so that it will be available to add in the editor.
Now, we will build on the code from before to add our behavior which consists of patrolling and if a player is near, start to chase it until it runs too far away, at which point it should return to patrolling.
The design choices in this tutorial is just made for illustrating how it could be implemented. For your own game, you can change parameters and logic to better fit the game play you want to create.
The first thing we need for patrolling is finding the location to which the character should move towards. We will accomplish this with a custom leaf node.
Add the following code at the bottom of the file ai-character.ts.
In the constructor of this code, we define 3 things.
private navigation: Navigation
This is a service that will need to pass from the actor and is used by the node to query the navigation mesh we created before.
private currentPosition: Vector3
This is a reference to the current position of the character that will be used for knowing if we are too far away from our initial spawn point and to select a new random around the character.
startPosition: Vector3
We pass in the start position which is going to be the initial location at which the character was spawned. We clone this position to ensure that it is not being updated as we are just passing in a reference.
We define the property foundPosition
which is an output of this node that will be used in another node of our behavior tree.
Inside the tick function, we have our logic for finding the position to which we should move.
First, we have the following code which simply checks if we are further from than start position than a specific distance defined as another property on the node. Because we later will just pick a point at random around the character to walk from, this help us ensure that we don't wander too far away from the initial spawn point.
Then, we have the code for finding a direction at random around the character and adding it to the current position with a configurable distance to get a new random point.
Using the navigation service, we find the closest point on the navigation mesh to this random point. It is possible that no such point can be found such as when the navigation mesh is is still being generated. In this case, we return the node state FAILURE to prevent running the next step in our sequence of actions.
With our new custom node defined for finding the patrol position, we can update the createBehaviorTree
function with the code below.
An instance of the class is created and the distance is configured to 3 meters. Then, we use a built in node that can be used with the built in character movement controller to move the character toward a specific position by setting its target property to a function that returns a position vector.
After creating these nodes, we create a sequence node which is a built in composite node that lets us define multiple nodes that should run after each other as long as the previous node in the sequence was successful. After the sequence we add a WaitNode
to not do anything for a random duration (expressed in milliseconds) between 0.3 seconds and 3 seconds.
The next node we create is a SelectorNode
with our patrol node as it only child. The selector node will run any of its child node for as long as they are running or successful. If a child node fails, it will try the next one. For now, we only have one node which is our patrol node but later on, we will have another node to chase the player.
The last node is the RepeatNode
which will run the provided node in a loop forever. This will be the root node of the behavior tree. We normally want the root node to be repeating so that it can retry other nodes in case they fail.
To chase the player, we need to find the position of the player which can be done with a new custom node. Add the following code at the bottom of to the file ai-character.ts.
The property foundPlayer will be our output that can be used by other nodes. In the constructor, we have a reference to the world and to the AI controlled character itself.
In the tick function, we first try to find the player character by filtering actors of the specific type Character. To ensure that we don't find the AI controlled character itself or another AI controlled character. We then filter character to only find one if it is within 5 meters from the AI controlled character. If we find the player, we return SUCCESS, otherwise FAILURE. By returning FAILURE, we ensure that the sequence will implement next does not continue and also tell the selector node in the behaviour tree to try another child node.
Next, we will add the following code to the function createBehaviorTree
in the file ai-character.ts just before the line where we create the SequenceNode
.
First, we create an instance of the FindPlayerNode class we created previously to which we pass in the injected World instance and a reference to the actor itself. We then create another instance of the built in CharacterMoveToNode
and give it a target of the found player's position.
In our sequence node, we first add the findPlayer node as our character needs to know where to go to if there is a player nearby that was found. If no player is found, the sequence itself will fail.
The next node we add is the moveToPlayer node. However, for some added functionality, we use a built in decorator node (RepeatUntilNode
) to stop moving toward the player in case the player is farther away than 5 meters. This decorator node takes a function as a second argument in which we have our logic for when the character to stop.
Lastly, we add a wait node to the sequence so that the character briefly stops when it reaches the character. This adds a bit of realism such as when the player is starts moving after the character reaches the player, the AI controlled character doesn't immediately start moving also but instead takes a moment to react.
Now that we have our chasePlayer node, we need to add it to our selector. Do this by adding a line to where we created the SelectorNode previously. The order in which we add the child nodes is important. The nodes will be selected in the order in which they were added. If we were to switch the positions of these nodes, the selector node will prioritize the patrol node which will go on forever even if a player is nearby. By adding the chasePlayer node first, the behaviour tree will always attempt to run the node on each tick, checking if there is a player nearby. If no player is nearby, the chasePlayer node fails and it then resumes patrolling.
Now that we have our new AI controlled character, we are ready to setup the scene. The scene created in the previous tutorial contained a landscape and a spawn point. In addition to this, we need to add a navigation mesh and and our new AI Character actor.
The first thing we need is a navigation mesh which can be added by selectin Actors in the Asset Browser and dragging NavMesh into the scene.
After a few seconds, you should see a yellow grid in top of your landscape like below which displays the walkable area. This is useful for ensuring that your AI controlled characters are able to move where you want them to be able to move and vice versa. You can disable this by selecting the NavMesh object in the outliner and set "Debug" to "No"
Select Actors in the asset browser where you now should find the AICharacter actor. Click and drag this into the scene to place it. Ideally, place it somewhat nearby the spawn point so that when playtesting, you will be able to find it. In case you don't find the AICharacter, ensure that it is exported in the file src/actors/index.ts. Also, ensure there are no compilation errors in your actor code.
That's it! You have now implemented an AI controlled character that will start chasing you when you play the game.
To playtest the game, click the play button on the top right which should open the game in your browser.