Objects make up the substance of the world.
—Ludwig Wittgenstein (1889–1951), Tractatus
nothing
In Inform, objects are little bundles of routines and variables tied
up together. Dividing up the source code into objects is a good way
to organise any large, complicated program, and makes particular
sense for an adventure game, based as it usually is on simulated
items and places. One item in the simulated world corresponds to one
“object” in the source code. Each of these pieces of the story file
should take responsibility for its own behaviour, so for instance a
brass lamp in an adventure game might be coded with an Inform object
called brass_lamp
, containing all of the game rules which
affect the lamp. Then again, objects do have to interact.
In West Pit
You are at the bottom of the western pit in the twopit room. There
is a large hole in the wall about 25 feet above you.
There is a tiny little plant in the pit, murmuring “Water,
water,…”
In this moment from ‘Advent’, the player
is more or less openly invited to water the plant. There might be many
ways to bring water to it, or indeed to bring liquids other than water,
and the rules for what happens will obviously affect the bottle carrying
the water, the water itself and the plant. Where should the rules appear
in the source code? Ideally, the plant
object should
know about growing and the bottle
about being filled up
and emptied. Many people feel that the most elegant approach is for
the bottle
, or any other flask, not to interfere with
the plant directly but instead to send a “message” to the
plant
to say “you have been watered”. It's then easy to add
other solutions to the same puzzle: a successful rain dance, for
instance, could also result in a “you have been watered”
message being sent to plant
. The whole behaviour of the
plant
could be altered without needing even to look at
the rain-dance
or the bottle source code. Objects like this can frequently
be cut out of the source code for one game and placed into another, still
working.
This traffic of messages between objects goes on
continuously in Inform-compiled adventure games. When the player tries
to pick up a wicker cage, the Inform library sends a message to the cage
object asking if it minds being picked up. When the player tries to go
north from the Hall of Mists, the library sends a message to an object
called Hall_of_Mists
asking where that would lead, and
so on.
· · · · ·
Typical large story files have so many objects (‘Curses’, for instance, has 550) that it is convenient to group similar objects together into “classes”. For instance, ‘Advent’ began life as a simulation of part of the Mammoth and Flint Ridge cave system of Kentucky, caves which contain many, many dead ends. The Inform source code could become very repetitive without a class like so:
Class DeadEndRoom with short_name "Dead End", description "You have reached a dead end.", cant_go "You'll have to go back the way you came.";
Leaving the exact syntax for later, this code lays out some common features of dead ends. All kinds of elegant things can be done with classes if you like, or not if you don't.
Objects can belong to several different classes
at once, and it is sometimes convenient to be able to check whether
or not a given object belongs to a given class. For instance,
in adventures compiled with the Inform library, a variable called
location
always holds the player's current position,
so it might be useful to do this:
if (location ofclass DeadEndRoom) "Perhaps you should go back.";
· · · · ·
Items and places in an Inform game always belong
to at least one class, whatever you define, because they always belong
to the “metaclass” Object
. (“Meta”
from the Greek for “beyond”.) As we shall see, all of the
objects explicitly written out in Inform source code always belong
to Object
. Though you seldom need to know this, there are
three other metaclasses. Classes turn out to be a kind of object in
themselves, and belong to Class
. (So that Class
belongs to itself. If you enjoy object-oriented programming, this
will give you
a warm glow inside). And although you almost never need
to know or care, routines as in §1
are internally considered as a kind of object, of class Routine
;
while strings in double-quotes are likewise of class String
.
That's all, though. If in doubt, you can always find out what kind
of object obj
is, using the built-in function
metaclass(obj)
. Here are some example values:
metaclass("Violin Concerto no. 1") == String metaclass(Main) == Routine metaclass(DeadEndRoom) == Class metaclass(silver_bars) == Object
Classes are useful and important, but for most game-designing purposes it's now safe to forget all about metaclasses.
It turns out to be useful to have a constant called
nothing
and meaning “no object at all”.
It really does mean that: nothing is not an object. If you
try to treat it as one, many programming errors will be printed up
when the story file is played.
▲
If X
is not an object at all, then metaclass(X)
is nothing
, and in particular metaclass(nothing)
is nothing
.
Objects declared in the source code are joined together in an “object tree” which grows through every Inform story file. Adventure games use this to represent which items are contained inside which other items.
It's conventional to think of this as a sort of
family tree without marriages. Each object has a parent, a child
and a sibling. Such a relation is always either another object in
the tree, or else nothing
, so for instance the parent
of an orphan would be nothing
. Here is a sample object
tree:
Meadow |
||||||||
↓child | ||||||||
Mailbox |
sibling | Player |
||||||
→ | ||||||||
↓child | ↓child | |||||||
Note |
Sceptre |
sibling | Bottle |
sibling | Torch |
sibling | Stone |
|
→ | → | → | ||||||
↓child | ||||||||
Battery |
The Mailbox
and Player
are both children of the Meadow
, which is their parent,
but only the Mailbox
is the child of the Meadow
.
The Stone
is the sibling of the Torch
, which
is the sibling of the Bottle
, and so on.
Inform provides special functions for reading off
positions in the tree: parent
, sibling
do
the obvious things, and child
gives the first child:
in addition there's a function called children
which
counts up how many children an object has (note that grandchildren
don't count as children). Here are some sample values:
parent ( Mailbox ) == Meadow children ( Player ) == 4 child ( Player ) == Sceptre child ( Sceptre ) == nothing sibling ( Torch ) == Stone
• WARNING
It is incorrect to apply these functions to the value nothing
,
since it is not an object. If you write a statement like print
children(x);
when the value of x
happens to be
nothing
, the interpreter will print up the message:
[** Programming error: tried to find the “children” of nothing **]
You get a similar error message if you try to apply these tree functions to a routine, string or class.
Objects are made with the directive Object
.
Here is a portion of source code, with the bulk of the definitions
abbreviated to “...”:
Object Bucket ... Object -> Starfish ... Object -> Oyster ... Object -> -> Pearl ... Object -> Sand ...
The resulting tree looks a little like the source code turned on its side:
Bucket |
||||
↓child | ||||
Starfish |
sibling | Oyster |
sibling | Sand |
→ | → | |||
↓child | ||||
Pearl |
The idea is that if no arrows ->
are given
in the Object
definition, then the object has no parent.
If one ->
is given, then the object is made a child
of the last object defined with no arrows; if two are given, it's made
a child of the last object defined with only one arrow; and so on.
An object definition consists of a “head” followed by a “body”, itself divided into “segments”, though there the similarity with caterpillars ends. The head takes the form:
Object
‹arrows›
‹identifier› "textual name"
‹parent›
"textual name"
can be given if the object's
name ever needs to be printed by the program when it is running.All four parts are optional, so that even this bare directive is legal:
Object;
though it makes a nameless and featureless object which is unlikely to be useful.
move
, remove
,
objectloop
The positions of objects in the tree are by no means fixed: objects are created in a particular formation but then shuffled around extensively during the story file's execution. (In an adventure game, where the objects represent items and rooms, objects are moved across the tree whenever the player picks something up or moves around.) The statement
move
‹object›
to
‹object›
moves the first-named object to become a child of the second-named one. All of the first object's own children “move along with it”, i.e., remain its own children.
For instance, starting from the tree as shown in the diagram of §3.2 above,
move Bottle to Mailbox;
results in the tree
Meadow |
||||||||
↓child | ||||||||
Mailbox |
sibling | Player |
||||||
→ | ||||||||
↓child | ↓child | |||||||
Bottle |
sibling | Note |
Sceptre |
sibling | Torch |
sibling | Stone |
|
→ | → | → | ||||||
↓child | ||||||||
Battery |
When an object becomes the child of another in this
way, it always becomes the “eldest” child: that is, it
is the new child()
of its parent, pushing the previous
children over into being its siblings. In the tree above, Bottle
has displaced Note
just so.
You can only move one object in the tree to another: you can't
move Torch to nothing;
because nothing
is not an object.
Instead, you can detach the Torch
branch from the tree
with
remove Torch;
and this would result in:
Meadow |
Torch |
|||||||
↓child | ↓child | |||||||
Mailbox |
sibling | Player |
Battery |
|||||
→ | ||||||||
↓child | ↓child | |||||||
Bottle |
sibling | Note |
Sceptre |
sibling | Stone |
|||
→ | → |
The “object tree” is often fragmented like this into many little trees, and is not so much a tree as a forest.
• WARNING
It would make no sense to have a circle of objects each containing
the next, so if you try to move Meadow
to Note
;
then you'll only move the interpreter to print up:
[** Programming error: tried to move the Meadow to the note, which would make a loop: Meadow in note in mailbox in Meadow **]
· · · · ·
Since objects move around a good deal, it's useful
to be able to test where an object currently is, and the condition
in
is provided for this. For example,
Bottle in Mailbox
is true if and only if the Bottle
is one of the direct children of the Mailbox
.
(Bottle in Mailbox
is true, but
Bottle in Meadow
is false.) Note that
X in Y
is only an abbreviation for parent(X)==Y
but it occurs so often that it's worth having. Similarly, X
notin Y
means parent(X)~=Y
. X
has
to be a bona-fide member of the object tree, but Y
is
allowed to be nothing
, and testing X in
nothing
reveals whether or not X
has been
removed from the rest of the tree.
· · · · ·
The remaining loop statement left over from
§1 is objectloop
.
objectloop(
‹variable-name›)
‹statement›
runs through the ‹statement› once for each object in the game, putting each object in turn into the variable. For example,
objectloop(x) print (name) x, "^";
prints out a list of the textual names of every object in the game. More powerfully, any condition can be written in the brackets, as long as it begins with a variable name.
objectloop (x in Mailbox) print (name) x, "^";
prints the names only of those objects which are
direct children of the Mailbox
object.
The simple case where the condition reads “‹variable›
in
‹object›” is handled in a faster and more
predictable way than other kinds of objectloop
: the loop
variable is guaranteed to run through the children of the object
in sibling order, eldest down to youngest. (This is faster because
it doesn't waste time considering every object in the game, only the
children.) If the condition is not in this form then no guarantee
is made as to the order in which the objects are considered.
•
WARNING
When looping over objects with in
, it's not safe to
move these same objects around: this is like trying to cut a branch
off an elm tree while sitting on it. Code like this:
objectloop(x in Meadow) move x to Sandy_Beach;
looks plausible but is not a safe way to move everything
in the Meadow
, and will instead cause the interpreter to
print up
[** Programming error: objectloop broken because the object mailbox was moved while the loop passed through it **]
Here is a safer way to move the meadow's contents to the beach:
while (child(Meadow)) move child(Meadow) to Sandy_Beach;
This works because when the Meadow
has
no more children, its child is then nothing
, which is
the same as false
.
▲
But it moves the eldest child first, with the possibly undesirable
result that the children arrive in reverse order (Mailbox
and then Player
, say, become Player
and then
Mailbox
). Here is an alternative, moving the youngest
instead of the eldest child each time, which keeps them in the same
order:
while (child(Meadow)) { x = child(Meadow); while (sibling(x)) x = sibling(x); move x to Sandy_Beach; }
Keeping children in order can be worth some thought when game designing. For instance, suppose a tractor is to be moved to a farmyard in which there is already a barn. The experienced game designer might do this like so:
move tractor to Farmyard; move barn to Farmyard;
Although the barn was in the farmyard already, the second statement wasn't redundant: because a moved object becomes the eldest child, the statement does this:
Farmyard |
Farmyard |
|||||
↓child | => | ↓child | ||||
tractor |
sibling | barn |
barn |
sibling | tractor |
|
→ | → |
And this is desirable because the ordering of paragraphs in room descriptions tends to follow the ordering of items in the object tree, and the designer wants the barn mentioned before the tractor.
▲
An objectloop
range can be any condition
so long as a named local or global variable appears immediately
after the open bracket. This means that
objectloop (child(x) == nothing) ...
isn't allowed, because the first thing after the
bracket is child
, but a dodge to get around this is:
objectloop (x && child(x) == nothing) ...
The loop variable of an objectloop
can
never equal false
, because that's the same as
nothing
, which isn't an object.
▲▲
The objectloop
statement runs through all objects of
metaclass Object
or Class
, but skips any
Routine
or String
.
with
and provides
So far Objects
are just tokens with
names attached which can be shuffled around in a tree. They become interesting
when data and routines are attached to them, and this is what the body
of an object definition is for. The body contains four different kinds
of segments, introduced by the keywords:
with has class private
These are all optional and can be given in any order.
▲ They can even be given more than once: that is, there can be two or more of a given kind, which Inform will combine together as if they had been defined in one go. (This is only likely to be useful for automated Inform-writing programs.)
· · · · ·
The most important segment is with
, which
specifies variables and, as we shall see, routines and even arrays,
to be attached to the object. For example,
Object magpie "black-striped bird" with wingspan, worms_eaten;
attaches two variables to the bird, one called
wingspan
, the other called worms_eaten
.
Commas are used to separate them and the object definition
as a whole
ends with a semicolon, as always. Variables of this kind are called
properties, and are referred to in the source code thus:
magpie.wingspan magpie.worms_eaten
Properties are just like global variables: any value you can store in a variable can be stored in a property. But note that
crested_grebe.wingspan magpie.wingspan
are different and may well have different values, which is why the object whose wingspan it is (the magpie or the grebe) has to be named.
The property wingspan
is said to be
provided by both the magpie
and crested_grebe
objects, whereas an object whose with
segment didn't name
wingspan
would not provide it. The dot .
operator can only be used to set the value of a property which is
provided by the object on the left of the dot: if not a programming
error will be printed up when the story file is played.
The presence of a property can be tested using
the provides
condition. For example,
objectloop (x provides wingspan) ...
executes the code ...
for each
object x
in the program which is defined with a
wingspan
property.
▲
Although the provision of a property can be tested, it can't be changed
while the program is running. The value of magpie.wingspan
may change, but not the fact that the magpie provides a
wingspan
.
▲▲ Some special properties, known as “common properties”, can have their values read (but not changed) even for an object which doesn't provide them. All of the properties built into the Inform library are common properties. See §3.14.
· · · · ·
When the magpie is created as above, the initial values of
magpie.wingspan magpie.worms_eaten
are both 0. To create the magpie with a given wingspan, we have to specify an initial value, which we do by giving it after the name, e.g.:
Object magpie "black-striped bird" with wingspan 5, worms_eaten;
The story file now begins with magpie.wingspan
equal to 5, though magpie.worms_eaten
still equal to 0.
· · · · ·
A property can contain a routine instead of a value. In the definition
Object magpie "black-striped bird" with name 'magpie' 'bird' 'black-striped' 'black' 'striped', wingspan 5, flying_strength [; return magpie.wingspan + magpie.worms_eaten; ], worms_eaten;
The value of magpie.flying_strength
is given as a routine, in square brackets as usual. Note that the
Object
continues where it left off after the routine-end
marker, ]
. Routines which are written in as property
values are called “embedded” and are the way objects
receive messages, as we shall see.
▲ If, during play, you want to change the way a magpie's flying strength is calculated, you can simply change the value of its property:
magpie.flying_strength = ExhaustedBirdFS;
where ExhaustedBirdFS
is the name
of a routine to perform the new calculation.
▲ Embedded routines are just like ordinary ones, with two exceptions:
magpie.flying_strength
instead.]
end-marker of an embedded
routine, then it returns false
, not true
(as
a non-embedded routine would).▲▲ Properties can be arrays instead of variables. If two or more consecutive values are given for the same property, it becomes an array. Thus,
Object magpie "black-striped bird" with name 'magpie' 'bird' 'black-striped' 'black' 'striped', wingspan 5, worms_eaten;
You can't write magpie.name
because
there is no single value: rather, there is an -->
array (see §2.4). This array must be accessed
using two special operators, .&
and .#
,
for the array and its length, as follows.
magpie.&name
means “the array held in magpie's name property”, so that the actual name values are in the entries
magpie.&name-->0, magpie.&name-->1, ..., magpie.&name-->4
The size of this array can be discovered with
magpie.#name
which evaluates to the twice the number of entries, in this case, to 10. Twice the number of entries because that is the number of bytes in the array: people fairly often use property arrays as byte arrays to save on memory.
▲
name
is a special property created by Inform, intended
to hold dictionary words which can refer to an object.
private
properties▲
A system is provided for “encapsulating” certain properties
so that only the object itself has access to them. These are defined
by giving them in a segment of the object declaration called
private
. For instance,
Object sentry "sentry" private pass_number 16339, with challenge [ attempt; if (attempt == sentry.pass_number) "Approach, friend!"; "Stand off, stranger."; ];
provides for two properties: challenge
,
which is public, and pass_number
, which can be used
only by the sentry's own embedded routines.
▲▲
This makes the provides
condition slightly more interesting
than it appeared in the previous section. The answer to the question
of whether or not
sentry provides pass_number
depends on who's asking: this condition is true
if it is tested in one of the sentry's own routines, and elsewhere
false. A private
property is so well hidden that nobody
else can even know whether or not it exists.
has
and give
In addition to properties, objects have flags attached, called “attributes”. Recall that flags are a limited form of variable which can only have two values, sometimes called set and clear.) Unlike property names, attribute names have to be declared before use with a directive like:
Attribute hungry;
Once this declaration is made, every object in the
tree has a hungry
flag attached, which is either true
or false
at any given time. The state can be tested with
the has
condition:
magpie has hungry
is true if and only if the magpie's hungry
flag
is currently set. You can also test if magpie hasnt hungry
.
There's no apostrophe in hasnt
.
The magpie can now be born hungry
, using
the has
segment in its declaration:
Object magpie "black-striped bird" with wingspan, worms_eaten has hungry;
The has
segment contains a list (without
commas in between) of the attributes which are initially set: for
instance, the steel grate in the Inform example game ‘Advent’
includes the line
has static door openable lockable locked;
The state of an attribute can be changed during
play using the give
statement:
give magpie hungry;
sets the magpie's hungry
attribute, and
give magpie ~hungry;
clears it again. The give
statement
can take more than one attribute at a time, too:
give double_doors_of_the_horizon ~locked openable open;
means “clear locked
and set
openable
and open
”.†
† The refrain from the prelude to Act I of Philip Glass's opera Akhnaten is “Open are the double doors of the horizon/ Unlocked are its bolts”.
▲▲
An attribute can also have a tilde ~
placed in front
in the has
part of an object declaration, indicating
“this is definitely not held”. This is usually what
would have happened anyway, except that class inheritance (see
below) might have passed on an attribute: if so, this is how to get
rid of it again. Suppose there is a whole class of steel grates like
the one in ‘Advent’ mentioned above, providing for a
dozen grates scattered through a game, but you also want a loose grate
L
whose lock has been smashed. If L
belongs
to the class, it will start the game with attributes making it locked
like the others, because the class sets these automatically: but if
you include has ~lockable ~locked;
in its declaration,
these two attributes go away again.
class
inheritanceA class is a prototype design from which other objects are manufactured. These resulting objects are sometimes called instances or members of the class, and are said to inherit from it.
Classes are useful when a group of objects are to
have common features. In the definition of the magpie
above,
a zoologically doubtful formula was laid out for flying strength:
flying_strength [; return magpie.wingspan + magpie.worms_eaten; ],
This formula ought to apply to birds in general, not just to magpies, and in the following definition it does:
Attribute flightless; Class Bird with wingspan 7, flying_strength [; if (self has flightless) return 0; return self.wingspan + self.worms_eaten; ], worms_eaten; Bird "ostrich" with wingspan 3, has flightless; Bird "magpie" with wingspan 5; Bird "crested grebe"; Bird "Great Auk" with wingspan 15; Bird "early bird" with worms_eaten 1;
Facts about birds in general are now located in a
class called Bird
. Every example of a Bird
automatically provides wingspan
, a flying_strength
routine and a count of worms_eaten
. Notice that the Great
Auk is not content with the average avian wingspan of 7, and insists
on measuring 15 across. This is an example of inheritance from a class
being over-ridden by a definition inside the object. The actual values
set up are as follows:
B | B.wingspan |
B.worms_eaten |
ostrich | 3 | 0 |
magpie | 5 | 0 |
crested grebe | 7 | 0 |
Great Auk | 15 | 0 |
early bird | 7 | 1 |
Note also the use of the special value self
in the definition of Bird
. It means “whatever bird
I am”: if the flying_strength
routine is being
run for the ostrich, then self
means the ostrich, and
so on.
The example also demonstrates a general rule: to
create something, begin its declaration with the name of the class you
want it to belong to: a plain Object
, a Class
or now a Bird
.
▲
Sometimes you need to specify that an object belongs to many classes,
not just one. You can do this with the class
segment of
the definition, like so:
Object "goose that lays the golden eggs" class Bird Treasure;
This goose belongs to three classes: Object
of course, as all declared objects do, but also Bird
and
Treasure
. (It inherits from Object
first and
then Bird
and then Treasure
, attribute
settings and property values from later-mentioned classes overriding
earlier ones, so if these classes should give contradictory instructions
then Treasure
gets the last word.) You can also make
class definitions have classes, or rather, pass on membership of other
classes:
Class BirdOfPrey class Bird with wingspan 15, people_eaten; BirdOfPrey kestrel;
makes kestrel
a member of both BirdOfPrey
and of Bird
. Dutiful apostles of object-oriented programming
may want to call BirdOfPrey
a “subclass” of
Bird
. Indeed, they may want to call Inform a “weakly-typed
language with multiple-inheritance”, or more probably a “shambles”.
▲▲
For certain “additive” common properties, clashes between
what classes say and what an instance says are resolved differently:
see §5. Inform's built-in property name
is one of these.
Objects communicate with each other by means of messages. A message has a sender, a receiver and some parameter values attached, and it always produces a reply, which is just a single value. For instance,
x = plant.pour_over(cold_spring_water);
sends the message pour_over
with a
single parameter, cold_spring_water
, to the object
plant
, and puts the reply value into x
.
In order to receive this message, plant
has to provide a pour_over
property. If it doesn't, then
the interpreter will print something like
[** Programming error: the plant (object number 21) has no property pour over to send message **]
when the story file is played. The pour_over
property will normally be a routine, perhaps this one:
pour_over [ liquid; remove liquid; switch(liquid) { oil: "The plant indignantly shakes the oil off its leaves and asks, ~Water?~"; ... } ];
Inside such a routine, self
means the object
receiving the message and sender
means the object which sent
it. In a typical Inform game situation, sender
will often
be the object InformLibrary
, which organises play and
sends out many messages to items and places in the game, consulting
them about what should happen next. Much of any Inform game designer's
time is spent writing properties which receive messages from
InformLibrary
: before
, after
,
each_turn
and n_to
among many others.
You can see all the messages being sent in a game as it runs using the debugging verb “messages”: see §7 for details. This is the Inform version of listening in on police-radio traffic.
▲
It was assumed above that the receiving property value would be a
routine. But this needn't always be true. It can instead be: nothing
,
in which case the reply value is also nothing
(which is
the same as zero and the same as false
). Or it can be
an Object
or a Class
, in which case nothing
happens and the object or class is sent back as the reply value. Or
it can be a string in double-quotes, in which case the string is
printed out, then a new-line is printed, and the reply value is true
.
▲
This can be useful. Here is approximately what happens when the Inform
library tries to move the player northeast from the current room
(the location
) in an adventure game (leaving out some
complications to do with doors):
if (location provides ne_to) { x = location.ne_to(); if (x == nothing) "You can't go that way."; if (x ofclass Object) move player to x; } else "You can't go that way.";
This neatly deals with all of the following cases:
Object Octagonal_Room "Octagonal Room" with ... ne_to "The way north-east is barred by an invisible wall!", w_to Courtyard, e_to [; if (Amulet has worn) { print "A section of the eastern wall suddenly parts before you, allowing you into...^"; return HiddenShrine; } ], s_to [; if (random(5) ~= 1) return Gateway; print "The floor unexpectedly gives way, dropping you through an open hole in the plaster...^"; return random(Maze1, Maze2, Maze3, Maze4); ];
Noteworthy here is that the e_to
routine,
being an embedded routine, returns false
which is the
same as nothing
if the ]
end-marker is
reached, so if the Amulet isn't being worn then there is no map
connection east.
▲▲
The receiving property can even hold an array of values, in which case
the message is sent to each entry in the array in turn. The process stops
as soon as one of these entries replies with a value other than nothing
or false
. If every entry is tried and they all replied nothing
,
then the reply value sent back is nothing
. (This is useful
to the Inform library because it allows before
rules to
be accumulated from the different classes an object belongs to.)
▲
It fairly often happens that an instance of a class needs to behave
almost, but not quite, as the class would suggest. For instance, suppose
the following Treasure
class:
Class Treasure with deposit [; if (self provides deposit_points) score = score + self.deposit_points; else score = score + 5; move self to trophy_case; "You feel a sense of increased esteem and worth."; ];
and we want to create an instance called Bat_Idol
which flutters away, resisting deposition, but only if the room is
dark:
Treasure Bat_Idol "jewelled bat idol" with deposit [; if (location == thedark) { remove self; "There is a clinking, fluttering sound!"; } ... ];
In place of ...
, what we want is all
of the previous source code about depositing treasures. We could
just copy it out again, but a much neater trick is to write:
self.Treasure::deposit();
Instead of sending the message deposit
,
we send the message Treasure::deposit
, which means
“what deposit
would do if it used the value defined
by Treasure
”. The double-colon ::
is
called the “superclass operator”. (The word “superclass”,
in this context, is borrowed from the Smalltalk-80 language.)
▲▲
object.class::property
is the value of property
which the given object would normally inherit from the given class
.
(Or it gives an error if the class doesn't provide that property
or if the object isn't a member of that class).
▲▲
It's perfectly legal to write something like x = Treasure::deposit;
and then to send Bat_Idol.x();
.
In an adventure-game setting, object creation is useful for something like a beach full of stones: if the player wants to pick up more and more stones, the game needs to create a new object for each stone brought into play.
Besides that, it is often elegant to grow structures organically. A maze of caves being generated during play, for example, should have new caves gradually added onto the map as and when needed.
The trouble with this is that since resources cannot be infinite, the cave-objects have to come from somewhere, and at last they come no more. The program must be able to cope with this, and it can present the programmer with real difficulties, especially if the conditions that will prevail when the supply runs out are hard to predict.
Inform does allow object creation during play, but it insists that the programmer must specify in advance the maximum resources which will ever be needed. (For example, the maximum number of beach stones which can ever be in play.) This is a nuisance, but means that the resulting story file will always work, or always fail, identically on every machine running it. It won't do one thing on the designer's 256-megabyte Sun workstation in Venice and then quite another on a player's personal organiser in a commuter train in New Jersey.
· · · · ·
If you want to create objects, you need to define a class for them and to specify N, the maximum number ever needed at once. Objects can be deleted once created, so if all N are in play then deleting one will allow another to be created.
Suppose the beach is to contain up to fifty pebbles. Then:
Class Pebble(50) with name 'smooth' 'pebble', short_name "smooth pebble from the beach";
Pebble
is an ordinary class in every respect, except
that it has the ability to create up to N = 50 instances of
itself.
Creating and destroying objects is done by sending
messages to the class Pebble
itself, so for instance sending
the message Pebble.create()
will bring another pebble
into existence. Classes can receive five different messages, as follows:
remaining()
How many more instances of this class can be created?
create(
‹parameters›)
Replies with a newly created object of this class, or else with
nothing
if no more can be created. If given, the parameters
are passed on to the object so that it can use them to configure itself
(see below).
destroy(I)
Destroys the instance I
, which must previously have been created.
You can't destroy an object which you defined by hand in the source
code. (This is quite unlike remove
, which only takes
an object out of the tree for a while but keeps it in existence,
ready to be moved back again later.)
recreate(I
,
‹parameters›)
Re-initialises the instance I
, as if it had been destroyed
and then created again.
copy(I, J)
Copies the property and attribute values from I
to be
equal to those of J
, where both have to be instances of
the class. (If a property holds an array, this is copied over as well.)
▲
It's rather useful that recreate
and copy
can
be sent for any instances, not just instances which have previously
been created. For example,
Plant.copy(Gilded_Branch, Poison_Ivy)
copies over all the features of a Plant
from
Poison_Ivy
to Gilded_Branch
, but leaves
any other properties and attributes of the gilded branch alone. Likewise,
Treasure.recreate(Gilded_Branch)
only resets the properties to do with Treasure
,
leaving the Plant
properties alone.
▲
If you didn't give a number like (50)
in the class definition,
then you'll find that N is zero. copy
will work as
normal, but remaining
will return zero and create
will always return nothing
. There is nothing to destroy
and since this isn't a class which can create objects, recreate
will not work either. (Oh, and don't try to send these messages to
the class Class
: creating and destroying classes is
called “programming”, and it's far too late when the game
is already being played.)
▲▲
You can even give the number as (0)
. You might do this
either so that Class Gadget(MAX_GADGETS)
in some library
file will work even if the constant MAX_GADGETS
happens
to be zero. Or so that you can at least recreate
existing
members of the class even if you cannot create new ones.
· · · · ·
The following example shows object creation used in a tiny game, dramatising a remark attributed to Isaac Newton (though it appears only in Brewster's Memoirs of Newton).
Constant Story "ISAAC NEWTON'S BEACH"; Constant Headline "^An Interactive Metaphor^"; Include "Parser"; Include "VerbLib"; Class Pebble(50) with name 'smooth' 'pebble' 'stone' 'pebbles//p' 'stones//p', short_name "smooth pebble from the beach", plural "smooth pebbles from the beach"; Object Shingle "Shingle" with description "You seem to be only a boy playing on a sea-shore, and diverting yourself in finding a smoother pebble or a prettier shell than ordinary, whilst the great ocean of truth lies all undiscovered before you.", has light; Object -> "pebble" with name 'smoother' 'pebble' 'stone' 'stones' 'shingle', initial "The breakers drain ceaselessly through the shingle, spilling momentary rock-pools.", before [ new_stone; Take: new_stone = Pebble.create(); if (new_stone == nothing) "You look in vain for a stone smoother than the fifty ever-smoother stones you have gathered so far."; move new_stone to Shingle; <<Take new_stone>>; ], has static; [ Initialise; location = Shingle; "^^^^^Welcome to...^"; ]; Include "Grammar";
In this very small adventure game, if the player types
“take a pebble”, he will get one: more surprisingly, if
he types “take a smoother pebble” he will get another
one, and so on until his inventory listing reads “fifty
smooth pebbles
from the beach”. (See §29
for how to make sure identical objects are described well in Inform
adventure games.) Notice that a newly-created object is in nothing
,
that is, is outside the object tree, so it must be moved to Shingle
in order to come to the player's hand.
· · · · ·
However smooth, one pebble is much like another. More complicated objects sometimes need some setting-up when they are created, and of course in good object-oriented style they ought to do this setting-up for themselves. Here is a class which does:
Class Ghost(7) with haunting, create [; self.haunting = random(Library, Ballroom, Summer_House); move self to self.haunting; if (self in location) "^The air ripples as if parted like curtains."; ];
What happens is that when the program sends the message
Ghost.create();
the class Ghost
creates a new ghost
G
, if there aren't already seven, and then sends a
further message
G.create();
This new object G
chooses its own
place to haunt and moves itself into place. Only then does the class
Ghost
reply to the outside program. A class can also
give a destroy
routine to take care of the consequences
of destruction, as in the following example:
Class Axe(30); Class Dwarf(7) with create [ x; x = Axe.create(); if (x ~= nothing) move x to self; ], destroy [ x; objectloop (x in self && x ofclass Axe) Axe.destroy(x); ];
A new axe is created whenever a new dwarf is created, while stocks last, and when a dwarf is destroyed, any axes it carries are also destroyed.
Finally, you can supply create
with
up to 3 parameters. Here is a case with only one:
Class GoldIngot(10) with weight, value, create [ carats; self.value = 10*carats; self.weight = 20 + carats; ];
and now GoldIngot.create(24)
will create a 24-carat gold ingot.
▲
§3.9 was about sending messages to
Object
s, and then in §3.11 it
turned out that there are five messages which can be sent to a
Class
. That's two of the four metaclasses, and it
turns out that you can send messages to a Routine
and a String
too.
The only message you can send to a Routine
is call
, and all this does is to call it. So if
Explore
is the name of a routine,
Explore.call(2, 4);
and Explore(2, 4);
do exactly the same as each other. This looks redundant, except that it allows a little more flexibility: for instance
(random(Hello, Goodbye)).call(7);
has a 50% chance of calling Hello(7)
and a 50% chance of calling Goodbye(7)
. As you might
expect, the call
message replies with whatever value
was returned by the routine.
Two different messages can be sent to a String
.
The first is print
, which is provided because it logically
ought to be, rather than because it's useful. So, for example,
("You can see an advancing tide of bison!").print();
prints out the string, followed by a new-line, and
evaluates as true
.
The second is print_to_array
. This
copies out the text of the string into entries 2, 3, 4,… of
the supplied byte array, and writes the number of characters as a word
into entries 0 and 1. (Make sure that the byte array is large enough
to hold the text of the string.) For instance:
Array Petals->30; ... ("A rose is a rose is a rose").print_to_array(Petals);
will leave Petals-->0
set to 26
and place the letters 'A'
, ' '
,
'r'
, 'o'
, …, 'e'
into
the entries Petals->2
, Petals->3
,
…, Petals->27
. For convenience, the reply
value of the message print_to_array
is also 26. You
can use this message to find the length of a string, copying the
text into some temporary array and only making use of this return
value.
Property
▲▲
Many classes, the Bird
class for example, pass on
properties to their members. Properties coming from the class
Object
are called “common properties”.
Every item and place in an adventure game belongs to class
Object
, so a property inherited from Object
will be not just common but well-nigh universal. Properties which
aren't common are sometimes called “individual”.
The Inform library sets up the class Object
with about fifty common properties. Story files would be huge if
all of the objects in a game actually used all of these common properties,
so a special rule applies: you can read a common property for
any Object
, but you can only write to it if you've
written it into the object's declaration yourself.
For instance, the library contains the directive
Property cant_go "You can't go that way.";
This tells Inform to add cant_go
to the
class definition for Object
. The practical upshot
is that you can perform
print_ret (string) location.cant_go;
whatever the location is, and the resulting text
will be “You can't go that way.” if the location
doesn't define a cant_go
value of its own. On the other
hand
location.cant_go = "Please stop trying these blank walls.";
will only work if location
actually provides
a cant_go
value of its own, which you can test with the
condition location provides cant_go
.
▲▲ Using the superclass operator you can read and even alter the default values of common properties at run-time: for instance,
location.Object::cant_go = "Another blank wall. Tsk!";
will substitute for “You can't go that way.”
▲▲ The Inform library uses common properties because they're marginally faster to access and marginally cheaper on memory. Only 62 are available, of which the compiler uses 3 and the library a further 47. On the other hand, you can have up to 16,320 individual properties, which in practical terms is as good as saying they are unlimited.
▲▲ “Socialism is all very well in practice, but does it work in theory?” (Stephen Fry). While the chapter is drizzling out into small type, this last section is aimed at those readers who might feel happier with Inform's ideas about classes and objects if only there were some logic to it all. Other readers may feel that it's about as relevant to actual Inform programming as socialist dialectic is to shopping for groceries. Here are the rules anyway:
X
is declared as a member of class
C
, then X
“inherits” property
values as given in the class definition of C
.Exact rules of inheritance aren't relevant here,
except perhaps to note that one of the things inherited from class
C
might be the membership of some other class, D
.
Class Shrub with species;
The class Shrub
will generate a class-object
in the final program, also called Shrub
. This class-object
exists to receive messages like create
and destroy
and, more
philosophically, to represent the concept of “being
a shrub” within the simulated world.
The class-object of a class is not normally a member
of that class. The concept of being a shrub is not itself a shrub,
and the condition Shrub ofclass Shrub
is false
.
Individual shrubs provide a property called species
, but
the class-object of Shrub
does not: the concept of
being a shrub has no single species.
Class
,
Object
, Routine
and String
.It follows by rule (6) that every Inform program contains
the class-objects of these four, also called Class
,
Object
, Routine
and String
.
Class
, and no
other class.Routine
and no other class.String
, and of no other class.Object
,
and possibly also of other classes defined in the source code.It follows from (8.1) that Class
is the
unique class whose class-object is one of its own members: so
Class ofclass Class
is true
.
Class
.Object
are read-only and cannot be set.· · · · ·
To see what the rules entail means knowing the definitions
of the four metaclasses. These definitions are never written out in
any textual form inside Inform, as it happens, but this is what they
would look like if they were. (Metaclass
is an imaginary
directive, as the programmer isn't allowed to create new metaclasses.)
Metaclass Object with name, ...;
A class from which the common properties defined
by Property
are inherited, albeit (by rule (10), an
economy measure) in read-only form.
Metaclass Class with create [ ...; ... ], recreate [ instance ...; ... ], destroy [ instance; ... ], copy [ instance1 instance2; ... ], remaining [; ... ];
So class-objects respond only to these five messages
and provide no other properties: except that by rule (9), the class-objects
Class
, Object
, Routine
and
String
provide no properties at all. The point is that
these five messages are concerned with object creation and deletion
at run time. But Inform is a compiler and not, like Smalltalk-80 or
other highly object-oriented languages, an interpreter. Rule (9) expresses
our inability to create the program while it is actually running.
Metaclass Routine with call [ parameters...; ... ];
Routines therefore provide only call.
Metaclass String with print [; print_ret (string) self; ], print_to_array [ array; ... ];
Strings therefore provide only print
and print_to_array
.
•
REFERENCES
L. Ross Raszewski's library extension "imem.h"
manages genuinely dynamic memory allocation for objects, and is
most often used when memory is running terribly short.