home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
No Fragments Archive 10: Diskmags
/
nf_archive_10.iso
/
MAGS
/
SYNTAX
/
SPECIALPD1.MSA
/
TADS4.ART
< prev
next >
Wrap
Text File
|
1994-10-25
|
10KB
|
241 lines
TADS Programming (4) - Fuses and Daemons
Contributed by Michael J Roberts
One of the more powerful but obscure features in TADS is the
ability to schedule future operations using "fuses" and "daemons".
This scheduling feature can be used for many purposes, from
animating characters to setting off traps.
What are fuses and daemons? The term "fuse" refers to the bit of
string that you'd attach to a stick of dynamite in order to
produce a delay before setting off the explosive. Just as with a
fuse made of string, a TADS fuse lets you set up an event to
happen after a delay. The term "daemon" is borrowed from its
usage in Unix and other operating systems to refer to programs
that run in the background, performing system maintenance
functions automatically without user intervention. TADS daemons
are functions that are executed after each turn, without the game
program needing to call them explicitly.
In TADS, the unit of time is a turn. Both fuses and daemons are
scheduled based on turns. When you set a fuse, you specify the
number of turns until the fuse "burns down", at which point the
fuse function will be called. Daemons are called after each
command the player enters.
As an example, let's set a simple fuse that displays a message
after three turns. First, we need to define the function that the
fuse will call when it burns down.
myFuse: function(parm)
{
"\bHello from myFuse!!!";
}
Second, we need to set the fuse. To do this, simply call
setfuse(), the built-in function that schedules a new fuse. We
could define a new verb whose only purpose is to set this fuse to
be called three turns from now:
fuseVerb: deepverb
verb = 'setfuse'
action(actor) = { setfuse(myFuse, 3, nil); }
;
You may be curious about two features of the myFuse function.
First, what's that argument "parm"? Second, why is that "\b"
sequence there?
The argument "parm" is provided because TADS always passes one
argument to a fuse or a daemon when it's called. The value of
this argument is simply the value that you passed to the setfuse()
or setdaemon() built-in function in the first place. This value
isn't used by the system at all -- it's entirely for your use.
The reason it's provided is so that you can use the same fuse
function in several different ways if you want to; the function
can figure out what it's supposed to do based on the value of the
argument. In practice, most fuse and daemon functions have only
one use, so the parameter is ignored, and you can just pass "nil"
as the parameter value in setfuse() or setdaemon().
The "\b" sequence is included because you can't easily predict
what will be displayed immediately before a fuse or daemon is
invoked. Since these functions will be called by TADS itself
between turns, the messages they print need to be set off from the
adjacent text. The best way to do this is to display a blank line
before a fuse's messages. Some people may prefer to simply print
a newline and a tab; to do this, substitute "\n\t" for "\b".
Daemons are very similar to fuses. The difference, of course, is
that a daemon is called after every turn; a fuse is only called
once, after a specified number of turns has elapsed.
You should also be aware of "notifiers", which are similar to
fuses and daemons, but invoke a method of an object, rather than a
function. These are sometimes more convenient to code, but are
otherwise the same as fuses and daemons. We could rewrite the
fuse above using a notifier.
notifyVerb: deepverb
myNotifier = { "\bHello from notifyVerb.myNotifier!!!"; }
verb = 'notify'
action(actor) = { notify(self, &myNotifier, 3); }
;
That ampersand, "&", in the call to notify() is quite important.
It tells TADS that you're only referring to the property
myNotifier for future reference, and you don't want to evaluate it
immediately. Always remember to include the ampersand when
calling notify(). Note that the third argument to notify() is the
number of turns to wait before calling the property; if the number
of turns is zero, it means that the property should be called
after every turn -- which means that it acts like a daemon. Note
that the new built-in function rundaemons() calls both kinds of
daemons: those set with setdaemon(), and those set with notify()
used with 0 as the third argument. However, a daemon started with
notify() is removed with unnotify(), not with remdaemon().
Let's look at some uses for fuses and daemons.
A fuse would be useful if you wanted to create a door on springs,
which automatically closes a few turns after it's opened. Here's
a doorway object that would behave this way.
screenDoor: doorway
sdesc = "screen door"
noun = 'door'
adjective = 'screen'
location = porch
springClose =
{
if (self.isopen)
{
if (Me.location = self.location)
"\bThe screen door swings shut.";
self.isopen := nil;
}
}
doOpen(actor) =
{
notify(self, &springClose, 3);
pass doOpen;
}
;
The springClose method demonstrates another couple of important
things you should keep in mind when writing fuses and daemons.
First, note that the method checks self.isopen before doing
anything; this is because the player could have manually closed
the door before the fuse is fired. This is often true of fuses
and daemons -- because they happen after some number of player
moves, the player could do something that changes the state of the
game between the time the fuse is scheduled and the time it is
fired. So, you should always check the current conditions at the
time the fuse is fired to make sure everything is as you expect.
Second, note that the method checks the player's location
(Me.location) prior to displaying a message; this is because the
player could have left the room, in which case the message about
the door closing would be out of place. Regardless of the
player's location, though, the door is closed.
Daemons have many uses. One of the most obvious is for animating
characters. For an example of this, you can look at lloyd.follow
in Ditch Day Drifter; this is a daemon that causes Lloyd (the
insurance robot) to follow the player, or to display a wacky
message any time the player and Lloyd are in the same room. This
daemon makes Lloyd do things on his own, which makes the game feel
more alive.
A less obvious use for daemons is to take some special action when
a set of conditions in the game has been met. Using a daemon to
check conditions can often make your coding job a lot easier,
because you only have to figure out what the conditions are -- you
don't have to figure out all the different ways they can be
satisfied.
For example, suppose that you want to design a trap similar to the
venerable puzzle involving the pedestal and gold skull in the TADS
Author's Manual, only you wanted to generalize it. The big
opportunity for improvement is to make the trap go off whenever
the pedestal is down to too little weight, regardless of how the
weight got removed. One way to do this would be using the
pedestal's Grab method, which is called whenever anything is
removed from the pedestal.
But suppose that you implemented an object that involved an
evaporating liquid. Using a daemon, naturally, you could
implement a flask that lost a unit of weight each turn the flask
was open. Now, if you put the open flask on the pedestal,
eventually enough liquid could evaporate that the pedestal trap
should fire. This wouldn't be detected with Grab, because the
flask isn't removed from the pedestal -- it simply gets lighter.
The solution, of course, is to use a daemon. You could design a
simple daemon that checks the weight of the objects on the
pedestal on each turn, and sets off the trap if the weight is too
low.
pedestal: fixeditem, surface
noun = 'pedestal'
sdesc = "pedestal"
location = altarRoom
checkWeight =
{
if (addweight(self.contents) < 5)
{
if (Me.location = self.location)
{
"\bA volley of poisonous arrows shoots from the
walls! You try to avoid them, but you cannot...\b";
die();
}
else
{
"\bYou hear a loud noise somewhere nearby.";
unnotify(self, &checkWeight);
arrows.moveInto(self.location);
}
}
}
;
Now, we have to start the daemon somewhere. This could be done in
the enterRoom(actor) method in the altarRoom the first time the
player enters the room:
enterRoom(actor) =
{
if (not self.isseen) notify(self, &checkWeight, 0);
pass enterRoom;
}
The checkWeight daemon runs after every turn, and checks the
contents of the pedestal to see if they provide enough weight to
keep the trap from going off. When the weight becomes too low --
for whatever reason -- the trap goes off. Note the unnotify()
call in the checkWeight daemon. This call stops the daemon. This
is necessary, because the trap can only spring once; after that,
you never want it activated again. If you left the daemon
running, it would set off the trap on every subsequent turn, which
isn't exactly what we had in mind.
Fuses and daemons have many other uses. After you've experimented
with these features a little bit, you'll probably find that they
aren't too difficult to use, once you know the basic tricks:
always pay attention to message formatting, and always check
conditions at the time of the fuse's or daemon's invocation to
make sure they're what you expect.
- o -
ə