Efficient button rollovers
January 1, 1998
by Alex Zavatone
Over my last project, I've had to create an entire user interface prototype with draggable groups of sprites and pseudo windows, toolbars with multiple rollover states and a windowing system complete with graphics, UI elements and z ordering. While last week's article covered the draggable groups part of this project, I had so much fun remembering how to do all this in Director 5, sans behaviours mind you, I felt compelled to share the procedures with you crazy kids (and your little dog too).
While creating the multiple rollover states in Dir 5, I was once again faced with the eternal argument, "do I make every object/button/thing self aware or do I use one state manager for all the buttons and their states." Since I was in a behaviourless environment, the self aware objects/buttons/things approach would require a lot of thought and for some reason I felt that it would be rather ineffecient. Six self aware buttons would require servicing six button objects rather than servicing just one manager which determined which of the six buttons to examine. Got that? I went with a button manager. So we're in Director 5 and I've got to write some code to do the following:
- Determine button states from a set list of "N" buttons
- Determine which button to monitor
- Determine if that button has changed state. If it has, then and only then, set a flag which will update the state of that button
- Repeat with the rest of the buttons if necessary
- If an updateflag was set, update the buttons
Now after dealing with netlingo operations, I'm a big proponent of NOT using repeat loops. They hog the processor and exclude events like netDone and keyDown. That is bad when you expect to use those events or most any event for that matter. They are fast because of that. That is good. My first approach was totally state based and contained no repeat loops. The code which is listed below iterated through the list of supplied buttons on enterframe and its performance was not acceptable. Cranking the tempo could have helped out here, but the flag went off in my head that said "If you want to get this project done in a timely manner, try a more standard method."
code:
Parent Script: Button mgr
property pButtonList -- list of button sprites property pCurrentButton -- current button whose state we are checking property pButtonRolloverState -- list of rollover states property pButtonDepressedState -- list of depressed states (not implimented) property pButtonNames -- Uhhh the button names property pRolloverSuffix -- name of the rollover member suffix property pLastRollover -- last sprite rolled over
on new me init me return me end
on Init me set pCurrentButton = 1 set pButtonList = [33,34,35,36,37,38,39] set pButtonRolloverState = [0,0,0,0,0,0,0] set pButtonDepressedState = [0,0,0,0,0,0,0] set pButtonNames = ["1", "2", "3", "4", "5", "6", "7"] set pRolloverSuffix = "over" end
on EnterFrame me if rollover (getAt( pButtonList, pCurrentButton)) then setAt(pButtonRolloverState, pCurrentButton,1) set rolloverFlag = 1 else setAt(pButtonRolloverState, pCurrentButton,0) end if if rolloverFlag then set mySprite = getAt( pButtonList, pCurrentButton) UpdateDisplay me, mySprite set pLastRollover = mySprite else -- UpdateDisplay me end if if pCurrentButton < count(pButtonList) then set pCurrentButton = pCurrentButton + 1 else set pCurrentButton = 1 end if end
on UpdateDisplay me, mySprite set the member of sprite mySprite = the number of member ¬ (getAt( pButtonNames, pCurrentButton) && pRolloverSuffix) if not pLastRollover or pLastRollover = ¬ mySprite then return set the member of sprite ¬ pLastRollover = the number of member ¬ (getAt( pButtonNames,getPos(pButtonList, pLastRollover))) end
As you drag your mouse over the folders in the above movie, notice that the speed of updating rollovers is not optimal. The display below the little sample windoid is my object inspector which reports on the status of the button rollover object every .25 seconds. Rest assured that the updating speed was slow even before I added the object property display code.
So with state-based rollover detection not fast enough, I decided to switch to a rollover manager that would check the rollover state of all buttons within the button set. In the interests of effeciency, the following approach was taken in the designing of the code:
When checking for rollover of a sprite, store the rollover state in a variable. The next time you check the sprite for rollover if the new rollover state is different from the previous state variable, only then should you set the variable and a flag indicating to update the display.
That resulted in an easy code change and also an easy addition of checking for the depressed state of a button. Here, values of 0 indicate no rollover, 1 indicates rollover and 2 indicates depressed. Since rollover and depressed states can happen on the same button at the same time, the if statement was modified to prevent rollover from kicking in if the button sprite was already depresssed. See, this code and the above code works by storing the states of all the buttons and then after checking all the buttons, run the display routine if any of the button's states have changed.
code:
Parent Script: Button mgr
property pButtonList property pButtonState property pButtonNames property pRolloverSuffix property pDepressedSuffix on new me Init me return me end on Init me set pButtonList = [42,43,44,45,46,47] set pButtonState = [ 0, 0, 0, 0, 0, 0] set pButtonNames = ["Active", "Tools", "Help", ¬ "Prefs", "Comm", "Web"] set pRolloverSuffix = "Over" set pDepressedSuffix = "Down" end on Service me repeat with mySprite in pButtonList set mySpriteState = getAt(pButtonState, ¬ getPos(pButtonList, mySprite)) if rollover ( mySprite ) then if not the mouseDown then -- if rolledover and the mouse Up if mySpriteState = 0 then -- here we want to preserve the mousedownstate set updateFlag = 1 setAt(pButtonState, getPos(pButtonList,¬ mySprite),1) end if else if mySpriteState <> 2 then -- if rolledover and the mouse Down set updateFlag = 1 setAt(pButtonState, getPos(pButtonList, ¬ mySprite),2) end if end if else if mySpriteState <> 0 then -- if not rolled over set updateFlag = 1 setAt(pButtonState, getPos(pButtonList, ¬ mySprite),0) end if end if end repeat if updateFlag then UpdateDisplay me end on UpdateDisplay me repeat with mySprite in pButtonList set myButtonState = getAt(pButtonState, ¬ getPos(pButtonList, mySprite)) set myButtonName = getAt( pButtonNames, ¬ getPos(pButtonList, mySprite)) case myButtonState of 0 : set the member of sprite mySprite = ¬ the number of member myButtonName 1 : set the member of sprite mySprite = ¬ the number of member (myButtonName ¬ && pRolloverSuffix) 2 : set the member of sprite mySprite = ¬ the number of member (myButtonName && ¬ pDepressedSuffix) end case end repeat updatestage end
In the sample below, try rolling over the sprites and clicking on them as well.
This script forces all buttons in the group to be checked for rollover. Though in Director 6 this would be easier, if 5 or 4's all ya got, this'll work like a charm. Here the changes in the code are evident first in that enterframe has been changed to Service. EnterFrame is supposed to be called from objects in the actorlist but for some reason this was not working for me. The rollover code is called from this exitframe script:
on exitFrame Service (getAt( the actorList, 3)) -- bug enterFrame should be automatically executed go the frame end
The actorlist is used here because while monitoring objects with my object browser, the object browser parses any list of objects and displays all the properties of all objects within the list. Because of this, I normally throw an additional reference to the object into the actorlist for monitoring purposes. If you have several groups of rollovers to check, and they are assigned to happy little globals like gButtonObj1, gButtonObj2, gButtonObj3 then the exitFrame script would look like this:
on exitFrame Service (gButtonObj1) Service (gButtonObj2) Service (gButtonObj3) go the frame end
Back to the structure of the code, the repeat loop loops through all the buttons within the list pButtonList and checks the button's state. If the current condition is different than the current state then the proper state for the button is set and the updateflag is set. This way, if you keep rolling over the same sprite, the code doesn't waste time and continue updating the display for no reason. The UpdateDisplay routine then draws the entire button list with the proper graphics with another repeat loop. Here the pRolloverSuffix and pDepressedSuffix variables are used to find the proper cast member graphic to display.
Even though this is more efficient, it would seem that UpdateDisplay should be called with the number of the sprite to be updated and then when done, with the repeat loop, updateStage should be called. Since my code was "good and fast enough" for the contract, I didn't bother to further optimize it but in this example it makes perfect sense. Actually, it's about 30 seconds of hard work and it is much faster. Simply remove the repeat loop from the UpdateDisplay handler, add mySprite as a parameter to UpdateDisplay, call UpDateDisplay after you set the button's state and perform an updateStage if updateFlag is non zero. Piece o cake. In case you're wondering, it looks like this:
Code identical to the previous example. Only modified handlers are displayed:
on Service me repeat with mySprite in pButtonList set mySpriteState = getAt(pButtonState, ¬ getPos(pButtonList, mySprite)) if rollover ( mySprite ) then if not the mouseDown then -- if rolledover and the mouse Up if mySpriteState = 0 then -- here we want to preserve the mousedownstate set updateFlag = 1 setAt(pButtonState, getPos(pButtonList, ¬ mySprite),1) UpdateDisplay me, mySprite end if else if mySpriteState <> 2 then -- if rolledover and the mouse Down set updateFlag = 1 setAt(pButtonState, getPos(pButtonList, ¬ mySprite),2) UpdateDisplay me, mySprite end if end if else if mySpriteState <> 0 then -- if not rolled over set updateFlag = 1 setAt(pButtonState, getPos(pButtonList, ¬ mySprite),0) UpdateDisplay me, mySprite end if end if end repeat if updateFlag then UpdateStage end on UpdateDisplay me, mySprite set myButtonState = getAt(pButtonState, getPos(pButtonList, mySprite)) set myButtonName = getAt( pButtonNames, getPos¬ (pButtonList, mySprite)) case myButtonState of 0 : set the member of sprite mySprite = ¬ the number of member myButtonName 1 : set the member of sprite mySprite = ¬ the number of member (myButtonName && pRolloverSuffix) 2 : set the member of sprite mySprite = ¬ the number of member (myButtonName && pDepressedSuffix) end case end
Here's another Shockwave for ya with the final code. Check out the speed in rollovers and faster click response here:
And finally here's a downloadable simple sample for you to play with. Dir 5 file (HQX or ZIP). All I ask is if you use this code, please give me and DOUG credit.
Copyright 1997-2024, Director Online. Article content copyright by respective authors.