LOOPE - Lingo Object Oriented Programming Environment by Irv Kalb

Previous Chapter

Table of Contents

Next Chapter

 

Section 1 - Parent Scripts and Objects

Chapter 5 - More examples of 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.

 

Random range

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.

 

Array

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()

 

Sprite Manager

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.

 

Previous Chapter

Table of Contents

Next Chapter