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

```
-- freeSpin Behavior
-- 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 [:]
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
-- 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 [:]
sound member?", #format:#sound, #default:0]
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
end if
end if

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