Doing Multiple Things at Once

JoeStrout - May 13 '23 - - Dev Community

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
Enter fullscreen mode Exit fullscreen mode

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:

Heart beating once per second

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
Enter fullscreen mode Exit fullscreen mode

Again we have an "update" method called from a main loop. And when run, the wumpus correctly jumps over and over.

Jumping Wumpus

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
Enter fullscreen mode Exit fullscreen mode

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.

Wumpus jumps, then heart beats, and repeat

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
Enter fullscreen mode Exit fullscreen mode

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.

Wumpus updates only once per second, with each heart beat

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!

Correct display: wumpus jumps and heart beats simultaneously

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!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .