In working on my own IF projects, I am making use of behaviour trees (I think this is a good article that describes them conceptually and in-depth). In a nutshell, they are a way of combining queries (that ask questions about the game state) and actions (that alter the game state) into a structure that has a clever trick. They are programming without a lot of programming.
TLDR? Okay, a few key concepts:
We are going to talk about “tasks”, which are either composites (that structure other tasks), queries (that succeed or fail based on the game state), or actions (that alter the game state). And tasks “succeed” or “fail”. Let’s introduce a couple of composite tasks:
SELECT A, B, C, D
SELECT
is an example of a task and is a composite task because it has “child” tasks that it operates on. In this case, it executes the tasks A
, B
, C
, and D
. If any of them succeeds, it stops there, and SELECT
is considered to have succeeded. If A
, B
, C
, and D
all fail, then SELECT
fails also.
SEQUENCE A, B, C, D
This executes tasks A, B, C, and D in turn. If any of them fails, it stops, and SEQUENCE
fails. If they all succeed, SEQUENCE
succeeds.
So, SELECT
picks the first task that succeeds, and SEQUENCE
ensures that all tasks succeeded. We can use just these two concepts to build quite sophisticated behaviours.
Here are a couple of simple SEQUENCE
examples:
SEQUENCE
BOB_IS_THIRSTY
BOB_HAS_WATER
BOB_DRINKS
- If Bob isn’t thirsty, this sequence will fail.
- If Bob is thirsty but has no water, the sequence will fail.
- If Bob is thirsty & has water, he takes a drink.
A likely consequence of BOB_DRINKS
is that he’s no longer thirsty and has less or no water.
SEQUENCE
BOB_SEES_WATER
BOBS_FLASK_ISNT_FULL
BOB_FILLS_FLASK
- If Bob doesn’t see water, this sequence will fail
- If Bob’s flask is full, this sequence will fail
- If Bob bob sees water & his flask isn’t full, he’ll fill the flask.
Here’s a slightly more complex example arising out of a game I am building that uses SELECT
to combine SEQUENCES
and build an NPC behaviour:
SELECT
SEQUENCE
BLACK_RONIN_IS_WOUNDED
SELECT
SEQUENCE
BLACK_RONIN_IS_IN_MONASTERY
BLACK_RONIN_HEALS
BLACK_RONIN_MOVES_TOWARDS_NEAREST_MONASTERY
SEQUENCE
BLACK_RONIN_SEES_PLAYER_TRAIL
BLACK_RONIN_MOVES_IN_PLAYER_DIRECTION
SEQUENCE
BLACK_RONIN_SEES_PLAYER
BLACK_RONIN_AMBUSHES
BLACK_RONIN_MOVES_IN_RANDOM_DIRECTION
In my game, the Black Ronin hunts the player unless he’s wounded, in which case he heads for a monastery and, when he reaches one, heals. If he catches up with the player, he will attempt to ambush them; otherwise, he heads in a random direction hoping to pick up the player’s trail.
Something interesting about this is that the person building these behaviours doesn’t have to be the person implementing the queries (like BLACK_RONIN_IS_WOUNDED
) and actions (BLACK_RONIN_HEALS
). As long as they can master the concepts of SELECT
and SEQUENCE
, they can use them to build relatively complex behaviours.
It’s programming, but with a built-in structure and a relatively limited set of concepts you need to understand/use (there are more than SEQUENCE
and SELECT
, but these are fundamental). I wonder if people who are more authors than programmers might succeed with this where they wouldn’t cope with, for example, an equivalent Javascript function (or building this in something like TADS or Inform).
In practice, instead of BLACK_RONIN_IS_WOUNDED
, I have a query task ACTOR_TEST actor=black_ronin attribute=health test=LT value=75
. This is a little more verbose but allows me to apply tests to different attributes of different actors with one type of query. But if you wanted, you could build per-actor queries that directly modelled what you wanted to know.
Does anyone else find this approach interesting for creating NPCs with their own agency & motivation?*
Matt
- In fact, I use them beyond this in terms of how things like items & even locations can respond to/modify the game state.