LOOPE - Lingo Object Oriented Programming Environment by Irv Kalb

Previous Chapter

Table of Contents

Next Chapter

 

Section 2 - Behaviors

Chapter 11 - Intersprite Communication

 

In this chapter, we will discuss how to send messages between sprites. This is known as intersprite communication. We know that Lingo provides the sendSprite command to allow you to send a message to a sprite, but the real underlying question is: how can you know what sprite number to send a message to? In this chapter we will discuss a variety of different approaches to answering this question.

 

1) Use sprite numbers

The most straightforward approach is the simplest. Whenever you want to send a message to a sprite, you simply code in the channel number in the score where the sprite lives. For example, if you want to send an "mDoIt" message to (call the mDoIt method(s) of) all behaviors attached to sprite 10, then you can write the following line of code:

sendSprite(10, #mDoIt)

This is commonly referred to as hard coding - meaning that you are putting constants in your code. You will often see this type of coding in the work of inexperienced programmers, as it is the most obvious way to write a sendSprite statement. This will work perfectly well ... at least for a while. This code will work as long as you don't move sprites in the score, or try to re-use your code in another project where the sprite you want to send the message to is not in channel 10, or if you want to send the same message to two or more sprites.

In the real world of developing Director applications, the requirements of a program constantly change. For example, you may have all channels from one to 10 filled with sprites, and then a change request comes in where you need to add a new sprite to the screen. But because of layering, you need to have the new sprite appear "below" the sprite currently in channel 10. So, you move the current sprite from channel 10 to channel 11, and put the new sprite in channel 10. If you have written code that sends a message to a sprite in channel 10, then after making this change in the score, your program stops working correctly and you are left to track down a new bug in your program. Even worse, sendSprite will not give an error if the target does not exist. If you try to send an mDoIt message to sprite 10, but no behavior attached to sprite 10 has an on mDoIt behavior, nothing happens.

The layout in the score is one of the things that changes quite often during the development of a Director application. Therefore, an important design goal when writing Lingo is to make your code flexible enough to respond to changes like this with as minimal an impact as possible. You would like to be able to shift sprites around in the score with minimal or no re-coding.

If you move a sprite from channel to 10 to 11 and have used hard coded sprite numbers, then you must search all your code to find all occurrences of the number 10, and change them all to the number 11. However, in doing so you also must insure that you don't inadvertently change a 10 that was not a reference to a channel number. For example, if you have two lines of code that said:

nPixels = 10

sendSprite(10, #mDoIt)

You would not want to change the first line, but you would want to change the second line to:

sendSprite(11, #mDoIt)

Therefore, you would have to go through all your code looking for every occurrence of the number 10, and decide on a case-by-case basis whether or not the 10 should be changed to an 11. For these reasons, using hard coded sprite numbers turns out to be a very bad approach. If you remember back in the introduction of this book, I talked about the "ilities" of well-written software. Using hard coded sprite numbers leads to low reliability, poor generality, and near zero modifiability.

Some programmers think that they can be clever by allowing the user to define a sprite number via getPropertyDescriptionList. That is, let the user supply a sprite number in a Parameters dialog box generated by a gpdl handler, like this:

-- Sample GPDL

property spriteNum
property pchSomeSprite

on getPropertyDescriptionList me
  lDescription = [:]

  addprop(lDescription, #pchSomeSprite, [\
      #comment:"Sprite to send message to:", \
      #format:#integer, \
      #default:spriteNum])

  return lDescription
end

The claim is that this is no longer hard coding because a sprite number can be changed at author time. However, doing this is actually worse than hard coding sprite numbers in code. If you hard code a sprite number in code, Director gives you a search and replace tool to find all occurences of numbers that you may want to change. If you code sprite numbers into gpdl dialogs, and you want to change a sprite number, you must go through the score and find every occurence of behaviors that have these types of gpdl's and change sprite numbers by hand. Allowing the user to enter sprite numbers via a Parameters dialog box generated from a getPropertyDescriptionList handler is a terrible approach.

 

2) Relative sprite numbers

The next approach is to use relative sprite numbers. That is, to calculate the number of a sprite on the fly based on its relative position to another sprite. For example, assume that you were coding a slider behavior. When coding something like this you have two sprites that are typically called the slider thumb and the extent. The thumb is the part that moves horizontally or vertically. The extent is a separate graphic that defines the horizontal or vertical range over which the thumb can travel. In cases like these, these two sprites need to communicate with each other. You can also typically make an assumption that the thumb will move over the top of the extent and therefore needs to be in a higher numbered channel than the related extent sprite. If you take that assumption a little further, you could say that the thumb must be in the next channel up from the extent. Another way of saying this is that the extent is one channel down from the thumb. Therefore, the behavior attached to each could address the other by using relative sprite numbers. Remember that each sprite gets the variable spriteNum for free. So, in the extent behavior, we could have the following code:

-- Extent - thumb is relative

property spriteNum
property pchThumb

on beginSprite me
  -- Assume that the thmb is in the next channel up
  pchThumb = spriteNum + 1
end

And in the thumb behavior, we could similarly write:

-- Thumb - extent is relative

property spriteNum
property pchExtent

on beginSprite me
  -- Assume that the extent is in the next channel down
  pchExtent = spriteNum - 1
end

After doing this, the thumb could send a message to the extent behavior by doing a:

sendsprite(pchExtent, #mSomeMessage)

and the extent behavior could send a message to the thumb behavior by doing a:

sendSprite(pchThumb, #mSomeOtherMessage)

Overall, this approach to finding relative sprites is fairly safe. Obviously, the only downside is that you must know and remember the implied ordering of sprites and follow the ordering precisely. Although this works fine for simple cases like this thumb and extent where the implied order is clear, it does present problems for situations where you have many inter-related sprites that don't have any implicit logical ordering. For example, if you were defining a screen full of buttons that needed to communicate with each other, then there might not be any particularly obvious ordering for the buttons, but the code would only work if the buttons were laid in the "correct" order in the score.

 

3) Sprite Names

An ideal solution would be to allow sprites to have names. It would be great if you could dynamically assign a name to a sprite. If sprites had names, then you could send a message from one sprite to another sprite by name, and it wouldn't matter where that sprite appeared in the score. Here are two different approaches to naming sprites.

3.1 Search sprites for member names

Although the sprites themselves don't have built-in names, every member can have a name. As a "quick and dirty" approach, when looking for a sprite to send a message to, we could write a routine to search through the score looking for a sprite by looking for a match on the name of the member. Here is a utility routine that does this:

on FindSpriteWithName theName, fNotFoundReturnZero
  -- Search through all channels looking for a match
  -- between the passed in name and the first word
  -- of the member in each channel
  nChannels = the lastChannel
  repeat with ch = 1 to nChannels
    if sprite(ch).member.type <> #empty then
      if sprite(ch).member.name.word[1] = theName then
         -- Found it, return the channel
        return ch
      end if
    end if
  end repeat

  -- Did not find match, what should we do?
  if
voidp(fNotFoundReturnZero) then
    fNotFoundReturnZero = FALSE
  end
if
  if fNotFoundReturnZero then
    return
0
  else
    alert("Could not find a sprite with the name:"
&& theName)
  end
if
end

This handler just looks through the sprites in the current frame looking for a match between what we pass in and the first word of the name of the member in each sprite. If we find a match, we return the channel where the match was found. If there is no match, we can either return a zero, or display an alert. This approach works well but has three disadvantages. First, it can be slow because it has to search all channels. Second, it will only find the first occurrence of a match. And third, it doesn't really allow you to assign a name to a sprite, it just relies on the name of the member in the sprite.

 

3.2 Giving each sprite a name

Before version 10, Director didn't have have a built-in way of giving sprites names. For those using earlier these version, we can build our own sprite naming code. We can write a single behavior that can be attached to any sprite that you wish to name. It would allow you to enter a name for the sprite, and will have a single method that will return the name given to that sprite. Here is the code of a behavior that does this:

-- Name the sprite

property spriteNum
property psThisSpriteName

on getPropertyDescriptionList
  if
the currentSpriteNum = 0 then -- avoid compile errors
    return
  end
if

  -- the default sprite name is set to the name of its member
  lDescription = [:]
  sDefaultName = sprite(the
currentSpriteNum).member.name
  sDefaultName = word
1 of sDefaultName
  addProp(lDescription, #psThisSpriteName, [ \
            #comment: "Sprite's name:", \
            #format: #string, \
            #default: sDefaultName])
  return lDescription
end


on mGetSpriteName me
  return psThisSpriteName
end

 

If we were to drop this behavior onto a sprite that had a member named "triangle" in it, we should see the following gpdl dialog box:

Now we have a way of giving each sprite a name. The name "lives" (is attached) for the life of the sprite span. This is exactly what we want because we may have one member in a channel for a few frames, then a different member in the same channel for a second set of frames, etc. By attaching this same behavior to different sprite spans, we can give the same channel different names at different times.

But now we need to have a way of translating this name into a sprite channel number. That is, we need a piece of code, that when given a sprite's name, returns the channel that the sprite was found in. Here is the code that will find a sprite by name:

on SpriteChannel sTargetName
  theLastChannel = the
lastChannel
  repeat
with ch = 1 to theLastChannel
    thisSpriteName = sendSprite(ch, #mGetSpriteName)
    if thisSpriteName = sTargetName then
      return ch
-- Found it, return this channel
    end
if
  end
repeat
  -- Fell through, no match
  alert("No sprite was found with the name:"
&& sTargetName)
  return
0
end

Because this code is not specific to any one behavior or parent script, this is a utility routine and we would put this code into a movie script. The code looks through all channels (from lowest numbered channel to highest numbered channel) calling the mGetSpriteName method for each. As we previously mentioned when you issue a sendSprite and there is no receiver in a particular channel, (i.e., there is no behavior attached that has the method), nothing happens and no error is generated. In this case, this is a very good thing. If we do a sendSprite to send the mGetSpriteName message to a sprite that does not have this behavior on it, no error is generated. If and when a match is found, the handler returns the sprite number of the matching sprite.

With the SpriteName behavior attached to sprites and the SpriteChannel utility handler in place, now we can find any sprite by name. For example, if we wanted to find the channel number of the Triangle sprite, we could do that by using the line:

ch = SpriteChannel("Triangle")

This approach to finding sprites by name works well. The major downside to this approach is speed. Whenever you want to find a sprite by name, the spriteChannelHandler must look through many sprites looking for a match.

As a minor limitation, this code only finds the first sprite with a name that matches. We can code around this limitiation if we need to. If we wanted to allow for multiple sprites with the same name, then we could write a similar handler that would look through all sprites and return a list of sprite numbers that match the given name, like this:

on SpriteChannels sTargetName
  lChannels = []
  theLastChannel = the
lastChannel
  repeat
with ch = 1 to theLastChannel
    thisSpriteName = sendSprite(ch, #GetSpriteName)
    if thisSpriteName = sTargetName then
      append(lChannels, ch) -- found it, add to our list
    end
if
  end
repeat
  return lChannels
end

 

4) Registration

My personal favorite approach for identifying sprites is to have sprites "register" themselves. Imagine you have a frame where you have a number of different sprites that need to work together to make up some larger piece of functionality. You choose one sprite to be a "manager" sprite. The manager sprite is told the channel assignments of all other sprites. Whenever it needs to, the manager sprite sends messages to one or more of the other sprites. For example, as circumstances change, it may send a message to tell some sprites to make themselves available or unavailable.

Registration might be best explained with a small example. Let's say that you have a frame with one button and two graphics. As a silly example, let's say that when every time the user clicks on the button, you want to hear a beep and make the other graphics rotate 90 degrees - one rotating to the left and one rotating to the right. Although this is not a very useful example, it directly addresses the need for communicate between sprites. We'll give a much more real world example in a little bit.

In this example, we will make the button be the manager sprite. To make the registration process work, each of the graphic sprites needs to tell the manager sprite what piece they are (the rotate left sprite or the rotate right sprite), and what channel they are in. But how can the graphic sprites send a registration message to a manager sprite if they don't know what channel the manager sprite is in?

Introducing sendAllSprites - sendAllSprites is a Lingo function that broadcasts a message to all sprites. That is, it tries to call a handler in every behavior attached to every sprite. The syntax is:

sendAllSprites(#messageName, <optional parameters>)

Because the behavior attached to the graphics can't know what channel the manager sprite behavior is in, it will use sendAllSprites to broadcast a message to every sprite saying that it wants to be registered with the manager sprite. It assumes that there is only one registration receiver for this message. As parameters, the behavior will send its channel number and which part it is. The manager sprite will have a behavior that receives the registration message, and will remember the channel assignments of the graphics using property variables.

Before getting into the code, there is one more important thing to know. Behaviors are instantiated in numerical order - lowest channel number to highest. The behavior(s) attached to sprite 1 are instantiated first, then the behavior(s) attached to sprite 2, etc. For this reason, when using a registration scheme, the manager sprite must be in a lower numbered channel than any sprite(s) that wish to register with it. This way, you can ensure that the manager sprite behavior is instantiated before any registration message is sent out.

Now we can write the sender and receiver code for the registration process of our example. On the button, we attach a manager behavior that looks like this:

-- Hit the button 1

property spriteNum
property pchRotateRight
-- Channel of the sprite we want to rotate right
property pchRotateLeft
-- Channel of the sprite we want to rotate left


-- Handle registration messages
-- Save channel assignments into property variables
on mButtonRotate_Register me, symType, ch
  case symType of
    #rotateRight:
      pchRotateRight = ch
      
    #rotateLeft:
      pchRotateLeft = ch
      
    otherwise:
      alert("Unknown type of registration:"
&& symType && "from sprite" && string(ch))
      
  end
case
end

-- The button has been hit.
-- Tell each graphic to rotate in the appropriate direction
on mHit me
  beep
  sendSprite(pchRotateRight, #mRotate, #right)
  sendSprite(pchRotateLeft, #mRotate, #left)
end

This behavior only has two methods and two property variables. The first method, mButtonRotate_Register handles the registration messages that will be sent by the graphic sprites. It is set up to receive one message from each of the two graphic sprites. When they send out their messages, they will identify which piece they are and their sprite number. When these messages come in, this method remembers the channel number of each piece by placing them into the property variables pchRotateLeft and pchRotateRight.

When the user successfully hits the button, the button behavior sends out an mHit message. This behavior receives the mHit message from the button behavior and reacts by doing a beep, then it sends out mRotate messages to the two arrow graphic sprites. It is up to them to do whatever they need to do to accomplish the rotation.

On each of the arrow graphic sprites, we attach the following behavior:

 

-- Register the sprite and rotate

property spriteNum
property psymWhichDirection

-- To identify which part we are
on
getPropertyDescriptionList
  lDescription = [:]

  addProp(lDescription, #psymWhichDirection, [ \
            #comment: "Rotate in which direction:?", \
            #format: #symbol, \
            #range: [#rotateRight, #rotateLeft],\
            #default: #rotateRight])
  return lDescription
end


-- Broadcast the registration message
-- pass our identifier and our sprite number
on
beginSprite me
  sendAllSprites(#mButtonRotate_Register, psymWhichDirection, spriteNum)
end

-- Rotate this sprite left or right
on mRotate me, symLeftOrRight
  if symLeftOrRight = #right
then
    sprite(spriteNum).rotation = sprite(spriteNum).rotation + 90

  else
-- rotate left
    sprite(spriteNum).rotation = sprite(spriteNum).rotation - 90
  end
if
end

 

When we drop this behavior on each of the two arrow graphics, we are presented with a Parameters dialog box. In the dialog box, we must identify which direction we wish to rotate the current sprite. This choice is stored in the property psymDirection. When the play head reaches the beginning of this sprite span, the beginSprite method is called. It broadcasts, to all sprites, a well-named message, mButtonRotate_Register, with parameters which supply the sprite number and which direction to rotate. This message is received by the earlier behavior attached to the button that is the manager sprite.

After the beginSprite executes, this behavior just sits and waits. When the user hits the button, the button behavior sends an mRotate message to both arrow graphic sprites and each instance of this behavior rotates the sprite in the appropriate direction.

 

You might think that all this registration is overkill. Why not just use sendAllSprites everywhere to send messages? The problem is that sendAllSprites is slow. For example, let's say that you had written a behavior that has a method called mMove that would move the sprite by a set number of pixels in a particular direction. Further assume that you had a frame where you attached this behavior to a number of different sprites. Now imagine that you wanted to move all sprites that had this method by 10 pixels in the up direction. You could issue the following command:

sendAllSprites(#mMove, 10, #up)

The message would be sent to all sprites, and the ones that had a behavior with an mMove method would receive the message and react accordingly. But sendAllSprites is slow enough that you should NOT rely on it for communicating between sprites on a on-going basis. The more channels in your movie, the slower sendAllSprites becomes.

Instead, the idea is to use sendAllSprites ONCE, to register the sprite or sprites you want to communicate with. You remember these channels numbers in property variables. When you want to send a message to one or more channels, use sendSprite to send the message to just the targeted sprites.

 

4.1) Variation 1 - No master sprite

In our example above, it seemed obvious that we should make the button be the master sprite. But what do you do if there is no clear sprite to use as a master sprite? The answer is to use an additional sprite. In fact, moving the registration and messaging code from a master sprite to another sprite, can make the operation of the all sprites more clear and simple. Each sprite is only responsible for itself - a very object oriented approach. This also allows you put all the code that manages the sprites into one central location.

The first thing to do is to create another sprite. But what cast member do we put in the new sprite? We would want something that has the same sprite span as all other sprites but does not clutter the screen. We could choose to use any member and place it "off-stage", that is, above the top or to the left of the left edge of the visible stage. However, there is an easier way.

You can use the Director paint window to create a cast member that is a zero pixel bitmap. Here's how. Open the Director paint window, click on the plus sign to create a new member, then just give the new member a name. I typically use the name "Hide". Because this member has a name, Director will not automatically delete it. You can drag it to the score and it will behave like any other sprite. It will receive all the standard Director messages. But because it has no height or width, it will not interfere with any graphics on the screen. Although the rect of the member is (0,0,0,0), the locH and locV of the sprite provide a valid screen location. Where you place it on the screen makes no difference.

Let's modify the requirements of our previous example slightly. Let's add a restriction that you are only allowed to click on the button a set number of times - let's say eight times. (For example, this could be part of a game where we only have a certain number of shots or you could be writing a test where you only give the user a certain number of attempts.) Now that we have a Hide cast member in the score, we can attach a registration receiver behavior to it. Then we need to get all of the other sprites to communicate with the Hide sprite instead.

We'll use a lot of the same code, but split it up differently. We'll write four behaviors: 1) ButtonRotate will contain the bulk of the registration and control logic, 2) Register with ButtonRotate sends the registration message, 3) Hit Button handles what happens with you hit the button, and 4) Rotate handles the rotation of the arrow graphics. Here is the score layout and what behaviors are attached:

Sprite number Member Behavior(s) and Parameter(s)
x Hide ButtonRotate
x + 1 Button

Register with ButtonRotate (Button)
Button Behavior
Hit Button

x + 2 Left Arrow Register with ButtonRotate (LeftArrow)
Rotate
x + 3 Right Arrow Register with ButtonRotate (RightArrow)
Rotate

 

First, we'll write a single registration sender behavior that can be attached to all significant sprites:

-- Register with ButtonRotate

property spriteNum
property psymWhichPart

-- To identify which part we are
on
getPropertyDescriptionList
  lDescription = [:]

  addProp(lDescription, #psymWhichPart, [ \
            #comment: "Which part is this:?", \
            #format: #symbol, \
            #range: [#button, #rotateRight, #rotateLeft],\
            #default: #button])
  return lDescription
end


-- Broadcast the registration message
-- pass out identifier and our sprite number
on
beginSprite me
  sendAllSprites(#mButtonRotate_Register, psymWhichPart, spriteNum)
end

This behavior's job is to identify the significant sprites to the receiver registration behavior. In our example, it is dropped on the button sprite, the sprite that we want to rotate left, and the sprite that we want to rotate left. When we drop this behavior we choose the proper identifier from the popup list. When the program reaches this frame in the score, each of the three instances of this behavior receive the beginSprite message from Director, and each sends out its registration messages to identify itself. As an interface becomes more complicated and more items are added to an interface, you add more options to the #range of the above.

 

Next, is the Rotate behavior which we attach to each of the arrow graphic sprites:

-- Rotate

property spriteNum

-- Rotate this sprite left or right
on mRotate me, symLeftOrRight
  if symLeftOrRight = #right
then
    sprite(spriteNum).rotation = sprite(spriteNum).rotation + 90

  else
-- rotate left
    sprite(spriteNum).rotation = sprite(spriteNum).rotation - 90
  end
if
end

Because these sprites also have the registration behavior identifying them as rotate left or rotate right, the manager sprite will remember which message to send them. The only thing this behavior needs to do is to rotate the sprite when the manager sprite sends it the mRotate message.

 

Next we look at the button sprite. To this sprite we attach our standard Button behavior so that reacts like a standard button. When the button is successfully hit, it sends out an mHit message. Now we can write the behavior that will react when the button is pushed:

-- Hit the button 2

property spriteNum
property pchButtonRotate -- the channel of the manager sprite


on
beginSprite me
  -- Broadcast a message to get the channel of the ButtonRotate behavior
  -- the ButtonRotate will return its channel number
  pchButtonRotate = sendAllSprites(#mButtonRotate_GetCh)
end

on mHit me
  beep
  sendSprite(pchButtonRotate, #mHitButton)
end

Notice that instead of broadcasting the registration message, this behavior broadcasts a new message, mButtonRotate_GetCh. This is an alternative approach. Rather than simply broadcasting the registration message, the button behavior needs to know the spriteNumber of the manager sprite (which is the Hide sprite with the "ButtonRotate" behavior attached). This is because every time the button is hit, it must notify the ButtonRotate behavior. So, as a variation, we broadcast a message asking for the ButtonRotate behavior to identify its sprite number. That behavior in the manager sprite will return its sprite number. We save that channel assignment in a property variable called pchButtonRotate. Every time the button is hit, we can do a sendSprite directly to the sprite that has the ButtonRotate behavior.

 

Next we can write a behavior that contains the bulk of the code for the logic of what we want to do. It also contains the registration receiver code and performs the intersprite communication using sendSprite. As we said, we'll call this behavior "ButtonRotate", and attach it to the Hide sprite.

-- ButtonRotate

property spriteNum
property pchButton -- Channel of the button sprite
property pchRotateRight -- Channel of the sprite we want to rotate right
property pchRotateLeft -- Channel of the sprite we want to rotate left
property pnTimesHit

on beginSprite me
  pnTimesHit = 0
end

on mButtonRotate_GetCh
  return
spriteNum
end

-- Handle registration messages
-- Save channel assignments into property variables
on mButtonRotate_Register me, symType, ch
  case symType of
    #button:
      pchButton = ch
      
    #rotateRight:
      pchRotateRight = ch
      
    #rotateLeft:
      pchRotateLeft = ch
      
    otherwise:
      alert("Unknown type of registration:"
&& symType && "from sprite" && string(ch))
      
  end
case
end

-- The button has been hit.
-- Tell each graphic to rotate in the appropriate direction
on mHitButton me
  sendSprite(pchRotateRight, #mRotate, #right)
  sendSprite(pchRotateLeft, #mRotate, #left)
  pnTimesHit = pnTimesHit + 1
  if pnTimesHit = 8
then
    sendSprite(pchButton, #mButton_SetAvailability, FALSE)
  end
if
end

The mButtonRotate_Register is the receiver method for all the registration messages. When a registration message comes in, the method saves the current channel number in the appropriate property variable; pchButton, pchRotateRight, or pchRotateLeft. If an interface is more complicated, we could add additional property variables, possibly add additional branches in the case statement, and perhaps build lists of channels.

This behavior contains logic of what to do when the button is hit. The "on beginSprite" method initializes a property, pnTimesHit, to count the number of times the button has been hit. Then there is a new "on mHitButton" method. When the button is hit, it does a sendSprite to tell the left arrow sprite to rotate and and another sendSprite to tell the right arrow to rotate right. Then it increments the count of the numbers of times the button has been hit, and if we've reached the limit, it sends a message to the button sprite telling it to make itself unavailable. (Because our button behavior is self-contained, when it receives the message to make itself unavailable, it will show the grayed out version of the graphic, and will no longer respond to mouse clicks - just what we want.)

The behavior also has a new mRotateButton_GetCh method. This method just returns the manager sprite's channel number. As we saw in this example, a sprite may not only need to register with the manager sprite, but also send additional messages to the manager later. If this is the case, a manager sprite should have a simple accessor method like this to give out its own sprite number.

CAUTION: SendAllSprites is actually a function. That is, it can return a single value. If you use sendAllSprites and the message is received by multiple sprites AND each returns a value, then only the last value returned will actually be received by the caller. For example, imagine you attach the ButtonRotate behavior to sprites 7, 8, and 9, then in a higher sprite number (or from the message window) you issue:

put sendAllSprites(#mButtonRotate_GetCh)

to find out the sprite number of the ButtonRotate sprite. The message would be sent to sprite 7, 8, and 9, and each would return its sprite number, but the value eventually returned by the sendAllSprites would be 9 - the value returned from the last sprite. (In section 5 below, we will show how we can take advantage of this oddity.)

4.2) Variation 2 - Multiple sprites

Let's extend our example a little more. Rather than rotating a single sprite left and a single sprite right, let's say that we need to send a rotate message to any number of sprites. Imagine that we had an interface that looked like this:

 

We could certainly add a new property variable for each sprite that we wanted to rotate left or right. But in cases like this, it is much easier to use lists. The layout in the score is almost the same, except that there can be many left arrow, and many right sprites.

Sprite number Member Behavior(s) and Parameter(s)
x Hide ButtonRotate
x + 1 Button

Register with ButtonRotate (Button)
Button Behavior
Hit Button

x + 2, 3, 4 ... Left Arrows Register with ButtonRotate (LeftArrow)
Rotate
x + n, n+1, n+2 ... Right Arrows Register with ButtonRotate (RightArrow)
Rotate

 

Here is the code of a new ButtonRotate behavior (which we will call ButtonRotate Multiple) that can handle any number of graphics rotating left or right.:

-- ButtonRotate Multiple

property spriteNum
property pchButton -- Channel of the button sprite
property plchRotateRight -- List of channels to rotate right
property plchRotateLeft -- List of channels to rotate left
property pnTimesHit

on beginSprite me
  plchRotateRight = []
  plchRotateLeft = []
  pnTimesHit = 0
end

on mButtonRotate_GetCh
  return
spriteNum
end

-- Handle registration messages
-- Save channel assignments into property variables
on mButtonRotate_Register me, symType, ch
  case symType of
    #button:
      pchButton = ch
      
    #rotateRight:
      append(plchRotateRight, ch)
      
    #rotateLeft:
      append(plchRotateLeft, ch)
      
    otherwise:
      alert("Unknown type of registration:"
&& symType && "from sprite" && string(ch))
      
  end
case
end

-- The button has been hit.
-- Tell each graphic to rotate in the appropriate direction
on mHitButton me
  repeat
with ch in plchRotateRight
    sendSprite(ch, #mRotate, #right)
  end
repeat
  repeat
with ch in plchRotateLeft
    sendSprite(ch, #mRotate, #left)
  end
repeat
  pnTimesHit = pnTimesHit + 1
  if pnTimesHit = 8
then
    sendSprite(pchButton, #mButton_SetAvailability, FALSE)
  end
if
end

In this new version of the code, we have replaced the individual pchRotateLeft and pchRotateRight properties with plchRotateLeft and plchRotateRight properties - implying with our naming convention that these are now lists. In the beginSprite method, we initialize these property variables to the empty list. In the mButtonRotate_Register method, instead of setting a single property, we add the channel number being passed in to a list of channel numbers already passed in. As registration messages come in, these lists expand to remember the channel numbers. Finally, when the button is hit, our modified mHitButton method will send out messages to all sprites in both the plchRotateRight and plchRotateLeft lists.

One very nice thing about this approach is that this one behavior is the only place where a change has to be made. We can continue to use the registration sender behavior as is, we just need to attach it to multiple sprites.

 

4.3) Variation 3 - Multiple instances of an interface

We have said in this chapter that the registration method works well when there is a single manager sprite and multiple sprites that register with it. But how can we use this technique if you want to able to have multiple copies of an interface group. For example, suppose that now our program needs to have two (or more) sets of buttons and rotatable graphics. Imagine if we were trying to implement something like this:

 

In this version of the program, when we hit the top button, would only want the graphics in Group 1 to rotate. When we hit the bottom button, we would only want the graphics in Group 2 to rotate.

If we did nothing but attach the behaviors that we have already developed, it would almost work. When the rotatable graphics in the first group send out their registration messages, they are received by the first instance of the RotateButton behavior. However, when the rotatable graphics in the second group do a sendAllSprites to register themselves, the registration message is received by both the first and second instances of the RotateButton behavior. Subsequently, if we run the program and click on the first button, the graphics in the first and second set will rotate. Not what we wanted.

We need a way to associate each sprite with the group they are in. If we make a slight modification to the registration process, we can can do this fairly easily. The first thing to do is to modify the registration sender behavior to include a group identifier:

-- Register with RotateButton Group

property spriteNum
property psymWhichPart
property pGroupNumber -- identify the group we belong to

-- To identify which group and part we are
on
getPropertyDescriptionList
  lDescription = [:]

  addProp(lDescription, #pGroupNumber, [ \
            #comment: "Which group is this in?", \
            #format: #integer, \
            #default: 1])
  addProp(lDescription, #psymWhichPart, [ \
            #comment: "Which part is this?", \
            #format: #symbol, \
            #range: [#button, #rotateRight, #rotateLeft],\
            #default: #button])
  return lDescription
end


-- Broadcast the registration message
-- pass out identifier and our sprite number
on
beginSprite me
  sendAllSprites(#mButtonRotate_Register, pGroupNumber, psymWhichPart, spriteNum)
end

The thing to notice here is that we have added a new property variable, pGroupNumber that is set in the getPropertyDescriptionList handler. When you drop this behavior onto a sprite, now you will have to supply a group number that this sprite belongs to as the first parameter. The other change is that we now pass this group number when we do the sendAllSprites with the registration message.

 

Next we need to make a similar change the behavior that is dropped on the buttons. We must identify the group number that each button belongs to:

-- Hit the button 3

property spriteNum
property pchButtonRotate -- the channel of the manager sprite
property pGroupNumber


-- To identify which group we are in
on
getPropertyDescriptionList
  lDescription = [:]

  addProp(lDescription, #pGroupNumber, [ \
            #comment: "Which group is this in?", \
            #format: #integer, \
            #default: 1])
  return lDescription
end

on beginSprite me
  pchButtonRotate = sendAllSprites(#mButtonRotate_GetCh, pGroupNumber)
end

on mHit me
  beep
  sendSprite(pchButtonRotate, #mHitButton)
end

Notice also that we are sending the group number when we broadcast the message to get the channel of the ButtonRotate behavior.

 

Now, of course, we need to modify the registration receiver behavior. Here we have to add a group number also to identify which group the behavior is managing:

-- ButtonRotate Multiple Group

property spriteNum
property pchButton -- Channel of the button sprite
property plchRotateRight -- List of channels to rotate right
property plchRotateLeft -- List of channels to rotate left
property pnTimesHit
property pGroupNumber

-- To identify which group and part we are
on
getPropertyDescriptionList
  lDescription = [:]

  addProp(lDescription, #pGroupNumber, [ \
            #comment: "Which group is this?", \
            #format: #integer, \
            #default: 1])
  return lDescription
end

on beginSprite me
  plchRotateRight = []
  plchRotateLeft = []
  pnTimesHit = 0
end

on mButtonRotate_GetCh me, groupNumber
  if groupNumber <> pGroupNumber then
    return
  end
if
  return
spriteNum
end

-- Handle registration messages
-- Save channel assignments into property variables
on mButtonRotate_Register me, groupNumber, symType, ch
  if groupNumber <> pGroupNumber then
    return
  end
if

  case symType of
    #button:
      pchButton = ch
      
    #rotateRight:
      append(plchRotateRight, ch)
      
    #rotateLeft:
      append(plchRotateLeft, ch)
      
    otherwise:
      alert("Unknown type of registration:"
&& symType && "from sprite" && string(ch))
      
  end
case
end

-- The button has been hit.
-- Tell each graphic to rotate in the appropriate direction
on mHitButton me
  repeat
with ch in plchRotateRight
    sendSprite(ch, #mRotate, #right)
  end
repeat
  repeat
with ch in plchRotateLeft
    sendSprite(ch, #mRotate, #left)
  end
repeat
  pnTimesHit = pnTimesHit + 1
  if pnTimesHit = 8
then
    sendSprite(pchButton, #mButton_SetAvailability, FALSE)
  end
if
end

Notice that in both the mButtonRotate_GetCh and mButtonRotate_Register methods, there is now a check to ensure that whenever a message comes in, that the message is intended for this instance of this behavior. That is, the group number of the sender and the group number of this behavior (the receiver) must match. Very importantly, in mButtonRotate_GetCh a value is only returned when there is a match. If the group numbers do not match, the methods simply return without passing back any value so as not to overwrite the correct value from an earlier correct response. For example, when the second button executes its beginSprite handler it issues:

pchButtonRotate = sendAllSprites(#mButtonRotate_GetCh, pGroupNumber)

and the value passed for pGroupNumber is 2. This message is sent to two different instances of the ButtonRotate behavior, one with the group number of 1 and the other with the group number of 2. When it is received by the first instance, the group numbers do not match and mButtonRotate_GetCh just returns. When the second instance is called, the group numbers match and the mButtonRotate_GetCh returns the proper sprite number.


5) Group using a list

The final approach involves using a list to link sprites together without the need to designate a manager sprite. For example, you could use this technique if you wanted to build a set of radio buttons or a tabbed palette. This approach was first discussed by John Dowdell of Macromedia and later expanded upon by James Newton. Before explaining how it works, we must first explain a unique element of how lists work.

When you call a handler or a method and pass a parameter, Director usually makes a copy of the thing you are passing. For example, if you execute the following two lines:

x = 2
SomeHandler(x) -- call some handler

and SomeHandler starts like this:

on SomeHandler myLocalVariable

When execution passes to SomeHandler, the value of x, (which is 2), will be copied into the local variable myLocalVariable. This is commonly referred to as passing a parameter by value. You can do whatever you want to the local variable myLocalVariable, and it will not affect the variable x in the caller. You could execute the statement:

myLocalVariable = myLocalVariable + 1

and the value of x would still be 2.

However, if you were to pass a list variable as a parameter, rather than make a copy of the list, Director passes the memory address of the list. This is commonly referred to as passing by reference. If you executed the following code:

lData = [1, 2, 3, 4, 5]
SomeOtherhandler(lData)

and SomeOtherHandler started like this:

on SomeOtherHandler myLocalListVariable

When execution passes to SomeOtherHandler, rather than making a copy of the list lData, myLocalListVariable is set to same memory address as the original lData list. Therefore, as a side affect, any changes you make to the list WILL also be made to the original lData list in the caller. For example, if you were to execute the following line inside SomeOtherHandler:

append(myLocalListVariable, 6)

when control is passed back to the caller, you would find the contents of lData had become [1, 2, 3, 4, 5, 6]. If you do not want this to happen, Lingo gives you the explicit "duplicate" command that allows you to duplicate the list. If you first make a duplicate of a list before doing any manipulation, then you are making changes on a copy of the list, not the original list.

In fact, even when you just assign a variable whose value is a list to another variable, Director sets the new variable to the memory address of the other variable. For instance if you ran the following code:

lData = [1, 2, 3, 4, 5]
lSomeOtherListVariable = lData

then the variables lData and lSomeOtherListVariable contain the same address and therefore point to the same list. Therefore changing the contents of one list appears to change the other. If we then executed the following line:

append(lSomeOtherListVariable, 6)

we would see that both lData and lSomeOtherListVariable would be set to [1, 2, 3, 4, 5, 6]. We will take advantage of the fact that lists are passed and are assigned by reference in the following description.

Imagine that we have a set of radio buttons - the number of them does not matter. With a set of radio buttons, only one button can be selected at a time. Clicking on an unselected button makes that button appear selected and causes the previously selected button to change its state to unselected. Rather than using a manager sprite we would like to build a single behavior that could be dropped onto all the radio buttons. When the user clicks on a radio button, this one behavior would be responsible for showing the selected state of the current button and for sending messages to all other radio buttons telling them to show their unselected state.

To accomplish this, we need to figure out a way that each of these radio buttons can know the channel numbers of all other radio buttons. If we could write such code, then we could easily send an appropriate messages to tell the sprites to show their proper states.

We will worry about the graphics in a little bit. For now, we'll concentrate on the intersprite communication. Consider the following minimal behavior. At first it will seem very strange, but as we walk through it you will see that these two methods with just two lines each will do exactly what we are looking for. This behavior will allow each sprite to build up a list of all channels that have this same behavior attached.

-- RadioButtons 1

property spriteNum
property plchGroupMembers -- list of channels of group members

-- Broadcast message looking for other radio buttons
on
beginSprite me
  lchFromBeginSprite = []
  sendAllSprites(#mRadioButtons_FindGroupMembers,
lchFromBeginSprite)
end
beginSprite

-- Add ourselves to the list
on mRadioButtons_FindGroupMembers me, lchCollection
  append(lchCollection, spriteNum)
  plchGroupMembers = lchCollection
end mRadioButtons_FindGroupMembers


Now let's say that we dropped this behavior on the three radio buttons above. For the purpose of working through an example, let's assume that the three radio button graphics are in channels 5, 6, and 7 (although they could be placed in any channels). Remember that behaviors are instantiated and the beginSprite methods are called in numerical order from lowest to highest. Let's walk through the code and see what happens.

First, the behavior attached to sprite 5 is instantiated and the beginSprite method is called. It initializes its list of channels to the empty list. Then it does a sendAllSprites broadcasting an mRadioButtons_FindGroupMembers message and passes the empty list as a parameter. Because this is the only instance of this behavior so far, the message is only received by the current instance of this behavior in its own mRadioButtons_FindGroupMembers method - the one attached to sprite 5. mRadioButtons_FindGroupMembers for sprite 5 executes, and when it is finished, the property variable plchGroupMembers contains the value [5].

The only thing to notice is the last line of mRadioButton_FindGroupMember. This line sets plchGroupMembers to the value of the local variable lchCollection, which came from the variable lchFromBeginSprite in the caller. After executing this line, plchGroupMembers and lchFromBeginSprite will point to the same list in memory. (Additionally, the local variable lchCollection will also point to the same list, but this is not important for our discussion because all local variables are thrown away when we exit a method). Here is a memory map of what memory might look like immediately after executing the last line of code in mRadioBuuttons_FindGroupMembers (all addresses are made up):

Next the same behavior attached to sprite 6 is instantiated and its beginSprite method is called. Just as with sprite 5, the beginSprite method of sprite 6 broadcasts an mRadioButtons_FindGroupMembers message and passes an empty list as a parameter. This version of the message will be received by the behaviors attached to sprite 5 and sprite 6. Let's walk through each of these. The message is received in sprite 5. Just as before, when the method finishes, the value of plchGroupMembers is [5]. Because the plchGroupMembers and lchFromBeginSprite (from the sendAllSprites) both point to the same list, the value of lchFromBeginSprite is also [5].

But now the magic happens. After being sent to sprite 5, the mRadioButtons_FindGroupMembers message (from the sendAllSprites in the beginSprite of sprite 6) is then sent to sprite 6. This time the parameter being passed is no longer the empty list, it is now [5]. When the mRadioButtons_FindGroupMembers runs for sprite 6, the first thing it does is to add its sprite number to the list. This list now becomes [5, 6]. Then the method sets plchGroupMembers to the list. Again, we have two variables pointing to the same list - so changing one changes the other. At the end of execution of sprite 6's mRadioButtons_FindGroupmembers, memory would look like this. Notice that sprite 5's and sprite 6's version of plchGroupMembers point to the same list.


Finally, Director creates another instance of the same behavior for sprite 7, and calls its beginSprite method. The same series of steps happens and this time the message broadcast by sendAllSprites is received by sprites 5, 6, and 7. At the end of the sendAllSprites, sprites 5, 6, and 7's version of plchGroupMembers are set to [5, 6, 7]. A memory map now might look like this:

As the sendAllSprites message is sent from each successive sprite, it overwrites the values that were set by the previous sendAllSprites. In fact, only the final sendAllSprites (that is from the highest numbered channel) sets the proper values. (However, because our goal was to use a single behavior and attach it to all sprites, the sendAllSprites executing from all lower numbered channels don't do any harm.) Finally, when the beginSprite method of sprite 7 exits, the local variable lchFromBeginSprite is thrown away and no longer points to the common list.

Looking back at how this works, we can see the local variable lchFromBeginSprite, whose value was initialized to an empty list, was used as a "place holder". It is a list variable (that points to a memory address) that we can use to accumulate sprite numbers as the sendAllSprites call works its way through all previous channels. As a further simplification, we don't even need to use a local variable for this purpose - we could just use an empty list. The following code will work just as well, and even eliminates one line of code:

-- RadioButtons 2

property spriteNum
property plchGroupMembers -- list of channels of group members

-- Broadcast message looking for other radio buttons
on
beginSprite me
  sendAllSprites(#mRadioButtons_FindGroupMembers, [])
end
beginSprite

-- Add ourselves to the list
on mRadioButtons_FindGroupMembers me, lchCollection
  append(lchCollection, spriteNum)
  plchGroupMembers = lchCollection
end mRadioButtons_FindGroupMembers

 

5.1 Adding the graphics

Now that we have the ability to create a list of group members, we can add the code to show the appropriate graphical states of the radio buttons. As with our earlier button code, we need a naming convention. Without dealing with the unavailable state for now, a radio button has two states; unselected and selected. As a simple naming convention and to save a lot of typing, we'll create a naming convention that there is a base name followed by a second word of "up" (meaning unselected) or "down" (meaning selected). For example, the two states of button whose base name is "Large" would be named "Large up" and "Large down". Given this naming convention, here is the code that can implement the graphic switching when you click on a radio button:

 

-- RadioButtons 2

property spriteNum
property plchGroupMembers -- list of channels of group members
property pmUnselected
property pmSelected
property pfTracking
property pBaseName
property psymState -- #unselected or #selected

-- Broadcast message looking for other radio buttons
on
beginSprite me
  theName = sprite(spriteNum).member.name
  pBaseName = theName.word[1]
  pmUnselected = member(pBaseName & " up") -- member of the "unselected" graphic
  pmSelected = member(pBaseName & " down") -- member of the "selected" graphic
  if
sprite(spriteNum).member = pmUnselected then
    psymState = #unselected
  else
    psymState = #selected
  end
if
  sendAllSprites(#mRadioButtons_FindGroupMembers, [])
end
beginSprite

-- Add ourselves to the list
on mRadioButtons_FindGroupMembers me, lchCollection
  append(lchCollection, spriteNum)
  plchGroupMembers = lchCollection
end mRadioButtons_FindGroupMembers

on mouseDown me
  pfTracking = TRUE
end

on mouseUp me
  if not(pfTracking) then
    return
-- didn't click down on this sprite, ignore
  end
if

  -- Deselect all radio buttons in the group
  repeat
with ch in plchGroupMembers
    sendSprite(ch, #mRadioButtons_SetState, #unselected)
  end
repeat
  me.mRadioButtons_SetState(#selected) -- Select this one
  sendSprite(spriteNum, #mHit, pBaseName) -- Notify any sibling behavior(s)
end

on mRadioButtons_SetState me, symState
  psymState = symState
  if psymState = #unselected then
    sprite(spriteNum).member = pmUnselected
  else
    sprite(spriteNum).member = pmSelected
  end
if
end

 

 

Much of this code is nearly identical to our button behavior, so I won't go into detail documenting it. There are three things worth pointing out.

First, as in the button behavior, a button is successfully hit when you do a mouseDown and mouseUp on the same button.

Second, in the mouseUp method, you can see how we have used the plchGroupMembers list to send a message to all radio buttons telling them to show their unselected state. Then we show the selected state of the current sprite.

Lastly, when the button is successfully hit, we send out an mHit message to any sibling behaviors telling them that the button was hit. When we do this, we also pass the base name of the button for use by any action behavior that we may attach to the same sprite.

 

5.2 Multiple sets of radio buttons

Finally, as with the earlier example of the registration technique, we need to be able to handle the case of multiple sets of radio buttons. Imagine if we had a screen with two sets of radio buttons, something like this:

To make this work, we must add some identifier to each radio button to say which group it is in. Again similar to the way we handled this in the registration examples above, we will add an identifier that is editable using a getPropertyDescriptionList handler. This identifier will allow us to have any number of sets of radio buttons.

 

-- RadioButtons 3

property spriteNum
property plchGroupMembers -- list of channels of group members
property pmUnselected
property pmSelected
property pfTracking
property pBaseName
property psymState -- #unselected or #selected
property pGroupName

on getPropertyDescriptionList me
  lDescription = [:]

  -- The user must supply a group name for this button
  addprop(lDescription, #pGroupName ,\
           [ #default: "radiobuttons", \
             #format:#string, \
             #comment:"Group Name"])

  return lDescription
end
getPropertyDescriptionList

-- Broadcast message looking for other radio buttons
on
beginSprite me
  theName = sprite(spriteNum).member.name
  pBaseName = theName.word[1]
  pmUnselected = member(pBaseName & " up") -- member of the "unselected" graphic
  pmSelected = member(pBaseName & " down") -- member of the "selected" graphic
  if
sprite(spriteNum).member = pmUnselected then
    psymState = #unselected
  else
    psymState = #selected
  end
if
  sendAllSprites(#mRadioButtons_FindGroupMembers, pGroupName, [])
end
beginSprite

-- Add ourselves to the list
on mRadioButtons_FindGroupMembers me, theGroupName, lchCollection
  if theGroupName <> pGroupName then
    return
-- This message is not for us. It's for a different group
  end
if

  append(lchCollection, spriteNum)
  plchGroupMembers = lchCollection
end mRadioButtons_FindGroupMembers

on mouseDown me
  pfTracking = TRUE
end

on mouseUp me
  if not(pfTracking) then
    return
-- didn't click down on this sprite, ignore
  end
if

  -- Deselect all radio buttons in the group
  repeat
with ch in plchGroupMembers
    sendSprite(ch, #mRadioButtons_SetState, #unselected)
  end
repeat
  me.mRadioButtons_SetState(#selected) -- Select this one
  sendSprite(spriteNum, #mHit, pBaseName) -- Notify any sibling behavior(s)
end

on mRadioButtons_SetState me, symState
  psymState = symState
  if psymState = #unselected then
    sprite(spriteNum).member = pmUnselected
  else
    sprite(spriteNum).member = pmSelected
  end
if
end

 

When you drop this radio button behavior onto a sprite, you will get a Parameters dialog box that asks you to give an identifying name for the group it is in. We have used a group name here instead of a number. Either will work fine, it doesn't matter at all, as long as you enter the same name for all radio buttons in the same group.

The only changes to the code are: the addition of the gpdl handler, the beginSprite method now includes the name of the group when it does a sendAllSprites, and the mRadioButtons_FindGroupMembers now checks to see if the group name passed in is the same as the group name assigned to the current sprite.

 

Conclusion

This chapter has discussed many different ways in whicha sprite can find out the sprite numbers of other sprites with which it needs to communicate. The method you choose is up to your programming style and the situation. The only one I would strongly advise against would be the use of hard-coded constants.

 


Previous Chapter

Table of Contents

Next Chapter