Articles Archive
Articles Search
Director Wiki
 

Game Spinner

May 30, 1999
by Pat McClellan

Dear Multimedia Handyman,

How would I go about putting together a spinning pointer like the one in the board game of LIFE. I would like the user to be able to click on and drag the arrow with a kind of a throwing motion, Ideally the speed and duration of the arrow spinning would be tied into the mouse velocity and direction...A real bonus would be if It could have a clicking noise as it passed in and out of the 7 different areas it spins through...any help would be much appreciated...

Saul Rosenbaum
Visual Chutzpah

Dear Saul,

Wow. This is a nice challenge. For starters, I'm going to assume we're talking about Director 7, so we can use rotation. If you're using a previous version of Director, you'd need to use the Rotation effect that is part of the Effector Set for AlphaMania (a great Xtra). I'm also going to use some of Director 7's "dot syntax" in my Lingo to help people make that transition to the more concise coding.

In the behavior library palette that ships with Director 7, James Newton wrote a terrific behavior called "Drag to Rotate". We'll start with that behavior. Drop that on your spinner sprite. This will take care of the all rotation while the user has the mouse held down.

We'll start with a behavior I'll call "freeSpin", which will continue the rotational motion, applying a friction factor so that it slows to a stop. Note: I'm not using any real physics here, just a simple linear deceleration.

The idea is that while the mouse is being held down, on every exitFrame, we'll set a property to the rotation of the sprite as it changes. Then, when the mouse is released, we'll compare the current rotation to the rotation from a frame ago. This change in rotation over the duration of one frame is effectively our "speed" of rotation. So, we'll just continue that rotation in the same direction, subracting our friction factor from the speed on each exitFrame until the speed drops to zero. Make sense? Here's the demo and the behavior.

Download the sample movie in Mac or PC format.


-- freeSpin Behavior
-- copyright © 1999 ZZP Online, LLC
-- free use for readers of Director Online
property pLastRot, pLastRot2
property pFriction
property pDrag
property pSprite
property pRotation
property pSpeed
property pFlag
on getPropertyDescriptionList me
  set pdlist to [:]
  addprop pdlist, #pFriction, [#comment:"Friction ¬
    factor", #format:#float, #default:1.0000]
  return pdlist
  
end getPropertyDescriptionList
on beginSprite me
  pSprite = me.spriteNum
  pFlag = #click
  pDrag = pFriction
end
on mouseDown me
  pFlag = #click
  pLastRot = sprite(pSprite).rotation
  pLastRot2 = pLastRot
end
on exitFrame me
  if pFlag = #click then
    pLastRot2 = pLastRot
    pLastRot = sprite(pSprite).rotation
  else if pFlag = #spinDown then
    spinToStop me
  end if
  
end
on mouseUp me
  pSpeed = sprite(pSprite).rotation - pLastRot2
  
  if pSpeed > 180 then
    pSpeed = pSpeed - 360
  else if pSpeed < -180 then
    pSpeed = pSpeed + 360
  end if
  
  if pSpeed > 0 then
    pDrag = pFriction * -1
  end if
  
  if abs(pSpeed) > 1 then
    pFlag = #spinDown
  else
    pFlag = #stop
    pDrag = pFriction 
    sendSprite(pSprite, #showSegment)
  end if
  
end
on mouseUpOutside me
  pSpeed = sprite(pSprite).rotation - pLastRot2
  if pSpeed > 180 then
    pSpeed = pSpeed - 360
  else if pSpeed < -180 then
    pSpeed = pSpeed + 360
  end if
  
  if pSpeed > 0 then
    pDrag = pFriction * -1
  end if
  
  if abs(pSpeed) > 1 then
    pFlag = #spinDown
  else
    pFlag = #stop
    pDrag = pFriction 
    sendSprite(pSprite, #showSegment)
  end if
  
end
on spinToStop me
  currentRot = sprite(pSprite).rotation
  sprite(pSprite).rotation = currentRot + pSpeed
  pSpeed = pSpeed + pDrag
  
  if abs(pSpeed) < pFriction then
    pFlag = #stop
    pDrag = pFriction
    sendSprite(pSprite, #showSegment)
  end if
  sprite(pSprite).rotation = sprite(pSprite).rotation mod 360
  
end

In the exitFrame handler, you'll notice that I actually track the rotation value of the sprite for the previous 2 frames. After playing with this demo a while, I found it to be more dependable using the value from 2 frames ago.

In the mouseUp handler (and the identical mouseUpOutside handler), I start by setting pSpeed to the difference between the current rotation and the rotation 2 frames ago. I had to account for some special possibilities. For example... let's say that you spin the sprite counter-clockwise. 2 frame ago, the rotation was 10 degrees, and upon release, the rotation is 350 degrees. The sprite has traveled 20 degrees, but pSpeed will result in... current rotation - previous rotation = 340. When actually, the rotation should be -20. So, I assumed that it's impossible for someone to spin the spinner more than 180 degrees in a period of 2 frames. Therefore, if the pSpeed is greater than 180, I subtract 360 to get the real pSpeed. Similarly, if pSpeed is less than -180, I add 360.

Next, I have to determine which direction the rotation and drag is. The only parameter that the author sets is the value for friction. This pFriction property is always positive. However, you'll notice that in the mouseUp handler, I convert that value to a pDrag value. pDrag can be either positive or negative, depending on the direction of the spin. So, if pSpeed < 0, then that means that pSpeed is a negative value and I'll need to add a positive drag value to bring it UP to zero (slow to a stop.)

After figuring out all the pSpeed and pDrag information, I set a flag and the exitFrame handler repeatedly calls the spinToStop handler to decelerate the rotation. Since rotation is a float value calculated to four decimal points, the chances that rotation would be decelerated to exactly zero are slim. So, I just put a switch in the handler that stops the spinDown at the point where (the absolute value of) pSpeed is less than the friction factor.

Now, let's handle the "spinClick" behavior. Start by reading your Lingo Dictionary for the term "mod". This will be critical in this behavior because of the fact that the value for rotation doesn't "wrap". By that, I mean that if you rotate the spinner exactly 2 times, the rotation doesn't equal zero... or even 360. It's 720. So, as you spin the spinner around and around, the value will continue to go way up or way down. We'll want to convert that value back to a scale between zero and 360.

We'll set this up so that the author can specify which sound member, which sound channel, and how many clicks there are in a revolution. In the beginSprite handler, we'll set pAngle equal to 360 divided by the number of clicks (pDivisions). We'll also set pFlag to #quiet, so that it doesn't click immediately when the sprite appears (no matter where the rotation is set.) I'm using the pLastRot property again, to track the previous point of rotation. However, this property isn't reset every frame.

Here's the behavior.

-- spinClick Behavior
-- copyright © 1999 ZZP Online, LLC
-- free use for readers of Director Online
property pSoundMem
property pSoundChannel
property pDivisions
property pFlag
property pAngle
property pSprite
property pLastRot
on getPropertyDescriptionList me
  set pdlist to [:]
  addprop pdlist, #pSoundMem, [#comment:"Which ¬
    sound member?", #format:#sound, #default:0]
  addprop pdlist, #pSoundChannel, [#comment:"Which ¬
    sound channel?", #format:#integer, #default:1,¬
    #range:[#min:1, #max:8]]
  addprop pdlist, #pDivisions, [#comment:"How many ¬
    clicks per revolution?", #format:#integer, #default:6]
  return pdlist
  
end getPropertyDescriptionList
 
on beginSprite me
  pSprite = me.spriteNum
  pAngle = (1.0000 * 360)/pDivisions
  set pLastRot = sprite(pSprite).rotation
  
  if pLastRot < 0 then
    pLastRot = pLastRot + 360
  end if
  pFlag = #quiet
  
end
on exitFrame me
  myRot = sprite(pSprite).rotation mod 360
  
  if myRot < 0 then
    myRot = myRot + 360
  end if
  if pFlag = #quiet then
    if abs(myRot - pLastRot) > 10 then
      set pFlag = #ready
    end if
  end if
  
  if pFlag = #ready then
    if (myRot mod pAngle) < 10  OR ¬
      abs(myRot - pLastRot) > pAngle then
      puppetSound pSoundChannel, pSoundMem
      pFlag = #quiet
      pLastRot = myRot
    end if
  end if
  
end

On every exitFrame, myRot is set to the rotation of the sprite mod 360. That "mod 360" converts it to a value between -360 and 360. The next lines convert a negative value to the corresponding angle between zero and 360. If pFlag is set to #quiet, it checks to see if the spinner has moved more than 10 degrees. If so, pFlag is set to #ready -- meaning that it will check to see if it should play the click sound.

When pFlag is #ready, it two possibilities which will result in a click. The actual "clickpoints" can be represented as "myRot mnod pAngle". Remember that pAngle is simply 360 divided by the number of click points in a revolution. The handler checks to see if myRot is within 10 degrees of a click point, and if so, it plays the click sound. Another possibility is that in this particular frame, we're not within 10 degrees of a clickpoint, however, we're rotating so quickly that we've passed a clickpoint. So, if myRot - pLastRot is greater than pAngle, we play the click sound as well.

One improvement you might want to make on the spinClick behavior is to change the cast member based on how fast the rotation is moving (so that the pitch is higher when it's faster.) That's pretty ambitious and I had enough to worry about otherwise. I'll leave that challenge for you.

The last thing you'll want to do is figure out which sector the spinner is pointed to. I'll create the behavior called "showSegment" for this and I'll call the behavior from the freeSpin behavior (when it comes to a stop.)


-- showSegment Behavior
-- copyright © 1999 ZZP Online, LLC
-- free use for readers of Director Online
property pDivisions
property pSprite
property pSeg1, pSeg2, pSeg3, pSeg4, pSeg5 
property pSeg6, pSeg7, pSeg8, pSeg9, pSeg10
property pSeg11, pSeg12
property pRange
on getPropertyDescriptionList me
  set pdlist to [:]
  
  addprop pdlist, #pDivisions, [#comment:"How many ¬
    divisions?", #format:#integer, #default:4]
  addprop pdlist, #pSeg1, [#comment:"Sector 1 name ¬
    (one word only)", #format:#symbol, #default:#seg1]
  addprop pdlist, #pSeg2, [#comment:"Sector 2 name ¬
    (one word only)", #format:#symbol, #default:#seg2]
  addprop pdlist, #pSeg3, [#comment:"Sector 3 name ¬
    (one word only)", #format:#symbol, #default:#seg3]
  addprop pdlist, #pSeg4, [#comment:"Sector 4 name ¬
    (one word only)", #format:#symbol, #default:#seg4]
  addprop pdlist, #pSeg5, [#comment:"Sector 5 name ¬
     (one word only)", #format:#symbol, #default:#seg5]
  addprop pdlist, #pSeg6, [#comment:"Sector 6 name ¬
    (one word only)", #format:#symbol, #default:#seg6]
  addprop pdlist, #pSeg7, [#comment:"Sector 7 name ¬
    (one word only)", #format:#symbol, #default:#seg7]
  addprop pdlist, #pSeg8, [#comment:"Sector 8 name ¬
    (one word only)", #format:#symbol, #default:#seg8]
  addprop pdlist, #pSeg9, [#comment:"Sector 9 name ¬
    (one word only)", #format:#symbol, #default:#seg9]
  addprop pdlist, #pSeg10, [#comment:"Sector 10 name ¬
    (one word only)", #format:#symbol, #default:#seg10]
  addprop pdlist, #pSeg11, [#comment:"Sector 11 name ¬
    (one word only)", #format:#symbol, #default:#seg11]
  addprop pdlist, #pSeg12, [#comment:"Sector 12 name ¬
    (one word only)", #format:#symbol, #default:#seg12]
  return pdlist
  
end getPropertyDescriptionList
on beginSprite me
  pRange = 360.0000 / pDivisions
  put "pRange = " & pRange
  pSprite = the spriteNum of me
end
on showSegment me  
  myAngle = sprite(pSprite).rotation mod 360
  if myAngle < 0 then
    myAngle = myAngle + 360
  end if
  put myAngle
  set the itemDelimiter to "."
  selectionString = string ((myAngle / pRange) + 1)
  selection = value(item 1 of selectionString)
  
  case selection of
    1: put pSeg1 into whichSector
    2: put pSeg2 into whichSector
    3: put pSeg3 into whichSector
    4: put pSeg4 into whichSector
    5: put pSeg5 into whichSector
    6: put pSeg6 into whichSector
    7: put pSeg7 into whichSector
    8: put pSeg8 into whichSector
    9: put pSeg9 into whichSector
    10: put pSeg10 into whichSector
    11: put pSeg11 into whichSector
    12: put pSeg12 into whichSector
    otherwise
      whichSector = ""
  end case
  
  put whichSector into field "display"
  
end

The only tricky part to this behavior is that when you calculate myAngle divided by pRange, you get a float value with 4 decimal places. But we want to convert that to an integer which will correspond to one of our segments, right? The problem is that if you use integer(myAngle/pRange) it doesn't just drop the decimal places, it rounds the result. However, even if the result is 7.9950 (for example), we want the sector to be 7. So we don't want to round, we just want to drop the decimals. In order to do that, I just set the itemDelimiter to the decimal point, then grabbed the first item in the string as the segment. Then, I plug that into a case statement to convert that integer into the name you gave each segment.

In this behavior, I assumed a maximum of 12 sectors, but you can add more if you need. Having extras won't cause any problems.

I think that's everything you'll need. Good luck with your project.

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.