Recently, I started designing a game engine with a few simple goals in mind:
- My game engine is concerned with managing a virtual world model. It is not an animation engine or a sequence/transition manager.
- Although I developed a rough and ready animation API, the model must be separate from that API. This is so that I can later rewrite view using a 3D engine or whatever else.
I’ve been muddling a little bit with behavior programming. After ‘going dark’ for a few days I’ve decided to share my experience as I believe this could illuminate some design points.
Behavior programming – a draft API
A few important classes from the draft API:
- Condition – a condition is an object that returns a boolean value, so it has a (BOOL)check method.
- Action – an action is an object with a (void)apply method.
- ReflexMap – an action mapping a trigger (a Condition) to a response (an Action). When apply is invoked, the reflex map iterates the current action, if any. If no action is currently selected, ReflexMap iterates its list of triggers. The first satisfied trigger condition causes it’s response to become current.
As you can see, the API is based on objectified functions. This is a choice, maybe I’ll get a chance to explain why another time.
An example
Here is sample code used to program simple behavior for a guarding dog:
Actor* dog=[[Actor alloc]initWithX:0 y:0 z:0 tag:@"dog"];
//
Condition* outer=[[OnApproach alloc]initWithSource:dog target:@"human" range:60.0f];
Condition* inner=[[OnApproach alloc]initWithSource:dog target:@"human" range:30.0f];
Condition* close=[[OnApproach alloc]initWithSource:dog target:@"human" range:10.0f];
//
XAction* bark=[[ActorGesture alloc]initWithActor:dog action:BARK];
XAction* bite=[[ActorGesture alloc]initWithActor:dog action:BITE];
XAction* chase=[[MoveTo alloc]initMovingActor:dog to:nil];
//
ReflexMap* reflexes=[[ReflexMap alloc]init];
[reflexes add:bite on:close];
[reflexes add:chase on:inner];
[reflexes add:bark on:outer];
The @”foo” bits just represent strings (in case you’re not too familiar with Objective C). I use those strings as tags, so the @”human” tag allows OnApproach to only target human intruders (not rabbits, leprechauns and so forth…)
As illustrated by this code, although the idea of defining rules using conditions and actions seems OK at first, the design isn’t. The problem is that a Condition (in this case OnApproach) can be fulfilled in various ways. When the condition is fulfilled, we need to pass data to the action to effect it correctly. So in this case onApproach is fulfilled with a given target, but MoveTo cannot bind the target detected by OnApproach. Let’s try to fix this.
Binding precondition output to postcondition input – 1: using arbitrary parameters
My first impression is that the OnApproach condition should be replaced by an event call processed by a listener, so we’d have something like:
on:(Actor*)actor approaches:(Actor*) agent{...}
The downside is that doing this will require that we re-implement the event handling function for every trigger/response pair. We don’t want to write a new class for that every time. We do need to bind the output of the trigger to the input of the matching action. We might then change change the Action and Condition classes:
@interface Condition -(BOOL)check; // unchanged. we need to return a boolean value anyway. -(NSArray*)result; // OnApproach will place a reference to the approaching actor in this array. @end @interface Action -(void)apply:(NSArray*)param // allows injecting data into the triggered action. @end
We still need to tell ReflexMap which parameters to inject into MoveTo. So we change a function in ReflexMap:
add:(Action*)action on:(Condition*)trigger;
Now becomes:
add:(Action*)action with:(int[])params on:(Condition*)trigger;
And our reflexes are then setup as follows:
int[] args={0}; // addresses the only output from OnApproach
[reflexes add:bite with:args on:close]; [reflexes add:chase with:args on:inner]; [reflexes add:bark with:args on:outer];
This solves the problem, but lacks clarity because conditions and actions now return/accept arrays of arbitrary length and content.
Binding precondition output to postcondition input – 2: sharing placeholders
The next idea is as follows: what if we could arrange that whatever target defined by OnApproach is addressable using MoveTo?
To do this requires modifying both OnApproach and MoveTo. On the bright side, we needn’t change the Action and Condition signatures:
- I defined an ActorRef class. ActorRef owns an Actor property (I could use a pointer to pointer, but that would be a little confusing).
- OnApproach defines targetRef as an ActorRef instance. When a target is detected, targetRef.actor is assigned.
- MoveTo also defines a targetRef property. When MoveTo is initialized, we pass the ActorRef instance owned by our OnApproach condition.
We can now change the original example as follows (skipping the bits that didn’t change):
OnApproach* inner=[[OnApproach alloc]initWithSource:dog target:@"actor" range:30.0f]; XAction* chase=[[MoveToActor alloc]initMovingActor:dog to:inner.target];
This seems to solve our problem nicely:
- Type safety isn’t compromised.
- The core interfaces remain simple
- The binding process doesn’t involve ReflexMap
- The code used to define our test script is simple and readable, compared to solution (1).
OK, enough for today then. Next time I’ll look into a way to further simplify behavioral scripts, as they are still a little heavy.


Comments