|
Features

GDC
2002: Social Activities: Implementing Wittgenstein
The Implementation
of Activity-Orientated structure
Requirements
for an Implementation of Activities
Our
activity implementation must satisfy these requirements
- It must
be easy to add new activities
- Processing
activities must be fast
- Activities
must be easily debuggable
- The
consequential structure of activities must be able to be queried by
agents, so that they can
determine whether or not it is in their interest to accede to requests.
Already
these requirements seem almost incompatible: if activities must be efficient
and debuggable, they should be written in C++ so we can use the existing
optimisation and debugging tools, but if they are written in C++ it won't
be very easy for content authors to add a new activity, because it will
require a programmer, and it will be very difficult for the structure
of the activity to be exposed to the agents: it might necessitate duplication
of information, once to do things, and once to tell the agents what will
be done. If, on the other hand, an activity is defined in a text file
in a nice easy-to-use language, then content authors will be able to add
new activities easily, and the agents will be able to use the information,
but we lose the optimisation and debugging tools which C++ gives us. What
to do?
The solution we have adopted is to define a new activity in a text-file,
in a special-purpose easy-to-use language, but when this activity is parsed,
c++ files are generated. This way we get the best of both worlds!
Implementation: Overview
A new activity is defined in a text file. When the activity is parsed,
a new c++ activity is defined in cpp and header files. This activity inherits
from the base Activity class:
The alternative
to this might be some C preprocessor based technology (Game Programming
Gems Vol 1, Section 3.1 - A Finite State Machine Class). This would
have the advantages of being simpler (one would not need to write a parser)
and one would gain the benefit that the code the debugger debugs would
be precisely the same as that the user wrote, rather than merely generated
from it. However, this would unacceptably straightjacket us - the preprocessor
is a very weak tool. Moreover, writing a parser is not very hard using
lex and yacc (note also that the hard part of writing a compiler from
scratch, the semantics, can be partly avoided as we build our language
on C++ and can leave some of the semantics to the C++ compiler). Moreover,
on certain systems, via use of techniques like #line preprocessor directives,
the debugger can be made to take the user directly to the appropriate
line of the activity file, if the system breaks in an activity.
Activities
in More Detail
Now we have decided that a dedicated 'front' to C++ as a form for
an activity system, we must ask what form we would like that front to
take. The first approximation to an answer would, for most game programmers,
be a state machine. State machines have a proven history, and are very
useful. However, it turns out to be useful to extend this slightly, to
make the states hierarchical, and give states local variables. These are
automatically constructed on entry and destroyed on exit. This is very
similar to programming using the local stack machines in (Game Programming
Gems 2, Section 3.2, Micro-Threads for Game Object AI) Thus an activity
defines a state hierarchy:
activity
Name(Parameter1,
ParameterN) {
State1:
State
1.1:
State
1.1.1
State
1.1.2
State
1.2
State
1.2.1
}
Each particular
state can have its own parameters, its own preconditions, its own message
handling, its own local variables with initialisations, its own actions,
and its own sub-states.
Needs
Needs conditions specify what needs to be true for the activity to remain
in this state, and what state to switch into if circumstances change.
If we are in a particular state, all needs conditions in all parents states
are checked before checking our own particular state's. If we have the
state tree:
S1:
needs 1
S2:
{
needs
2
..
}
S3:
and we are
in state S2, then needs1 will be checked, followed by needs2.
Requests
The only way an activity can affect the game-world is by issuing a request
to an agent. Request statements ask agents if they wouldn't mind performing
actions, and waits to find out if they actually bother to perform these
activities.
A requests
statement will appear in a state of the form:
//
needs conditions
requests
agent1.action1
!Þ Rejection(1)
®
Finished
{
// actions
to perform while waiting
}
The first time this state is entered, the requests are passed to the agents
concerned. From then on, we wait to see if any agent rejects the request;
if so the rejection message is passed. If all agents finish their requested
activities, state moves to Finished.
Messages
Messages are a way for activities to communicate without having to knowing
each other's implementation details. Activities respond to messages using
message-handlers. A message can have various parameters which are passed
to the handler. If a message is passed to an activity, the handler function
is called immediately. The handler may or may not change the state of
the activity. Messages can be defined at any level of the state hierarchy
-- handlers lower down the chain take priority over more general handlers.
______________________________________________________
|