Inter-sprite Communications
June 15, 1999
by James Newton
Hardcoding is the Dark Side of Lingo. It is fast and powerful but inevitably it takes control over the person who wields it. If you go the way of Hardcoding, your enslaved sprites will come back to haunt you.
To become a Jedi Knight of Lingo you must learn to move in harmony with your sprites, to communicate with them from a distance, wherever they are. Sprites everywhere were born to be free, and as a Jedi Knight you must respect their freedom.
Behaviors are a simple means of freeing your sprites from hard code. Behaviors are particularly easy to use when they just deal with the sprite they are attached to. But what if a click on sprite A should have an effect on sprite B? How do you get behaviors to talk to each other?
In this article, I will describe the inter-sprite communication techniques I used to create a simple Space Invaders game. When you click on the mouse, the cannon sprite at the bottom of the stage tells a bullet sprite to fire. It also tells the ammunition dump sprite to show one less round of ammunition. If the bullet sprite encounters an Invader spacecraft , it tells the spacecraft to explode. When the last bullet is used up, it sends a message to a Game Over field to display either "You win" or "You lose".
A sample movie is available for download in Mac or PC format. Note: this movie is in Director 7 format.
The only user interaction is the mouse click. All the consequences of the mouse click are determined by messages sent between the different sprites. I have deliberately used a series of different techniques, for demonstration purposes. (The fact that I have used a particular technique for a given purpose does not necessarily mean that I consider it the best one to use at that point).
We shall look at the following techniques:
- sendAllSprites #CustomMessage, parameters
- call #CustomMessage, behaviorReference, parameters
- mutually exchanging behavior references
- call #CustomMessage, [<list of behavior references>], parameters
- creating a list which is shared by a number of behaviors
- call #CustomMessage, behaviorReferences, [<list of parameters>]
- interrupting a call to a list of behaviors
- forwarding messages and data from one behavior to another
- cleaning up after the party
I have arranged the techniques in order of complexity, not in order of execution. We won't be looking at how the Space Invaders game itself works, but rather at the way the sprites communicate. If you want to study how the Space Invaders game is designed, then I recommend you visit Brennan Young's site, where he has created an excellent online tutorial.
If you are not familiar with the syntax of the commands sendAllSprites, sendSprite and call then it may help to look them up in your Lingo Dictionary before going any further.
Hardcoding, firmcoding and sendAllSprites
Before we look at good ways to make sprites communicate, let's look at some bad ways.
The simplest tactic is hardcoding. It is also the worst:
Bad Tactic #1
-- Behavior on sprite 1 on doSomething me beep end -- Behavior on sprite 2 on mouseUp me doSomething sprite 1 end
This will work fine... until you decide that you really need a background image behind all your other sprites. How easy it is to open the Score window and drag sprite 1 into channel 3 to make room for the background image. And how disastrous for your hardcode: "Script Error: handler not found" and other unpleasantnesses.
Actually you could avoid the Script Error alert, by using a more orthodox technique:
BadTactic #2
-- Behavior on sprite 2 on mouseUp me sendSprite 1, #doSomething end
If sprite 1 doesn't have a handler to deal with the #doSomething call, Director is forgiving and doesn't alert you. The problem with this is that you get no feedback as to where your hardcoding has failed you. Your movie simply stops working.
What about firmcoding the sprite-to-call into the behavior, through the Behavior Parameters dialog? You could write a behavior like this:
Tactic #3
-- Behavior on sprite 2 property pSpriteToCall on getPropertyDescriptionList return [ #pSpriteToCall: [ #comment: "Sprite to ¬ call on mouseUp:", #format: #integer, #default: 1 ]] end on mouseUp me sendSprite pSpriteToCall, #doSomething end
This looks much more professional. At least it allows you to have different instances of the same behavior sending messages to different sprites. You don't need to write a different behavior for each sprite addressed. This is good news.
Unfortunately it doesn't solve the problem of moving sprites around the Score; it simply displaces it. Instead of opening up the behavior script to change your hardcoded value, you have to open up the Behavior Parameters dialog for sprite 2 to change the firmcoded value of pSpriteToCall.
An intravenous injection of the Lingo Dictionary can help you find a better solution:
Tactic #4
-- Behavior on sprite 2 on mouseUp me sendAllSprites #doSomething end
Now you can move your sprites around and they will continue to do their stuff. Director will execute any "on doSomething" handler it finds in any of the behaviors on any sprite. You can thus be sure that doSomething will be done. But you might get more than you bargained for. You might get a number of behaviors executing the call.
sendAllSprites is Lingo spam. It should be used in small doses. Imagine an extreme case where a behavior on every sprite sends out a sendAllSprites message on every frame. In D6, that means 120 messages per frame, each of which needs to query 120 sprites (including itself): 14400 messages delivered per frame. This could conceivably slow down the execution of your movie.
Using sendAllSprites with moderation
If you download either the Mac or PC version of the Director 7 invaders.dir movie, you will see that I only use sendAllSprites once per behavior. (To see the text as it appears below, disable Script Autocoloring using menu File > Preferences > Script ... before opening any scripts).
The simplest example of this is when the game is over (I told you I wasn't going to explain the game in order). In the Cannon Behavior Script you will find the following lines:
(Cannon behavior)
on Bullet_Spent ... if invadersLeft then if not myUnusedRounds then -- The user has run out of bullets -- (A) Standard sendAllsprites message sendAllSprites (#GameOver, #lose) end if else -- All the invaders have been destroyed sendAllSprites (#GameOver, #win) end if end Bullet_Spent
Spamming all the sprites with a #GameOver message is not going to slow the game down at all... since the game is already over. Indeed, I could have used an on GameOver handler for each sprite to freeze it on the screen. In fact, only one behavior actually handles the message in my version. This behavior is attached to a field sprite which will display either "You win" or "You lose":
(GameOver field behavior)
on GameOver me, winLose -- sent by Cannon Sprite when it receives a -- #Bullet_Spent event from the -- Bullet Sprite and there are no bullets left. -- (A) Standard sendAllsprites message case winLose of #win: myMember.text = myWinMessage #lose: myMember.text = myLoseMessage end case end GameOver
(Notice that I did not hardcode the text that is displayed. I used the "on getPropertyDescriptionList" handler to invite the author to type an appropriate message. This simplifies localisation).
How do I allow my behaviors to communicate freely if I use only one sendAllSprites per behavior? The answers is: I send out a sendAllSprites messenger on beginSprite to bring back a list of personal phone numbers. Subsequently, each behavior can use a direct line to communicate with other behaviors in private.
Here in an Initialize me handler (sent by beginSprite), the Ammunitions behavior sends out a pointer to itself. The pointer is none other than its "me" property:
(Ammunitions behavior)
on Initialize ... -- (B1) Simple exchange of data theRounds = sendAllSprites (#Ammunitions_Register, me) ... end Initialize
Director proposes the #Ammunitions_Register event to all behaviors on all sprites. Only the Cannon Behavior picks up on it:
(Cannon behavior)
property myAmmunitions -- list [pointer to Ammunition Instance] on Ammunitions_Register me, theAmmunitions -- sent by Ammunitions Behavior on -- beginSprite (Initialize) -- (B1) Simple exchange of data -- Here the calling instance leaves its object -- reference and receives -- an integer in return. No list is needed for -- this one-on-one exchange. myAmmunitions.append(theAmmunitions) return myRounds end Ammunitions_Register
What does the Cannon Behavior do? It adds the Ammunitions instance's me reference to a list called myAmmunitions (we'll come back to this), and it returns the number of rounds of ammunitions that the Ammunitions behavior is to display.
Warning: this is not entirely recommended.
What would happen if a second behavior subsequently answered the #Ammunitions_Register message? What value would be returned to the Ammunitions behavior? You can't tell.
Moral: while it is safe to send out information in a sendAllSprites message, it is not wise to use sendAllSprites as a function which will bring data back to the calling handler. In this particular case it works, because my movie only contains one sprite with an on Ammunitions_Register handler.
Tricks with lists
Why does the Cannon instance store the me reference to the Ammunitions instance in a list? (Be patient: we'll take the long way round to answer this).
Associated question: this exchange of information implies that the Cannon Behavior instance already exists when the Ammunitions Behavior instance sends it a message. Doesn't that mean that the Cannon Sprite MUST be in a lower-numbered channel? Isn't this some sort of hardcoding in disguise? What happens if the Cannon Sprite is in a higher-numbered channel?
The solution is to provide a fallback call. When the Cannon Behavior is instantiated it first creates an empty myAmmunitions list, then it tries to fill it. It does this by sending out a sendAllSprites message, with the list as a parameter:
(Cannon behavior)
property myAmmunitions -- list [pointer to Ammunition Instance] on Initialize ... -- (B2) Fallback tactics: -- If the Ammunitions Behavior is instantiated -- first, it will contain a -- handler which can treat the following message: myAmmunitions = [] sendAllSprites (#Cannon_FindAmmunitionsBehavior, ¬ myAmmunitions, myRounds) end Initialize
Suppose that the Ammunitions Behavior was instantiated first. It would then have a handler all ready to field the call: (Ammunitions behavior)
on Cannon_FindAmmunitionsBehavior me, listPointer, theRounds -- sent by Cannon Behavior on beginSprite (Initialize) -- (B2) Fallback tactics: -- This handler will only be executed if -- the Ammunitions Sprite is already -- initialized at the moment the -- #Cannon_FindAmmunitionsBehavior -- message is sent. If not, this behavior -- will send a fallback #Ammunitions_Register message -- which the existing Cannon instance -- will treat. Both messages have the same effect. listPointer.append (me) Cannon_DisplayAmmunition me, theRounds end Cannon_FindAmmunitionsBehavior
Notice that the on Cannon_FindAmmunitionsBehavior handler doesn't return anything to the calling behavior. It doesn't need to. The (empty) myAmmunitions list that was passed as the listPointer parameter exists now contains a value. When the Cannon_FindAmmunitionsBehavior handler ends, the local variable listPointer ceases to exist. But listPointer is only one pointer to the list. The Cannon Behavior instance holds another pointer (myAmmunitions) to the same list, so the list itself continues to exist... the Cannon Behavior instance "knows" the value of the Ammunitions Behavior's me instantly.
Perhaps it's time to explain what a pointer is.
Pointers
Variables come in two flavors. Plain vanilla and pointers. Plain vanilla variables contain data. Pointers contain an address where you can find data. Lists are illusionists. They look as if they contain data, but in fact the data is held elsewhere. Here's a little list illusion for you to astound your friends:
x = ["Now you see me"] y = x
-- Watch my hands carefully: I'm not going to touch x again.
z = y[1].word[1..2]&&"don't" y[1] = z -- Abracadabra... put x -- ["Now you don't"]
Any variable that points to a list can be used to change the value of the data at the memory address in question. All other variables that point to the same list will now read the modified data since they all point to the same memory address.
Object references are more forthright. They don't hide their address. Try this in a parent script. Call your parent script "Test":
on new me put me end -- In the Message Window new (script "Test") -- <offspring "Test" 2 3426724>
Your Test object will have a different final number, perhaps with letters mixed in. This is simply the number of a block of transistors in the RAM memory where the data concerning the object "Test" is stored. Lists are ex-directory: they don't let you see their address, but they work in a similar way.
What the Cannon_FindAmmunitionsBehavior handler does is to exploit both types of pointer. It places a pointer to the Ammunition Behavior instance (the me of the behavior's instance) at the address pointed at by the listPointer variable... and it magically appears in the myAmmunitions property at the same instant, since that is simply a pointer to the same address. The Cannon Behavior instance can now send messages directly to the Ammunitions Behavior instance. When the player fires a bullet the Cannon tells the Ammunitions Sprite to display one less round:
(Cannon behavior)
property myRounds -- number of bullets left to fire end ReactToPlayer ... myRounds = myRounds - 1 ... call (#Cannon_DisplayAmmunition, myAmmunitions, myRounds) ... end ReactToPlayer
Earlier I asked : "Why does the Cannon instance store the me reference to the Ammunitions instance in a list?" We now have most of the answer to the question: so that the Cannon_FindAmmunitionsBehavior handler can return a pointer to its me address in RAM.
Here is the rest of the answer. Try this in the Message Window:
call #CustomMessage, void call #CustomMessage, []
In the first case, Director hits you with a "Script Error: Object expected". In the second case nothing happens. The #CustomMessage was simply not sent anywhere. Using call on a list makes for more robust code. If you really need to know if the list you're calling is empty, you can write a couple of lines of code to test, and alert yourself when working in author mode.
Exchanging calling cards
Let's take this technique a step further. The Cannon behavior and the Bullet behaviors need to be able to communicate in both directions: the Cannon needs to tell a given bullet to fire itself, the Bullet needs to report back to the Cannon if it hits an invader (or if it misses and movies off into endless space).
When each Bullet behavior is instantiated, it will send out a #Bullet_Register event to all sprites, with an empty list as a parameter.
(Bullet behavior)
property myCannon -- [pointer to the Cannon Instance] on Initialize me -- sent by beginSprite ... -- (C1) Exchange of behavior references using -- a list as a parameter myCannon = [] sendAllSprites (#Bullet_Register, me, myCannon) end Initialize
If the Cannon behavior was instantiated first, it is ready to reply to this.
(Cannon behavior)
property myBulletsList -- list [pointers to Bullet Instances] on Bullet_Register me, theBullet, listPointer -- sent by Bullet Behavior on -- beginSprite (Initialize) -- (C1) Exchange of behavior references using -- a list as a parameter myBulletsList.append(theBullet) listPointer.append(me) end Bullet_Register
Not only does the Cannon behavior add the Bullet's me pointer to a myBulletsList, it also adds its own me to the empty list sent out by the Bullet behavior. The Bullet can now call the Cannon instance, and the Cannon can call the Bullet.
But what happens if the Bullet is instantiated before the Cannon? The Cannon behavior will never know that a #Bullet_Register event was sent. So it has to send out its own sendAllSprites message, looking for any Bullets that might already exist:
(Cannon behavior)
property myBulletsList -- list [pointers to Bullet Instances] on Initialize me -- sent by beginSprite ... -- (C2) Exchanging calling cards: -- Create an empty list ... myBulletsList = [] -- ...then pass it as a parameter in a sendAllSprites -- message to all other behaviors, along with a pointer -- to the current instance. sendAllSprites (#Cannon_FindBulletBehavior, myBulletsList, me) -- Other sprites append their reference to the list, -- which can now be called. ... end Initialize
If no Bullets exist yet, the list remains empty: any Bullets that are instantiated later will add themselves to the list using the #Bullet_Register call that we just looked at. Any Bullet that does already exist will treat the #Cannon_FindBulletBehavior event in a similar way: it will store the pointer to the Cannon instance's me in its myCannon list and add its own me to the Cannon behavior's myBulletsList.
(Bullet behavior)
property myCannon -- [pointer to the Cannon Instance] on Cannon_FindBulletBehavior me, listPointer, cannonInstance -- sent by Cannon Sprite on beginSprite (Initialize) -- (C2) Exchanging calling cards. The calling -- instance leaves its behavior reference, and the -- current instance appends its reference to a -- reply-paid list. listPointer.append (me) myCannon.append(cannonInstance) end Cannon_FindBulletBehavior
Since you don't know which order the behaviors are going to be instantiated in, each behavior must be able to call the other, and respond to a call. All four handlers are thus essential.
Note that the Cannon behavior's myBulletsList contains a pointer to each Bullet instance, while each Bullet's myCannon list contains a unique pointer to the unique Cannon. We will see shortly how the Cannon fires a single bullet by calling all the instances in its myBulletsList.
Communicating faster than the speed of light
I use a similar technique to determine if a given Bullet destroys any Invaders.
(Bullet behavior)
property ourInvadersList -- [pointers to the Invaders Instances] on HitOrRun me -- sent by exitFrame ... -- Test for intersection with Invader sprites -- (D) Create an empty list... hitResult = [] -- ...then pass it as a parameter in a #call to a list -- of other instances call (#Bullet_Impact, ourInvadersList, spriteNum, hitResult) -- see below -- The list may now contain data which the other instances added if hitResult.count() then deadInvader = getLast(hitResult) ourInvadersList.deleteOne(deadInvader) ... end if ... end HitOrRun
(Invaders behavior)
on Bullet_Impact me, bulletSprite, hitResult -- sent by Bullet Sprite on exitFrame (HitOrRun) -- (D) A list is passed as a parameter. if hitResult() then exit if sprite spriteNum intersects bulletSprite then -- Any data added to it is immediately available -- to the calling instance hitResult(me) ImAlive = false mySprite.locH = -999 ... end if end Bullet_Impact
For the moment I will pass over how ourInvadersList is created. Just take it as read that it contains a pointer to each of the remaining Invaders. Basically, on each exitFrame each speeding bullet asks each remaining Invader if the Invader's sprite intersects the Bullet sprite. If so, the Invader is destroyed. Note that each bullet can only destroy one Invader. When it receives the #Bullet_Impact event, each Invader first checks if the hitResult list already contains a pointer to another Invader. If so, it promptly exits the handler. All remaining Invaders will receive the #Bullet_Impact event, but they will not react to it. It is as if the call had been stopped. We shall see another method of effectively stopping a call in a little while.
Since both the destroyed Invader and the destroying Bullet share a pointer to the same hitResult list, both behaviors "know" at exactly the same moment that an impact has occurred. However, the Invader's Bullet_Impact handler must terminate before the Bullet's HitOrRun handler can continue. The Bullet behavior reacts to the impact slightly after the Invader has been destroyed.
Sharing a list
We saw earlier how each Bullet instance has a myCannon list which contains a pointer to the Cannon instance. This means that Cannon instance has many different pointers pointing to it. If one Bullet replaces the contents of its myCannon list with a different value, the other Bullets would not be affected.
The ourInvadersList has to work differently. If an Invader is destroyed by one Bullet, it is removed from that Bullet's ourInvadersList. All the other Bullets need to be informed of this: there is no point in trying to destroy a nonexistent Invader. The fastest way of doing this is to share ourInvadersList between all the Bullets. If all Bullets point at the same list, when one Bullet alters the list, all the other Bullets see the alteration immediately. There is no need for any inter-bullet communications.
So how do you get all the Bullets to point at one list? The answer is to get the first Bullet instance to create the list, and to give a pointer to it to all later Bullet instances. In order to do this, the other Bullet instances will send out a reply-paid list (tempList) for the first instance to wrap the ourInvadersList pointer in.
(Bullet behavior)
on Initialize me -- sent by beginSprite ... -- (E1) Sharing a list between instances -- of a behavior: First ask any existing -- instances for a pointer to the list. tempList = [] sendAllSprites (#Bullet_GetListPointer, tempList) -- (see below) if listP (tempList[1]) then -- If a pointer to the list was found, unwrap it... ourInvadersList = tempList[1] else -- If not, create the list from scratch... ourInvadersList = [] ... -- (to be continued) end if ... end Initialize on Bullet_GetListPointer me, tempList -- sent by other Bullet sprites on beginSprite (Initialize) -- (E1) Sharing a list between instances of the -- same behavior: Only the first instance with a pointer -- to the list need respond. if not tempList() then tempList.append (ourInvadersList) end if end Bullet_GetListPointer
Note that here, too, I stop the execution of the Bullet_GetListPointer handler as soon as the first Bullet instance has added a pointer to ourInvadersList to tempList. Existing later instances already point to the same list, but it is faster to test if tempList already contains a value than to get each subsequent instance to confirm the address of ourInvadersList.
Now we have to ensure that all the Invaders instances get added to the list. Once ourInvadersList exists, any new Invaders can simply add themselves to it. Any Bullets instances which are subsequently created will immediately inherit the list of existing Invader pointers.
(Invader behavior)
on Initialize me -- sent by beginSprite ... -- (E2) Simple sendAllSprites message used to -- transmit a behavior reference. sendAllSprites (#Invader_Register, me) ... end Initialize (Bullet behavior) on Invader_Register me, invaderRef -- sent by Invader Sprites on beginSprite (Initialize) -- (E2) Simple sendAllSprites message used to transmit -- a behavior reference. Since the reference is to be -- added to a shared list, it is important to avoid -- adding duplicates. if not ourInvadersList.getPos(invaderRef) then ourInvadersList.append(invaderRef) end if end Invader_Register
Since all existing Bullet instances will receive the #Invader_Register event, only the first Bullet instance should actually add the Invader's me pointer. Otherwise the pointer would be added to the list many times. This is why the Invader_Register handler first checks if the invaderRef is already on the list and only appends it if it isn't.
But what about any Invaders that were instantiated before the first Bullet? When ourInvadersList is first created, the first Bullet instance will ask all existing sprites if they are Invaders. Here is another chunk of the Bullet behavior's Initialize handler, which dovetails with the chunk which appears above.
(Bullet behavior)
on Initialize me -- sent by beginSprite ... if listP (tempList[1]) then ... else -- If not, create the list from scratch. -- Create an empty list... ourInvadersList = [] -- ...then pass it as a parameter in a call -- to all other behaviors. sendAllSprites (#Bullet_FindInvaders, ourInvadersList) -- (E3) -- The list may now contain data or it may still -- be empty. In either case it can now be shared -- with other instances. Whatever modifications -- are made by one instance will be immediately -- available to all those that share the list. end if ... end Initialize
And here is the corresponding handler in the Invader behavior:
(Invader behavior)
on Bullet_FindInvaders me, listPointer -- sent by Bullet Sprite on beginSprite (Initialize) -- (E3) Signing a petition: the current instance adds -- its object reference to the reply-paid list provided. -- This is then automatically available to the -- calling instance. listPointer.append (me) end Bullet_FindInvaders
Aside: Ordering parameters with a property list
When sending several parameters from one behavior to another, you must ensure that the parameters are sent in the order that the receiving handler expects them in. Failure to do this can lead to debugging problems. One solution is to send only one parameter, which automatically arrives in the right order. Here's an example where I use a property list. I hope it is self-explanatory.
(Cannon behavior)
on ReactToPlayer me ... -- (F) Ordering Parameters: To avoid problems due to -- the order of parameters, all data is shrinkwrapped -- in a property list... set bulletData = [#roundLoaded: [FALSE], #muzzleLoc: ¬ mySprite.loc, #muzzleVelocity: myMuzzleVelocity ] -- ... and then delivered direct by a #call. call (#Cannon_Fire, myBulletsList, bulletData) ... end ReactToPlayer
(Bullet behavior)
on Cannon_Fire me, bulletData -- sent by Cannon Sprite on exitFrame (ReactToPlayer) -- (F) Ordering Parameters: bulletData has the format: -- [#roundLoaded: [<boolean>], #muzzleLoc: -- <point>, #muzzleVelocity: <integer>] -- Using a property list rather than a series of -- parameters makes the order of the variables -- unimportant ... -- Test to see if the handler should be executed if bulletData.roundLoaded[1] then -- Another bullet was already loaded for this shot exit end if -- Load and prepare to fire mySprite.loc = bulletData.muzzleLoc myVerticalSpeed = bulletData.muzzleVelocity ... end Cannon_Fire
Stopping a call
We've already seen twice how the execution of a call sent to a number of behaviors can be stopped, by testing if a list already contains a value. In the ReactToPlayer and Cannon_Fire handlers above, you can see a variation on this theme. The roundLoaded parameter is first set to [FALSE], that is, to a list containing the value FALSE. The Cannon_Fire handler then tests...
if bulletData.roundLoaded[1]...
If the value of the first item of roundLoaded has somehow become TRUE, the rest of the handler will not be executed. What is the rest of the handler ?
(Bullet behavior)
on Cannon_Fire me, bulletData ... -- Load and prepare to fire mySprite.loc = bulletData.muzzleLoc myVerticalSpeed = bulletData.muzzleVelocity myLastTicks = the ticks -- (G) Stopping a call: Modify one of the parameters -- passed as a list. Subsequent instances in the #call -- list will receive the modified version. bulletData.roundLoaded[1] = TRUE end Cannon_Fire
The first available Bullet which receives the Cannon_Fire event will execute right to the end. It will thus change the value contained in the roundLoaded list. The bulletData parameter received by the subsequent Bullet instances will be different from what was sent by ReactToPlayer. If roundLoaded were a straight value, all Bullet instances would receive it with the value FALSE. Making it a list means that the original parameter can be modified on the fly.
Okay, so that's tricky. It might help to step through this section with the Debugger to see all this happen in slow motion. The good news is that it's all downhill from here.
Tidying up
In my Space Invaders movie, all the sprites start on the same frame and they end on the same frame. This might not be the case in movie that you create. If you try to call a behavior on a sprite which no longer exists, this could cause problems. The best solution is to dispose of all pointers to a behavior when the sprite it is attached to is disposed of. The following handlers speak for themselves.
(Cannon behavior)
on endSprite me -- (H1) Unsubscribing: Calling an instance -- which no longer exists would cause a runtime -- error. Calling an empty list has no ill -- effects. call (#Cannon_Unsubscribe, myBulletsList, me) end endSprite
(Bullet behavior)
on Cannon_Unsubscribe me, cannonInstance -- sent by Cannon Behavior on endSprite H1) -- Unsubscribing: Calling an instance which no -- longer exists would cause a runtime error. -- Calling an empty list has no ill effects. myCannon.deleteOne(cannonInstance) end Cannon_Unsubscribe
(Bullet behavior)
on endSprite me -- (H2) Unsubscribing: Calling an instance which no -- longer exists would cause a runtime error. Calling -- an empty list has no ill effects. call (#Bullet_Unsubscribe, myCannon, me) end endSprite
(Cannon behavior)
on Bullet_Unsubscribe me, bulletInstance -- sent by Bullet Behavior on endSprite (H2) -- Unsubscribing: Calling an instance which no -- longer exists would cause a runtime error. -- Calling an empty list has no ill effects. myBulletsList.deleteOne(bulletInstance) end Bullet_Unsubscribe
Asking another behavior to help out
We've almost gotten back to our starting point (which was, if you remember, the #GameOver call).
The game will end in one of two cases:
- when the player runs out of Bullets
- when the last Invader sprite is destroyed
The last Bullet "knows" (through the number of instances left on ourInvadersList) whether there are any Invaders left. What it doesn't know is that it is The Last Bullet. The Cannon instance keeps a count of how many bullets are left in its myUnusedRounds property. If the last Invader has not been destroyed when the last Bullet has finished its course then the player has lost.
The Last Bullet instance and the Cannon instance have to join forces to test for this particular case. I change the name of the event handler from SpentBullet to Bullet_Spent,but this is just for clarity. The same name could be used in both behaviors.
(Bullet behavior)
on SpentBullet me -- sent by HitOrRun ... -- Check if the game is over (J) Forwarding messages. -- The Cannon Behavior holds additional data.. Under -- certain conditions, the message will be forwarded -- to Game Over. call (#Bullet_Spent, myCannon, ourInvadersList.count()) end SpentBullet
(Cannon behavior)
on Bullet_Spent me, invadersLeft -- sent by Bullet Behavior on exitFrame (HitOrRun -- => SpentBullet) (J) Forwarding messages: the calling -- behavior does not have enough information to contact -- the final behavior directly, so it asks an -- intermediary behavior for help. myUnusedRounds = myUnusedRounds - 1 if invadersLeft then if not myUnusedRounds then -- <= The missing information -- The user has run out of bullets ... sendAllSprites (#GameOver, #lose) end if ... end if end Bullet_Spent
Note: to avoid confusion with the number "one", I jump from (H) to (J) without using (I)
Over to you
My entire movie contains not one hardcoded sprite number. Now you are ready to test if the inter-sprite communications I have set up are sound. Open the Score and move the sprites randomly to other channels. (Do try to respect the frames, though). Regardless of the order in which the various elements appear in the Score, the communications network set up between the behaviors should work without fail.
Extending these techniques to objects
The movie and this article deal only with behaviors, but the same techniques can be used with objects, with the difference that objects do not react to sendAllSprites events. An alternative is to place your objects on the actorList, and use...
call #CustomHandler, the actorList, parameters
...to set up the initial links. Once the various objects have communicated their instance references to the behaviors and objects that need to know them, you can take them off the actorList again.
May the Force be with you. You might like to check out the PopButton behavior on my site. This uses a variation on the temporary-pointer-on-the-actorList technique. For technical reasons (related to the way Scott Kildall's Popup Xtra works) the PopButton behavior cannot receive a callback to an event that it triggered. My workaround is to place a pointer to the behavior on the actorList and catch the callback from there. The PopButton behavior is fully commented.
Acknowledgments: Artwork for the Space Invaders movie by Charlotte Thibault.
Copyright 1997-2024, Director Online. Article content copyright by respective authors.