The Third Person Controller is the ultimate framework for any third person genre such as third person shooter, adventure, RPG, etc. Both Behavior Designer and the Movement Pack are integrated with the Third Person Controller allowing for realistic AI agents. The Third Person Controller tasks and sample project can be downloaded from the samples page.
Note: for an even more comprehensive behavior tree over, take a look at the Deathmatch AI Kit overview page.
The Third Person Controller sample project contains a complete behavior tree that gives an example of how to create a realistic enemy agent with a behavior tree. This sample project can be played on the AI Demo page from the Third Person Controller webpage.
The goal of this behavior tree is to have the agent react when he sees, hears, or is shot by the player. In addition, the agent should go get health or ammo when he is running low. When the agent loses sight of the player he should search for the player at the last known position. When the agent is not aware of the player at all he should patrol a set of waypoints. This is a screenshot of the entire behavior tree (click to enlarge):
This tree looks complicated but it really is easy to understand once you break the tree up into the different branches. When the tree starts the Is Alive task is the very first task that runs.
This task is parented by a Sequence task so the tree will continue execution one child at a time, from left to right. Conditional aborts are used on the Sequence task to reevaluate the Is Alive task every tick. This setup will stop the tree from executing when the agent dies.
After Is Alive is done executing a Parallel task is used to run branches at once. The Get Health branch is the smallest branch that is run by this Parallel task:
A Repeater task is used to reevaluate the Get Health task every tick. This is done so the behavior tree will have the current health value of the agent every time the tree updates. This value is used by a different branch.
In addition to the Get Health branch, the Update Position branch is executed by the parent Parallel task:
Similar to the Get Health task, the Update Position branch uses a Repeater task so the branch is executed every tick. The first action task that runs is a Bool Comparison task. This task will compare the Update Position Shared Variable with a true boolean value. When Update Position is equal to true the Bool Comparison task will return true and allow the parent Sequence task to run the next child (in this case the next child is another Repeater task).
The Update Position variable is set to true when the agent starts to seek the player because of an event such as being able to see, hear, or is shot by the player. When this happens the Last Position Shared Variable should be updated to the player's position. The tasks under the second Repeater task do this update. A Selector task is used to parent the next set of branches. The position should only be updated for as long as the Update Position variable is true. The left branch under the Selector accomplishes this. Because the Selector is parented by a Repeater task this branch will be executed every tick for as long as Update Position is true. As soon as Update Position is set to false (because the agent lost sight of the player), the Bool Comparison task will return failure and the right branch will run.
This right branch uses two new tasks: the Return Failure task and Wait task. The very first action task that runs is the Wait task. When the agent loses sight of the player he will start to search for the player at the last known location. The Wait task along with the Get Position task simulates the agent anticipating the player's location a short amount of time after the agent loses sight of the player. This is useful for example when the player drops from a platform and the agent loses sight of the player. Before the player drops from the platform the last known location is on the platform so when the agent loses sight of the player he would search for the player on top of the platform. However, he should instead search for the player on the ground below the platform so this branch simulates that behavior. This branch is parented by the Return Failure task so the entire Update Position branch doesn't end. When the agent is done updating the position the tree should go back to the beginning and continuously check against the Bool Comparison task.
The final branch that executes under the Parallel task can be considered the heart of the tree and provides the actual functionality for the agent (click to enlarge):
Similar to the other two branches, this branch is parented by a Repeater task so it will continue to be reevaluated even if a child branch returns success. The Selector task is a child of the Repeater task to allow the next child branch to run if the previous child branch returns failure. This brings up an important point in designing your behavior tree - since behavior trees are executed in depth first search order, the behavior tree should be designed with the highest priority branches on the left followed by the lower priority branches to the right. In this tree the highest priority branch is to get health if running low or to get ammo if running low. The lowest priority branch is to patrol. Conditional aborts are used so the conditional tasks are reevaluated every tick. This allows the agent to seek for a health pack if his health is low even though the patrol task is running, for example. The Health branch is the furthest on the left because it is the most important:
The Float Comparison task compares the Current Health Shared Variable with another float to determine if the agent is running low on health. When the agent is low on health the Set Aim task will run which will put the agent in a non-aiming state. The agent is only aiming when he is firing on the player so this task isn't necessary in all cases but it doesn't hurt to run. Following the Set Aim task, the Wait task will wait a small amount of time to make the transition smoother between the agent's last active task and the current task. The Seek task is then used to move the agent to the health pack. As soon as the agent picks up the health pack this branch will return failure because of the original Float Comparison task and a Both conditional abort set on the parent Sequence task.
If the agent does not need health the next highest priority item is to get ammo if the agent is out of ammo. This branch is setup very similarly to the Health branch:
The Has Ammo conditional task is reevaluated every tick to determine if the agent has ammo. This task is parented by an Inverter task because this branch should only execute when Has Ammo returns failure, rather than success. In most cases the agent is going to have ammo so Has Ammo will return success. However, this branch should only run when the agent does not have ammo so an Inverter task is needed to flip the failure task status to success. The rest of the tasks are the same as the Health branch, with the only difference being that the Seek task will seek the ammo location instead of the health pack location
The next branch is where all of the main action occurs. This branch will attack the player if the player is within sight and distance. Before this can happen the agent must first be able to see the player:
A Lower Priority Conditional Abort is used to allow the Can See Object task to be reevaluated every tick. When the agent can see the player the Wander Shared Variable will be set to true to allow the agent to start wandering if the agent loses sight of the player. This branch will be described later on. Similarly, the Update Position bool is enabled to allow the position to be updated within the Update Position branch. This branch has already been described and is parented to the main Parallel task. A Selector task is then used to start executing the tasks within the Attack branch:
The player can only be attacked if they are alive. The first task that is executed is the Is Alive task to check the health of the player. A Self Conditional Abort is used to allow this task to be reevaluated every tick. If the player is alive a Parallel task is then run to allow the agent to do the actual attacking. If the player dies the Is Alive task will return failure and the Parallel branch will stop running. As soon as this happens the right branch will run which will prevent the agent from needing to wander. A task that prevents the agent from updating the position is not needed because that is set in a different branch.
There are two branches under the Parallel task. The left branch will do the actual firing of the agent's weapon:
The agent will Use the weapon when the Can See Object task returns success. The Return Success task is used because the Use task may return failure and the following Wait task should always be executed. This prevents the agent from trying to fire his weapon every time the tree is ticked. No Conditional Aborts are needed because of the top Repeater task. This Repeater task is used to continue to execute this branch for as long as the other reevaluating conditional tasks allow it to run.
The right branch controls the actual movement of the agent. This branch ensures the agent is near and is looking at the player so the agent will successfully hit the player when he fires (click to enlarge).
Unlike previous similar branches, this branch is parented by a Sequence task instead of a Selector branch. When the agent first sees the player he should Seek the player's Last Position until he is Within Distance. When the agent is within distance he should then only Seek the player's Last Position when the player is out of sight. The following branch will do the initial Seek to get the agent within firing distance of the player:
A Self Conditional Abort is used to stop the Seek task from running when the Within Distance task returns success. When the agent initially has to move towards the player, he may not be close enough to the player so the Within Distance task is going to return failure. The Inverter task is then used so the Seek task will only run when the Within Distance task returns failure, or the agent is not close to the player. Remember that we are not actually Seeking the player, instead we are seeking the position of the Last Position variable which is being updated in a higher level branch.
Once the agent is within distance of the player he will need to keep the player in sight so when he fires he will successfully hit the player:
The Within Distance task is used again to stop the agent from seeking the player's Last Position. This task uses a larger distance than the previously described Within Distance task and the Can See Object task within this screenshot. This allows a small buffer zone where the agent will continue to seek the player and stop seeking if the distance becomes too far. For example, this Within Distance task can check to see if the player is within a distance of 20 units. The Can See Object task can then have a value of 10 so the agent will seek the player any distance between 10 and 20 units.
For as long as the Within Distance task returns true the right Can See Object branch will run. This is done using a Self Conditional Abort on the top Sequence task. A Selector task is then used to have the agent Seek the player's Last Position if the player is not within sight or distance. If the player is within sight and distance the Rotate Towards task keeps the agent facing the player.
This Attack branch provides the most visible agent functionality and while it branch looks complicated it can be broken down and explained really easily. The remaining branches have significantly less tasks than this attack branch. The next highest priority branch is a branch that will react to the agent taking damage from being shot at:
The Is Damaged task will return success when the agent takes damage. In this example scene the agent will take damage when the player shoots at the agent. A Both Conditional Abort is used to allow the Is Damaged task to stop the lower priority tasks from running if the agent takes damage. If the agent does take damage the Is Alive task will then check to see if the player is alive. In most cases this task won't be necessary but the player may have shot a projectile at the agent and died shortly after from something else, such as a different agent killing the player. Following the Is Alive check, the Update Position and Wander Shared Variables are set to true. This is similar to the variables being set to true within the higher priority See branch. After these variables are set to true the agent will then start to Seek the player's Last Position. No more tasks are needed for this branch because eventually the agent will see the player and the See branch will take over with its Lower Priority Conditional Abort.
In addition to reacting if the agent takes damage, the agent should also react if the agent hears a sound coming from the player. This could include footsteps or a weapon firing:
This branch is setup very similarly to the Damaged branch. The only difference being the first conditional task is Can Hear Object instead of Is Damaged. Can Hear Object will return success as soon as it hears a sound coming from the player within a specified distance. When this task returns success the Is Alive task will then run. In this case the Is Alive task is more useful than within the Damaged branch because the agent may hear the player dying and should not start to Seek the player if the player is dead. The Update Position and Wander variables are then set to true and the Seek task takes over. When the agent can see the player the See branch will stop the Seek task from running.
If the agent recently saw the player but the player is no longer within distance/sight of the agent then the agent will start to search for the player. The Wander Shared Variable is used by this branch (click to enlarge):
The Bool Comparison task is used to check to see if the Wander Shared Variable is set to true. Notice that unlike the previous branches, this branch does not have a Conditional Abort set because it is not needed. Wander is never going to be set to true by a lower priority branch so there is not a case where this branch can ever interrupt a lower priority branch. The first task that runs after Bool Comparison returns success is a task that sets the Update Position Shared Variable to false. This will prevent the Last Position from updating to the player's position so the agent cannot cheat to know where the player is.
The Parallel Selector task is then used to run both child branches at the same time. The Selector version of the Parallel task is used to stop the branch from running if one of the child branches returns success. It may look incorrect that another Can See Object task is used. After all, the See branch has a Lower Priority Conditional Abort set so it should interrupt this Wander branch if the player comes within sight. This is true but it hides an important detail of Conditional Aborts: Conditional Aborts are triggered when task execution status changes, not only when the conditional task returns success. Because of the way the Attack branch is setup it is possible for the agent to continue to see the player but not be within the Attack branch because the player is no longer within distance. In this case the Can See Object task is going to continue to return success while the Wander task is running. The Can See Object task within the Wander branch prevents the agent from wandering when the player can be seen. The Until Success task will return a status of running until the child task returns success. This allows the Parallel Selector task to keep running its children.
The right branch of the top Parallel Selector contains a Sequence task that will first Seek to the Last Position of the player. As soon as the agent arrives at that position another Parallel Selector task is used to run both the Wander task and Wait task. The Wander task always returns a status of running so the Wait task will stop the agent from wandering after a specified amount of time. As soon as the agent gets done wandering or can see the player the Set Bool task is run which will set Wander to false. Another higher priority branch can set the Wander value to true again.
The final branch within this tree is a small branch that will patrol the area if there is nothing else to do:
This branch does not have any conditional tasks because it is the lowest priority branch and should run if there are no higher priority branches to run. The first task will stop the agent from aiming, and the second task will set the Update Position Shared Variable to false to prevent the player's Last Position from updating. These tasks may not always be necessary depending on the previous branch that was run. The final task will Patrol a set of waypoints and will always return a status of running.
This tree was put together to give a complete example on how to design a behavior tree. Since behavior trees are so flexible there are many ways to accomplish this same behavior.
<- Realistic FPS