LOOPE - Lingo Object Oriented Programming Environment by Irv Kalb
Section 1 - Parent Scripts and Objects
In this chapter we will give more examples of objects. We will give examples of a Random Range object, an Array object, and a Sprite Manager object.
Often in developing different games, there is a need for getting random numbers within a given range. You may want to present ten items to the user (for example, test questions or pieces of art), but you want the presentation to happen in a random order. In a quiz you might want to randomize the order of questions, and also randomize the order of the potential answers to each question. To create a generalized solution to these types of problems, you can create a randomize range object. You tell it the maximum number of numbers and then you can ask it to give you back a randomized number within that range. It should ensure that you never get the same number twice until it runs out of numbers. As a basic algorithm, we'll start by create a list of the numbers 1 to n:
-- Generate
the list
lNumbers = []
repeat with
i = 1 to
n
append(lNumbers, i)
end repeat
When we want a random number, we select a random element from the list, return that element, then delete that element from the list. Further, when we have exhausted the items in the list we would like to regenerate the list so we can continue to ask for random numbers. Now that we have devised an approach, our task is to take this basic algorithm and build an object than provides all the functionality we need. Here is a parent script that does the job:
-- RandomRange Script
property plNumbers -- list
of numbers
property pMaxNumbers -- Max
number of numbers
on new
me, howMany
pMaxNumbers = howMany
me.mInit()
return me
end new
on mInit me
-- Generate the list
plNumbers = []
repeat with
i = 1 to
pMaxNumbers
append(plNumbers, i)
end repeat
end
on mGetNextRandom me
nItems = count(plNumbers)
if nItems = 0
then --
Time to regenerate the list
me.mInit()
nItems = pMaxNumbers
end if
randomIndex = random(nItems) --
choose a random index into the list
valueToReturn = plNumbers[randomIndex] --
get the value there
deleteAt(plNumbers, randomIndex) --
delete that item from the list
return valueToReturn
end mGetNextRandom
The object has two property variables; plNumbers is the
list of random numbers, and pMaxNumbers which is used to remember the maximum
number of numbers. There are three methods; the standard "new", mInit,
and mGetNextRandom. When you instantiate
the object, you pass in the maximum number of numbers you want, and the object
saves away that number into the property variable pMaxNumbers. Then, it calls
the mInit method to initialize the list.
The "mInit" method just
creates a list of numbers from one to the maximum number of numbers.
We have shown earlier that when you want to call a method of
an object, that you use a line like this:
goSomeObject.mSomeMethodName()
But here we have a case where we are inside an object, and we want to call another method of that same object. In this case - and this case happens very often -where we would normally put the object reference, we use the keyword "me". The keyword "me" is an object reference, but it always refers to the current instance of the current object. So we use "me" when we want to call a different method within the same object:
me.mSomeMethodName()
Whenever you want a new random number, you call the mGetNextRandom method. The mGetNextRandom method works by selecting a random element from the list, and then it eliminates that item from the list. At the beginning of the method, it always checks to see if there is anything left in the list by seeing if the count of the list has gone to zero. If the list has no more items, it calls the object's mInit method to regenerate the list.
There is something else interesting to note in this script. This object has three methods, but only two of these methods are really available to be called from "outside" the object; the "new" method and the mGetNextRandom method. The mInit method provides functionality which is needed in two places - it is called internally by both the new and the mGetNextRandom method, but it is not intended to be called from outside the object. Methods that are designed to only be called internally (like mInit here) are known as "private" methods. Methods that are intended to be called from outside or inside an object are known as "public" methods.
In fact, some other computer languages use "PUBLIC" and "PRIVATE" as keywords that you can add to the definition of a method to insure that it is called the correct way. But, Lingo has no such restriction. If you were publishing a spec describing the API of the RandomRange object for other programmers, you would only include the new and mGetNextRandom methods. You would intentionally leave out the mInit method because it not intended to be available to outside callers. (Another option would be to extend the naming convention for methods and change the name to something like "imInit" for internal method Init.)
If you closely read the code of the RandomRange object you may notice a potential flaw in the coding. Let's say that you instantiate a RandomRange object that handles the numbers from 1 to 10. In the course of your program you call it ten times and it returns the numbers in a random order. For the sake of argument, let's say that the last number you got back was 5. If you call the mGetNextRandom method again, the object will need to regenerate the list of random numbers. It is entirely possible that after regenerating the list that the first number to be returned will again be 5. From the user's point of view, this would result in the number 5 coming up twice in a row. This is an unfortunate side effect of the implementation. It would be nice if we could ensure that the same number never came up twice in a row even if the object internally has to regenerate its own list of numbers. Here is a modified form of the parent script that takes care of this problem.
-- RandomRange Script
property plNumbers -- list
of random numbers
property pMaxNumbers -- Max
number of numbers
property pLastValue
on new
me, howMany
pMaxNumbers = howMany
me.mInit()
return me
end new
on mInit me
-- Generate the list
plNumbers = []
repeat with
i = 1 to
pMaxNumbers
append(plNumbers, i)
end repeat
end
on mGetNextRandom me
nItems = count(plNumbers)
if nItems = 0
then --
Time to regenerate the list
me.mInit()
nItems = pMaxNumbers
end if
-- Ensure that the new value chosen is not
the same as the last one
repeat while
TRUE
randomIndex = random(nItems)
-- choose a random index into the list
valueToReturn = plNumbers[randomIndex] --
get the value there
if valueToReturn <>
pLastValue then
exit
repeat
end if
end repeat
deleteAt(plNumbers, randomIndex) --
delete that item from the list
pLastValue = valueToReturn
return valueToReturn
end mGetNextRandom
Note that we have added a property
called pLastValue. It keeps track of the last value when the list returned.
Then in the mGetNextRandom method, when we choose a random value to return to
the caller, we check to ensure that it is different than the previous value
returned. If it is the same, we stay in a repeat loop until we choose a value
which is different that the previous one returned.
We have just demonstrated an important concept of object oriented programming here. Although we made a change to the implementation of the RandomRange script to fix a flaw, we made no changes to the API of the object. Therefore, there is no need to change any code anywhere else in the rest of the program. Specifically, no clients of the RandomRange object will need to make any changes. We have made a change to one small area of one parent script, but the benefits of this change will be felt by any piece of code that uses a RandomRange object anywhere in the program. In this way, putting this code in a parent script has isolated it from the rest of the system. If, by mistake, we had introduced a bug by adding this new code, we know that the bug must be in the RandomRange parent script because we didn't change any of the calls to the object.
Many other computer languages have a basic data structure called an array. A spreadsheet is a good example of a simple two dimensional array - a collection of data which can be represented by rows and columns. It is common to say that you have an n by m array, for example a 6 by 4 array where there are 6 rows and 4 columns. Each item or in the array is called a cell or an element. You refer to a cell by its row and column numbers. The cell at row 3 column 2 would be referred to as: array(3,2). The cell at row 1 column 5 would be array(1,5).
As of Director 7, Director allows a new syntax for getting at elements of a nested list. Suppose we wanted to have a 4 by 5 array where all values in the array were set to 7. This code would allow us to do that:
on
createArray
global glArray
glArray = [[7, 7,
7, 7, 7],[7,
7, 7, 7,
7],[7, 7,
7, 7, 7],[7,
7, 7, 7,
7],[7, 7,
7, 7, 7]]
When we want to access a piece of that data, we can get the value of a given cell using this syntax:
glArray[rowNumber][colNumber]
The syntax of the above definition of the nested array is rather cryptic. Further, the nested list approach does not handle all cases. Lingo lists are "1-based", that is, the first item is always at index number 1. What if we wanted a "zero-based" array where we wanted to start these indices at zero? Further, what if we wanted to ensure that the row number and column numbers that we are using to index into the array are valid before we tried to access an element?
An important use of objects is to model data so that clients can think of data in the way that they want to think of it and not have to worry about the underlying implementation. Internally, we can use linear list to represent the data and build algorithms to access that list as though it were really a two dimensional array. We can then provide interfaces which work in the way that a potential client of this object would find obvious. Here is an array parent script which accomplishes these things:
-- Array script
property pFirstRow -- lower
row bound
property pLastRow -- upper
row bound
property pFirstCol -- lower
column bound
property pLastCol -- upper
column bound
property pInitialValue --
initial value (for initialization)
property pnCols -- number
of columns, (saved for speed)
property plCells -- the data
of the array
property pfRangeCheck --
flag used for debugging
property pnCells -- the number
of cells
-- Create the array passing the row and column lower and
upper
-- bounds and an initial value
on new
me, firstRow,
lastRow, firstCol, lastCol, initialValue
pFirstRow = firstRow
pLastRow = lastRow
nRows = pLastRow - pFirstRow + 1
pFirstCol = firstCol
pLastCol = lastCol
pInitialValue = initialValue
pnCols = pLastCol - pFirstCol + 1
pnCells = nRows * pnCols
-- Create the actual array as a list
plCells = []
repeat with
i = 1 to
pnCells
add(plCells, initialValue)
end repeat
pfRangeCheck = FALSE
return me
end birth
-- Used to set the value of one cell in the array
on mSet me, theRow,
theCol, theValue
if pfRangeCheck then
if (theRow < pFirstRow)
or (theRow > pLastRow) then
alert("Invalid
row index to mSet, value is:" &&
theRow & RETURN &
\
"Valid values
are from" && pFirstRow &&
"to" &&
pLastRow)
exit
end if
if (theCol < pFirstCol)
or (theCol > pLastCol) then
alert("Invalid
column index to mSet, value is:" &&
theCol & RETURN &
\
"Valid values
are from" && pFirstCol &&
"to" &&
pLastCol)
exit
end if
end if
theCell = ((theRow - pFirstRow) * pnCols) + (theCol - pFirstCol)
+ 1
plCells[theCell] = theValue
end mSet
-- Used to get the value of one cell in the array
on mGet me, theRow,
theCol
if pfRangeCheck then
if (theRow < pFirstRow)
or (theRow > pLastRow) then
alert("Invalid
row index to mGet, value is:" &&
theRow & RETURN &\
"Valid values
are from" && pFirstRow &&
"to" &&
pLastRow)
exit
end if
if (theCol < pFirstCol)
or (theCol > pLastCol) then
alert("Invalid
column index to mGet, value is:" &&
theCol & RETURN &\
"Valid values
are from" && pFirstCol &&
"to" &&
pLastCol)
exit
end if
end if
theCell = ((theRow - pFirstRow) * pnCols) + (theCol - pFirstCol)
+ 1
return plCells[theCell]
end mGet
-- Used to turn on or off range checking
on mSetRangeChecking me,
trueOrFalse
pfRangeCheck = trueOrFalse
end mSetRangeChecking
-- Prints the contents of the array to the message window
on mDebug me
repeat with
i = pFirstRow to pLastRow
thisRow = ""
repeat with
j = pFirstCol to pLastCol
thisRow = thisRow && mGet(me,
i, j)
end repeat
put thisRow
end repeat
end mDebug
This parent script may seem complicated, but it really is fairly simple. If we wanted to create a similar 4 by 5 array with all values initialized to 7, we would do that like using this code:
global goArray
goArray = new(script "Array", 1, 4, 1, 5, 7)
In the "new" handler, some straight-forward calculations are done to determine how many cells are needed to represent this array as a linear list. In this case, we need 20 cells (4 times 5) to represent the array. While this case is easy, the code is set up to handle any lower bound. If we had wanted a zero based array, then the code would have calculated that we would need 30 cells (5 times 6). The code saves away the values of the lower and upper bounds for each dimension in property variables. The code executes a repeat loop for this number of cells, setting the initial value into each cell. Cell number 1 is the storage for row 1 column 1 in the array. Cell number 2 is the storage for row 1 column 2 of the array, etc.
The main functionality of the object is to allow you to get or set the value of a particular cell in the array. Skipping the code dealing with range checking for a minute, you can see that the mGet and mSet methods have a common line of code at the end. To access a particular cell, they both use the formula:
theCell = ((theRow - pFirstRow) * pnCols) + (theCol - pFirstCol) + 1
This line of code calculates an index into the linear list representing the array for the particular cell being accessed. In our original example of a 4 by 5 array (with row and column values starting at 1, imagine if we wanted to access the cell at row 3, column 3. According to this formula, it would be cell number 11. The mGet method does this calculation and then returns the value of that cell to the caller. The mSet method allows the caller to set a new value for that cell.
Now look at the property variable called pfRangeChecking. This is a flag variable that tells the Array object whether or not it should do range checking, that is, check to ensure that the values that are passed in as indices into the array, are in fact valid values to be used in accessing this array. During code development, this is a very useful feature which can be helpful in tracking down bugs in code that attempts to use an array object. In the "new" method, we have initialized this property to FALSE. However, the script has a method called mSetRangeChecking that you can call to set the value of the property to TRUE or FALSE. (Of course, from outside the object, the user doesn't really know what it does specifically, you just know that it allows or disallows range checking.) During development, just after instantiating the array object, if you want to allow for range checking, you would add the following line:
goArray.mSetRangeChecking(TRUE)
The implementation of this method is just to set the property variable pfRangeChecking. In the beginning of both the mSet and mGet methods, if pfRangeChecking is TRUE, these routines will validate the indices passed in to ensure that these values are within the proper range for the array. When you are convinced that there are no logic errors in your program which would generate invalid indices, you can safely remove this call to get better performance.
Finally, there is a debugging method called mDebug. Anytime you would like to see all the data of the array, you can call the mDebug method. mDebug outputs the data in a format that looks like the user's perception of the array, rather than it's internal linear list representation.
goArray.mDebug()
In some game applications you may have to dynamically add or remove things from the screen. As the game progresses, the number of people, missiles, bullets, butterflies, etc., could change. A typical game design is to allocate one sprite channel for each of these sprites. But how do you keep track of these sprite channels? A good solution is to build a sprite manager. This is an object that maintains a list of sprites and each sprite's status, that is, whether or not it has been allocated. When you need a new sprite channel, you ask the sprite manager for one, it finds the first available channel and returns the sprite number to you. When you no longer need a sprite channel, you tell the sprite manager that you are done using it. Here is the code of a sprite manager that implements this functionality.
--SpriteMgr
property plAllocation --
property which is a list of allocations
property pchLow -- channel
of the first sprite to manage
property pchHigh -- channel
of the last sprite to manage
on new
me,
chLow, chHigh
pchLow = chLow
pchHigh = chHigh
me.mInit()
return me
end
on mInit me
plAllocation = [:]
repeat with
ch = pchLow to pchHigh
addProp(plAllocation, ch,
FALSE) -- initialize all
to FALSE (unallocated)
end repeat
end
on mAllocateChannel me
-- find the first empty channel
repeat with
ch = pchLow to pchHigh
fAllocated = getAProp(plAllocation,
ch)
if not(fAllocated) then
-- found one
setProp(plAllocation,
ch, TRUE) -- now mark it
as allocated
return ch --
and return it to the user
end if
end repeat
alert("Attempting
to allocate a channel, but there are none left")
end
on mDeallocateChannel me,
chToDeallocate
if (chToDeallocate < pchLow) or
(chToDeallocate > pchHigh) then
alert("Trying
to deallocate" && chToDeallocate
&& "but it is not being managed.")
return
end if
fAllocated = getProp(plAllocation,
chToDeallocate)
if not(fAllocated) then
alert("Trying
to deallocate" && chToDeallocate
&& "but it has not been allocated")
return
end if
setProp(plAllocation, chToDeallocate,
FALSE) -- set the allocation
back to false
end
on mDebug me
put "Channel
Allocation:"
repeat with
ch = pchLow to pchHigh
fAllocated = getProp(plAllocation,
ch)
if fAllocated then
put ch &&
": Allocated"
else
put ch &&
": Not allocated"
end if
end repeat
end
When you instantiate the sprite manager, you give it starting and ending channel numbers of the range it should manage. Then there are two basic methods; mAllocateChannel is called to find the first available channel, and mDeAllocateChannel is used to free up a previously used channel. mDebug is available to give you a quick dump to the message window of the status of all channels managed by the sprite manager.
Internally, the sprite manager maintains a property list. Each element in the property list is of the form channelNumber:fAllocated. For example, if you created a sprite manager to manage sprites 20 to 24, the value of its property plAllocations would initially be:
[20:FALSE, 21:FALSE, 22:FALSE, 23:FALSE, 24:FALSE]
When a client calls mAllocateChannel to allocate a channel, the appropriate FALSE would turn to TRUE. When a client calls mDeallocateChannel, the appropriate TRUE would turn to FALSE.