LOOPE - Lingo Object Oriented Programming Environment by Irv Kalb

Previous Chapter

Table of Contents

Next chapter

 

Section 2 - Behaviors

Chapter 8 - Introduction to Behaviors

 

In the introduction to this book, I said that behavior scripts (also known as sprite scripts or score scripts) share many concepts with parent scripts. Similar to parent scripts, behavior scripts (hereafter referred to simply as behaviors) by themselves do not create any objects. Rather, an object is created only when you instantiate a behavior. However, there are two main differences between behaviors and parent scripts. First, parent scripts typically work independent of the score, but behaviors are used in conjunction with a sprite (or sprites) in the score. Second, behaviors are different from parent scripts in the way that they are created and destroyed. It turns out that these two differences are bundled together in the basic way that behaviors work.

As a reminder, we defined an object as,"data, plus code that acts on that data, over time". Behaviors have the identical concept (data plus code that acts on that data), but the time element is handled differently. In the first section of the book we demonstrated that, with parent scripts, the programmer creates an object from a parent script whenever one is needed - and can destroy the object whenever it is no longer needed. But the lifespan of behaviors is different.

Even before we get into writing a behavior, you should already know that to use a behavior, you drag a behavior that lives in a cast and drop it onto a sprite in the score. By doing this, you are defining the time frame over which the behavior should live. The lifespan of a behavior starts whenever the play head first enters the sprite span, and ends when the play head leaves the sprite span.

 

When the play head reaches the first frame of a sprite span with a behavior attached (or wherever it first enters a sprite span), Director creates an instance of that behavior. To create an object from a parent script, you must explicitly call the "new" method and pass the name of the parent script. But with behaviors, Director does this work for you. By attaching the behavior to the sprite, Director itself knows when to create an instance of the behavior. Just like parent scripts, Director allocates enough memory to store all the property variables of the behavior. However, (and these are very good things), you do not have to have a "new" method, and you do not need to deal with an object reference for a behavior. When the play head exits the sprite span where a behavior is attached, Director disposes of the object for you. It de-allocates the memory that was allocated to store the property variables, and it clears the object reference to the allocated memory. Because the time element of a behavior is clearly defined, Director knows when to instantiate and when to dispose of the object and can do it for you automatically.

Because behaviors and parent scripts are both used to instantiate objects, I want to be consistent about terminology. I will refer to handlers inside both parent scripts and behaviors as "methods".

Unlike parent scripts, behaviors receive a standard set of messages from Director. We'll just talk about two to start with - beginSprite and endSprite. When the play head enters a sprite span with a behavior attached, right after Director instantiates an object from a behavior it sends the object a beginSprite message, once. If the behavior wants to execute any code at startup, it must have an "on beginSprite" method to receive this message. This is the proper place to put any initialization code that you may need to execute within a behavior. When the play head is about to leave the sprite span, just before Director disposes of the object, it sends out an endSprite message. If the behavior needs to execute any clean up code before leaving, that code should be put in an "on endSprite" method.

There are a number of other messages that Director sends out to behaviors based on time and user actions. We will get into all of these system messages in a little bit. But for now a simple example behavior will help clarify things. Let's say that you want to write a simple rollover behavior. That is, you want to write a behavior that would work like this: when the mouse is not over the sprite, you want to show the initial graphic that is in the score, whenever the mouse is over the sprite, you want to show a different graphic. In this example, you need to know that Director also sends out a mouseEnter and mouseLeave message whenever the mouse moves over and then leaves the sprite. Here is the code of such a behavior:

-- Rollover behavior

property spriteNum -- special property, is automatically given the number
-- of the channel to which this behavior is attached
property pmStart -- starting member (one found in the score)
property pmRoll -- roll member (one past the starting member in the cast)

-- This method executes when the behavior first begins
on
beginSprite me
  -- get the member in the sprite
  pmStart = sprite(spriteNum).member
  -- get the member of the next member in the cast
  pmRoll = member(member(pmStart).number + 1)
end

-- This method executes when the cursor enters the sprite
on
mouseEnter me
  -- Switch the sprite's member to the rollover cast member
  sprite(spriteNum).member = pmRoll
end

-- This method executes when the cursor leaves the sprite
on
mouseLeave me
  -- Switch the member back to the starting member
  sprite(spriteNum).member = pmStart
end

 

In this behavior we have defined three property variables: spriteNum, pmStart, pmRoll. The first, spriteNum, is a special reserved keyword. When you explicitly declare spriteNum using a "property spriteNum" statement, Director automatically gives spriteNum the value of the channel number to which the behavior is attached. If you drop the behavior onto a sprite in channel 1, inside the behavior spriteNum will get a value of 1, if you drop it onto a sprite in channel 123, spriteNum will be set to 123.

The other two properties, pmStart and pmRoll, will be used to store the member references of the starting and roll versions of the member in the score for that sprite. For this behavior, we will make an important assumption: whenever we want to use a member that has a roll version, the roll version of the member must be the next member in the cast. For example, if we have a graphic in member 5, then we would put a roll version of that same graphic in member 6. Assumptions like this about layout within the cast (and other naming conventions that we will get into later) are generally a good thing because they allow you quick way to compute references to cast members. Assumptions like this should be documented in comments within the behavior.

Notice that in this behavior, there is an on beginSprite method. The on beginSprite method is called only once when the behavior is first instantiated. Because the variable spriteNum is automatically given the value of the current channel, we can use it to find what member is in that channel by specifying: sprite(spriteNum).member. We assign that value to the property variable pmStart to save it. Knowing the member reference for the starting member, we can then use a little math to generate the member reference for the roll member (the next member in the cast). Now that we have calculated and stored values into these two property variables, we can then use these variables in other methods in the same behavior. Whenever a computed value like this will be used multiple methods of a behavior, it is a good idea to calculate the value once in the "on beginSprite" method and then assign it to a property value - rather than re-calculating every time you need it.

Whenever the mouse enters a sprite on the screen, Director sends out a mouseEnter message to the behavior(s) attached to that sprite. Similarly, whenever the mouse leaves the sprite, Director sends out a mouseLeave message. If a behavior wants to do something when the mouse enters or leaves a sprite, it needs to have an "on mouseEnter" and/or "on mouseLeave" method to receive and react to the message(s). That's precisely what we have in this rollover behavior. Whenever the mouse enters this sprite, the mouseEnter method is called. In response, the "on mouseEnter" method changes the sprite's member to the roll member. Whenever the mouse leaves the sprite, the "on mouseLeave" method is called. In response, the behavior changes the member back to the starting member.

 

Using the same behavior on multiple sprites

Now, let's look at what happens if we drop this same behavior on more than one sprite. Lets start with a sample cast that has graphics in members 11 through 16. In member 11 is some graphic, and member 12 contains the version of the graphic you want to show when the mouse rolls over it, member 13 contains some other graphic and member 14 contains its roll version, and finally, member 15 contains some graphic and member 16 contains its roll version.

Now lets put the starting "normal" graphics into channels in the score. Let's put member 11 into channel 3, member 13 into channel 6, and member 15 into channel 9. Finally, we drop a copy of our rollover behavior onto each sprite. (We will add a typical "go to the frame" script into the frame script channel - and to be complete, we would allow some way to continue past that frame.)

When the play head reaches the frame where these sprites are defined, Director will instantiate 3 objects from the same behavior. In exactly the same way that instantiation works with parent scripts, each of the three instances of the Rollover behavior is allocated its own block of memory for a copy of all the properties defined in the behavior. The instantiation is done in order from the lowest numbered channel to the highest. When Director instantiates the rollover behavior for the sprite in channel 3, it allocates memory for three property variables, and sets the value of the spriteNum property to 3. Then it sends a beginSprite message to the behavior. The on beginSprite method runs, and it sets pmStart and pmRoll to be member references for member 11 and member 12. The process is repeated for sprite 6, and then again for sprite 9. If we were able to look at memory just after all this occurred, we would see that three blocks of memory had been allocated for the three behaviors, and the contents would look like this:

The program runs and sits in a "go to the frame loop". When the user moves the mouse over one of our three sprites, Director issues a mouseEnter message to the affected sprite. Upon receiving the message, the on mouseEnter method changes the member of that sprite. If you roll over sprite 3, the member will change from member 11 to member 12. When you roll off, the behavior receives a mouseLeave message and it changes the member back to member 11.

 

Is a sprite an object?

Although it is not documented anywhere, if you think about it carefully, inside Director itself, each sprite must be an object. Each sprite has a set of properties: member, locV, locH, rotation, forecolor, etc. Even though the property names are the same for each sprite, the values are typically different for each sprite. If you were to drag the same cast member into two different channels and place the resulting sprites in different locations on the screen, you would see that that the two sprites have the same member value, but their values for locH and locV would be different.

Sprites have many properties. One property of each sprite is called "the scriptInstanceList". As its name implies, this property of each sprite will contain a list of script instances. When you put a cast member into a channel to create a sprite and the play head enters the sprite span, the scriptInstanceList starts off empty, or in Lingo terms: [ ]. For each behavior that is attached to the sprite, when Director instantiates an object from the behavior, it puts the resulting object reference into the scriptInstanceList of that sprite. Remember that a object reference is basically a pointer to a block of memory that contains the properties. In our Rollover behavior example, when the program is running, we can use the message window to find the scriptInstanceList of sprite 3:

put sprite(3).scriptInstanceList
-- [<offspring "Rollover" 1 27737ce0>]

Similarly, the scriptInstanceList of sprite(6) can also be found from the message window:

put sprite(6).scriptInstanceList
-- [<offspring "Rollover" 1 277b583c>]

Just as with objects created from parent scripts, these object references look almost identical. The object references tell us that they were both instantiated from the Rollover script and that there is a single variable pointing to the object. The only difference is the memory address of where the data lives. If you had attached more than one behavior to channel 3 or 6, or 9, then the scriptInstanceList of those sprites would contain one element, an object reference, for each behavior attached. This is how Director keeps track of where the data for each behavior is located. So, when an event happens, such as when the mouse enters the sprite in channel 3, Director can look down its own list of sprite objects, pull out its entry for sprite 3, get the scriptInstanceList of sprite 3, and send the message to the proper script (in this case the Rollover script), but also know the address of where the memory has been allocated for this instance of the Rollover behavior. Director must do this to send the message to the correct instance of the Rollover behavior. If not, you could roll over sprite 3, and sprite 6's graphic might change.

We will discuss the scriptInstanceList of behaviors in a later chapter. It is an important concept because it allows you to dynamically add behaviors to a sprite at run time. But let's not get ahead of ourselves.

 

It's "me" again

Just to ensure that we are saying here is correct, we can add a debugging line into the beginSprite method of our RollOver behavior. If we add a debugging line as follows:

on beginSprite me
  put
"In sprite" && spriteNum && "me is:" && string(me) -- Debugging
  pmStart = sprite(spriteNum).member
  pmRoll = member(member(pmStart).number + 1)
end

In the message window we get the following output:

-- "In sprite 3 me is: <offspring "Rollover" 5 27737ce0>"
-- "In sprite 6 me is: <offspring "Rollover" 5 277b583c>"
-- "In sprite 9 me is: <offspring "Rollover" 5 277b5e2c>"

What we can see from this is that the value of "me" that is passed into all methods of a behavior works exactly the same way that "me" works in objects made from parent scripts. The memory address of the value of me for sprite 3 is exactly the same as what we saw when we were looking at the value in the scriptInstanceList. Whether an object is created from a parent script or a behavior, "me" is always an object reference to where the property variables for the current object are stored. When dealing with objects created from parent scripts, we need to store and manage object references. However, when dealing with objects created from behaviors, Director stores the object references for us into each sprite's version of the scriptInstanceList.

Sometimes, however, you may see (or write) a method that does not have the keyword "me" - and it will work perfectly well. If a method has no parameters, and it does not call any other methods inside the same behavior, you can leave off the word me. If you have one or more parameters, then you must put "me" as the first parameter in order for the other parameters to match up correctly. Further, if you call any other methods within a behavior (e.g., me.mDoSomeThing()), then you must have "me" as a parameter or the Lingo parser will give you a script error because the value of "me" is undefined. I would advise that when you are defining a method, that you always put the keyword "me" after the name of every method whether you need it or not. It is a simple thing to do, and will help eliminate errors as your methods evolve.

 

The frame script

The frame channel is the only channel where the rules are slightly different. A key difference is that there can only be one behavior as the frame script. Once you drag a behavior and drop it as a frame script, Director will not allow you to add another one. Further, most of the properties of regular sprites don't really apply or work in a frame script because there is no on-screen sprite to show. Numerically, the frame script is considered to be sprite 0. (Historically, the frame script started out as sprite -5. Some older commands will work if you use -5, but all will work if you use 0. For example sprite(0).scriptInstanceList works correctly, but sprite(-5).scriptInstanceList returns a zero.)

The frame script does get the same system messages as all other sprites. For example, we glossed over the standard "go to the frame" script in the example above. It introduces another standard Director message: exitFrame. Whenever Director is leaving a frame, it sends an exitFrame message to all sprites - including the frame script. Here is a typical go to the frame behavior to be used as a frame script:

-- Go to the frame

on exitFrame me
  go
to the frame
end


Now, let's say that you wanted to wait on a frame for a certain amount of time and then just move on. For example, if you wanted to wait on a frame for two seconds to show a graphic or credits screen, and then just let the play head keep going. Let's build a behavior to do that.

-- Wait for 2 seconds

on exitFrame me
  -- Set the variable msEnd to 2 seconds in the future
  msEnd = the
milliseconds + (2 * 1000)
  repeat
while the milliseconds < msEnd
    nothing
  end
repeat
end

This script accomplishes our goal. But it also has a nasty side affect. While the repeat loop in this script is executing, no other script would have had any time to execute. Director "optimizes" repeat loops, and does not allow other scripts to get any time while executing code inside one. All user interactivity will be locked out for the two seconds that the program is in this repeat loop. We need a different approach.

A different way to code this would be to set some variable to the msEndTicks, as we did in the first script, but check every once in a while to see if the time has expired. This would allow other scripts to get time to execute. We can code this by taking advantage of the fact that Director sends out a beginSprite message to every behavior, including the frame script.

-- Wait for 2 seconds

property pmsEnd

on beginSprite me
  -- Set the property variable pmsEnd to 2 seconds in the future
  pmsEnd = the
milliseconds + (2 * 2000)
end

on exitFrame me
  -- Is the time up yet?
  if the ticks > pmsEnd then
    go
to the frame + 1
  else
    go
to the frame
  end
if
end

In the beginSprite method, we initialize a property to be the value of the ticks at two seconds into the future. Then, knowing that the exitFrame method will be called every time the play head leaves the frame, we constantly check to see if our time limit has elapsed. If the time has not expired then we explicitly issue a go to the frame to stay on the same frame. If our frame rate were set to say, 10 frames per second, we would go through this check 20 times, each time staying on the same frame. When the time does expire, we execute a go to the frame + 1 and the play head will move along.

The splitting up of repeat loops into multiple pieces or checks is an important concept which we will delve into later. But it is important to start thinking about it now. This is sometimes referred to as "unwrapping" a repeat loop. By using this style of coding, you can simulate "multithreading" - that is, the ability to be doing multiple things at the same time, or executing multiple threads of code. A behavior attached to a sprite does incremental work during each exitFrame. Think of a game where you have many sprites moving on the screen. If each sprite has a behavior attached that moves the sprite a few pixels at each exitFrame, it gives the illusion that many things are happening at once.



Previous Chapter

Table of Contents

Next chapter