Article
files/roadworks

Understanding Behavioral Exceptions and How to Deal with Them

Alex J. Champandard on December 23, 2007

A few months ago, in the early days of this blog, I noted the importance of understanding the different possible types of failures of actions, and all other tasks for that matter. This article looks into ways for your AI logic, in particular your scripts or behavior trees, to deal with these problems more elegantly.

Taking an example, the MoveTo action can fail in two ways:

  • Detecting that there’s no valid path to the target before executing, and bailing out cleanly without having made any changes to the world. Call these failures.

  • Starting movement under the belief that everything should be O.K., but then running into a variety of collision and locomotion problems due to the unpredictable nature of the world. Call these exceptions.

In the second case, the position of the actor when the MoveTo action started probably changed. You could consider it a dirty failure as the original assumptions have been broken by unwanted side-effects.

This is important to know because the parent tasks must take this into account to find an alternative course of action. In this case, the AI needs to replan what to do again based on the new position, as the nearby navigation geometry may have changed. So how do you do this in practice?

Options for Dealing with Problems

Ideally, you should try to implement your actions such that they have no side effects if they would fail shortly after without achieving their purpose, but that’s not always possible. With that in mind:

  1. Clean failures are ideally dealt with using logic that makes minor changes to the course of action, a.k.a. local replanning. In a behavior tree for example, the nearest (ancestor) selector deals with the Fail return status and falls back to a different sub-tree.

  2. Exceptions are not quite so straightforward. In certain cases, it’s O.K. to deal with them locally just like failures, if the exception is irrelevant to the current goal or if the fallback options are capable of dealing with different starting situations (e.g. goal-driven behaviors).

    In other cases, the exceptions affect the current behavior irreparably and require whole subtrees to bail out, and finding alternatives on a completely different level.

Finding ways to deal with exceptions is tricky because so often they are special cases — by definition!

Possible Implementations

How the code should propagate these exceptions is another matter also. You have two choices:

  1. Give your task API in the behavior tree only one return status for expressing problems, and assume this means a clean refusals to execute. Then, in the case of exceptions, provide a separate communication channel.

  2. Model exceptions explicitly as an extra return status and require each composite task to deal with these different possible values.

In practice, the behavior tree looks similar however you implement it. But it affects the low-level BT code quite a lot… I’ve done both recently, and learnt a lot from the process!

Lessons Learnt

The following notes should seem obvious in retrospect; just keep them in mind when designing your system.

  • Clean failures have a very specific meaning and require no extra information to communicate that nothing went wrong.

  • Exceptions come in many different forms, and often need additional data attached to them to explain what was the problem.

  • Conceptually, exceptions are dealt with at only few levels in the tree itself, regardless of how they are implemented.

  • Having an explicit return status for exceptions sets a good discipline and coding guideline for dealing with them consistently — which makes the code more robust.

  • Having an extra return type complicates the implementation of each of the composite tasks in the tree, especially when a different communication channel could be used.

  • With a separate API for exceptions, it’s much easier to raise them from various parts of the code rather than relying on the actions to do so.

Words of Advice

In mainstream programming, the use of exceptions as a software pattern is still debated. When it comes to behavioral logic, you have no choice but to deal with them in one form or another if you want a certain level of intelligence.

My first few implementations used no explicit exception status, which seems like the right approach in retrospect! However, the behaviors suffered because of a lack of awareness about the problem and its solution. Making the return status explicit forced me to model these special cases…

In a way, you have a choice to make about your default policy. Should all actions signal errors explicitly in the return status, and force the parent behaviors to deal with it or pass it on? Or should all actions silently fail, and communicate errors via a different channel to tasks who are interested (for example, handlers that check assumptions on a higher level)? The tree may end up looking similar, but you’ll find yourself thinking about the problem very differently.

Personally, I’d recommend establishing a standard API for them so they can be raised with additional information, but keeping this interface separate from the return status, and maintaining the discipline to raise and capture them everywhere. Either way, you must admit that dealing with exceptions is a top priority!

Discussion 5 Comments

kingius on December 24th, 2007

I think I understand the problem on a high level. It's the difference between returning a generic error (i.e. a problem has occured, bail out and try the next sister node in the tree [I'm assuming the parent at some point is selector] ) and a specific error (i.e. my path is blocked by a friendly unit, handled perhaps by a conditional branch in the selector [if path blocked by friendly unit, run 'wait for next ai cycle' behavior. Is this a good way to handle this situation, putting some form of logic into the selector, or would you see an exceptions api making calls directly into the behavior tree at a level set elsewhere, rather than being handled by the tree itself?

alexjc on December 24th, 2007

I mentioned this briefly at the end of the article; I would personally separate exceptions from the traditional control flow. I left out the details of the details of this particular solution to keep this article balanced, but I'll discuss this in a future post: [LIST] [*]Using a central working memory or blackboard to set exceptions. [*]Providing a set of observers that can be notified as soon as something changes. [/LIST] This gives you a lightweight modular framework, like a signal/slot mechanism for passing exceptions around. By naming your slots, you establish meaning for the exceptions, and can store data in the blackboard as appropriate. I hope that clarifies things a bit! Alex

kingius on December 26th, 2007

Hi Alex, Yes it does a little, thanks. I look forward to your article where you cover this in more detail. Being a developer, I'm constantly trying to move from the high level concepts and into the low level coding implementation to improve my understanding. I'm also working on implementing a behavior tree (inspired by your articles) on a simple strategy element of an RPG I'm working on as a personal project (in Macromedia Flash), so the questions are very relevent for me. So far I've got a working prototype that is implemented on the code level as a stack (of nodes that have been traversed so far, so the code can climb back up the tree on success or failure conditions, but analyses the current node itself to get to any children) and a simple test tree in XML that allows a "unit" to turn to face a target location (the unit stops when it is facing the correct direction). I'm on version two of my code already and my data structure has undergone a radical revision as well. Anyway, keep up the good work, it's inspiring me to try to put some of these ideas into practice.

bknafla on February 18th, 2008

Another idea to handle exceptions might be to have a "parallel" node at a higher level in the tree than the exception "throwing" nodes and use a special exception condition to detect/handle an exception. Though it might be cleaner or more explicit and therefore more visible to have special exception-handling nodes. Cheers, Bjoern

alexjc on February 18th, 2008

Thanks again Bjoern, that's a great solution too. See these articles for more about that: [LIST] [*][URL=http://aigamedev.com/essays/event-handling-purposeful-behaviors]Event-Handling Strategies for Juggling Purposeful Behaviors[/URL] [*][URL=http://aigamedev.com/questions/popular-behavior-tree-design]Popular Approaches to Behavior Tree Design[/URL] [*][URL=http://aigamedev.com/theory/teleo-reactive-programs-agent-control]Teleo-Reactive Programs for Agent Control[/URL] [/LIST] Alex

If you'd like to add a comment or question on this page, simply log-in to the site. You can create an account from the sign-up page if necessary... It takes less than a minute!