Sprite Management
January 25, 2000
by Zac Belado
Lets begin with a small note. This article isn't intended as a "definitive" approach to the subject. It is meant as a gedankenexperiment, a means of thinking aloud about an approach to a problem via this article.
Part of the problem of trying to build and specify the architecture of a project (especially a game or an open ended interactive presentation) is that you don't always know how many sprites are going to be used on screen. You might design your code for 5 sprites and then have the client decide they want 25 sprites active on screen. Additionally you might need to be able to specify subsets of the sprites to send events to.
Typically sprite control is handled through the use of behaviors. While they are wonderful things (they allow you to encapsulate code and properties into sprites without having to write more complicated objects) they are less than optimal when you need to create a higher level of organisation for your sprites or if you need to provide a way to organise an unknown number of sprites.
Managing multiple sprites, and multiple groups of sprites requires that you build a framework "over" the sprites. An object, a sprite manager if you will, that will store the references to the sprites, organise them and also communicate with them by sending event data to the sprites.
This article will look at a the following issues:
- using a parent object to co-ordinate communication with an unknown number of sprites
- registering sprites with a parent object
- sending arbitrary events to behaviors on a sprite
The background
For the purposes of this article we will look at how to move an arbitrary number of sprites on the screen, but the techniques discussed can be expanded and used in any number of situations. To complicate matters we will also try to develop a system whereby we can arrange sprites into groups and send movement commands to those specific sprites only.
The sample movie has 24 sprites (graphics taken from screenshots of the author's favourite arcade game Galaga) which will be divided into three groups: boss, red and blue.
The sample files can be downloaded in Mac or PC format. These files are Director 7 movies. As well you can view the test movie in action.
Cold call your sprites
Before we get into the details of building our system we need to look at the differences between two Lingo commands: call and sendSprite. Both commands effectively do the same thing but they have enough differences that the implementation of the commands in a project will be quite different.
The format for call is:
call <method name> <script reference(s)> {params}
<method name> is the name of the method (handler) in the script as a symbol. So if the method name was addSprite the call command would require you to preface the name with a symbol #addSprite. You can store the method name as a symbol as well.
<script reference(s)> is a reference to a single behavior or object or a list of behaviors and objects that the event will be sent to.
sendSprite is a bit different.
sendSprite(<sprite number>, <method name>, {params} )
<sprite number> is the number of the sprite channel holding the sprite with the behaviour that you wish to target.
<method name> is the same as in the call example.
call is very useful since it will take a list of scripts or behaviors and send the same event to all of them with a single line of Lingo code. But it has a downside in that you need to know the exact object reference to the behavior that you wish to target. This is easy enough if you combine the sprite's main behavior with a small handler to register it with whatever object (or a global list) that will be used to reference it later. Getting this script reference after the behavior has been initialised is a bit more difficult. Not because you can't do it easily enough through Lingo...
thisInstance = sprite(1).scriptInstanceList[1]
...but because you might have more than one behavior on a sprite and you would then have to check to make sure that you are storing a reference to the correct behavior so that you can target the correct one later.
sendSprite avoids this problem since it simply targets a sprite number and sends the event to all the behaviors on that sprite. But unlike call, sendSprite has no way to send the same event quickly to a list of sprites. If you want to send eventA to a list of 30 sprites then you have to step through a repeat loop and send each sprite the event one at a time.
The sample movie to which this article will refer uses call but the sample files also include a version of the movie that uses sendSprite instead so you can see the difference that this makes in the project's architecture and performance.
The sprite manager
The first object we need to build is the sprite manager. It will have two basic functions. First, it will allow sprites (or functions or handlers) to register a sprite reference (because it uses a call command) to a group and to a universal list. It will also have a method that will send an arbitrary event to a group or to all the sprites it currently has registered. The object also should have a method to unregister a sprite or group of sprites but this is left as an exercise for the reader.
The sprite manager has two properties: pSpriteList and pGroups
pSpriteList - a list of all the sprites registered
pGroups - a property list that contains the group names and the list of sprites in each group
property pSpriteList, pGroups on new me pSpriteList = [] pGroups = [:] return me end
When the script is started it initialises its two properties and then returns a reference to itself to be stored for later reference.
The method to add a sprite is a bit more complicated. The method requires two parameters: a script reference to the behavior and a groupName. The groupName is optional. The handler first checks to ensure that a script reference was passed and that it is an object. It then assigns a blank string as a default value for the groupName.
on addsprite me, spriteRef, groupName if voidP(spriteRef) then exit if NOT objectP(spriteRef) then exit if voidP(groupName) then groupName = ""
Then a check is made to see if the script reference has already been added to the universal list. If it hasn't then it is added to that list. If there was no group name supplied as a parameter then the execution is terminated at this point (with the exit command). This means that all sprites will be added to the universal list once.
if NOT getOne(pSpriteList, spriteRef) then -- it's not here already so add it to the universal list append pSpriteList, spriteRef end if -- do we need to add a group entry? if groupName = "" then exit
If the handler was also supplied a group name, it first checks that to see if the group exists (creating it if it isn't) and then if the script reference is already in the group. Finally the script reference is added to the appropriate group.
-- make the name a symbol groupName = groupName.symbol -- see if the group exists thisList = getAProp (pGroups, groupName) if voidP(thisList) then -- create the group setAProp pGroups, groupName, [spriteRef] else -- add the sprite to the group if it isn't there already if getOne(thisList, spriteRef) then exit -- add it since it isn't there append thisList, spriteRef end if end
As each script reference is added to the sprite manager it generates two lists. The universal list which has all the scripts stored in it and a property list of groups in which each group is maintained as a separate list. This way the sprite manager can quickly and easily send messages to all the sprites if it needs to.
So now we have all the sprite references stored... we just need to be able to send an event to them.
Before we continue, though, we need to look at one overlooked aspect of Director. Typically when you write a handler or function, you declare the parameters that will be sent to that method.
For example if you had a handler like...
on foo booleanParam, stringParam
...you might call it like so:
foo FALSE, "aString"
What you might not know is that every parameter you send to a method or function is actually available for you to utilise. By declaring the variable names ahead of time in the handler declaration (on foo booleanParam, stringParam) you simply tell Director how you want the parameters stored. In the example above we've simply told Director to take the first parameter it encounters and put it into a variable called booleanParam and then take the second on place it in a variable called stringParam. If you send more parameters to the handler Director still allows you to access them.
You can use the paramCount () and param () functions to tell how many parameters have been sent to a handler and to access the values of each of those parameters.
In the sample movie there is a handler called testParams.
on testParams limit = the paramCount put "This call had" && limit && "parameters" if limit > 0 then repeat with index = 1 to limit put param(index) end repeat end if end
Open the Message Window and type
testParams 3,5,6,"foo", [1,2,3,4,5]
You should see
-- "This call had 5 parameters" -- 3 -- 5 -- 6 -- "foo" -- [1, 2, 3, 4, 5]
This is an important concept because the sprite manager won't ever know (nor does it need to know) how many parameters will be sent to a behavior in a given event. So the param () and paramCount() functions allow the sprite manager to test to see how many parameters are being passed to it and then store them in a list to send to the behavior if extra parameters have been sent.
The code needs to be able to do this so that it can handle any type of event call to a behavior. For instance it needs to be able to handle a call like:
sendEvent, #boss, #startMoving, thisMoveList, moveType
...as well as:
sendEvent, #blue, #stopMoving
In the first example the sprite manager would need to pass along two additional parameters and in the second example it has no additional parameters to pass to the behavior.
So now that we know how to test for and store those additional parameters we can build a handler to send this data to the behaviors.
The sendEvent method has only three declared parameters: the parent object's script reference (me), the group name to send the event to (group), and the name of the event (eventName). By default the code assumes that there are no additional parameters to send to the behaviors. It then checks the actual number of parameters sent and appends them all into a list if there are more than the 3 we initially declared.
on sendEvent me, group, eventName limit = the paramCount pList = [] -- do we need to build a list of params? if limit > 3 then repeat with index = 4 to limit thisParam = param(index) append pList, thisParam end repeat end if
Note that at no point does the object try to validate any of the additional parameters it is sent. It simply assumes that they are needed, and valid, and prepares to send them along.
Then the sprite manager checks to see if it needs to send the data to a group or to all the sprites. If it is a group, it then checks to make sure that the group is valid. If it isn't, it simply exits.
if voidP(group) then group = "" if voidP(eventName) then exit if group = "" then sendEventToSprites me, pSpriteList, eventName, pList else group = group.symbol spriteList = getAProp (pGroups, group) if voidP(spriteList) then exit sendEventToSprites me, spriteList, eventName, pList end if end
So the object now has a list of behavior references that it needs to send the data to. This list is handed off to another handler to actually process. The reason for doing this is that it allows the code to be compartmentalised so that it can be changed or expanded without worry. Secondly it exposes the process of sending the event data to the behaviors for use by other methods in the sprite manager.
The final handler, sendEventToSprites, then takes the list of behavior references, and the parameter data (if any) and uses a call command to send all that data to the appropriate behaviors.
on sendEventToSprites me, spriteList, eventName, aList eventName = eventName.symbol call (eventName, spriteList, aList) end
Register now to receive your free gift
So now that we have a parent object how do we get the scripts to communicate with it? All we need to do is have a behavior, or a handler in the sprite's main behavior, that will send either the sprite number or, in our case, the behavior's script reference to the sprite manager.
Remember that in this example movie the behaviors need to send the behavior's script reference since the call command uses references instead of sprite numbers. As such this means that the behavior that registers the sprite will also be the target of the sprite manager's calls. So if you want to have numerous behaviors on the sprite make sure that the registration is done in the behavior that you want to target.
You can see this in the second sample movie. Since it uses sendSprite to send the event calls to the behaviors the registration is actually handled by a separate behavior. In the main sample movie, which uses call, the registration and main functions of the behavior are all stored in the same script.
So in order to register itself with the sprite manager, the behavior simply sends its script reference along with its group name (if any) to the sprite manager. But how, you ask, does the behavior know to send it to the proper object?
There are two ways to approach this. Either you can declare and access the global variable that stores the sprite manager's object reference in the behavior itself, or you can have the behavior call a wrapper handler that will access the global variable.
Which method you choose is up to you but I chose to send the data to a wrapper method instead (the sendSprite sample movie uses a global just to be different). This is primarily to ensure that the behavior is entirely self-contained, that it has no references to any variables or data outside itself nor does it require them and also because the author is just that type of fussy person who finds globals in behaviors unsettling.
In the behavior's beginSprite handler, the code calls the wrapper handler and passes the behavior's script reference and group name (if any).
on beginSprite me mAddsprite me, pGroup end
Then the wrapper method calls the addSprite handler in the sprite manager.
global gSpriteMgr on mAddsprite objectRef, groupName addSprite gSpriteMgr, objectRef, groupName end
Obligatory "calling all cars" reference
Now before the sprites appear on stage they will register themselves with the sprite manager and we'll be able to easily send events to the sprites without having to know anything about the sprites or even how many of them there are.
Have a look at the scripts in the button members on the stage. The script for the "Move boss" button has the following script
global gSpriteMgr, gMoveList on mouseUp sendEvent gSpriteMgr, #boss, #startMoving, ¬ gMoveList.circle, #move end
gMoveList is a global list that is used to store the data that the sprite behaviors use to move on screen. In this case the data stored in the circle property is being sent to the sprites in the boss group as a parameter for the startMoving handler.
tangent
The sprite behavior uses a list of differential sprite positions to move. So the list of data stored in the circle property is a series of lists that describe the changes to the sprites' current loc. It's certainly not an optimal system but it works for the purpose of this demonstration.
This data will get translated by the sprite manager so that it is actually executing a call statement (the actual variables have been inserted for illustration).
call (#startMoving, [<list of script references>], ¬ [gMoveList.circle, #move])
And the actual event will be "seen" by the behavior as:
startMoving me, [gMoveList.circle, #move]
The first thing you might notice is that this is a rather odd way to be sending data to the behavior. What you might expect to see is something more like this...
startMoving me, gMoveList.circle, #move
...with each parameter as a separate element.
Remember from the earlier discussion about the sprite manager that it is designed to take any type or length of parameter data. In order to accomplish this we had to wrap all the "extra" data into a list. This means that when it comes time to write the behavior's handlers the code will have to unpack the data from the list and put it into the appropriate variables.
The behavior that is attached to all the sprites on stage (with the exception of the buttons) has a startMoving handler.
on startMoving me, paramList if voidP(paramList) then exit if paramList.count = 0 then exit -- assume it's a #move type = #move moveList = paramList[1] if NOT listP(moveList) then exit if paramList.count > 1 then type = paramList[2] pMoveList = moveList pState = type end
The handler requires one piece of information, the moveList, and has a second optional parameter, the move type (either #move or #loop). There is some error checking code to ensure that the handler has actually received a parameter list, that the parameter list has at least one entry and then, in the case of the move type, that there is a second parameter to retrieve. Once it has checked all these states it takes the data out of the parameter list and assigns it to the sprite's properties and then starts moving. The same process, with no error checking, holds true for the stopMoving handler.
While it isn't implemented in the sample movie, you could easily make a button that stops the blue bugs from moving by adding the following code to a button member.
global gSpriteMgr on mouseUp sendEvent gSpriteMgr, #blue, #stopMoving end
Again notice that the code has no reference to how many sprites it is referring to or even which channels they are in.
Summing up
With a little bit of work you should now be able to create a sprite communication system that allows you to attach sprites into groups for quick access. At its simplest, this system is just two events.
The behavior sends its registration data to the sprite manager and then the sprite manager, at some later date, sends event data back to the behavior. The main benefit of the system is that it is self-contained and allows you to interact with sprites in your project without having to constantly be aware of the references to the sprites. You can also add or subtract sprites to the system without having to interact with any of the original data.
The article has also looked at the differences between the call and sendSprite commands and shown how the use of one command over the other can substantially effect the structure of the system, as well as your ability to expand and modify the sprites in the system.
Some final thoughts
Sprites can be grouped, and ungrouped if you add the ungroup handlers, during runtime, you needn't restrict yourself to setting groups in a beginSprite event. Also notice that there isn't anything stopping you from having a sprite in more than one group.
While the call method of sending data to a series of sprites in one command is very expedient and elegant, it is rather restrictive. As mentioned before, you are effectively limited to using a single behavior (at least in terms of registration and acting as a target for events from the sprite manager). In the sample movie using sendSprite, the registration is actually handled in a separate behavior. This also means that you can easily register a sprite for more than one group by adding multiple registration behaviors. You can't do this if you are using call to send the events to the behaviors.
The sample movies contain the basic framework for the system and will need several methods added to them before they can be fully utilised in a movie. These handlers are left for the enterprising reader to add and expand upon.
Thanks to Cary Neufeldt for fixing my crappy scrolling background behavior. Thanks to Andrew White, Mark Reijnders, Raúl SIlva and all "the gang" for their input. Thanks to Marvyn Hortman for his help in getting me a working copy of Galaga. Its just for research purposes...honest!
Copyright 1997-2024, Director Online. Article content copyright by respective authors.