This library provides artificial intelligence routines for the interactive fiction title. It requires the Basic RPG library, and well as the Basic IF library to run.
01 cAI – Artificial intelligence
Information about what storylines are and how to create them.
cAI - Artificial intellgience
Circumreality supports more complex artificial intelligence abilities
than are found in most IF or MUD authoring kits.
All of the AI programming is placed in this library, the
artificial intelligence library. Most of the AI code is
accessed from cAI, which is automatically included in
the cCharacter class, so all characters will have AI.
The Circumreality AI system includes the following features:
-
Goals - The ability for an AI to have goals,
and the use of a finite state machine to complete those
goals. Goals might be as simple as the desire to another
place in the world or to attack another character.
-
Memories - AIs remember information about
other characters. This information could include the
character's names, how much they liked the last character,
when they last saw them, etc.
-
Conversations - AIs support primitive natural
language parsing and conversation abilities. (Although it's
primitive, it's infinitely more complex than conversations
in most IF titles and MUDs though.)
Important: If your game is a multiplayer game
and you want a character (based off cCharacter, which
is based off cAI) to remember players across reboots,
then that character must also be based off
cSaveToDatabase. However, characters that are based off
cSaveToDatabase aren't allowed to enter private
player instances, since those characters might end up trapped
in the instance of one specific player, and not be available to
other players.
02 cAIgoals – Goals
Information about what storylines are and how to create them.
cAIGoals - Goals and finite state machines
When a game developer creates the code to handle the intelligence
behind a computer-controlled character they usually use an algorithmic
technique called a "Finite State Machine" (FSM). The FSM provides
a semblence of intelligence, and allows the character to walk
around the world, attack any enemies that come into range,
and run away if it's wounded. FSMs don't handle conversations
though.
A FSM works by defining several "states" for a character's AI.
A state is, essentially, a state of mind. Enemies in most
first-person shooter games (FPS), have the following states:
-
Partol - The NPC wanders around on a predefined
route. If the NPC sees an enemy it switch to the "Attack"
state.
-
Attack - The NPC sees an enemy (such as the
player) and attacks. If the player dies or manages to run
away, the AI returns to the "Patrol" state. If the NPC
is badly wounded the AI goes into the "Flee" state.
-
Flee - The NPC runs away from the enemy
and finds a space to hide. It then heals up over time, and
returns to the "Patrol" state.
Finite state machines work pretty well for FPS games because
the player to NPC interactions are pretty simple: Kill or be
killed.
FSMs do start showing their weaknesses though. One major
weakness in a simple FSM is that sub-FSMs are needed.
For example: The "Attack" state really requires a number of
sub-states, such as "Load weapon", "Fire", "Run towards enemy",
"Take cover", etc. Each of these sub-states are a FSM by
themselves. In effect, the ability for a FSM to have sub-FSMs
(and sub-states) is like the C++ main() function being able
to call other functions.
The other weakness about FSMs is that they have a single
track mind. If the programmer didn't program in a state
transition, there's AI will never diverge from the state.
This is especially noticable when FSMs are applied to
AIs for IF titles or MUDs.
You might want to design a character that wanders around
the world. If the character sees something valuable it picks it
up. If it happens by a merchant, it'll sell the object. If
it's attacked, it will attack back. You can impliment this in
a FSM, but the number of states and complexities of transitioning
between states is huge. What you really need is a FSM for
wandering around, a FSM for picking things up, a FSM for selling
items to a merchant, and a FSM for attacking. However, a normal
FSM system cannot handle four FSMs running at once.
Circumreality's AI system can handle both sub-FSMs and multiple FSMs running
at once.
If you want to learn more about Finite State Machines, do a search
on the Internet, or read some books about game AI.
A finite state machine in Circumreality is called a "goal", because it
does a lot more than a traditional FSM. Each goal is an object
derived from cAIGoal. The object construct is used to group
the code together rather to provide a common storage method.
(In fact, you shouldn't store any information in your goal object
because the same object will be used for several AIs, all running
at the same time.)
To create a goal object:
-
Create a new object and name it "oAIGoalXXX", where
XXX described what the goal does. A goal that wanders around
randomly might be called "oAIGoalWanderAroundRandomly".
-
Make it a sub-class of cAIGoal.
-
Check the option to create as an object so
it's an actual object, not just an abstract class.
-
Write the AIGoalStart() method, as well as
method for other states. See below.
-
Optionally, write the AIGoalPriorityAdjust() method.
See below.
-
Optionally, you may want players to be able to ask the NPC
what they're doing. To support this, provide
a pAIGoalWhatDoingSpeak, pAIGoalWhatDoingLike,
and pAIGoalWhatDoingImportance or
write your own AIGoalWhatDoing().
Speaking in FSM terminology, AIGoalStart() is the first "state"
in the FSM. Usually AIGoalStart() doesn't even act as a state
in the traditional FSM meaning, but acts like an "AI planner"...
When an AI creates a goal, it can send a parameter (or list
with multiple parameters) to the goal just like a call to
a function or method can be passed parameters. Many goal objects
are written so the parameter doesn't contain enough information
to determine what states need to be run. Instead, the first
task of the goal object is to plan out the specifics of how
to interpret the parameters.
If this doesn't make sense (which it probably doesn't), then
let me explain in English: An AI may have a goal of walking
from its current room to the neighborhood bakery. The neighborhood
bakery is a room in the world that isn't necessarily near
the character's current position. When the oAIGoalMove goal
is invoked by the AI, it will be passed in a parameter for
the bakery's room, oRoomBakery. It's then up to the
oAIGoalMove.AIGoalStart() method to figure out exactly how to
get to the bakery, such as going north to oRoomStreetMain, then
east to oRoomStreetOriely, then south into oRoomBakery. Only
once it has a plan will AIGoalStart() pass control to another goal.
Therefore, if you wanted to write oAIGoalMove, you'd need to
write the AIGoalStart() method with the following pseudo-code:
-
Figure out what room the AI is in.
-
Figure out how to get from the character's current room to the new one.
-
Remember this sequence of rooms so you don't need to recalculate it
each time.
-
Switch to the "walk to next room" state.
You'll also need a "Walk to next room" state which has the following
code:
-
Look at the list of rooms to walk to. If it's empty then the AI
has met it's goal, and should return TRUE to whatever began
the goal.
-
Wait 5 seconds (or whatever) so that the NPC appears to
be waking at a normal pace. If the AI doesn't wait, the NPC
will get to the bakery in mere microsends.
-
Otherwise, look at the next room in the list and walk there.
-
Remove the name from the list.
Goal objects can do easily do this. Here's how:
-
Write the code for AIGoalStart() as described
above.
AIGoalStart() is passed the character's object
in the Actor parameter. This can be used to determine
where the character is.
The Parameter parameter of AIGoalStart() should
expect the destination room to be passed as an object.
With the character's current room and destination room known,
AIGoalStart() can calculate the path needed using the A*
algorithm. I won't get to that right now
The AIGoalStart() method is passed a list in
the Settings paramter. You can store any information
in this list that you like. (It acts as a substitiute for member
variables, which you cannot use to store information.
Remember your oAIGoalMove object may be used by more than
one NPC simultanously.
Once the path has been written into the "Settings" list,
AIGoalStart() should call AIGoalNext() and return
the value. AIGoalNext() is provided by cAIGoal,
and wraps up the input parameters into a list, that can
be returned from AIGoalStart(), and is interpreted by
AIGoalStart's caller. The code might look
like return AIGoalNext (WalkToNextRoom, 5.0,
NULL); WalkToNextRoom is the method that handle the
"walk to next room" state. 5.0 is the number of seconds to
delay before reaching that state. NULL is the parameter that
will be passed into WalkToNextRoom, but it isn't needed.
-
Create a private method called WalkToNextRoom. It
should accept the same paramters as AIGoalNext().
WalkToNextRoom() receives the same Settings list that was
passed into AIGoalsStart(). It can look at the list
and see what rooms it needs to move to. If the list
is empty then WalkToNextRoom should return
AIGoalNextFinished (TRUE); This informs the goal
monitoring code in cAI that the goal is finished, and
that is has been successfully completed (by returning TRUE).
If there are still rooms on the list,
call ActorMove() with the room, and remove
the room from the Settings list. Then, return
AIGoalNextRepeat(NULL) to repeat the same state (of
WalkToNextRoom()), 5 seconds from now.
If you want the NPC to walk to the bakery as soon as its
created you can just modify pAIGoalsEmpty in the
NPC object, and
change it to [[oAIGoalMove, 0, oRoomBakery]].
If you start up your IF title, and can log on quickly enough,
you'll see the NPC walking to the bakery. He will move to
a new room every 5 seconds.
The pAIGoalsEmpty parameter is used by cAI whenever
it comes across an AI without any goals... which basically
means when the object is first created. The list contains
a sub-list for every goal the AI should start out with. Each
sub-list starts with a goal object, followed by a priority, and
then the parameter to pass into the goal's AIGoalStart() method.
(I'll get to priority in a bit.)
What happens if you want the NPC to wander from his starting
room to the bakery, and then to the local pub, and then
back to his starting location?
You could modify the oAIGoalMove object that you wrote to
handle all three locations, or you could use sub-goals, letting
oAIGoalMove handle the sub-goal of moving from one spot to another.
To create a goal that handles "Follow a path" that would
move the NPC from A to B to C and back to A, just do the following:
-
Create a new object, oAIGoalFollowAPath just like
you created oAIGoalMove.
-
Write the AIGoalStart() method for the goal.
It should expect that the parameter passed into it is a list
of rooms, such as [oRoomBakery, oRoomPub, oRoomStart].
AIGoalStart() should remember this list in the Settings list.
You can just do Settings.ListConcat(0, Parameter);,
which will leave Settings with [0, oRoomBakery,
oRoomPub, oRoomStart]. The first paremter, "0", is the next
room in the list to go to.
AIGoalStart() then should call return AIGoalNext
(WalkedToRoom, 0, TRUE); This causes the WalkedToRoom
goal to be run in 0-seconds and be passed in TRUE (which means
that no error has occurred).
-
Write a private method, WalkedToRoom, with the
same parameters as AIGoalStart().
If the Parameter != TRUE then there has
been an error, and WalkedToRoom() should return
AIGoalNextFinished (FALSE); This will stop the NPC's
wandering around from room to room.
Otherwise, the character should walk to the next room.
Remember var vRoom = Settings[Settings[0]+1];,
which is the next room on the list. Advance to the next
room by calling Settings[0] = (Settings[0] + 1) %
(Settings.ListNumber()-1);
Then, to walk to vRoom, just return AIGoalNextSubGoal
(WalkedToRoom, oAIGoalMove, 0, 0, vRoom); This
tells the AI system to run the sub-goal, oAIGoalMove, which
you wrote previously. oAIGoalMove is run with a priority of
0 (which I'll explain later), and a delay of 0 seconds (why
wait?). The parameter is vRoom, which is the next room to
walk to. The first parameter, WalkedToRoom, is the state in
this goal that will be called with oAIGoalMove reaches its
goal. When oAIGoalMove.WalkToNextRoom() reaches its destination
it will call AIGoalNextFinished(TRUE). The "TRUE" parameter
will be passed into oAIGoalFollowAPath.WalkedToRoom(), so
the "follow a path" goal knows that everything went okay.
-
Modify pAIGoalsEmpty to
be [[oAIGoalFollowAPath, 0, [oRoomBakery, oRoomPub,
oRoomStart]]. Now, the NPC will walk between the
bakery, pub, and start room and won't ever stop.
Running multiple goals at once
What if you want your NPC to pick up any objects that it sees
along its path?
You could modify your oAIGoalMove code to also look for unclaimed
objects and pick them up. However, since not all NPCs will
be cleptomaniacs, you now need two goal objects, oAIGoalMove and
oAIGoalMoveClepto. Plus, you'd need two "follow a path" objects,
oAIGoalFollowAPath and oAIGoalFollowAPathClepto.
There is an easier way...
-
Create a new goal, oAIGoalPickUpAnything that
ends up looking around and picking up objects that it sees.
That's all it does. It will require a few methods just like
my previous examples. I won't go into detail.
-
Modify pAIGoalsEmpty to
contain [[oAIGoalFollowAPath, 0, [oRoomBakery, oRoomPub,
oRoomStart], [oAIGoalPickUpAnything, -10] ]. This
new property tells the AI for the NPC object to create two goals,
one of which is to wander around and the other is to pick
up objects. The list element after oAIGoalPickUpAnything is
-10, which is the priority. I didn't put a third element,
the parameter, because pick up anything doesn't require
any parameters.
-
Modify your world to include some objects
placed along the NPC's path.
-
Start your IF.
You'll be disappointed. The AI just wanders around the world
and doesn't ever pick any of the objects up. That's because
the priority score for oAIGoalPickUpAnything is less than
the priority for oAIGoalFollowAPath. If you make the priority
higher, you'll discover that the NPC picks up all the objects
in the starting room, but doesn't ever move.
The solution is to write a AIGoalPriorityAdjust() method
for the oAIGoalPickUpAnything object.
When the AI system realizes that more than one goal is active
for a NPC, it uses the priority to determine which goal to run.
However, you may want the priority to be dynamic, depending upon
the character's location, surroundings, or health. The AI
system automatically calls AIGoalPriorityAdjust() for each
goal and adds
the number it returns to the goal's score. Most goals don't
bother with this function, but some need to.
In the case of the cleptomaniac NPC, if oAIGoalPickUpAnything's
AIGoalPriorityAdjust() returns 20 (or any number >= 11), then
the oAIGoalPickUpAnything goal will temporarily have a higher
priority than oAIGoalFollowAPath. Therefore, write code in
oAIGoalPickUpAnything.AIGoalPriorityAdjust() that checks
the room for objects to pick up. If it finds any it
can return 20. If it doesn't find any it will just return 0.
If you make these changes the NPC will wander around its route
and pick up any objects along the way.
You can use multiple goals with dynamic priorities for lots of
things:
-
The "drink healing potion" goal would normally have a low
priority that would increase if the character was wounded.
-
The "run away" goal could likewise have a low priority that
increases if the character is wounded.
-
A "find the store and eat goal" could increase its priority
based upon the hunger of the character.
-
Etc.
Temporarily suspending goals
An NPC wandering around the world and picking up anything
can also be achieved by
using AIGoalNextSuspend(). A goal which temporarily
suspends itself waits a given amount of time before waking up
and doing processing. The "pick up anything" goal could wake up
every 5-10 seconds, and see if any objects were around. If there
weren't any it would go back to sleep, suspending itself for
another 5-10 seconds. If it found an object it would initiate
the oAIGoalObjectGetDrop goal to pick up it up.
Programmatically adding new goals
So far I've been using pAIGoalsEmpty to add goals
to the AI's list of goals. If you wish to have some other
code add a new goal, you can call oNPC.AIGoalAdd() to
add one or more goals to the list.
Remember, that when a goal is added the highest priority ones
will run first, while lower priority goals will wait until
they're highest priority. (Unless, of course, the goal supports
AIGoalPriorityAdjust()).
You can use this to your advantage. For example: A NPC has the
oAIGoalFollowAPath goal running. However, if another character
speaks to the NPC, then your code to add the goal
oAIGoalHavingConversation with a higher priority
than oAIGoalFollowAPath. This would cause the NPC to stop
walking and talk to whomever has asked the question. If the NPC
weren't asked a question for 20 seconds, or if the other
character walks away, the oAIGoalHavingConversation goal could
exit by returning AIGoalNextFinished(), and the NPC would
procede to follow its path.
cCharacter objects, as well as any other object with cAI as a superclass
also supports the following methods:
-
AIGoalAdd() - As mentioned above. Add a goal to
the AI's list of goals.
-
AIGoalFind() - Find a goal in the AI's list.
-
AIGoalNumber() - Return the number of goals.
-
AIGoalOverride() - This is a method that you write
yourself. It lets you create a custom personality for NPCs, so
that they use different goals than the norm. For example: If you
want a character that uses different attack aiming tactics than normal,
you would have the AIGoalOverride() look for
references to the default aim handler, oAIGoalCombatAimChoose,
and return your own goal, such as oAIGoalCombatMYAimChoose.
-
AIGoalQuery() - Get information about a goal,
such as its callback, timing, or whether it's suspeneded until
a sub-goal finishes.
-
AIGoalRemove() - Remove a goal from the AI's list.
Goals are the basic mechanism for getting a NPC to "do stuff"
like walk around, fight, etc. You can have goals run sub-goals,
as well as running several goals at once. By combinging sub-goals
and multiple goals (potentially with dynamic priorities) you can
create a wide variety of behaviours for your AIs.
For a complete lists of pre-programmed
goals, look for objects with the "oAIGoal" prefix.
Share with your friends: |