On rare occasions, you may find the need to perform some operation on a periodic basis with greater than the one second precision offered by sleep. Although the shell does not offer any precision timers, you can closely approximate such behavior through the use of a calibrated delay loop.
The basic design for such a loop consists of two parts: a calibration routine and a delay loop. The calibration routine should execute approximately the same instructions as the delay loop for a known number of iterations.
The nature of the instructions within the delay loop are largely unimportant. They can be any instructions that your program needs to execute while waiting for the desired amount of time to elapse. However, a common technique is to perform nonblocking I/O during the delay loop and then process any characters received.
For example, Listing 7-1 shows a very simple timing loop that reads a byte and triggers some simple echo statements (depending on what key is pressed) while simultaneously echoing a statement to the screen about once per second.
Listing 7-1 A simple one-second timing loop
#!/bin/sh |
ONE_SECOND=1000 |
function read_test() |
{ |
COUNT=0 |
local ONE_SECOND=1000 # ensure this never trips! |
while [ $COUNT -lt 200 ] ; do |
CHAR=`./getch` |
if [ $1 = "rot" ] ; then |
CHAR="," |
fi |
case "$CHAR" in |
( "q" | "Q" ) |
CONT=0; |
GAMEOVER=1 |
;; |
( "" ) |
# Silently ignore empty input. |
;; |
( * ) |
echo "Unknown key $CHAR" |
;; |
esac |
COUNT=`expr $COUNT '+' 1` |
while [ $COUNT -ge $ONE_SECOND ] ; do |
COUNT=`expr $COUNT - $ONE_SECOND` |
MODE="clear"; |
draw_cur $ROT; |
VPOS=`expr $VPOS '+' 1` |
MODE="apple"; |
draw_cur $ROT |
done |
done |
} |
function calibrate_timers() |
{ |
2>/tmp/readtesttime time $0 -readtest |
local READ_DUR=`grep real /tmp/readtesttime | sed 's/real.*//' | tr -d ' '` |
# echo "READ_DUR: $READ_DUR" |
local READ_SINGLE=`echo "scale=20; ($READ_DUR / 200)" | bc` |
ONE_SECOND=`echo "scale=0; 1.0 / $READ_SINGLE" | bc` |
# echo "READ_SINGLE: $READ_SINGLE"; |
# exit |
echo "One second is about $ONE_SECOND cycles." |
} |
if [ "x$1" = "x-readtest" ] ; then |
read_test |
exit |
fi |
echo "Calibrating. Please wait." |
calibrate_timers |
echo "Done calibrating. You should see a message about once per second. Press 'q' to quit." |
stty -icanon -isig |
GAMEOVER=0 |
COUNT=0 |
# Start the game loop. |
while [ $GAMEOVER -eq 0 ] ; do |
# echo -n "Enter a character: " |
CHAR=`./getch` |
case "$CHAR" in |
( "q" | "Q" ) |
CONT=0; |
GAMEOVER=1 |
;; |
( "" ) |
# Silently ignore empty input. |
;; |
( * ) |
echo "Unknown key $CHAR" |
;; |
esac |
COUNT=`expr $COUNT '+' 1` |
while [ $COUNT -ge $ONE_SECOND ] ; do |
COUNT=`expr $COUNT - $ONE_SECOND` |
echo "One second elapsed (give or take)." |
done |
done |
stty sane |
In a real-world timing loop, you will probably have keys that perform certain operations that take time—moving a piece on a checkerboard, for example. In that case, your calibration should also perform a series to tests to approximate the amount of time for each of those operations.
If you divide the time for the slow operation by the duration of a single read operation (READ_SINGLE), you can discern an approximate penalty for the move using iterations of the main program loop as the unit value. Then, when you perform one of those operations later, you simply add that penalty value to the main loop counter, thus ensuring that the "One second elapsed” messages will quickly catch up with (approximately) where they should be.
You can approximate this further by using larger numbers in your loop counter to achieve greater precision. For example, you might increment your loop counter by 100 instead of by 1. This will give a much more accurate approximation of the number of cycles stolen by a slow operation.
Warning: If you perform significant multiplication (for example, to increase game play speed on subsequent levels) to change the rate of your timer, using larger values means that you are much more likely to exceed the maximum value that shell math or expr math can handle during your interim calculations. In such cases, you may find it better to use bc, which works with floating-point quantities.
Last updated: 2008-04-08