LOOPE - Lingo Object Oriented Programming Environment by Irv Kalb

Previous Chapter

Table of Contents

Next chapter

 

Section 1 - Parent Scripts and Objects

Chapter 7 - Sound Manager

 

Managing sound provides a good opportunity to demonstrate object oriented programming. We will show how a sound manager can be an excellent example of encapsulation, in that a single object can be written to handle all aspects of sound within a program. When designing an overall sound architecture for a project, there can be many different cases that need to be considered. A sound manager can be used to keep track of all the details of sound management while providing a clear interface to all clients.

In this chapter, we explain how to build different sound managers to handle differing sets of requirements. As we progress through this exercise, we will demonstrate how to turn a single object in an object manager object - a single object that manages other objects.

 

Simple Sound Manager

Consider an educational project that might have two different kinds of sounds, for example, narrator sounds and sound effects. Narrator sounds are long sounds played when arriving at a particular "page" in the score. Sound effects are very short sounds played in response to clicking on buttons, answering questions, short feedback, etc. Creating a sound manager would be an ideal way to approach such a requirement. The code for playing and monitoring the different types of sounds could be encapsulated all in one place in a single globally available sound manager. Here is the code of a simple sound manager that would handle such a set of requirements:

-- SimpleSoundMgr script

property pDelim
property pSoundChannelNarrator
property pSoundChannelSFX

on
new me
  pDelim = the
last char of the moviePath
  -- Use channel 1 for Narrator Sounds
  -- and channel 2 for sound effects
  pSoundChannelNarrator = 1
  pSoundChannelSFX = 2
  
  return
me
end

on mStartNarratorSound me, soundFileName
  sound
stop pSoundChannelNarrator -- in case a sound is already playing
  fullSoundPath = the
moviePath & "Sounds" & pDelim & soundFileName
  sound
playFile pSoundChannelNarrator, fullSoundPath
end


on mPauseNarratorSound me
  sound(pSoundChannelNarrator).pause()
end

on mContinueNarratorSound me
  sound(pSoundChannelNarrator).play()
end

on mPlaySfx me, soundName
  sound
stop pSoundChannelSFX -- in case a sound is already playing
  puppetSound pSoundChannelSFX, soundName
end



The approach here is very straight-forward. We would instantiate a global sound manager object like this (perhaps in a prepareMovie handler).

global goSimpleSoundMgr

goSimpleSoundMgr = new(script "SimpleSoundMgr")

On instantiation, the SimpleSoundMgr sets a property variable, pDelim, to the appropriate path delimiter. This is done once here then used later. It then sets the pSoundChannelNarrator and pSoundChannelSFX to channels 1 and 2 for sound channels 1 and 2. By putting these values inside the object, the SimpleSoundMgr hides implementation details from any client. This technique is known as information hiding. Outside of the object, no client knows or cares what sound channels are being used. In fact, clients of the sound manager don't even need to know about the concept of sound channels.

Some other computer languages have a concept of a data type called a constant. The idea is that you have a value that never changes, but you want to use a name rather than a number because the name has more meaning than a number. In some other languages, you could write something like this:

constant cSoundChannelNarrator = 1

constant cSoundChannelSFX = 2

Then you can use the constant anywhere. But Lingo does not support constants. The work around is to do as we have done in this code. If you are inside an object, you use a property variable and assign it a value in the "new" method. Otherwise, you can use a global variable and assign it a value in a prepareMovie handler.

Because the narrator sounds are long sounds (i.e., large file sizes), we will choose a design where these narrator sounds are external files. Further, to build a clean structure, we will put all the narrator sounds into a folder called "Sounds" that lives at the same level as the program. When we want to play a narrator sound, we will use "sound play file" and construct a sound file path on the fly to get to the full name of the file. This is just what we do in the method mStartNarratorSound. The first line stops any current sound that might be playing. Then, using the soundFileName passed in and the delimiter we set and saved away earlier, we generate the file name. Finally, we start the sound playing. Outside the object, whenever we want to play a narrator sound, all we need to do is to have code like this:

global goSimpleSoundMgr

goSimpleSoundMgr.mStartNarratorSound("SomeSoundFileName")

We have then added some simple control methods to pause and continue a narrator sound should the user want this functionality (see mPauseNarratorSound and mContinueNarrator sound using new features available in Director 8). Because we have saved the assignment of the narrator sound channel number in a property variable, these methods become trivial.

Sound effects are typically rather short (i.e., small file size). We will choose to have sound effects live as imported cast members. When the SimpleSoundMgr wants to play a sound effect, it need only issue a puppetSound command to play it. And in fact, if you look at the mPlaySfx method, you can see this simple implementation. When we want to play a sound effect, we need use code like this:

global goSimpleSoundMgr

goSimpleSoundMgr.mPlaySFX("SomeSoundCastMemberName")

 

A more complicated Sound Manager

Now, let's address a more difficult set of requirements. Let's say that we want to allow for the ability to play any number of sounds concurrently (internal or external as before), without having to worry about assignment of sound channels. We can write a sound manager that does the work of dynamically figuring out which sound channel to use. A further requirement might be that we would need the ability to modify a sound once it has started playing (stop, fade in or out, raise or lower volume, etc.).

To implement this variation, every time a request to play a sound comes in, the sound manager will generate a unique integer to identify that sound - we'll call this a soundID. When the sound manager wants to play a sound, it finds a free sound channel, generates a unique soundID, starts the appropriate sound, and saves away the soundID in a list of soundID's. For example, if we want to use 4 sound channels, we will first create a list (plSoundIDs) of 4 items. We'll initialize the list to all zeros, each zero meaning that there is no sound playing in this channel. When the first request to play a sound comes in, we'll increment a property (pCurrentSoundID) to generate a unique ID number, find a free sound channel, start the sound in that sound channel, and put the new soundID into the appropriate element of plSoundIDs.

This version of the sound manager also allows for any number of "action" methods. Each method represents some action that can affect a currently playing sound. In the following code, we have only implemented an mSoundStop action method. The soundID created by the sound manager for every sound play request is a handle - something that uniquely identifies each sound. This is an identical concept to the way that Lingo uses object references. When you create an object in Director, Director gives you back an object reference. When you want to call any methods of that object, you must identify which instance of the object you want to use by supplying the appropriate object reference. In this sound manager, whenever you want to perform an action of a sound, you call the appropriate method of the sound manager and pass in the appropriate soundID as a parameter.

Here is an analogy. Let's say you purchase an item over the internet and the seller chooses to ship you the item using UPS. When the seller gives the package to UPS, UPS assigns tracking number and gives it back to the seller. The seller may then give that tracking number to you. Once you have the number, any time you want to do something with the package, (most obviously find out where the package is), you need to contact UPS and give them the tracking number. The tracking number is your handle for any action you wish to perform on that package. If you want to change the delivery address or schedule a time of delivery for the package, you will need to supply the tracking number. The tracking number is in some internal format generated by UPS (I was given one for a recent purchase that looked like this: 1ZV44V58034409782). This value of this number is certainly meaningful to UPS because it uniquely identifies your package. But as a number outside of the context of a UPS tracking number, it is meaningless to you. In the same way, the soundIDs generated by the sound manager has meaning within the sound manager, but the value of the soundID is meaningless to any client.

The code here gets a little tricky, but with comments and explanations, it should become clear.

-- SoundMgrDynamic

property pDelim -- path delimiter
property pnChannels -- number of channels
property pCurrentSoundID -- current sound ID
property plSoundIDs -- list of sound ID's per channel


on
new me, nChannels
  pnChannels = nChannels
  pDelim = the
last char of the moviePath
  plSoundIDs = []
  repeat
with thisChannelNum = 1 to pnChannels
    append(plSoundIDs, 0)
  end
repeat
  pCurrentSoundID = 0

  return
me
end
new

on mSoundPlayExternal me, soundName
  soundID = me.mSoundPlay(soundName, #external)
  return soundID
end mSoundPlayExternal


on mSoundPlayInternal me, soundName
  soundID = me.mSoundPlay(soundName, #internal)
  return soundID
end mSoundPlayInternal

on mSoundPlay me, soundName, symInternalOrExternal
  channelNum = me.mAssignChannel()

  pCurrentSoundID = pCurrentSoundID + 1
-- generate a unique soundID every time a sound is played.
  plSoundIDs[channelNum] = pCurrentSoundID
  
  if symInternalOrExternal = #external then
    fullSoundPath = the
moviePath & "Sounds" & pDelim & soundName
    sound
playFile channelNum, fullSoundPath
  else
    puppetSound channelNum, soundName
  end
if
  return pCurrentSoundID -- return sound ID to client
end


on mAssignChannel me

  -- First check to see if any playing sounds are still playing
  -- If no longer playing, reset the soundID to zero

  repeat
with thisChannelNum = 1 to pnChannels
    if plSoundIDs[thisChannelNum] > 0
then
      if not(soundBusy(thisChannelNum)) then
        plSoundIDs[thisChannelNum] = 0
      end
if
    end
if
  end
repeat
  
  -- Now see if we have a free channel
  whichChannel = getOne(plSoundIDs, 0)
  if whichChannel > 0
then -- found a free channel, return it
    return whichChannel
  end
if
  
  -- No sound channels are available.
  -- Find the one with the oldest soundID (lowest number).
  -- Kill that sound to free up the channel,
  -- clear that soundID, and return that channel number
  oldestID = min(plSoundIDs)
  channelWithOldestID = getOne(plSoundIDs, oldestID)
  sound
stop channelWithOldestID
  plSoundIDs[channelWithOldestID] = 0
  return channelWithOldestID
end mAssignChannel

on mSoundStop me, soundID
  channelNum = me.mMapSoundIDToChannel(soundID)
  if channelNum = 0
then -- invalid channel or soundID
    return
  end
if
  
  sound
stop channelNum
  
end mSoundStop


on mMapSoundIDToChannel me, soundID
  if soundID = 0
then
    return
0
  end
if
  
  -- Find the ID. If not found, it will return 0
  whichChannel = getOne(plSoundIDs, soundID)
  return whichChannel
end mMapSoundIDToChannel


In the "new" method, we do some initialization of properties. We save away the number of channels in a property variable, save the proper path delimiter in a property variable, initialize a list of soundIDs, and initialize the current soundID to zero.

mSoundPlayExternal and mSoundPlayInternal are the two main methods available for use by clients to start a sound. These methods both call mSoundPlay which is a private method (not intended to be called from outside the object). After returning from mSoundPlay, these main methods return to the client a unique soundID for the sound that was requested to be played. If it wishes, the client can use this soundID to act on this sound later.

The mSoundPlay method calls mAssignChannel to get a sound channel in which to play the sound. Then it increments pCurrentSoundID to generate a unique soundID for the current sound, and saves that soundID into the appropriate slot in the plSoundIDs list. Depending on whether this is an internal or external sound, it calls sound playfile or puppetSound to start the sound playing. Finally, it returns the soundID to the caller.

The mAssignChannel method is the where the dynamic sound channel assignment decision is made. Here is the logic. First we run down the plSoundIDs list, and for every element where a soundID exists (a number greater than zero) indicating that a sound has been started in this channel, we check to see if that sound channel is no longer busy. If it is not busy, then we reset the appropriate soundID in plSoundIDs back to zero. Next we see if there is a free channel available to play the new sound - if so, we return that channel number. If we fall through, then all sound channels are in use and we have to make a policy decision. We'll make a rule that if all sound channels are busy, then we will choose to stop the "oldest" sound playing. This is done easily by finding the sound with the oldest ID (smallest number). Once we've identified that sound, we stop that sound, mark that channel as being free, and return that channel number. Because this policy is implemented in a single central place (encapsulated inside one method the sound manager), if for some reason we decide to change the policy later, all we need to do is to modify this one method to affect the entire program. There is no need to change any client calls to the sound manager.

As we mentioned, we could add any number of action methods to affect currently playing sounds. As an example of such an action method, we have provided mSoundStop. It first calls mMapSoundIDToChannel to find out the channel in which the sound with the given soundID is playing. Then it stops that sound. Additional "action" methods such as pause, continue, restart, lower volume, raise volume, fade in or out, etc. could easily be added to this architecture by simply adding additional action methods in the sound manager. The client would call the appropriate action method and pass the soundID as a parameter.

As the name implies, mMapSoundIDToChannel takes a soundID and returns the sound channel in which the sound with the given soundID was started. It is a private method that is intended to be called by all action methods. It searches the plSoundIDs list to find a match for the given soundID. If there is no match (e.g., this sound has already stopped playing), it returns a zero to signify that the appropriate channel wasn't found.

This version of the sound manager also does information hiding. A client of the sound manager (anywhere in the program where you want to play a sound) just wants to play a sound and does not know and does not need to know what sound channel is being used. Even further, the soundID which the sound manager hands back is in some internal format known only to the sound manager. Because the soundID is returned to the client, the client can look at the value of the soundID, but the value of the soundID is only understood by the sound manager. If requirements changed and we had to change the implementation of the sound manager so that it generated soundID's using a different algorithm, no client code would have to change.

 

Rethinking the Sound Manager architecture

As we add more functionality to a sound manager, we may find that the single sound manager starts to get complicated. From the computer's point of view, packing everything we need into a single script may be most efficient. However, from a human point of view, having a huge amount of code in a single script may wind up muddying the picture. At some point, we may want to modify the architecture slightly to break up the code into a layered system. Here is an evolutionary approach.

If we look closely at what the sound manager is doing, we realize that the sound manager is performing two distinct functions; 1) assigning sound channels, 2) managing operations on each individual sound channel (e.g., starting sounds, stopping sounds, fading, etc.). We can use this distinction to logically break up this functionality and split off some of the code and into a new type of object. The sound manager could still make decisions about assignment of channels, but the sound manager could also instantiate a sound channel object for each sound channel we wish to use in the program. In this way, each sound channel object would manage all the details of sound in its own channel. This is identical to the delegation of responsibility in a work environment. At work, it is the manager's job to make decisions on issues that affect all members of the group. Further, the manager assigns work out to individuals within the group. Here, it will be the sound manager's job to assign channels, but it will pass on requests to start and operate on sounds down to an individual sound channel object of its choosing.

Because of the object oriented approach, clients would still make calls to the same methods as in our previous dynamic sound manager. Again, clients don't know and don't need to know about the implementation of the sound manager, they just want play and act on sounds. The fact that there is another layer that the sound manager supports is of no concern to the client. Here is another example from the real world. Suppose you call someone to come out to do a repair on your house. If it is a very small company, the person who answers the phone may come out and actually do the repair. But as the repair company grows, the person who answers the phone will more likely assign some one else (a specialized repair technician) to actually come out and make the repair.

In the new split up sound manager, imagine that we wanted to start two sounds playing, A and B, and then later we want to programmatically stop sound A. We would call the sound manager to start both sounds and the sound manager would return two unique soundID's for these sounds. Internally, however, the sound manager assigns these two sounds to two different sound channel objects. Then later, we would call the sound manager to stop sound A (by passing in the proper soundID) and the sound stops. Inside the sound manager, the sound manager figures out which channel it had assigned this soundID, and passes on the request to stop the sound to that sound channel object. Here is a skeleton of this type of sound manager. Notice how we have moved actions and properties from the sound manager down into the sound channel object.

-- SoundMgrSplitUp

property pnChannels -- number of channels
property pCurrentSoundID -- current sound ID
property ploSoundChannels -- list of sound channel objects


on
new me, nChannels
  pnChannels = nChannels
  theDelim = the
last char of the moviePath
  ploSoundChannels = []
  repeat
with thisChannelNum = 1 to pnChannels
    oSoundChannel = new(script
"SoundChannel", theDelim, thisChannelNum)
    append(ploSoundChannels, oSoundChannel)
  end
repeat
  pCurrentSoundID = 0

  return
me
end
new

on mSoundPlayExternal me, soundName
  soundID = me.imSoundPlay(soundName, #external)
  return soundID
end mSoundPlayExternal


on mSoundPlayInternal me, soundName
  soundID = me.imSoundPlay(soundName, #internal)
  return soundID
end mSoundPlayInternal

-- This is called "im" SoundPlay to signify that it is an
-- internal method of the SoundMgr. That is, it is only
-- intended to be called from inside the SoundMgr
on imSoundPlay me, soundName, symInternalOrExternal
  channelNum = me.mAssignChannel()
  pCurrentSoundID = pCurrentSoundID + 1
-- generate a unique soundID every time a sound is played.
  
  -- Find the appropriate sound channel object
  -- and tell it to start the sound playing
  oSoundChannel = ploSoundChannels[channelNum]
  oSoundChannel.mSoundPlay(soundName, symInternalOrExternal, pCurrentSoundID)
  
  return pCurrentSoundID -- return sound ID to client
end



on mAssignChannel me

  -- First check to see if any playing sounds are still playing
  -- If no longer playing, reset the soundID to zero
  repeat
with thisChannelNum = 1 to pnChannels
    oSoundChannel = ploSoundChannels[thisChannelNum]
    thisSoundID = oSoundChannel.mGetSoundID()
    if thisSoundID > 0
then
      if not(soundBusy(thisChannelNum)) then
        oSoundChannel.mClearSoundID()
      end
if
    end
if
  end
repeat
  
  -- Now see if we have a free channel
  repeat
with thisChannelNum = 1 to pnChannels
    oSoundChannel = ploSoundChannels[thisChannelNum]
    soundIDInChannel = oSoundChannel.mGetSoundID()
    if soundIDInChannel = 0
then -- found a free channel, return it
      return thisChannelNum
    end
if
  end
repeat
  
  
  -- No sound channels are available,
  -- find the one with the oldest soundID (lowest number)
  -- Kill that sound to free up the channel, and save that channel number
  oldestID = the
maxInteger -- initialize, will be overwritten
  repeat
with thisChannelNum = 1 to pnChannels
    oSoundChannel = ploSoundChannels[thisChannelNum]
    soundIDInChannel = oSoundChannel.mGetSoundID()
    if soundIDInChannel < oldestID then
-- found an older one
      oldestID = soundIDInChannel
      channelWithOldestID = thisChannelNum
    end
if
  end
repeat
  
  -- "channelWithOldestID" has been calculated for us to use
  -- must kill oldest sound, so a new one can play
  oSoundChannel = ploSoundChannels[channelWithOldestID]
  oSoundChannel.mSoundStop()
  return channelWithOldestID
end mAssignChannel

on mSoundStop me, soundID
  channelNum = me.mMapSoundIDToChannel(soundID)
  if channelNum = 0
then -- invalid channel
    return
  end
if
  oSoundChannel = ploSoundChannels[channelNum]
  oSoundChannel.mSoundStop()
end mSoundStop


on mMapSoundIDToChannel me, soundID
  if soundID = 0
then
    return
0
  end
if
  
  -- Ask each channel for its current soundID
  -- If we find a match, return that channel number
  repeat
with thisChannelNum = 1 to pnChannels
    oSoundChannel = ploSoundChannels[thisChannelNum]
    soundIDInChannel = oSoundChannel.mGetSoundID
    if soundID = soundIDInChannel then
-- found it
      return thisChannelNum
    end
if
  end
repeat
  -- not found, return 0
  return
0
end mMapSoundIDToChannel


on mCleanUp me
  repeat
with thisChannelNum = 1 to pnChannels
    oSoundChannel = ploSoundChannels[thisChannelNum]
    oSoundChannel.mCleanUp()
    ploSoundChannels[thisChannelNum] = VOID

  end
repeat
end mCleanUp


Here is the code of an individual sound channel parent script. In i's "new" method, the sound manager would instantiate one sound channel object for each real sound channel the program wanted to manage.


-- SoundChannel

property pDelim -- path delimiter
property pSoundID -- current sound ID
property pChannelNum -- channel number of this sound channel


on
new me, theDelim, channelNum
  pDelim = theDelim
  pChannelNum = channelNum
  pSoundID = 0

  return
me
end
new


on mGetSoundID me
  return pSoundID
end

on mClearSoundID me
  pSoundID = 0
end

on mSoundPlay me, soundName, symInternalOrExternal, soundID
  pSoundID = soundID
  if symInternalOrExternal = #external then
    fullSoundPath = the
moviePath & "Sounds" & pDelim & soundName
    sound
playFile pChannelNum, fullSoundPath
  else
    puppetSound pChannelNum, soundName
  end
if
end

on mSoundStop me
  sound
stop pChannelNum
  pSoundID = 0
end mSoundStop



Now that we have this structure in place, it is easy to add new functionality. One example would be that each sound channel object could easily remember the name of the currently playing sound and we could provide methods to retrieve that name. Another example could be that each sound channel object could implement a queuing mechanism so that when one sound is finished, another sound starts. As a simple coding example, let's say that you want to add the ability to fade a sound out. Using the mSoundStop method in the sound manager and in the sound channel as prototypes, here is what we would do. In the sound manager, we would add the following method:

on mSoundFadeout me, soundID
  channelNum = me.mMapSoundIDToChannel(soundID)
  if channelNum = 0
then -- invalid channel
    return
  end
if
  oSoundChannel = ploSoundChannels[channelNum]
  oSoundChannel.mSoundFadeOut()
end mSoundFadeOut

 

And in the sound channel script, we would add the following:

on mSoundFadeOut me
  sound fadeOut pChannelNum

end
mSoundFadeOut


Cleaning Up

In this chapter we have shown how a single object can be turned into an object manager object so that it creates and then delegates responsibility down to subordinate objects. In the introduction to objects, we discussed how, in the process of instantiating an object, Director allocates memory for the properties of the object. We have also said that when you are finished using an object, you need to get rid of it (i.e., deallocate the memory used by the object and set the object reference back to VOID). If you don't, the memory allocated for an object will never be recovered by Director. And if memory is repeatedly lost this way, you can run out of memory and your program will crash.

But an object manager object presents a special problem. You have to be especially careful when you are finished using an object manager object or want to stop a program that has one or more object manager objects in it. If an object instantiates one or more objects (thereby making itself an object manager object), then when that object is about to be destroyed, it also has the responsibility of cleaning up the object or objects that it has instantiated.

In Lingo, when you create an object, you explicitly call the "new" method of the parent script. In some other programming languages, this is referred to as a constructor method because it allows you construct a new object. And in some other languages there is a built-in method for getting rid of an object, this is referred to as a destructor method. Ideally, we would like to have a method in each object that would get called to notify the object that it is about to go away. But unfortunately, Lingo does not provide a standard destructor method for use in objects.

To work around this shortcoming of Lingo, we can make a convention. The convention is that in every parent script that you write, you need to include a destructor method using the common name. If we assume this convention, then we can build a framework that can allow objects to clean up the world correctly. A specific suggestion is that every parent script include a method called "mCleanUp" (although any name will do equally as well). mCleanUp is a simple name that clearly describes the method's purpose. In the code of the mCleanUp method, you include any code that you need to clean up any objects that object has instantiated, and/or any other code that you wish to execute before the object goes away.

Here is an example using the "split up" sound manager. We would typically instantiate a globally available object like a sound manager in the prepareMovie or startMovie handler like this:

global goSoundMgr

on
prepareMovie
  goSoundMgr = new(script
"SoundMgrSplitUp", 4)
end

In the stopMovie handler, we need to have the code that allows us to eliminate any global objects like this that we have created. As stated earlier, just setting the variable to VOID will free up any memory allocated for that object. But, for example, if we just set the variable goSoundMgr to VOID, we would not be giving the sound manager a chance to "clean itself up". The result would be that the sound manager object would be eliminated, but all the sound channel objects that it had created would not be cleaned up and their memory would be lost forever. So, just before we set the goSoundMgr variable to VOID, we call the mCleanUp method of the sound manager. Here is the code:

on stopMovie
  goSoundMgr.mCleanUp()
  goSoundMgr = VOID
end

This way, the mCleanUp method in the sound manager has the opportunity to clean up the sound manager's world. Its world consists of a number of sound channel objects. So in the code of the sound manager's mCleanUp method, we call the mCleanUp method of each of the sound channel objects and then set the appropriate variable to VOID. Here is how we do that:

on mCleanUp me
  repeat with thisChannelNum = 1 to pnChannels
    oSoundChannel = ploSoundChannels[thisChannelNum]
    oSoundChannel.mCleanUp()
    ploSoundChannels
[thisChannelNum] = VOID
  end
repeat
end mCleanUp



Following this new convention, the sound channel script needs to have an mCleanUp method also. If a parent script has nothing to do in its mCleanUp method, it can just be a "degenerate" method with literally nothing in it. Or to be even more clear, it can contain a single "nothing" statement as follows.

on mCleanUp me
  nothing

end mCleanUp

If the sound channel object had instantiated any other objects, it would need to call the mCleanUp method of those objects and set the appropriate object reference variable(s) to VOID.

In the case of the sound manager, here is the full sequence of events. In the stopMovie handler, we call the mCleanUp method of the sound manager. The sound manager knows that it has created some number of sound channel objects, so it calls the mCleanup method of each one. Each sound channel object's mCleanUp method gets called but it doesn't have anything to do, so it just returns. Upon returning, the sound manager sets the object reference variable for each sound channel object to VOID, thereby eliminating the sound channel object. When the sound manager is finished, it returns control to the stopMovie handler. The stopMovie handler finally sets the global, goSoundMgr, to VOID thereby eliminating the sound manager object.

The keys to the clean up process are; 1) the convention that all objects must have a destructor method that has a common name, and 2) that in this destructor method, each object calls the desctructor method of all objects that it has created, and then sets the object reference to VOID. One call to the mCleanup method of a top level object manager object starts a pseudo-recursive chain of mCleanUp calls that ensures that each object gets the opportunity clean up its own world. In the sound manager example, the hierarchy was only two levels of objects deep. However, because at every level, each object manager object is responsible to cleaning up all objects that it has created, any number of levels can be cleaned up using this approach.

Further, because this is a destuctor method, in the mCleanUp method you know that this is the last thing an object is going to execute before being eliminated. For example, if you were coding a tracking object that tracks the user's progress through a program, in its mCleanup method you might want to execute code that writes the user's status out to a file before the tracking object goes away. Another example might be that of some type of communication object. Knowing that it is about to go away, in its mCleanUp method you made need to include some code that breaks a communication channel.

Previous Chapter

Table of Contents

Next chapter