Proximity Monsters
January 23, 2000
by Pat McClellan
Dear Multimedia Handyman,
I'm developing a Director game and need some assistance. I would like to have a sprite move towards or fire upon another sprite when one of the sprites is within a particular distance from the other sprite. Any pointers would be appreciated!
Bill
Dear Bill,
This is an interesting challenge to tackle because of the sprite interaction and multiple proximity calculations. I'm thinking there are a lot of applications for this type of behavior in games, so we'll want to make it fairly flexible. For example, we'll want to allow for some sprites to be more attractive than others. We'll want to allow for a variable number of "attractor" sprites. And for that matter, why not allow for a variable number of "magnet" sprites -- those sprites which move toward the attractor sprites.
The specific details of what the magnet sprite does when it spots an attractor in its proximity are completely up to you. For this article, I'll focus on simply sensing the proximity of the attractor sprites and moving toward them. Let me show you an example of what I'm going to explain. In this simple demo, the two "biters" can smell the food sprites -- but only if it's close enough to them. Of course, different food items have a different attraction. In this case, the hamburger is more attractive than the cheese, and the apple is least attractive. Move the food items within range of the biters to attract them.
A sample movie is available for download in Mac or PC format. This is a D7 movie.
When I started considering this behavior, I considered several approaches.
- The food sprites could send out a message to the biters every frame reporting their location, which the biters would evaluate to make a decision on where to go. This would require that the food sprites either use sendAllSprites to broadcast their location or "know" how many and which sprites are the biters and use sendSprite. Either way, this is a lot of messaging on each frame (multiplied by the number of food sprites. That just seems "noisy" to me.
- An object could monitor and control the position of all of the biters and food sprites. This would probably entail each of the sprites subscribing or reporting in to the object when they're initialized. This is not a bad approach, but an omniscient and omnipotent controller object is less appealing to me than having each sprite operate autonomously.
- Looking to the real world for the "natural order", it seems the best approach is to mimic what really happens. The biters need to be able to operate on their own, based on a balance of proximity and the power of attraction. This will entail the food sprites reporting themselves to the biters once, but from then on, the biters operate independently.
The exercise of evaluating optional approaches is very important when you're working on something like this. Not so much so that you come up with the "correct" approach -- in honesty, I don't know that one is more correct than another. Rather, so that you can find the approach which is the most flexible, the least complicated, and which fits your "style" for the rest of the program.
To implement this third approach will require that the food sprites have a behavior which allows the author to assign a power of attraction (in pixels). When told to do so, the food sprites need to add their sprite number and their respective power of attraction to a property list.
The biter sprites will have to send out a command to all sprites (which have the attractor behavior) to add themselves to the list. The biter behavior can then evaluate the loc of each sprite in the list every frame, balance its proximity to its power of attraction, and move toward the most attractive sprite in range. When it reaches that food sprite, the biter can remove it from its list and send a message to the food sprite that it has been consumed. The food sprite will act accordingly -- in this demo, by moving itself offscreen.
Let's look at the Lingo. First, here's the behavior for the food sprites -- and since you're application probably won't involve food, we can think of them more generically as the "Attractor" sprites.
-- Proximity Attractor Behavior -- copyright © MM, ZZP Online, LLC -- free use for Director Online Readers -- use in coordination with Proximity Magnet property pPower on reportToList me, powerList mySprite = (me.spriteNum) addProp powerList, mySprite, pPower end reportToList on consume me sprite(me.spriteNum).locH = 2000 end consume on getPropertyDescriptionList me set pdlist to [:] addprop pdlist, #pPower, [#comment:"Power of ¬ attraction:", #format:#integer, #default:50] return pdlist end getPropertyDescriptionList
When dropped on a sprite, the getPropertyDescriptionList handler will open this dialog box and allow you to specify the power of attraction for a given attractor sprite.
In the reportToList, it will add its spriteNum and power to the powerList supplied by the biter sprite (or Magnet sprite). And in the consume behavior, it simply moves itself offscreen. Really a very simply behavior, which suggests that all of the complicated stuff is in the Proximity Magnet behavior... and it is.
Actually, the Proximity Magnet behavior starts simply enough. All it requires of the author is to specify a speed for its movement. This is measured in pixels per frame.
The other properties are commented so that you'll understand how they're used. The only tricky part is that we don't know if the Magnet sprites (biters) will be in lower or higher sprite channels than the attractor sprites (food). I've set it up so that it works either way, but NOT if there are attractor sprites both higher and lower than the magnet sprite. Here's the code, which I'll explain in more detail below.
-- Proximity Magnet Behavior -- copyright © MM, ZZP Online, LLC -- free use for Director Online Readers -- use in coordination with Proximity Attractor property pSprite property pPowerList -- prop list of attractive sprites & their respective power property pCount -- number of attractive sprites in pPowerList property pAttractorList -- prop list of attractive sprites and their proximity property pTargetSprite -- the most attractive sprite at this moment property pSpeed -- speed in pixels per frame property pHdir -- horizontal direction of motion toward target property pVdir -- vertical direction of motion toward target on beginSprite me pSprite = sprite(me.spriteNum) pAttractorList = [:] initPowerList end beginSprite on initPowerList me pPowerList = [:] sendAllSprites(#reportToList, pPowerList) pCount = pPowerList.count end initPowerList on exitFrame me if pCount = 0 then initPowerList me calcAttraction me if pTargetSprite > 0 then moveTowardTarget end if end exitFrame on calcAttraction me myH = pSprite.locH myV = pSprite.locV repeat with looper = 1 to pCount whichSprite = getPropAt(pPowerList,looper) power = pPowerList[looper] deltaH = myH - sprite(whichSprite).locH deltaV = myV - sprite(whichSprite).locv distance = sqrt((deltaH * deltaH) + (deltaV * deltaV)) attraction = power - distance setaProp(pAttractorList,whichSprite,attraction) end repeat if max(pAttractorList) > 0 then whichPos = getPos(pAttractorList,max(pAttractorList)) pTargetSprite = getPropAt(pAttractorList, whichPos) else pTargetSprite = 0 end if end on moveTowardTarget me if inside(sprite(pTargetSprite).loc, pSprite.rect) then consumeSprite me exit end if myH = pSprite.locH myV = pSprite.locV if myH > sprite(pTargetSprite).locH then pHdir = -1 else pHdir = 1 end if if myV > sprite(pTargetSprite).locV then pVdir = -1 else pVdir = 1 end if newH = myH + (pSpeed * pHdir) newV = myV + (pSpeed * pVdir) pSprite.locH = newH pSprite.locV = newV end moveTowardTarget on consumeSprite me deleteProp pAttractorList, pTargetSprite pCount = pPowerList.count sendSprite(pTargetSprite, #consume) pTargetSprite = 0 end consumeSprite on getPropertyDescriptionList me set pdlist to [:] addprop pdlist, #pSpeed, [#comment:"Speed", ¬ #format:#integer, #default:2] return pdlist end getPropertyDescriptionList
The behavior starts with the beginSprite handler which initializes a couple of properties, then calls the initPowerList handler. The initPowerList handler is the one which sends out a message to the attractor sprites, instructing them to #reportToList -- to add their spriteNum and power of attraction to the pPowerList. The final line sets pCount to the number of attractor sprites in the pPowerList.
But here's the tricky part I referred to before. If the Magnet sprite is in a lower numbered sprite channel than the attractor sprites, it will send out the #reportToList message before the attractor sprites are ready to respond. That means pPowerList would be empty and pCount would be 0. In that case, the first line of the exitFrame handler will call the initPowerList handler again.
The exitFrame handler continues by calling the calcAttraction handler. This is where the math gets done... really some pretty routine stuff. Here's what's going on. At this point, pPowerList will look something like this:
put pPowerList -- [2: 150, 3: 200, 4: 240]
The way to read that list is this: in sprite channel 2 there's a sprite with a power of attraction of 150 pixels; in sprite channel 3 there's a sprite with an attraction of 200; and in sprite channel 4 there's a sprite with an attraction of 240. So sprite 4 is the most attractive to the magnet sprite(s).
Continuing with this example, the calcAttraction handler starts with the first sprite in the list (2) and subtracts its locH and locV from the locH and locV of the magnet sprite. These distances can be plugged into the pythagorean theorum to give us the distance between the two sprites.
distance = sqrt((deltaH * deltaH) + (deltaV ¬ * deltaV))
Now we need to subtract that distance from that sprite's power of attraction. So let's say that sprite 2 is 180 pixels from the magnet sprite. You can see in pPowerList that sprite 2 has a power of attraction of 150. If we subtract the distance from the power of attraction, we can see that it gives us an attraction value of -30. This value gets set into the pAttractorList. This same routine is done on each of the attractor sprites, until we have a pAttractorList that looks something like this:
put pAttractorList -- [2: -30, 3: 10, 4: 20]
Looking at the pAttractorList, you can see that sprite 2 has a negative value for attraction, which simply means that its further away than its power of attraction. Now look at sprite 3. We know from the pPowerList that it has a power of 200. So we can deduce from the pAttractorList that this sprite is currently 190 pixels away from the magnet. Likewise, we can deduce that sprite 4 is 220 pixels away. Note that even though sprite 4 is further away, its attractive power is still higher. To relate this to our demo example, the hamburger is more attractive to the biters even when it is further away than the cheese.
The next step in the calcAttraction handler is to look at the pAttractorList and see which sprite is the most attractive at the moment. The number of this sprite is assigned to pTargetSprite. There is the chance that all of the values in the pAttractorList will be negative. In this case, pTargetSprite is set to 0.
With the calcAttraction handler complete, we move back to the next line of the exitFrame handler. It checks to see if a spriteNum has been assigned to pTargetSprite. If so, it calls the moveTowardTarget handler. This handler is fairly simple -- it moves the magnet toward the target sprite at a rate equal to pSpeed per frame. When the magnet sprite reaches the target sprite, then it calls the consumeSprite handler.
The consumeSprite handler removes the target sprite from the pAttractorList and sends a message to the target sprite that it has been consumed. Remember in the attractor behavior, we had a consume handler which moves its sprite offstage.
That takes care of all the code in the demo. I'm sure you'll want to customize these behaviors to meet your own needs. Experiment with the code to add scoring and shooting... and let me know what you come up with. Good luck with your program.
Copyright 1997-2024, Director Online. Article content copyright by respective authors.