Game Development Community

dev|Pro Game Development Curriculum

Behavior-based AI framework: a technical overview

by Daniel Buckmaster · 11/20/2012 (7:06 pm) · 15 comments

Yes, I'll be departing the land of the living briefly on a trip to Melbourne, city of the ever-obscured sun. So before I leave, I want to take the chance to write one more post about my AI work, continuing from last week's post about the very general philosophy behind my design choices. The weekend has been a flurry of commits, though a lot of the work was based on earlier versions of code I'd written (most notably the Sensor class and datablock), so I pretty much knew what I was doing when I sat down to write stuff. All of my work is public in my fork of T3D, so I urge you to check it out if you want to get your hands on this code, or just see what it looks like.

What I want to do in this blog is look at how the design I introduced last week is implemented in practice. I'm actually really excited about getting to this stage at last. Since the last time I tried to work on AI, I created Walkabout, which was quite an exercise in user experience design. This time, when working on AI, I began to think more about how people - myself included! - would actually use this AI in missions. So without further ado, I'll present the system from the top down:

Placing AI characters

The first time I wrote this kind of AI, I left everything down to datablock properties - brain type, sensor type, etc. This has a certain elegance, in that once you create an object, the other objects used to give it AI (specifically, a Sensor and a BehaviorManager) are taken care of. However, it means you have to control everything through scripts, which isn't so nice when you're trying to write a level in a visual editor.

The first stage of adding AI characters to a level is obviously placing them in the world editor. I tend to favour using SpawnSpheres for placing things like characters and items in levels, so this time I decided to work with their spawnScript property to define the properties of spawned characters. That meant creating some utility functions to make that process quite quick and easy. Here's a screenshot of the current system in use:

img7.imageshack.us/img7/536/momentdebug201211210802.jpg
Defining a patrolling AI character in a mission

The text in the screenshot is a little small, so here's that script again:
AIBrainClass($SpawnObject, BasicBrain);
AIPatrolBehavior($SpawnObject, GatePath, 0.1);
These are two of my utility functions, and I'm really happy at how they give the mini-script a very declarative flavour. The first line creates a brain (BehaviorManager) for the AI character, and the second actually starts running a simple behavior to make the character patrol through the nodes of the path named GatePath (you can't really tell from the screenshot, but my testing level is a very blocky orange castle). This behavior runs with a priority of 0.1 - in effect, fairly low.

Braaaains

Ugh, did I just make a zombie reference? Whatever. Let's move on. The AIBrainClass function is really simple. Actually, I've simplified it for this blog post, but the idea is the same in my own scripts - I've just split it up to be more general and reusable.
function AIBrainClass(%obj, %class)
{
   if(isObject(%obj.brain))
      %obj.brain.delete();
   %obj.brain = new BehaviorManager() {
      class = %class;
      object = %obj;
	  resources[0] = "move";
	  resources[1] = "aim";
	  resources[2] = "voice";
   };
}
You can see we just create a BehaviorManager object with some resources and a specific class. That class isn't anything special - it's the standard TorqueScript inheritance specifier, which allows me to write functions in a particular namespace. This is very handy for callbacks, like this one:
function BasicBrain::onEvent(%this, %event, %evtData)
{
   // Dispatch event to running actions.
   %this.event(%event, %evtData);
   // React to event however we feel necessary.
   switch$(%event)
   {
      case "onDamage":
	     // Decide whether this is a small or large amount of damage.
         %amount = getWord(%evtData, 0);
         %pain = (%amount < 50)
            ? "ouch"
            : "ouch!";
	     // Start a pain reaction action.
         %this.startAction(AIPainAction, // Which action to run
            getMin(0.9, %amount / 100), // Priority of the action
            %pain); // Data for the action
   }
}
This is a simple handler that gets called whenever some event happens to the character. For example, I've hijacked PlayerData's onDamage callback to call %obj.brain.onEvent(...). This means that all running actions will be notified that the character just took damage. I'll come back to events a bit later, but right now let's go over the rest of this function. Once we've dispatched the event to running actions, we can actually take the chance to add some reaction to the event. Because this function is for a particular brain class (in this case, BasicBrain), these reactions are specific to characters using this class.

In this example, I've chosen to respond to onDamage callbacks with a pain reaction. This is implemented by simply starting a new action in the BehaviorManager. Notice that there's no mention of a Behavior here - this action is simply tossed into the manager at some priority between 0 and 0.9, and when it ends, it ends. Let's look in more detail at how this action works.

A simple AIAction

Actions, of class AIAction, are declared in much the same way as datablocks, except they're strictly server-side objects. have a look at the declaration for AIPainAction:
new AIAction(AIPainAction) {
   // Use this character's "voice" resource queue.
   resource = "voice";
   // If we can't start immediately, don't bother joining the queue!
   allowWait = false;
   // We want to be notified of any events that happen.
   receiveEvents = true;
};

function AIPainAction::onStart(%this, %obj, %data, %resume)
{
   // Use the AIPlayer::say function to play a bark given by %data.
   %obj.say(%data);
}

function AIPainAction::onEvent(%this, %obj, %data, %event, %evtData)
{
   // This event is generated when the AIPlayer finishes playing an audio file started by the say function.
   if(%event $= "onFinishedTalking")
      return "Complete";
   return "Working";
}
Let's break this down bit by bit. The definitions in the AIAction object are fairly self-explanatory, but I will explain allowWait a bit further. If I had set that member to true, the action would be happy to be superseded by higher-priority actions using the same resource, and if, when it was first started, it was not the highest-priority action, it would go into the queue and wait until the higher-priority actions finished. Setting it to false means the action is a one-shot deal: if it's run, and it's not the highest-priority action in the "voice" queue, it fails immediately. Similarly, if it's in progress, and a higher-priority action comes along, it stops immediately instead of waiting.

The onStart callback is called whenever an action reaches the head of the queue - the %resume variable is set if the action had previously started and was waiting. For this action, I just use the 'say' function (which you can find in my Moment script project!) which plays a sound and then generates the onFinishedTalking event. Which, you can see, is caught in the action's onEvent callback.

Actions also have an onEnd callback which is called when the action leaves the head of the queue (for whatever reason), but this action doesn't need to do anything at that time.

This callback-oriented design means that actions don't constantly poll conditions like how close to the goal we are. In my last pass at this sort of AI framework, I was writing script functions that would be called every tick, evaluating the distance to the target and returning success or failure based on that. I realised that all this work is already being done in classes like AIPlayer, which generates an onReachDestination event when it (surprise!) reaches its destination. This makes for clearer action design as well as better performance.

Putting it together: Behaviors

Now we can move onto the second part of our simple spawn script: the call to AIPatrolBehavior. This is just a convenience function that creates a Behavior of class AIPatrolBehavior. A Behavior is a simple object that only needs to do one thing: respond to a stopped action. Here's the event function for the patrolling behavior:
function AIPatrolBehavior::onActionStopped(%this, %action, %data, %index, %status)
{
   // Ignore failures and waiting.
   if(%status !$= "Complete")
      return;
   // Select next node.
   %this.pathNode++;
   if(%this.pathNode >= %this.path.getCount())
      %this.pathNode = 0;
   // Start a new action to move to the new node.
   %this.object.brain.startAction(AIWalkToAction, // Specify action
      %this.priority, // Give it a priority
      %this.path.getObject(%this.pathNode).getPosition(), // Location to go to
      %this); // We own this action
}
This function is called whenever an action ends that was owned by this Behavior. That is determined by the last argument of startAction, as you can see in the script. In this example, all we do is add a new action to the manager, and keep moving to locations around the path. In more complex behaviors, you might have a lot of logic to think about in these callbacks, and you'd want to consider whether the action ended on a success or failure.

Where do we go from here?

Well, my goal is obviously to create some more Behaviors that do interesting things! High on my list is a conversation behavior that orchestrates a conversation between two characters - and if I can implement the design I'm thinking of, it should be able to function as well for guard-to-guard conversations as it does for NPC-to-player dialogue. Also, of course, combat behaviors - once I have some weapons in my project, at least!

I've also been working furiously away at AI senses, which I'll save for another blog post (maybe that'll give me something to do on the 12-hour bus ride...). While detection is working well, I need to refine the callbacks so that the mose useful events can be given to the BehaviorManager.

Once that's all working, I can actually focus on designing the AI, not just implementing it. Doing things like making sure not every character in a room says 'hi!' to you at the same time when you walk in. Making teams and having AI respond differently to enemies and friendlies. Designating 'out-of-bounds' areas where guards don't like you to be. Adding weapon selection and handling. The list goes on... but I'm looking forward to all of it.

About the author

Studying mechatronic engineering and computer science at the University of Sydney. Game development is probably my most time-consuming hobby!


#1
11/21/2012 (3:11 am)
Pretty comprehensive readup Dan.

I'd actually been heading more or less the same direction myself with my stuff. I started with the Tactical AI kit as a base(because it is fantastic) and it wasn't quite fast or flexible enough for me. So I started working on what I'd been calling "Soft States".
I wrote a queue system pretty similar to how were were handing the events and actions and this pervaded into a generic Soft-State machine I'm using for Players and some special weapons. Like you found, having the state machine be reactive to flagged events as opposed to constantly ticking is a LOT kinder to the sytem, and way easier to organize(and cleaner to code as well).

I dunno how far you were into sorting the AI senses, but I opted to have a passive data tracking approach for the most part.
The Tactical AI kit snags a LOT of data about potential targets, which is great for realism, but tends to be pretty heavy on the processing needs when it happens at a regular tick rate.
So instead, I opted to let it check all that data, but it (almost) never updates it on potential targets.
Instead, that data is stored on the potential target itself. So their movement speed, stance, if they were in a low-visibility area(fog, grass, etc) and other such factors are stored on the player object, and updated when the player does something. Since we'd be doing the work to update the player anyways, storing the data doesn't encur and additional work. Then, later, when an AI checks if the given target is visible, I just need to do a LOS check and make sure it's in the FOV. Then it's just basic comparative math to see how visible they are.
Keeps it light, keeps it passive, but keeps it with all the nifty checks to keep the AI as realistic as possible.

I haven't completely moved all the AI stuff to the Soft-State Machine yet, but it's on my to-do list. Should yield pretty interesting results, and seems like an even better idea to hear you're going in more or less the same direction :)
I will say, I hadn't at all considered priority levels. I had accounted for(admittedly, pure combat AI doesn't have as wide of scope as other types) the AI just overriding irrelevant actions/states if they're busy with fighting stuffs. I'll have to roll that into things.
#2
11/21/2012 (5:49 am)
I agree great read up, I'll most definitely be following your development.
#3
11/21/2012 (7:44 am)
Quote:
Melbourne, city of the ever-obscured sun

What's a sun? I don't think we have one in Yorkshire ... :(
#4
11/21/2012 (10:21 am)
@Dan:

...was just lurking on your github and found this blog. Wow, great work and thanks for being so generous with it.

I had done something similar to your BehaviorManager all in script to deal with the differing types of vehicular/beast behaviors, but it got pretty tick-intensive. Once again you've made me re-think my approach...

I'm also curious to see how your sensors are implemented. I dealt with sensing by adding it to shapebase but I fear that again, in practice it may be a resource hog...

@Steve:

Quote:What's a sun? I don't think we have one in Yorkshire ... :(

...because it never leaves Hollywood long enough for it to rain...
#5
11/21/2012 (3:02 pm)
Daniel,

I want your brain.

Seriously, this whole series has been fantastic. Keep it up.

Ron
#6
11/21/2012 (3:48 pm)
Quote:...because it never leaves Hollywood long enough for it to rain...

Now Rain, yup we get rain in Yorkshire, Rain by the bucket Load.. anyone interested in buying some genuine Yorkshire rain water? only $1 a bucket plus $199.99p&p plus applicable taxes ;-)

but seriously always a good read Daniel,
Quote:I'll be following your progress with great interest young Skywalker


hmm how do you get a £ without the silly character in front of it?
#7
11/21/2012 (9:53 pm)
Thanks all for the kind words!

I'd love to hear more about your soft state machine, Jeff. I'm a bit jealous that is has a nice snappy name for you to call it by. I'm stuck with ARPQ (action resource priority queues). Event-based AI was a big step for me - I'd wanted to include events alongside ticking in previous versions, but never quite got there. This time the light clicked on and I just didn't even write any ticking code.

On the other hand, this has made it interesting to try to implement stuff like a quick glance reaction that I want to use when an entity sees something out of the corner of its eye. I want the entity to glance at it, then continue what it was doing in about a second. At the moment, I'm just scheduling a new event to happen, and when that event is caught the 'glance' action ends. This is really dodgy, though. I think the proper channel would be to have the Sensor itself tick contacts and generate events like 'you've identified this contact now', and end the glance when that happens.

In terms of sensing, I'm storing only basic information at the moment - last known position and velocity, and time since seen (since that will be unique for every object for every sensor). You can check out Sensor::processTick here (until I revise it and the line number changes!) to see what sort of stuff I'm doing. At the moment, it's very basic - I've been focusing on the SensorData block and the different rule types you can use to tailor sensing.

Gibby: my Sensor class is a separate object. Aside from ShapeBase being bloated enough as it is, I just wanted to have separate objects that would be more flexible. It does have the downside that now each AI character is represented by three separate objects (character, sensor and behavior manager) - not to mention any running behaviors (each of which is an object, unless you have a really simple behavior).

One neat benefit to having a separate class/object for sensors was inheriting the Sensor object from Trigger. Instead of performing radius searches for objects all the time, a sensor just waits for something to collide with its trigger volume, then starts tracking it. I haven't found a way out of polling the complex visibility function for all active contacts yet, though. I did think of keeping track of a bounding box for each tracked object, and only actually performing a detailed check if the object moved outside its box. That would optimise detection against campers, at least :P. But I doubt the small benefit would be worth it. Especially with a proper detection pipeline that performs easy checks like FOV and distance first, and only performs complex tests like raycasts if those ones pass.
#8
11/22/2012 (1:39 am)
Hah, I had the same general idea as you about the Trigger basis for sensing areas.
I went with a more hackish route because I was just messing with the concept, and just mounted triggers that called back, instead of a unique class based off triggers, but yeah, it seemed like a rather reasonable approach to me as well.
You'd automatically get an update the second something of interest comes into view(which I'd planned on using for stuff like grenades. Allowing the sensor to do an instant interrupt because they saw a grenade flying at them would be much more realistic than them having to wait till the next tick to see the grenade, by which time it may have already exploded) as well as a simple means of tracking any and all relevant objects in a easy-to-parse listing.

But, like said, I was going with a more hackish route, so I had reliability problems. It makes way more sense to go the route you did with a dedicated, derived class.

Also, the SSM(it is a pretty catchy name, isn't it?) is pretty slick. It's actually done almost entirely in script(I actually made 2 main modifications to the engine, namely allowed a processTick callback for shapebase objects if needed, and added the ability for ScriptObjects to self-tick and do callbacks on it)

It actually started because I wanted animation-driven events. Instead of needing to guess timings on when animations would end, and then have the players be able to do stuff, I rigged up a state queue system. It expanded to be more from there, but as it stands now, I can do stuff like %obj.queueState(stateToChangeTo, triggerEvent);
TriggerEvent could pretty much be anything at all. An animation ends, an animation starts, you take damage, you land, you run out of stamina, or in the context of AI, you spot something, a prior event/action ends, etc, etc. Then in other parts of the code, such as a callback that an animation ends, I just do %obj.executeQueue(triggerEvent); And it'll execute the state/event that was queued for that particular triggering event.

I dubbed it 'soft states' because as opposed to a rigid state machine, where you have say, a 'running' state, or a 'idle' state, etc, you could stack states.
States were intended as generic functional tidbits. So you'd have, say, a 'aimWeapon' state, which could be going on at the same time as 'transitionToDuck' state, etc. This was very easy to roll over for AI usage, as it would be like what you wanted with behaviors. Instead of full behavior actions, they'd be pieces that could combine to create complex acts of behavior.

So yeah, sounds like we were going more or less in the same direction, just different approaches :D
#9
11/22/2012 (4:29 am)
That's a great idea - being able to specify deferred state changes. Animation-based triggers are also excellent. I haven't really played with them but they always seemed a little difficult to use, even just from the perspective of having to give them a number and synchronise that with scripts.

I should mention that BehaviorManager is in C++, and the AIAction class has a C++ framework that at the moment just fires console callbacks. Ideally, in the future, some actions could be implemented in C++ by deriving from the base class and overriding those methods. Not sure if the performance of scripts is dire enough to warrant that, though.

Also, my inheritance from Trigger is pretty hacky as well :P. That class was not designed to be subclassed, unfortunately. But it's working, though I've realised that objects like Player will always call potentialEnterObject on Triggers, even if they're completely inside the trigger's boundary and stationary. Would result in a lot of unnecessary searches through a sensor's contact list to see if objects had been already added. I may put that on the list of things to improve...
#10
11/22/2012 (5:20 am)
Actually, it's not just animation triggers.
I actually added callbacks for (both regular and action animations) for triggers, onStart and onEnd.

So for simple starting and ending events(such as playing one aniamation, and queuing a state change when it completes so it can act like a proper transition animation) have a callback to script, where I simply do a %obj.executeQueue(onAnimationEnd, animationInQuestion);

Then the queue system checks for any queued events that trigger from an onAnimationEnd, and checks if there's a particular animation it's looking for. If so, it calls the state assigned to it.

This would also work with trigger IDs, just queuing a given state to kick off for an animation trigger happening with the ID X, etc.
The system's really flexible, as said, and I can case out special cases for how much data it anticipates needing, such as a trigger number, or just simply what animation we're on the lookout for, etc.

I might implement it into C++ eventually, but given how passive the system is, short of setting up a event that constantly calls(which is kinda against the point of the system), it's passive enough there isn't really a performance concern. It'd just be icing on the cake.

With my solution for the trigger mounting, I was actually running into it where it wasn't always detecting objects that intersected into the area when the trigger space was moved from being mounted on a player. If a player moved into the trigger, it worked, but not the other way around. That was one of the biggest issues I ran into. I wonder if subclassing it out fixed that for you somehow. And yeah, there would need to be a way to filter through to only add new contacts so we're not wasting time with things we already know exist. I don't think it'd be particularly difficult to implement though.
#11
11/22/2012 (1:34 pm)
I think the main reason for writing most things in C++ is that I just have a very hard time with TS. I also wouldn't want to be writing priority queues and sorting algorithms in TS :P.

Oh yeah, I forgot to mention issues with the Sensor moving. I thought that Sensor should automatically scan for new objects when setTransform was called, but apparently not. Basically, what I do is check every tick whether the Sensor's object is further than some distance away from the Sensor's centre. If it is, then I update the sensor's position and do a container search for new objects. It's not nice, but I think it's the only way to deal with the problem. At least these updates don't happen too often, depending on how large the trigger box is.
#12
11/22/2012 (3:24 pm)
At this point I'm a little bit of a TS ninja, so I didn't have much an issue with setting it up, but I can understand what you mean with that.
If I get a chance, I'll see about cleaning my implementation up a bit and pass it to you to look at how I approached it.
I think the advantage with doing it in TS is help flexible it lets me treat the same queue manager, but whatever works :P

I don't remember if I tried for a fix. I hit that issue and figured for now I'd just try to optimize the regular checks(leading to the passive visibility tracking stuff mentioned above). I'd planned on going back and revisiting it, and I vaguely remember thinking of a way to approach it, but I'd need to look through it again. Something to mull over. I definitely think a sensor approach like this is probably the best, but there are a few bugs to iron out.
#13
11/23/2012 (9:42 pm)
Heh, I'm trying to resist having to become one :P. The more I work with it the more I dislike it. Anyway, I'd love to take a look at your work. I've always wanted to see good generic AI capabilities in Torque. Seems like we're moving rapidly in that direction!
#14
11/26/2012 (8:26 pm)
I've posted a resource with my AIPlayer::say function in case anyone wants it. It's nothing profound, I just found it a handy little helper method.
#15
12/07/2012 (3:46 am)
We have a similar system with the Flags at our ControlPointTriggers. Those triggers start e.g. a sinking flag animation and if it is completely down it will change meshes if the trigger areas contains more foes than friends. To find the end of animation threads (the length is variable depending on the capturetime required for the Trigger Area) i also used the onEndSequence callback, but that showed to be ambigous. What i did was to add a 'animation' entry into the animated object and set this according to the currently playing animation. Here's the callback function:
function Flag::onEndSequence(%this,%obj,%slot)
{
	if (%slot == 0) {
	if (%obj.animation $= "up")
       %obj.animation = "stophigh";
	if (%obj.animation $= "down")
       %obj.animation = "stoplow";
    }
and an example of starting an animation would look like:
%theFlag = %trigger.brick;  // this is the animated object in the trigger area
    if (%theFlag.animation !$= "down") { // don't call it while its running
    		%theFlag.pauseThread(0); 
    		%theFlag.playThread(0,"sink"); 	
        	%theFlag.setThreadTimeScale(0,%theFlag.timescaler);
    		%theFlag.animation = "down"; 
    	}
// timescaler determines the speed of the animation
with this method you don't need to search for the animation but have it handy all the time.