Object Oriented Programming in Script/An Extended Example
Author: Jeff Stephenson
The only way to really understand OOP is not to read about it, but to dive into an example and get a feel for how it is used (or better yet to actually write code!). The following extended example goes through the development of a new class, the AutomaticDoor, for adventure games. Before continuing, it is advisable to read through Script Classes for Adventure Games in order to become familiar with the classes upon which the AutomaticDoor is built.
The AutomaticDoor concept was inspired by Space Quest, in which there are lots of doors which open whenever ego gets near them and close when he moves away. We would like the doors to do this by themselves, like good automatic doors, rather than having to explicitly write code in each room which checks ego's position, remembers whether the door is open or closed, opens or closes the door, etc.
The first step in defining a class is conceptual -- determining what the class represents and thus what properties and methods it should have in order to carry out its role in the scheme of things.
We'll say that a door is a subclass of the Actor class, since it will be an object visible on the screen. A door goes somewhere, so it should have an entranceTo property which tells us which room is on the other side of the door. The door may or may not be locked, so we'll need a locked property to keep track of this, along with a key property which is the ID of a key object which locks or unlocks the door. Doors generally make some sort of noise when opening and closing, so we'll add openSnd and closeSnd as properties to tell us what sound the door makes. Then there is the question of keeping track of whether the door is opening, open, closing, or closed. This state will be kept in the property doorState. Finally, since this is an automatic door, it will need some way of telling when an Actor is near enough to cause it to open. This will be dealt with by an object of class Code, whose ID will be kept in the actorNearBy property.
This gives us the beginnings of the class statement:
Code:(class AutomaticDoor kindof Actor (properties entranceTo 0 locked FALSE key 0 openSnd 0 closeSnd 0 doorState 0 actorNearBy 0 ) )
We'll need symbolic definitions for the state of the door:
Code:(enum doorOpen doorOpening doorClosed doorClosing )
To this we now need to add the methods section. The methods are the things we wish to have the door do. Thus, we will want methods open and close, as well as lock and unlock:
Code:(methods open ;open the door close ;close the door lock ;lock the door unlock ;unlock the door )
We'll start with the init: method (inherited from Prop), which adds the door to a room when we first enter the room. This should handle having the door be open if it is the door to the room which we have come from and closed otherwise.
Code:(method (init &tmp doorState) (= doorState (if (== prevRoomNum entranceTo) ;We just came from the room to which this door ;is an entrance -- the door should be open. doorOpen else ;We didn't come through this door -- have ;it closed. doorClosed ) ;Set the cel based on whether the door is open or closed. This ;assumes that cel 0 is the cel with the door entirely closed. (= cel (if (== doorState closed) 0 else (- (NumCels view loop) 1) ) ) ;Pass the initialization along to the super-class to add the ;door to the cast, etc. (super init:) ;Stop updating the door, to reduce the burden on the animation ;system. (self stopUpd:) )
Note that we have not added the actorNearBy code at this point -- that will be specific to each door.
We now want methods to open and close doors. They should know enough not to try opening a locked door or one which is either already opened or opening. Also, if a door-opening sound has been defined for the door (by putting a the object ID of a Sound in openSnd), that sound should be played as the door opens. The same goes for the door closing.
Code:(method (open) (if (and (! locked) (!= doorState doorOpening) (!= doorState doorOpen) ) ;If the door is not opened or opening, start it doing so by ;having it cycle to the end of its loop. When it is done, ;the cycle instance will cue us. (= doorState opening) (self setCycle: EndLoop self:) (if openSnd (openSnd doit:) ) ) ) (method (close) ;We don't have to worry about the door being locked, ;since in that case it wouldn't be open. (if (and (!= doorState closing) (!= doorState closed) ) (= doorState closing) (self setCycle: BegLoop self) (if closeSnd (closeSnd doit:) ) ) )
When either the EndLoop or BegLoop cycle type initiated by open or close is done, the cycle class will cue the door. The cue method must thus handle the change from doorOpening to doorOpen and from doorClosing to doorClosed:
Code:(method (cue) (= doorState (if (== doorState doorOpening) doorOpen else doorClosed) ) (self stopUpd:) )
Note that we stop updating the door when it is no longer cycling in order to reduce the load on the animation system.
Locking and unlocking the door may be done by anyone who has the key to it. Though not in the class system yet, each Actor will have a has: method which will test to see if the Actor has a certain inventory object. We use this to define the lock and unlock methods, which take the Actor who is trying to do the action as a parameter. Note the use of a common procedure to exploit the similarities in code:
Code:(method (lock who) (DoLock who TRUE) ) (method (unlock who) (DoLock who FALSE) ) (procedure (DoLock who newLockState) (if (who has: key) (= locked newLockState) else (Print "You don't have the proper key!") ) )
Remember that the property key has the ID of the object which is the key to this door.
Since the init: method of the door has added the door to the cast, the door will be sent the doit: message during each animation cycle. This is the ideal place to hook in the check to see if the door should be opened or closed. The door will check to see if any Actor is near enough (as defined by a TRUE return from the code whose ID is in actorNearBy), and if so will invoke the open: method. Otherwise, it will invoke the close: method. Note that since these methods already check to see if the given operation is in progress or is completed, we can invoke them blindly without creating any problems.
Code:(method (doit) ;If there is no test for a nearby actor, don't try to test. (if (== actorNearBy 0) (return)) ;See if anyone is near. (if (cast firstTrue: #perform: actorNearBy) (self open:) else (self close:) ) )
The test in this method works in the following way: we tell each member of the cast to perform: the code whose ID is in actorNearBy. If any member of the cast returns TRUE from this code, the firstTrue: method will end and return the ID of that member. If no member returns TRUE, firstTrue: will return NULL. Thus the conditional statement will be TRUE if any element of the cast returns TRUE to the code in actorNearBy. The general structure for this code (each door will have its own specific test for nearness) is:
Code:(instance nearByTest of Code (method (doit theObj) (return code to test the nearness of the Actor ) ) )
Now that the methods have been defined for the AutomaticDoor class, we can use it to define doors in any room in the game. Say we're in a room which has two doors (like the starting room of Space Quest). The first is a faceon door to a closet, which we'll call closetDoor. Since it's face-on and there are no obstructions, we'll use a simple position check to see whether ego is near it:
Code:(instance closetDoor of AutomaticDoor ;Set the initial position of the object and ;say that it opens into the closet. (properties x:100 y:60 view:vClosetDoor entranceTo:closet actorNearBy:nearCloset ) ) (instance nearCloset of Code (method (doit theObj) (return (and (< x (theObj x?)) (> (+ x (CelWide view loop cel)) (theObj x?)) (< (abs (- y (theObj y?))) 10) ) ) ) )
The code in nearCloset checks to see if the object's x position is within the bounds of the door and whether it is within 10 pixels of the door vertically. If all the above are true, it returns TRUE.
The second door will be an entrance to a secret room. Let's say that the room is so messy near the door that the only easy way to tell if ego is near it is to draw some control into the picture and see if ego is on the control. Also, since the room to which this door leads is secret, we'll need the cardKey object to unlock the door.
Code:(instance secretDoor of AutomaticDoor (properties x:10 y:80 entranceTo:secretRoom locked:TRUE key:cardKey actorNearBy:nearSecretDoor ) ) (instance nearSecretDoor of Code (method (doit theObj) (return (OnControl theObj secretControl) ) ) )
In the room code, we add the doors during the initialization phase:
Code:(method (init) ... (closetDoor init:) (secretDoor init:) ... ) (if (Said 'lock / door') (cond ((closetDoor actorNearBy: ego) (closetDoor lock:) ) ((secretDoor actorNearBy: ego) (secretDoor lock:) ) (else (Print "You're not near a door!") ) ) ) (if (Said 'unlock / door') (cond ((closetDoor actorNearBy:) (closetDoor unlock:) ) ((secretDoor actorNearBy:) (secretDoor unlock:) ) (else (Print "You're not near a door!") ) ) )
We use the door's own actorNearBy check to see if ego is close enough to open the door.
Thus concludes our excursion into Object Oriented Programming. The idea behind this style of programming is to create abstractions of the things you are modeling, decide how all these classes are related, and set up a hierarchy of super- and sub-classes which encapsulates these relationships. In the classes are hidden the methods which do things to objects which are instances of the classes, and the properties, which are the defining characteristics of the class. Objects are then instances of a given class which have particular values for the properties and may even have different methods for implementing a certain concept.
What all this setup gives you is the ability in your code to say
to unlock a door, rather than having to write the code in-line or writing an unlock routine which needs to handle all the possible cases of doors which occur in the game.