Articles Archive
Articles Search
Director Wiki
 

Steering Control Behavior

January 31, 2000
by Pat McClellan

Dear Multimedia Handyman,

I need to have the user be able to turn a sprite (via the rotation property) and then move it in the direction it is pointing. I tried the trig. sin and cos but I couldn't get them to work. Any ideas?

Andrew Oates

Dear Andrew,

You're on the right track, but trig functions are a bit tricky. I started by reviewing one of my previous columns which was written by Jim Collins. He's way into this geometry/trig stuff and it's always a pleasure to read his explanations. I understand trig functions conceptually, but when it comes down to writing the code, I end up experimenting a bit myself... playing with the positive/negative signs, etc. Here's what I've come up with.

Since our sprite can move in any direction from a center point, and since we want it to move at a uniform speed no matter which direction it goes, you can imagine our sprite at the center of a circle. Now let's say that the sprite is going to move at a speed of 5 pixels per frame, so we'll assume the circle around the sprite has a radius of 5 pixels. That means that after the move -- in any direction -- our sprite's loc will be at some point precisely on the circumference of the circle. So the question is, if we know the direction or angle of the motion, how many pixels do we move our sprite horizontally, and how many vertically? Let's look at some examples.

Again, the sprite starts in the center of a circle with a radius of 5 pixels. If the sprite is pointing North, then the angle of rotation is zero. A move of 5 pixels north would mean that we change the loc by [0,-5] -- zero pixels horizontally, and -5 pixels vertically. Alternately, if the sprite is pointing East, then a move would change the loc by [5,0]. So here's a table of motion in the 4 basic directions:

North [0,-5]
East  [5,0]
South [0,5]
West  [-5,0]

Keep in mind that 5 is prevalent in the examples above because that's the speed our sprite will move. But that is variable, so think of it this way instead:

North [0,-1] * speed
East  [1,0] * speed
South [0,1] * speed
West  [-1,0] * speed

It's easy to figure out the moves for North, South, East, and West, but how do we calculate everything in between? That's where sin and cos come in.

Without getting into a long geometry lesson with complicated diagrams, I'll just tell you that the sin() of the angle of rotation relates to the horizontal movement, while the cos() of the angle relates to the vertical movement. Of course, (2 * pi) figures into just about any equation related to a circle, so you'll see that in the code as well. Here's a utility handler I wrote to help me in my experiments.

on makeTrigList howManyAngles
  trigList = []
  increment = (pi * 2)/howManyAngles
  f = 0.0
  angle = 0
  repeat while f < (pi * 2)
    hFactor = sin(f) 
    vFactor = cos(f) * - 1
    hvList = [hFactor,vFactor]
    add trigList, hvList
    f = f + increment
  end repeat
  put trigList
  
end

Copy and paste it into a movie script, then call it from the message window. Now, let's say that our sprite can only move North, East, South or West. That means there are 4 angles. So let's see what the handler comes up with for 4 angles. Type this into the message window:

makeTrigList 4

And this is the result:

-- [[0.0000, -1.0000], [1.0000, 0.0000], ¬
  [0.0000, 1.0000], [-1.0000, 0.0000]]

If you look carefully, these numbers, they're the same as the table I showed you before, except the speed isn't factored in:

North [0.0000,-1.0000] * speed
East  [1.0000,0.0000] * speed
South [0.0000,1.0000] * speed
West  [-1.0000,0.0000] * speed

We can easily expand this notion to calculate the horizontal and vertical increments for any number of angles, and any speed. Let's add speed to our handler:

on makeTrigList howManyAngles, speed
  trigList = []
  increment = (pi * 2)/howManyAngles
  f = 0.0
  angle = 0
  repeat while f < (pi * 2)
    hFactor = sin(f) 
    vFactor = cos(f) * - 1
    hvList = [hFactor,vFactor] * speed
    add trigList, hvList
    f = f + increment
  end repeat
  put trigList
  
end

Let's use the message window to run this handler for 8 angles at a speed of 5 pixels.

makeTrigList 8, 5
-- [[0.0000, -5.0000], [3.5355, -3.5355], [5.0000, 0.0000], ¬
  [3.5355, 3.5355], [0.0000, 5.0000], [-3.5355, 3.5355], ¬
  [-5.0000, 0.0000], [-3.5355, -3.5355]]

This list represents the motion needed to move our sprite in any of 8 directions at a speed of 5 pixels per frame. The first item in the list, [0.0000, -5.0000], is motion North, or at an angle of zero degrees. So you would move it 0.0000 pixels horizontally, and -5.0000 pixels vertically. The second item, [3.5355, -3.5355], shows how many pixels you need to move in a Northeast direction, at an angle of 45 degrees. Of course, you can't really move 3.5355 pixels, so there will be some rounding errors. We'll do our best to minimize the effect of that. I'll point that out later.

Rather than having to run trig calculations for every individual move, I think it's better and faster to simply generate the list once, and then pull values from the list as needed. You will see a slight delay if you've got a lot of angles to calculate, but that only happens on beginSprite, and it's faster from then on. Here's a demo so you can see how it works. I've set it up to display the current rotation of the sprite, the speed, and the steering sensitivity. We'll also want to constrain the sprite to a specific area, so I've included a constraining rect which defaults to the rect of the stage.

A sample movie is available for download in Mac or PC format. This is a Director 7 movie.

We'll want to allow the author to specify the following properties:

Here's the code for the Steering Behavior. Don't get hung up with the getPropertiesDescriptionList handler. That's just to generate the dialog box above. Look through the code and I'll explain it below.


-- Steering Behavior
-- copyright © MM, ZZP Online LLC
-- free use for DOUG readers
property pRotation
property pSpeed
property pSprite
property pAngle
property pAngleSegments
property pTrigList
property pLoc
property pDeltaLoc
property pConstraintRect
on getPropertyDescriptionList me
  stageWidth = the stageRight - the stageLeft
  stageHeight = the stageBottom - the stageTop
  stageRect = rect(0,0,stageWidth, stageHeight)
  set pdlist to [:]
  
  addprop pdlist, #pSpeed, [#comment:"Speed", #format: ¬
    #integer, #default:5]
  addprop pdlist, #pAngle, [#comment:"Steering sensitivity ¬
    (degrees per frame):", #format:#integer, #default:10]
  addprop pdlist, #pRotation, [#comment:"Starting rotation ¬
    (degrees)", #format:#integer, #default:0]
  addprop pdlist, #pConstraintRect, [#comment:"Constraining ¬
     rect:",  #format:#rect, #default: stageRect]
  return pdlist
  
end getPropertyDescriptionList
on beginSprite me
  pAngleSegments = 360.00/pAngle
  pSprite = sprite(me.spriteNum)
  pLoc = pSprite.loc
  pDeltaLoc = [0,0]
  if pRotation mod pAngle <> 0 then
    pRotation = (pRotation/pAngle) * pAngle
    put "Starting rotation must be an even multiple of ¬
      steering sensitivity."
  end if
  pSprite.rotation = pRotation 
  makeTrigList me
  
end beginSprite
on makeTrigList me
  pTrigList = []
  increment = (pi * 2)/pAngleSegments
  f = 0.0
  angle = 0
  repeat while f < (pi * 2)
    hFactor = sin(f) 
    vFactor = cos(f) * - 1
    hvList = [hFactor,vFactor] * pSpeed
    add pTrigList, hvList
    f = f + increment
  end repeat
  --  put pTrigList
  
end makeTrigList 
on exitFrame me
  if keyPressed(37) then rotate me, #clockwise
  else if keyPressed(38) then rotate me, #counterclockwise
  if keyPressed(34) then moveSprite me, #forward
  else if keyPressed(40) then moveSprite me, #backward
  
end exitFrame
on rotate me, whichWay
  if whichWay = #clockwise then
    pRotation = pRotation + pAngle
  else
    pRotation = pRotation - pAngle
  end if
  -- keep pRotation between 0 and 359
  if pRotation > 359 then pRotation = pRotation - 360
  if pRotation < 0 then pRotation = pRotation + 360
  pSprite.rotation = pRotation
  
end rotate
on moveSprite me, whichDirection
  -- convert angle to list index
  whichIndex = (pRotation/pAngle) + 1
  thisMove = pTrigList[whichIndex]
  tempDeltaLoc = pDeltaLoc + thisMove
  if whichDirection = #forward then
    tempDeltaLoc = pDeltaLoc + thisMove
  else
    tempDeltaLoc = pDeltaLoc - thisMove
  end if
  newLoc = pLoc + tempDeltaLoc
  if inside(newLoc, pConstraintRect) then
    pDeltaLoc = tempDeltaLoc
    pSprite.loc = newLoc
  end if
  
end moveSprite

The beginSprite handler initializes several properties based on the author's settings. It saves the sprite's initial location in pLoc. Next, we have to check to make sure that the author's inputs are valid. For example, what if he wanted the sprite to move in at 10 degree angles (steering sensitivity), but set the starting rotation to 45 degrees. The starting rotation needs to be an even multiple of the steering sensitivity, so I stuck in a little if statement that checks for that and fixes it if it's off. The sprite's rotation is then set to the pRotation value. Finally, we call the makeTrigList handler which I explained above.

I won't re-explain the makeTrigList handler. But I do want to point out something significant. When you move a sprite UP on the screen, the locV value actually goes DOWN. And when you move a sprite DOWN on the screen, the locV value goes UP. That means that the vertical motion is inversely related to the cos of the angle. That's why in the makeTrigList handler you'll see this line:

vFactor = cos(f) * - 1

We've covered everything else in that handler.

The exitFrame handler checks to see which key(s) are pressed down and calls the appropriate handlers. The numbers you see referred to in the keyPressed() function correspond to the keycodes for the J, K, L, and I keys. The structure of the if-then-else-if statements are set up to allow the sprite to rotate and move at the same time; but NOT to allow the sprite to rotate in both directions at once nor move forward and backward at once.

The rotate handler simply adds or subtracts the pAngle from the current rotation of the sprite. I've added a couple of lines to assure that the sprite's rotation value stays between 0 and 359. That will be important in the moveSprite handler.

In the moveSprite handler, we need to find the current rotation of the sprite and then find the corresponding h and v move values from our pTrigList. To do this, we divide the pRotation by pAngle and add 1. How about an example that will make this formula clearer? Let's use the values we calculated above using 8 directions:


makeTrigList 8, 5
-- [[0.0000, -5.0000], [3.5355, -3.5355], [5.0000, 0.0000], ¬
  [3.5355, 3.5355], [0.0000, 5.0000], [-3.5355, 3.5355], ¬
  [-5.0000, 0.0000], [-3.5355, -3.5355]]

Remember that these are the h and v move values for North, Northeast, East, Southeast, South, Southwest, West, and Northwest. Now here's how this formula applies:

Direction pRotation pAngle pRotation/pAngle List Position
North 0 45 0/45 = 0 0 + 1 = 1
Northeast 45 45 45/45 = 1 1 + 1 = 2
East 90 45 90/45 = 2 2 + 1 = 3
Southeast 135 45 135/45 = 3 3 + 1 = 4
South 180 45 180/45 = 4 4 + 1 = 5
Southwest 225 45 225/45 = 5 5 + 1 = 6
West 270 45 270/45 = 6 6 + 1 = 7
Northwest 315 45 315/45 = 7 7 + 1 = 8

You can see that the formula gives us the list position, which will give us the corresponding h and v move values. We simply add those values to the sprites position and the move is done.

I mentioned before that the rounding errors associated with moving a sprite 3.5355 (or any other float value) could cause us some problems. Let's say you wanted to move at a 10 degree angle and at a speed of 5 pixels per frame. The makeTrigList would give you a the following values: [2.5000, -4.3301]. And if you tried to move the sprite by that much, here's what happens:

put sprite(3).loc
-- point(237, 178)
sprite(3).loc= sprite(3).loc + [2.5000, -4.3301]
put sprite(3).loc
-- point(240, 174)

Even though we told it to move 2.5 pixels to the right, it rounded the value and moved it 3 pixels. And the vertical motion was rounded down to 4 pixels. If you made only 10 frames of motion like this, your sprite would be off by 5 pixels to the right and 3 pixels too low. We can't split pixels, so how can we fix this problem?

The rounding error is really a cumulative problem. For any single rounding operation, we're only off by half a pixel at most. The solution is to keep a running total of the sprite's change in location (pDeltaLoc) which isn't rounded. We only round the culmulative total. Back to our example, if we move for 10 frames (at an angle of 10 degrees and a speed of 5 pixels) our position will have changed by [25.0000, -43.3010]. With this infomation, it's easy to place our sprite precisely 25 pixels to the right of its starting point, and up by 43 pixels. There is still a rounding error, but we're only off by less that half a pixel for the entire 10 moves.

Actually, most of the moveSprite handler is concerned with making sure that the new location is inside the constraining rect specified by the author. It simply checks to see if the newLoc is inside the rect before resetting the loc of the sprite. If it is, then the sprite is moved and pDeltaLoc is reset to reflect the change.

If you've stuck with me this far, then I'm impressed. There's a lot of math in there, but the results are lots of fun! I'm sure you'll find lots of uses for this behavior.

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.