This post is inspired by a real-life (or at least, school-life) problem encountered by my son in his engineering class. His group's task was to write a program to sort balls in a Vex Robotics machine. They had a routine which controlled the motor to send one ball at a time down the ramp. And they had a routine which could use the sensor to check the color of a ball and kick it into the correct path.
But they were stumped on how to do both at once. If they called the motor routine, by the time it returned, the ball had already rolled past the sensor. And if they programmed the sensor to just wait for a ball to appear, none would ever appear because the motor routine wasn't running.
This is a very common problem in programming: how do you make a computer do multiple things at once? Despite fancy video cards and multi-core machines, in most ordinary programming, the computer is essentially a serial processor: it does one thing at a time. Indeed, when running a program, the computer steps through your code, step by step; it's always at exactly one point in your program, not two or more places. So how can it possibly being doing more than one thing at a time?
The secret: it doesn't. A computer program only does one thing at a time. But you can make it look like it's doing multiple things at once, by switching between them very fast.
Let's illustrate with an example in Mini Micro.
The Setup
First, suppose we have a program to make a heart that beats once per second:
clear
heart = new Sprite
heart.image = file.loadImage("/sys/pics/Heart.png")
heart.x = 800
heart.y = 400
display(4).sprites.push heart
// Make the heart beat.
updateHeart = function
heart.scale = 1.2
wait 0.3
heart.scale = 1
wait 0.7
end function
// main loop
while true
updateHeart
end while
Simple enough, right? We even have an "update" function that's called by a "main loop," because we've heard this is good program design and should save us grief later. When run, this program produces a display like we wanted:
Now, separately, we write a program to make a wumpus jump:
clear
wumpus = new Sprite
wumpus.image = file.loadImage("/sys/pics/Wumpus.png")
wumpus.x = 300
wumpus.y = 100
display(4).sprites.push wumpus
// Make the wumpus jump
updateWumpus = function
dy = 30
while true
wumpus.y = wumpus.y + dy
if wumpus.y < 100 then break
dy = dy - 1
yield
end while
wumpus.y = 100
end function
// main loop
while true
updateWumpus
end while
Again we have an "update" method called from a main loop. And when run, the wumpus correctly jumps over and over.
But now, how can we do both of these things at once? We want the wumpus to jump continously, while the heart also beats once per second, all at the same time. This is like my son's school challenge, where they needed to control the motor and the check the sensor at the same time; or like any video game, where you have lots of sprites moving around and doing their own thing, including the one controlled by the player.
A First Try
So, what to do? A first try might be to simply call both update functions from the main loop.
clear
heart = new Sprite
heart.image = file.loadImage("/sys/pics/Heart.png")
heart.x = 800
heart.y = 400
display(4).sprites.push heart
wumpus = new Sprite
wumpus.image = file.loadImage("/sys/pics/Wumpus.png")
wumpus.x = 300
wumpus.y = 100
display(4).sprites.push wumpus
// Make the heart beat.
updateHeart = function
heart.scale = 1.2
wait 0.3
heart.scale = 1
wait 0.7
end function
// Make the wumpus jump
updateWumpus = function
dy = 30
while true
wumpus.y = wumpus.y + dy
if wumpus.y < 100 then break
dy = dy - 1
yield
end while
wumpus.y = 100
end function
// main loop
while true
updateHeart
updateWumpus
end while
The trouble with this is, it doesn't update the heart and the wumpus at the same time. First, the heart beats, which takes about a second; and then the wumpus jumps, which takes several more seconds. Then the heart beats again, and then the wumpus jumps. They're taking turns.
The Correct Approach
To fix this, we need to think differently about our "update" functions. In almost all cases, an update function needs to return very quickly, whether its task is "done" or not.
So let's consider each of our update methods one at a time, starting with the wumpus. This one is fairly easy, because it already contains an internal while
loop that we can unpack. Instead of doing the whole loop, our new and improved updateWumpus
function will do just one step of that loop. The only tricky part is dy
, which was a local variable before; now it needs to stick around between update calls, so we'll make it a global.
// Update the wumpus to continue its jump
dy = 30
updateWumpus = function
wumpus.y = wumpus.y + dy
if wumpus.y < 100 then
wumpus.y = 100
globals.dy = 30
end if
globals.dy = dy - 1
end function
If we replace updateWumpus
in the previous listing with this one, at first it seems worse: the wumpus is now moving in very jittery fashion, taking one step per beat of the heart. It's painfully slow because its update rate now depends on how fast updateHeart
returns, and that one is still taking a full second on every call.
So let's tackle that one next. This one is a little trickier because it involves wait
calls, and that's simply a no-no in an update routine. Update methods need to return as quickly as possible so other parts of the program can do their thing. So instead of waiting, you check the time
, and use that to know when to do your next step. In this case, we want the heart to be scale 1.2 for 0.3 seconds, and scale 1 for 0.7 seconds, so we can write it like this:
// Update the heart to beat every second
nextHeartTime = 0
updateHeart = function
if time < nextHeartTime then return // not time yet!
if heart.scale == 1 then
heart.scale = 1.2
globals.nextHeartTime = time + 0.3
else
heart.scale = 1
globals.nextHeartTime = time + 0.7
end if
end function
Note that we again need a variable that lives outside the method to keep track of state; in this case, that is nextHeartTime
, which is the time at which the heart should next change its size. When time
exceeds that value, we change the scale of the heart, and update nextHeartTime
accordingly.
There is one more change we need to make at this point: our original updateWumpus
method contained a yield
call, but we've refactored that away. Now that everything in the main loop returns very quickly, it actually happens too fast! We fix that by inserting a yield
in our main loop, where it almost always belongs. The final program looks like this:
clear
heart = new Sprite
heart.image = file.loadImage("/sys/pics/Heart.png")
heart.x = 800
heart.y = 400
display(4).sprites.push heart
wumpus = new Sprite
wumpus.image = file.loadImage("/sys/pics/Wumpus.png")
wumpus.x = 300
wumpus.y = 100
display(4).sprites.push wumpus
// Update the heart to beat every second
nextHeartTime = 0
updateHeart = function
if time < nextHeartTime then return // not time yet!
if heart.scale == 1 then
heart.scale = 1.2
globals.nextHeartTime = time + 0.3
else
heart.scale = 1
globals.nextHeartTime = time + 0.7
end if
end function
// Update the wumpus to continue its jump
dy = 30
updateWumpus = function
wumpus.y = wumpus.y + dy
if wumpus.y < 100 then
wumpus.y = 100
globals.dy = 30
end if
globals.dy = dy - 1
end function
// main loop
while true
updateHeart
updateWumpus
yield
end while
And when run, we can see that the wumpus is jumping, and the heart is doing a steady 60 beats per second, all at the same time. Huzzah!
Conclusion
The approach described here is the magic common to virtually all real-time programs. Sure, some environments hide some of the details from you in one way or another, but at some level they're just doing something like this: calling a bunch of little update functions, each responsible for updating one part of the program. And critically, these update functions need to return as quickly as possible, so they don't block any other update functions.
Now that you know the secret, you'll be able to find it in many Mini Micro games, including built-in demos like flappyBat, platformer, mochiBounce, and more. And you'll be able to apply it to your own games and programs, too. Happy coding!
Top comments (0)