|
© Andy Wilson, 1999
Tips and Tricks: Clocks and Timers
The start of the new year - and the rapid approach of my fortieth birthday - got me thinking about time. Often in a project you need to time the user for some reason - perhaps you have a game where the user only has so much time to complete a level, or a test which has to be completed in a given time. It turns out that this is easy to achieve in Director. Here I'm going to go a little bit further and create some code that allows you to run multiple timers at the same time. What we'll do is create a series of objects, each of which acts as a little stopwatch which can be started, stopped and reset.
To start with, I'll discuss the concept of ticks. Lingo has a function, startTimer, which kicks off an internal clock. The time itself is measured in 'ticks', where one tick is a 60th of a second. So far it couldn't be much easier - you can kick off the timer using the lingo command startTimer, then at any later point you can examine the timer and you'll know how long it was since the timer was started:
startTimer
put the timer
-- 64
The problem with using this method is that the timer itself is a property of the Director application (your projector, or the Director authoring environment when you are in development), which means that there can only ever be one timer running at any point. You can think of the timer as a global property of the application environment. So, for example, if you start the timer in one piece of code (a behavior, child object, whatever) it is possible for any other piece of code being run in Director to reset it. What this means is that it is worth using this cheap and easy method of timing event if, and only if, you can guarantee that your projector is only going to need one timer.
There is, however, an alternative to using the timer for tracking time. The Director property the ticks gives you a more reliable timing device. The ticks measures the number of ticks (again, each tick represents one 60th of a second) that have elapsed since the projector was launched (or, at design time, since Director was opened). In most cases you won't be interested in how long the projector is running, instead you'll be using the ticks simply as an accurate measure of time which, unlike the timer, cannot be interfered with or changed by your Lingo code. To measure the time elapsed between any two events we simply compare the ticks measured at the start with the ticks measured at the end - the difference between the two represents the time elapsed.
The gist of this approach lies in setting a variable to equal the ticks at the point when you want to start counting, then subtract the ticks at the moment that you want to stop counting. For instance;
set startTime = the ticks
set stopTime = the ticks
set timeElapsed = (stopTime - startTime)
put timeElapsed / 60
-- 12
put timeElapsed / 60.0
-- 12.5000
put timeElapsed mod 60
-- 30
The most important variable here is the timeElapsed, which corresponds to the number of ticks that have elapsed between the start and stop events. The calculations that follow are interesting for a number of reasons. Having set the timeElapsed variable, we can divide it by 60 in order to calculate how many seconds have passed between the two events. Notice however that, since both the timeElapsed variable and the divisor (60) are both integers, Director's maths engine will assume that you want an integer result and so will round down to the nearest integer. It's important to remember that Director will always round the numbers down, rather than returning the nearest number. For instance;
put 1999 / 1000
-- 1
In other words, this calculation returns the number of complete seconds elapsed rather than the time elapsed to the nearest second.
The second calculation (timeElapsed / 60.0) works around this situation by the simple means of turning the divisor into a float. By doing so you give Director the hint that you want a floating point result, so it returns the number of seconds elapsed to whatever accuracy you require (use the floatPrecision property to tell Director how accurate you require these calculations to be). In this case we learn that 12.5 seconds have passed.
The final calculation uses the Lingo modulus operator, mod, to give you the number of ticks remaining after the whole seconds have been deducted. You could use this sort of technique in combination with the integer division to tell you how many seconds and how many ticks have elapsed;
set secondsElapsed = timeElapsed / 60
set ticksElapsed = timeElapsed mod 60
put secondsElapsed & ":" & ticksElapsed
-- "12:30"
Now we have pretty much all the tools and techniques we are going to use to build our timers. Let's assume that we need to run three timers simultaneously. I'll also assume that the timers need to display the time elapsed in a field on stage, and that we will use buttons to start and reset the timers. Start off by creating three fields and placing them on stage one beneath the other. I've called mine Field1, Field2 and Field3. Similarly, create three buttons to start and reset the counters. Here's a screenshot so you can see what I mean:
We'll write the timers as objects, using Lingo's parent-child scripting model. The core code for the object is really pretty simple. Create a parent script, calling it timer, and add the following code;
-- parent script "timer"
property startTime, fieldName
on new me, fldName
startCounter me
set fieldName = fldName
return me
end
on getTicksElapsed me
return (the ticks - startTime)
end
on getMinutes me, tks
return formatInt (me, (tks / 3600))
end
on getSeconds me, tks
return formatInt (me, (tks / 60) mod 60)
end
on getTicks me, tks
return formatInt (me, tks mod 60)
end
on startCounter me
set startTime = the ticks
end
on paintTime me
set tks = getTicksElapsed(me)
put getMinutes (me, tks) & ":" &
getSeconds (me, tks) & ":" &
getTicks (me, tks) into field fieldName
end
on formatInt me, num
if (num < 10) then return ("0" & num)
return string (num)
end
The most important property of the objects created by this script is the startTime property. This is initialised to equal the current ticks setting when the object is created, in the on new handler (set startTime = the ticks). I've actually farmed this out into a separate handler, on startCounter, which is called from the new handler, to allow for the possibility of resetting the counter while the object is running although, as you'll see, I don't actually take advantage of this possibility here. The fundamental technique we are using is incredibly simple - we initialise the startTime property and then at any point we can compare this value with the current ticks value to see how much time has elapsed since startTime was initialised. This comparison is done by the on getTicksElapsed handler, which returns the value 'the ticks - startTime' whenever it is called.
I've given each object a fieldName property so that it knows which of the text fields on stage it is to update. This simply needs to be set when the object is first called by passing the name of the associated field to the new handler. For example, we can create a timer object that writes to the Field1 field with the code set timerObject = new (script "timer", "Field1").
The rest of the handlers in the parent script are there to convert the raw 'ticks elapsed' data into minutes, seconds and ticks. I'm assuming that the largest amount of time you want to tell the user about is in the order of minutes rather than hours or days, but you could just as easily write equivalent functions to format the information in any way you see fit. The formatInt handler is there purely to format the ticks and minutes values appropriately. All I'm doing is making sure that they are always represented by two digits when they are written to screen - 01, 02 … 60.
The ticks elapsed is calculated by taking the raw ticks data produced by getTicksElapsed and 'modding' it by 60. That way we subtract everything but ticks representing the fraction of the second that has elapsed. The seconds data is derived by dividing the total ticks by 60 (to lop off the fraction), and then modding the result by 60 to get rid of the minutes. Finally, the minutes elapsed are calculated by dividing the total ticks elapsed by 3600 (60 * 60), thereby dropping all the seconds and ticks over the round number of minutes elapsed. To actually get our display, we use the code;
set tks = getTicksElapsed(me)
put getMinutes (me, tks) & ":" & ¬
getSeconds (me, tks) & ":" & ¬
getTicks (me, tks) into field fieldName
This code is placed in the paintTime handler, which can be called at any time to refresh the timer display. It produces a text output of the form minutes:seconds:ticks.
All we have to do now is write the code that actually performs the updates by calling the respective object's paintTime methods, and code to make the buttons reset the counters. I decided that the best way to do this would be by using a list that holds all the timer objects. I called the list timers, and have made it a global variable. This means that the code for the reset buttons is as follows:
-- cast member script for the first reset button
global timers
on mouseUp
setAt (timers, 1, new (script "timer", "timer1"))
end
-- cast member script for the second reset button
global timers
on mouseUp
setAt (timers, 2, new (script "timer", "timer2"))
end
-- cast member script for the third reset button
global timers
on mouseUp
setAt (timers, 3, new (script "timer", "timer3"))
end
The final thing we have to do is write a movie script as follows;
-- movie script
global timers
on startMovie
set timers = []
end
on idle
repeat with timer in timers
if objectP(timer) then paintTime (timer)
end repeat
end
All this code does is, first, initialise the timers list to be empty when the movie is first run. The idle handler does the work of moving through the timers list, taking each item in turn - each of which will either be empty, if the appropriate timer has not yet been created, or will contain a child timer object. If the item is a timer object, the object's paintTime method is called. As we saw earlier, the paintTime method does the work of writing the time into the field associated with object.
And that's it - you've now got three independent timers that can be started and reset at will. In this case I actually recreate the object every time the start / reset button is pressed, but just a little more coding would have led to the slightly more efficient situation where each object is created only once, and then reset by calling it's startTimer method - but I'll leave that for you to do. You could also think about a number of ways of developing the timers further - making them fire off other events when a certain period of time has elapsed, for instance.
You can download the code from here and use it as the basis for further development.
|