Open a327ex opened 9 years ago
thanks for these posts. I'm just starting a small project (iOS in swift), and am new to game AI. Behavior trees seem like a great solution for a lot of AI behavior. I also read the gamasutra article you linked which was very helpful. Cheers!
Hey, glad you liked it! ^^
Great article. What is the program you used to draw these diagrams? It looks pretty neat.
@adonaac Thank you.
2014-08-17 03:46
This tutorial features a full implementation of behavior for an enemy using behavior trees. The previous part went over the basics of behavior trees as well as their implementation. So look there first if you somehow got here randomly. The enemy behavior we're going to implement is actually quite simple from a player/viewer standpoint, but it's a nice way to introduce most concepts behind behavior trees.
The enemy is going to have three (3) main modes of behaving:
In the idle mode the enemy will be able to either: wander around its spawn point, talk to another enemy or practice his TK (telekinesis) on a nearby object. The suspicious mode is triggered whenever the player gets close enough to an enemy. When it is triggered the enemy spawns a question mark above his head and turns towards the player. If the player leaves this trigger area then the enemy goes back to the idle mode, otherwise he stays here. The threatened mode happens when the player gets really really close to the enemy, in which case the enemy will spawn an angry face above his head, chase the player around and attempt to hit him.
Wander
Using the
findWanderPoint
andmoveToPoint
actions from the last article, we can start building the idle wander behavior. A small addition to that particular sequence is adding an action that makes the entity wait around for a few seconds before choosing the next point, otherwise he moves around too much.Here, like with the Timer decorator, we just set a timer so that after
wait_duration
seconds this action returns success. Before that happens it will just return running and the sequence won't be able to move on, which will make the entity do nothing. The tree for the behavior looks like this:We use a sequence to bind everything together. If at any point
moveToPoint
fails, thenwait
will not run and the tree will just start over by finding a new point. If the tree succeeds then the entity will have done what we wanted it to do: move to a point and then wait around a bit. After that the tree will just restart and so on...TK Practice
As you can see in that gif there's a box lying around. To add some more personality to each enemy we're going to make it so that sometimes they practice their TK on objects around the map. The idea of this game I'm working on is that it's a school full of people with TK, so it makes sense that sometimes they'd practice it!
To do this we're going to need two actions: one to check if there's any object that can be TKed around, and another to do the actual lifting. First, let's do the one that finds an object:
anyAround
is a conditional type of action that will ask a question to the game's world and return the answer. In this case, the side effect is changingcontext.any_around_entity
to point to some entity of some type around a certain point with some radius. The idea is that before the NPC can move to the object he wants to lift he needs to first find it. Separating finding and moving makes it so that both can be reused later and, as we'll see,anyAround
will be reused a lot.There's one problem so far, though. Ideally we want this subtree to look like this:
But
anyAround
setscontext.any_around_entity
to point to the entity, whilemoveToPoint
looks forcontext.move_to_point_target
to move to the target. There are two solutions to this: either add an additional node that always succeeds and translatesany_around_entity
tomove_to_point_target
, or changemoveToPoint
so that it also looks forany_around_entity
and then translates that into a point it can use. I'll use the second and this small refactor will be omitted.Finally, the
TKLift
action, which does all the lifting work, usesany_around_entity
and changes itsz
velocity:And then after that we tie it all to a sequence. And since we want the enemy to do one thing or the other, either try to lift objects or wander around, we use a selector on top of that. The order in which we place each subtree also matters a lot. Suppose we do selector -> wander, lift. The
wander
sequence rarely fails, which means that the selector would succeed wheneverwander
succeeded, which means thattkLift
would never really be picked.tkLift
fails whenever there isn't a particular object around, which means that placing it before thewander
subtree makes more sense.There's still a problem, though. As you can see in the gif, whenever the NPC finds a box once he'll be addicted to lifting it. That's because there are no checks in place to keep this from happening. The
anyAround
query will always return success after the first time, because there will be a box around, and so the whole lifting sequence will be performed and it will repeat again forever. To prevent this from happening we can either try creating a decorator or performing an additional check by creating another action. I'll choose to go with the decorator one:Now whenever
anyAround
succeeds twice in a row a failure will be forced. That failure existing, the tree will be able to move on to try out thewander
subtree, which means the NPC won't get stuck in the same box again.And so the whole tree looks like this:
And the code to do that like this:
Talking to Friends 1
This is the first kinda complex behavior and there are multiple ways of going about doing it. The goal is to get the NPCs to talk to each other somehow. The way I'll do it is by creating
FriendMeetingPoint
objects, to which NPCs will be able to attach themselves to and wait around for other NPCs to attach themselves to those points and then they'll be able to talk. The first step to doing that is finding and moving to one of thoseFriendMeetingPoints
. Similarly to how we move to a box in thetkLift
behavior, we can useanyAround
andmoveToPoint
:And this works fine. However, there's some repetition going on there. The DontSucceedInARow, anyAround -> moveToPoint subtree looks exactly the same in
TK practice
as it does in friend talk. Luckily, there's a way of removing this repetition by storing subtrees!Storing Subtrees
Storing a subtree simply means that you'll take the way in which those nodes are arranged and you'll store it so you don't have to type it all again whenever you wanna reuse it. Think of it like a function that you call and that builds that whole subtree with the arguments you pass to it.
And the way to achieve that, at least in Lua, is exactly like thinking of it as a function. We simply create a file that returns a function that returns the built subtree:
And then to call it:
Talking to Friends 2
Back to friends, I'll omit two actions because they're extremely dependent on how I've coded the
FriendMeetingPoint
entity:attachTo
, which attaches the NPC to theFriendMeetingPoint
entity andanyAroundAttached
, which looks for a friendly NPC that is also attached to aFriendMeetingPoint
entity. The subtree now looks like this:The
WaitUntil
decorator waits until the child node returns success before it can return success. This means that once an entity is attached to aFriendMeetingPoint
, it'll stay there until some other NPC comes along to talk to it.After this we add one more action before the actual
talk
action, which isseparate
. In case two NPCs attach themselves to the sameFriendMeetingPoint
we want to separate them a bit before they start talking, otherwise it looks a little too weird. Theseparate
action uses the separation steering behavior that the entities in my game have, so it's pretty simple code wise:And finally we add the
talk
action, which contains most of the work:With this, NPCs should move around randomly, find boxes and lift them with their TK sometimes, and whenever they get close to a
FriendMeetingPoint
they'll go there to try to talk to someone. There's the possibility they'll be stuck there forever if no one shows up, but that's a detail that can be fixed by adding a time limit to theWaitUntil
decorator for instance. Other than that we have a working implementation of an idle behavior for a particular NPC of this game. It wasn't super simple but to me at least it beats the approach I was taking before! (which was just hardcoding everything)The whole tree now looks like this:
And the code:
Suspicious
Now that the idle subtree is done we can move on to the suspicious behavior. This behavior will check to see if the player is around, if it is then it will set the NPC to be suspicious, which will turn him towards the player, change his animation to combat mode and spawn a little question mark above his head. This lasts a few seconds, after which we perform another check to see if the player is still inside the suspicious trigger area, if he is, then we bail out of the sequence and fail, and if he isn't then we set the NPC back to normal using an unsuspicious action.
The subtree should look like this:
I'm going to omit the specifis of each behavior since by now you should have a good idea of how I'm coding them and how you're coding them. If you're following along and trying this out with your game your tree may look slightly different based on how you've coded each behavior, but that just happens, since there are multiple ways of achieving the same thing. In any case, the whole tree should look like this:
We tie everything up with a selector at the top and by placing the suspicious behavior to the left. We want the tree to first check for any suspicious activity around, and then if that fails, be able to go to idle stuff. The code looks like this:
As you can see on that gif though, there's a problem here. Whenever the player enters the suspicious trigger area (yellow circle), the NPC doesn't become suspicious immediately. This is because he's still waiting for the
wait
action to be finished on the idle subtree, which means that he won't check the suspicious subtree until it ends. We want it to check this immediately, as soon as it happens, but using a normal selector that won't be possible.Active Selector
An active selector behaves like a selector, except instead of returning to the node that is still running, it always checks all children. So in our case whenever
wait
returnsrunning
, on the next frame, instead of just going directly to that node and running it again, it will run the suspicious subtree first, see if it returned running or failure, and then move on to the idle subtree where it will pick up from the runningwait
node. If suspicious succeeds, however, since it behaves like a selector it won't run the idle subtree.And now it should behave accordingly because it will always first check the suspicious subtree (since it's first on the list). Note that this is, in a way, parallelism. Although isn't real because it's doing each in sequence, for the purposes of the tree it is real, since what matters is what happens over multiple frames, and this makes it so that things happen in one frame simultaneously. For the threatened subtree I'll introduce yet another node called the Parallel, which does something similar to the active selector but has different success/failure logic.
Threatened
For the threatened behavior the same active selector logic will apply. We need the NPC to be threatened immediately when the Player comes too close. Similarly to
suspicious
, we'll useanyAround
for this:Now what we wanna do is make it so that when the NPC is threatened, it chases the player around and tries to hit him if it gets close enough. One way of doing that is, after the
threatened
node, add a subtree that repeatedly chases and tries to hit the player while doing so:repeatUntilFail
will make it so that this whole sequence of checking to see if the player is still close and then chasing/punching is repeated forever, which means the NPC will chase the player until he distances himself enough. After that, the only node we don't really know anything about (assuming the implementation ofchase
andmeleeAttack
) is the Parallel one.The implementation of the parallel node is probably one of the most complex ones, but it's similar to the active selector in the sense that it always goes through all nodes. The only difference is that on top of receiving behaviors as arguments it can also receive failure and success policies. A failure policy defines when the parallel node will fail, same for the success one. Both types of policies have two possible values:
one
orall
. Anone
failure policy means that the parallel node will fail whenever one of its nodes fail. Anall
success policy means that the parallel node will succeed only when all of its children succeed, and so on...For the attack subtree we used
('all', 'all')
, which means that we want it to fail or succeed whenever both behaviors fail or succeed, which is rare.chase
always succeeds.anyAround
can fail if the player is far away enough from the NPC.meleeAttack
always succeeds. Whenever they all succeed, the parallel node will succeed, which means the sequence will succeed, which will return success torepeatUntilFail
, meaning that the whole subtree will be repeated again. The only way out of this subtree is if the firstanyAround
outside of the parallel node fails, which is the intended behavior.And with that we have a working attacking enemy! The code for the whole tree now looks like this:
And the final tree looks like this:
END
And that's it! If you've followed through all this, even if just reading the explanations and code, you should have a nice idea of what behavior trees can do and how they do it. Hopefully this helped you in ways that I wanted to be helped when I was trying to understand all this. There are tons of issues with the way I'm doing it and there are tons of resources that go beyond what my implementation does, but since my game isn't really memory nor performance intensive I can get away with the simple solution. The resources on the first paragraph of the first part mostly cover these issues.