Whether you’re building your AI as a behavior tree, a finite state machine, or a planner, it must be able to interact with the game. That’s what actions and conditions are for.
Because they are so fundamental, it’s important to get them right — even more so than other modular behaviors. So, back to basics…
Similarities and Differences
Conceptually, there aren’t many differences between conditions and actions, but the following is typically established as a convention:
Actions do stuff in the world, and they have side effects. Actions run for as long as necessary to achieve their purpose.
Conditions just check for information, and are free of side effects. Conditions may be used for one-shot checks, or monitoring a truth value.
Now for the similarities; actions and conditions both:
Interact with the virtual environment in some way.
Are written in the same language as the game engine.
Should indicate whether they succeeded or failed.
Can terminate instantly or execute over multiple frames.
It’s best to have actions and conditions as similar as possible, as it simplifies the AI framework. It also implies a single API can be used…
Many games have an interface which allows computation to be spread over multiple updates. This is ideal for implementing conditions and actions. It typically looks like this:
- Initializes the action or condition by accessing the interface to other components in the game (e.g. audio, world representation), and/or setting up an internal data-structure (e.g. for animation control).
- Uses the interfaces or data-structures setup by start() to perform the necessary computation (e.g. finding nearby entities) or supervising the execution of some functionality (e.g. playing an animation).
- Shuts down the interface to other game components if necessary, and stops the external computation (e.g. request a sound to be faded out).
Actions and conditions typically do not need a life-cycle beyond their activation pattern. In contrast, higher-level behaviors may need persistent task memory, so additional init() and shutdown() can be used separately.
Words of Advice
1) Consider establishing three termination codes for succeeding, cleanly refusing to run, and failing with an error. This opens up many more options to the decision making system. (This particular tip is among the most important things I’ve learned recently.)
2) Get a sensory system to batch-up all of the hard processing so the conditions are lightweight. This applies for line of sight checks, capsule collisions, etc.
3) Similarly, decouple the actions from the simulation itself so everything can be updated at once. This applies to physics simulation and audio, for example.
4) Try to keep the interface as simple as possible. A complex API at such a low-level can cause many more problems than you anticipate. In particular, think wisely about the return values of the start() and update() functions, what they mean, and establish a contract to describe if/when these function are called based on the return values.
Do you have any particular conventions for your actions and conditions?