Table of Contents |
Cockpit Controls > Furnishing the
Cockpit
Furnishing the Cockpit
Up to this point we have left the cockpit as a bare room with not even a description. The time has come to start implementing it. The first question to consider, however, is just how much detail we want to go into. Interactive Fiction is not a good medium in which to implement a flight simulator, and we shall not attempt to do so here. On the other hand, we need to provide some means for the player to get the plane off the ground and win the game, and we ideally need to allow some amount of interactivity in the process.
The solution we’ll adopt here is to implement a basic set of controls with somewhat limited interactivity: a control column with a wheel, a thrust lever, and an engine start button. We’ll also implement three basic instruments: an airspeed indicator, an altimeter and a fuel gauge. Nothing will do anything until the engines are running, but once the engine start button is pushed we’ll use a cut-scene to taxi the aircraft away from the jetway to the start of the runway. An IF purist might complain that cutscenes aren’t best practice in IF, but it’s probably the least bad option here, and in any case trying to implement a taxying-round-airport simulator is way beyond the scope of this manual. Once the cutscene is ended, the player must push the thrust lever full forward, and then pull back on the control column when the aeroplane reaches the right airspeed. Pulling back too soon or too late may result in a fatal crash. Pulling back at around the right time will cause the plane to take off and the game to be won.
The first step is to give the cockpit a decent description:
cockpit: Room 'Cockpit' 'cockpit'
"The cockpit is quite small but has everything you might expect: a
windscreen looking forward, a pilot's seat from which you can operate all
the usual controls, and a door leading out aft. "
aft = cabinDoor
south asExit(aft)
out asExit(aft)
regions = [planeRegion]
;
Next we need to implement the pilot’s seat and the controls. For the
seat we can use a Platform, and set dobjFor(Enter)
asDobjFor(Board)
so that SIT IN SEAT will do the same as SIT ON
SEAT. We’ll use a single object to represent the controls as a group,
and then make the individual controls and instruments components of the
controls object by locating them within it. This will make it relatively
easy to enforce a couple of conditions: to operate the controls you have
to be sitting in the pilot’s seat, and from the pilot’s seat the only
things you can reach in the cockpit are the controls. Here’s how we can
start this off (putting these object definitions immediately after that
of the cabinDoor that’s already in the cockpit):
+ pilotSeat: Fixture, Platform 'pilot\'s seat'
dobjFor(Enter) asDobjFor(Board)
allowReachOut(obj)
{
return obj.isOrIsIn(controls);
}
;
+ controls: Fixture 'controls;; instruments;them'
"The instruments and controls of most immediate interest to you are
<<makeListStr(contents, &theName)>>. "
checkReach(actor)
{
if(!actor.isIn(pilotSeat))
"You really need to be sitting in the pilot's seat before you start
operating the controls. ";
}
;
The first point of note here is the use of a Platform to implement the
seat. For an actor to be able to get on (sit on/stand on/lie on) an
object, the object’s contType
must be
On
and its
isBoardable
property true; a Platform is
basically the subclass of Thing that fulfils these two conditions. For
an actor to be able to get in (sit in/stand in/lie in) an object its
contType
must be In
and its isEnterable
property true. The
subclass of Thing that fulfils those two conditions is
Booth
, but something can’t be both a Booth and
a Platform at the same time, since its contType can’t be simultaneously
In and On. To allow the player character to SIT IN SEAT as well as SIT
ON SEAT we therefore define dobjFor(Enter)
asDobjFor(Board)
on the seat, which means ‘Treat ENTER SEAT as
BOARD SEAT’. Since SIT IN SEAT is treated as ENTER SEAT, this makes SIT
IN SEAT behave like BOARD SEAT, which is what SIT ON SEAT also does. The
wider lesson here is that if you want to define a piece of furniture
which the player can get in or on without it meaning anything much
different, define it as a Platform and add
dobjFor(Enter) asDobjFor(Board)
.
The second point of note is the use of
allowReachOut(obj)
on the seat to control
which items are in reach of someone sitting on the seat (the seat itself
and anything on the seat are automatically in reach). Here we want the
controls object and all its contents to be reachable from the seat, so
we get the method to return
obj.isOrIsIn(controls)
, which will be true
either if obj is the controls
object or if it
is directly or indirectly contained in the
controls
object. The default behaviour if an
actor sitting on the chair tries to reach anything else is to move the
actor out of the chair, which is absolutely fine here.
The third point is the use of the
checkReach(actor)
method on the
controls
object to control where the controls
are reachable from. If the method displays anything (normally a reason
why the object or its contents can’t be reached) then the contents of
the object and the object itself are considered out of reach to the
actor. We can therefore use this method to display a message saying that
you need to be in the pilot’s seat to operate the controls if the actor
isn’t in the pilot’s seat.
The fourth point is the use of
\<\<makeListStr(contents, &theName)\>\>
to
provide a list of the controls, which we’re about to locate in the
controls
object. The
contents
parameter simply refers to the
contents of the controls object, which we’re about to define. The option
&theName
parameter tells the
makeListStr()
to use the theName property of
each item in the contents when building its list, instead of the aName
property it would otherwise have used by default, since it will look
more natural to list the names of the controls with the definite article
here.
Before we go on, let’s pause and look at the second and third points in
a bit more detail. Note first of all that although they have similar
names, they’re not mirror images of each other, and they do work a
little differently. allowReachOut(obj)
takes
the object to be reached as its parameter and returns true or nil
depending on whether an actor can reach the object from within the
container on which the method is defined.
checkReach(actor)
takes the actor doing the
reaching as its parameter and displays a message if the actor can’t
reach the object on which it’s defined (and so can’t reach inside the
object either). There is also a
checkReachIn(actor)
method which you can use
if you want reaching inside an object to be possible under different
conditions from reaching the object itself, but by default
checkReachIn(actor)
just calls
checkReach(actor)
.
The reason for the difference between the two methods is that they’re
modelling slightly different circumstances.
allowReachOut(obj)
is typically intended for
cases where an actor is on a piece of furniture like a chair or bed and
may not be able to reach everything in the room from that container. If
an object can’t be reached it’s because it’s too far away, and the
obvious default behaviour is for the actor to leave the container to
reach the object s/he’s trying to reach. If we want to disallow this
default for any reason, which we can do by setting
autoGetOutToReach
to nil, then the default
refusal message of the form “You can’t reach the bookcase from the
armchair” will nearly always be appropriate, so we don’t generally need
to provide a means for producing a custom message. On the other hand
checkReach(obj)
and
checkReachIn(obj)
are intended to model a
whole range of situations where an object can’t be reached; perhaps it’s
too high up on a shelf, or perhaps it’s too hot to touch, or perhaps
there’s a venomous spider on it, or perhaps there’s some other reason.
It’s therefore generally necessary to provide a custom refusal message
for each particular case, so a method that just returns true or nil
won’t do. We therefore define a couple of methods that work like a
check() routine; if they display anything they’re disallowing the
action; and these methods are in fact called at the check() stage (via
the touchObj precondition, if you want to get technical). It’s therefore
appropriate to give them names that start with check.
There is a further asymmetry between the two cases that results in one
taking the object to be reached as its parameter and the other the actor
doing the reaching. In the case of
allowReachOut(obj)
we know exactly where the
actor doing the reaching is; s/he’s in or on the piece of furniture on
which we’re defining the allowReachOut(obj)
method, and whether obj is reachable from there or not depends on which
obj it is and where it is located. In the case of
canReach(actor)
or
canReachIn(actor)
we already know precisely
which object is being reached and/or where it is; it’s the object on
which we’re defining the method, and whether the actor can reach it or
not may well depend on the location of the actor.
The most typical kind of case where we might use
checkReach()
and/or
checkReachIn()
is, say, to represent a high
shelf which the actor can only reach when s/he’s standing on a ladder or
a chair. Here we’re using it a bit differently to prevent the player
character operating the cockpit controls unless he’s sitting in the
pilot’s seat. Of course, strictly speaking, an actor wouldn’t actually
have to be sitting in the seat to reach the controls, but it’s far
easier to trap every attempt to touch them in a single checkReach()
method than it would be to attempt to trap every action that might count
as manipulating the controls to fly the plane. By placing the individual
controls within the controls
object, that is
just what we can do. So the next step is to define the individual
instruments and controls, locating them in the
controls
object, although at this stage their
implementations will be a little sketchy:
++ controlColumn: Fixture 'control column;;stick'
"It's basically a stick that can be pushed forward or pulled back, with a
wheel attached at the top. "
listOrder = 10
;
+++ wheel: Fixture 'wheel'
"The wheel can be turned to port or starboard to steer the aircraft. "
;
++ thrustLever: Fixture 'thrust lever'
"It's a lever that can be pushed forward or pulled back. "
listOrder = 20
;
++ ignitionButton: Button 'engine ignition button; big green'
"It's a big green button. "
listOrder = 30
isOn = nil
makePushed()
{
if(isOn)
"The engines are already running. ";
else
{
isOn = true;
"The plane judders as the engines roar into life. ";
}
}
;
++ asi: Fixture 'airspeed indicator; air speed; asi'
"It's currently registering an airspeed of <<airspeed>> knots. "
listOrder = 40
airspeed = 0
;
++ altimeter: Fixture 'altimeter'
"It's currently indicating an altitude of <<altitude>> feet. "
listOrder = 50
altitude = 0
;
++ fuelGauge: Fixture 'fuel gauge'
"It's currently registering full. "
listOrder = 60
;
+ windscreen: Fixture 'windscreen;; window windshield'
;
The implementation is deliberately incomplete at this stage, since
completing it will be the task that occupies the remainder of this
chapter. The main thing to note right now is the use of the Button
class to implement the ignition button. In the adv3Lite library a Button
is fixed by default (since buttons are nearly always part of something
else, and not free-standing items that can be picked up and carried
around by themselves), so there’s no need to define
isFixed = true
. A Button doesn’t do much by
default. If pushed a Button’s makePushed()
method is called, but does nothing by default, so this is a good place
to put code to make the Button do something useful. Here we make it
display a message and set isOn
to true if it
wasn’t already. A Button doesn’t have an isOn
property by default, but there’s nothing to stop our giving it one, as
here. If we hadn’t overridden the makePushed()
method the response to PUSH BUTTON would have been “Click”, but the text
output by our custom makePushed()
method
displaces this default response for reasons we’ll look at later.
We’ve also given custom airspeed
and
altitude
properties to the air speed indicator
and the altimeter respectively, so that their descriptions can mention
what these instruments are measuring. We haven’t bothered to do this
with the fuel gauge, because the game will end before the plane has used
a significant amount of fuel.
We’ve also defined the listOrder on each of the controls and
instruments; this is principally to ensure that
<<makeListStr(contents, &theName)>> in the description of the
controls
object lists these instruments in the
order we want.
In the next section we’ll start discussing how to make the controls respond to commands, which will involve our learning how to define actions.
adv3Lite Library Tutorial
Table of Contents |
Cockpit Controls > Furnishing the
Cockpit