Whenever you’re thinking of an NPC looping through a set of tasks, or performing a specific set of tasks when some specific condition is met, it’s probably worth considering using a “formal” finite state machine.
In this case, you could just use a property on the Actor
that takes one of a specific set of values (one for each state you need to model). You then have an AgendaItem
for each state, and each agenda’s isReady()
checks the value of the property.
That’s kinda a mouthful, so here’s a trivial/silly example:
#charset "us-ascii"
#include <adv3.h>
#include <en_us.h>
// These are identifiers for our various states
enum aliceFoo, aliceBar, aliceBaz;
// A class to hold our FSM logic.
class ActorFSM: Actor
actorFSM = nil
setActorFSM(v?) { actorFSM = v; }
getActorFSM() { return(actorFSM); }
checkActorFSM(v?) { return(actorFSM == v); }
;
startRoom: Room 'Void'
"This is a featureless void. "
;
+me: Person;
// We mix in our ActorFSM class so Alice now has a FSM.
+alice: Person, ActorFSM 'Alice' 'Alice'
"She looks like the first person you'd turn to in a problem. "
isProperName = true
isHer = true
// Set the initial value for the FSM
actorFSM = aliceFoo
;
// We now define an AgendaItem for each state, such as it is.
++AgendaItem
initiallyActive = true
// In this case all we do is check the state, but we could implement additional
// checks if we needed to.
isReady = (alice.checkActorFSM(aliceFoo))
invokeItem() {
// All the agenda actually does is a scrap of output and then set
// the next state.
"Alice mutters <q>Foo</q>. ";
alice.setActorFSM(aliceBar);
}
;
// Additional agendas are functionally identical to the first.
++AgendaItem
initiallyActive = true
isReady = (alice.checkActorFSM(aliceBar))
invokeItem() {
"Alice mutters <q>Bar</q>. ";
alice.setActorFSM(aliceBaz);
}
;
++AgendaItem
initiallyActive = true
isReady = (alice.checkActorFSM(aliceBaz))
invokeItem() {
"Alice mutters <q>Baz</q>. ";
alice.setActorFSM(aliceFoo);
}
;
versionInfo: GameID
name = 'sample'
byline = 'nobody'
authorEmail = 'nobody <foo@bar.com>'
desc = '[This space intentionally left blank]'
version = '1.0'
IFID = '12345'
;
gameMain: GameMainDef
initialPlayerChar = me
;
Here we define an ActorFSM
class for all our actors that need the new functionality. The class just adds a property (to hold the value of the current state) and a couple convenience methods for getting and setting the state (if we wanted to implement any sanity checking to verify that our state value is always valid—verify a new value before setting it, returning a failsafe value if no state has been set, and so on—we could do it here).
In each AgendaItem
we just check the value of the state, but we could also query other bits of the game state (is Alice in a specific room? is the player in the same room with Alice? and so on) if we need/want to (or, alternately, implement these conditions as explicit states in our FSM model).
In this example we use an enum
for the allowed state values, but we don’t have to—we could use arbitrary strings or numeric values or even objects if we wanted to.
We could also stuff all our NPC logic into a single AgendaItem
and just use a big switch()
statement with a case
for each state (and possibly a default
as a fallback to reset things if we’ve somehow managed to set a bogus value as the state).
The thrilling transcript:
Void
This is a featureless void. A television is mounted on one wall.
Alice is standing here.
>z
Time passes...
Alice mutters "Foo".
>z
Time passes...
Alice mutters "Bar".
>z
Time passes...
Alice mutters "Baz".
>z
Time passes...
Alice mutters "Foo".
…and so on.