Articles Archive
Articles Search
Director Wiki
 

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 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.

Patrick McClellan is Director Online's co-founder. Pat is Vice President, Managing Director for Jack Morton Worldwide, a global experiential marketing company. He is responsible for the San Francisco office, which helps major technology clients to develop marketing communications programs to reach enterprise and consumer audiences.

Copyright 1997-2024, Director Online. Article content copyright by respective authors.