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.
Copyright 1997-2024, Director Online. Article content copyright by respective authors.