So far our programs can run lines top to bottom, skip some lines with an if, or jump to a function and come back. Things which say which order to run lines are called Control Structures. The last one is a loop. It lets us run lines over and over.
Here’s a sample loop which you should never run:
It runs the line print("moo"); over and over. Not because it’s in Update – because while runs the thing after it over and over. This particular loop freezes all of Unity.
More than most things, loops need us to have a plan. We need to think of how to make it repeat enough times to do it’s work, and then quit and move on.
This non-useless loop runs 10 times, printing n=1 through n=10:
The rules for a while are probably obvious: put a true/false test in parens. It works the same as an if – runs the body if true. The difference is that a while always goes back and checks the test again. The body runs over and over until the test is false. Obviously, something inside must eventually turn it false.
This is the first time we can write something which will definitely freeze or crash
Unity. It won’t do any real damage, but you’ll lose anything not saved. and it’s a
pain in general. If you’re trying things out right away, you may want to pause that
for a bit until you get a feel for making loops end.
It one of your loops never quits it will freeze everything – the entire Unity window locks. You can’t press the Stop button. All you can do is force-quit Unity. Depending on the computer, right-click the icon below and force-quit, or go to the task manager. The rest of your computer will be fine – you’ll be able to browse for “how to make a program quit.”
Besides being a waste of a minute or two, you’ll lose anything unsaved. You won’t lose scripts (since you already saved them before running). But any Cubes you created, or moved or renamed, or added a script to – those will be gone.
Before running any risky loops, it’s not a bad idea to use save. Save->Scene gets most things. Save->Project isn’t as important, but do it occasionally. It saves changes in the Project panel, such as Materials (the things for colors and bounciness).
Setting up logic to make a loop run and eventually quit is often known as driving it. A common way is with an increasing number. When it gets too big, we quit. This loop counts 2, 4, 6, up to 20 and then stops:
Let’s do a walk-through. The first time is exactly like an if: n is 20 or less so we run. The body prints 2, and adds 2. Then the new rule for while’s sends us back to the test. The same thing happens again: 4 passes, we print 4 and add 2 more to n.
Clearly, this will continue, printing 6, 8, 10 …. The last time is on 20. n<=20 is just
barely true. We run, print 20 and n goes to 22. The loop jumps back, as usual. But
finally n<=20 is false and we’re done.
Basically, the loop made n get bigger until it got too big. The details are about
which exact values it hits.
We can make the same sequence in a few ways. This version adds to n before printing:
We had to start n at 0 in order for it to print 2 as the first thing. Likewise 18
had to be the last number it liked. That feels funny. The loop looks like it
should count from 0 to 18. The version with n+=2; at the end was much
nicer.
Another idea is to figure out the exact number when we stop. In this case, we quit when n is 22. Here’s how 2 to 20 by 2’s works that way:
It works, but it’s harder to read and we needed to do more math. If we were
counting by 3’s we’d need to figure out it stopped on 23. n<=20 works for anything,
which is why we prefer it.
This next one is the same idea as 2 to 20, except 50 to 100 by 10’s:
They’re a little spread out, but each value has a spot: start at 50, cut-off at 100, go up by 10.
Here’s an interesting one. Try to trace what it prints:
The numbers will go 1, 6, 11, 16. Sixteen is too big, so the loop prints 1, 6, 11. It’s
nicer to have the cut-off be the actual last value. (i<=11) might have looked better.
But sometimes that number represents some real cut-off value. Like we have a
15-day vacation and need to check-in every 5 days, starting when our plane
lands.
As an exercise, we can turn the start, step, cut-off pattern into a function that runs a loop with those 3 things as inputs:
We can use this to quickly test out a few:
printUpSeq(1,20,1) prints 1 to 20 – it goes by 1’s.
printUpSeq(0,20,8) prints 0, 8, 16. It’s another one where the cut-off number isn’t
perfectly aligned.
printUpSeq(-10,10,5 prints -10, -5, 0, 5, 10. Negative is fine.
printUpSeq(1,10,99) only prints 1. After adding 99 it’s too big and quits.
printUpSeq(1,-99, 1) does nothing. The very first check is while(1<=-99), which
is false.
Counting backwards is the same pattern as counting up. We count down, and flip the test. This counts from 40 down to 10 by 1’s:
We get 40, 39, 38 …12, 11, 10. When it stops, i is 9, which is too far.
Suppose we forgot to change one of them. This version forgot to fix the if:
The first test is (40<=10), which is false. The loop doesn’t run even
once.
Suppose we forgot to count down, and went up by mistake:
It counts up 41, 42, 43 and stops when it’s less than 10. So this is an infinite loop.
Oops. For real, it will eventually hit 2 billion. Then, depending on the computer it
will error-out and quit, or will keep running with n stuck there, or might wrap
around to negative 2 billion and actually quit.
To sum up: loops can be sensitive. Getting them wrong can give unpredictable
results.
Here’s a fun general purpose function using all of our tricks so far, making a loop that counts up or down:
We don’t need to only add. Anything which changes the number could eventually make us quit. This next loop doubles the number each time:
It will print 2, 4, 8, 16, 64. In this case the cut-off of 99 makes sense. We’re printing every 2-digit power of 2. It’s obvious we not trying to hit 99 exactly. Notice how everything else is the same, including i*=2; going last.
This is another good chance for an infinite loop. Accidentally starting i at 0
makes it so doubling does nothing. i would always be 0 and we’d really be
stuck.
We can do that in reverse by dividing:
That prints 150, 75, 37, 18, 9, 4, 2, 1. It works because of integer division, which will take anything down to 0.
Here are the mechanical rules and some comments. You’ve seen most, but a few
might be new:
while loops steal most of their rules from if’s. The form is:
Again, those are the same rules as an if. I just wanted to list them since it seems
funny how much if’s and while’s are alike, at least rule-wise.
There’s no else. It wouldn’t make any sense. Actually, some languages have a
weird special-case loop-else that might run one time. But a loop never has 2 bodies
where it alternates.
The rule where it jumps back to the start is built-in. You don’t do anything special. By writing while in front the computer knows after it runs the body it has to jump back and try again.
If seems funny how the end of a loop double-jumps, but it’s no problem for the computer. What I mean is, take this silly loop that runs once:
After the line i=0; we know it will quit. But the computer needs to jump back to the start. Then is sees i==4 is false, and jumps right back to the end.
That sequence means it won’t jump out midway. Loops only think about quitting at the end of the body. Here’s an example with an if. It prints moo:
As a while loop it would print "moo" once. Only then would it jump back and quit because i was too big.
A loop is good for doing something 10 times - just write a loop that counts 1 to 10 and put the thing you want to do inside of it.
We almost always think of the counter as “how many times so far.” We start at 0 and quit when it hits the count. Imagine counting sit-ups by holding out your fist and flipping up a finger after each one. Like that, a normal “do it 10 times” loop runs on 0 to 9, and quits on 10.
This prints 10 stars:
We like that style because the number of times, 10, is right there in the loop. And the < gives a hint that we care about how many times. If the point was to make 0 to 9 we’d have written i<=9.
After a while you can quickly tell a how-many-times loop from a
number-sequence-making loop.
Suppose we want to make a string with fifty stars. Running w+="*"; fifty times would do it. So we make a 50-times loop and put that line inside:
Mentally, we group i=0;, i<50 and i++ as the loop driver for “run 50 times.”
Then we push those lines aside. We see just w+="*"; as the real inside of the
loop.
It’s easy enough to make a generic “run N times loop.” This function takes any string and a count and adds that many times. It looks almost the same as the fifty stars loop:
w=copyMe("*",50); would give 50 stars. w=copyMe("-arf",10); would make ten arfs. Fun note: if we wanted ten arfs with correct dashes we could combine 9 with and 1 without:
I think that’s a neat example of using return values and abusing functions with
cleverness.
These kinds of loops can get harder to spot when we’re doing other math. Suppose we want to compute 2 to the 10th. We start with 1 and double it ten times. The key is, it’s just a 10-times loop. The doubling is what we’re doing 10 times:
Mentally, we pull out i=0, i<10, and i++ as the do-10-times driver. In our minds
n*=2; is the real body, being done 10 times.
Another “do X times” plan would be to count down. Imagine both hands with ten fingers up, flipping a finger down after each sit-up until we have two fists:
There’s nothing wrong with this, but everyone does it the other way, making this look weird, and a little more confusing than it needs to be.
You can use a float to drive a loop. Everything works the same. This will print from 1.5 up to the cut-off of 10, going by 0.6:
The 10.001 is from the chapter on special float stuff, to account for rounding. When n hits 10 exactly, it might “randomly” be rounded to a tiny bit larger. Arrg! This is why we prefer int’s.
This section is about a completely different loop plan, which doesn’t involve counting or a sequence. Sometimes we want to run some lines over and over (and over) until we’re “done”. A loop is good for that.
There aren’t any new rules, but this feels like a different type of loop because it
uses a different plan.
Suppose we want to roll 1-6, but not 5’s. My first non-loop plan is to act like we have a real 6-sided die. We’ll roll it, reroll up to twice if we get 5’s, and finally give up and make it some number near the middle, like 3:
We could clearly add more if’s to have it be more fair. Or we could change the if to a while. Then it will re-roll as many times as needed:
This is super-cool. That while is like an infinite number of identical if’s, which cut off when we don’t need them any more.
The loop usually won’t run, since we usually won’t get a 5. That’s legal and fine.
Loops can run 0 times. But if we happen to get lots of 5’s in a row, this will keep
rolling until we don’t.
That’s the basic idea of an until-done loop. We don’t care about exactly how
many times it runs, or even if it runs at all. We need to repeat something and we’ll
know when we’re done.
I once played a board game where you roll two dice, doubles add and re-roll. If you roll a 4 and a 5, you have a 9. But if you roll a pair of 4’s, you keep going. Maybe you roll a pair of 3’s, finally a 5 and a 2. Your roll this turn counts as (4+4)+(3+3)+(5+2) = 21.
This go-until-done loop does that. The stopping condition is “not doubles”:
Starting both at 0 is to get things started. We’re tricking the loop into running
the first time by faking that we got doubles. That’s common for loops like
this.
Here’s a simpler go-until-done die-rolling loop. It rolls 1-100 but not 37 to 50:
If we roll 87 then we quit right away, which is great. But we could roll bad
numbers 39, 47, 42 then finally 17 and quit.
Various math-type problems can be solved with until-done style loops. Suppose I want to find the first power of 2 past 300. My plan is to keep doubling 2 until I get past. The loop is the same as the old doubling loop but in our minds we don’t care about the sequence – only finding that one number:
I used the no-curly 1-line trick. This just spins 2, 4, 8, 16, 32, 64, 128, 256 then
stops on 512, which is the answer I wanted.
A slicker, trickier version would find the highest power of 2 which is n or less. For 20 it would tell us 16; asking about 300 would tell us 256. The plan is the same – double until we get past. Then, since that overshoots, divide by 2. We’ll write it as a function:
We should test this. We should run it for every input from 1 to 100 and print the results. A good way to do that is a basic number-moving loop:
Using a number-moving loop for testing is a common trick.
Suppose some game uses perfectly square grids, like 4x4, or 5x5, and I want the smallest board which can hold all of the pieces. If one round uses 55 puzzle pieces, it needs an 8x8 board.
The plan is simple. Count up board sizes, 1, 2, 3, until the first one with enough space:
Sample runs:
getSizeSquareGridThisOrMoreSpaces(8) is 3 (for a 3x3 board)
getSizeSquareGridThisOrMoreSpaces(36) is 6 (perfect!)
getSizeSquareGridThisOrMoreSpaces(55) is 8 (7x7 is 49, not quite. 8x8 is
64)
The condition looks really strange, but it’s easy. A board size n has n*n spaces. The loop keeps going if the spaces we have is less than the spaces we need.
As a walk-though, for 8 the loop checks: 1*1<8, 2*2<8, and finally quits on 3*3<8. A 3x3 board is the first one which can hold 8 spaces.
Sometimes the short loops are really sneaky.
Back to the dice examples, I want every Update to roll a number from 1 to 4, but never the same one twice. Suppose the last roll was a 2. My next roll should be 1-4, but not 2. Obviously, this will look like the old “1-6 but not 5” loop.
There were a few tricks here. Saving the old roll in a global is common. Usually we update it as the very last line. As soon as we leave Update, the number we rolled now turns into the old number we rolled.
oldRoll starts out-of-range, at -1. That’s a trick so the first roll can be anything (it can’t be -1, which is no restriction).
The last trick is convincing the loop to go at least once, by starting roll=oldRoll. A less sneaky method is to roll once at the start:
This is better, since it’s easier to read. But worse since we had to copy the
“roll 1-4” code into 2 places, even though it’s logically the same roll both
times.
That code will spray 1-4 in the console. If you let it run for a while and scroll,
you’ll see it gets all 4 values, but doesn’t repeat. That’s not absolute proof – it’s
possible the code has a bug which the randomizer never found – but it should be
somewhat convincing.
Sometimes the condition gets too complicated to fit into the test. We can use our old bool tricks to precompute it. Here’s the “1-6 but not 5” rewritten using a done flag:
This is overly complicated for this small example, but look how pretty it is: the
loop runs while it’s not done, at the start we say we’re not done, and when we get a
non-5, we say we’re done.
Here’s a good use of a done flag. Every few seconds a Cube should pop somewhere else, at least 3 away from where it is now. This function finds that new spot:
We can take as many lines as we need to decide whether we want to quit the loop.
In this case, it took the last 3 lines of the loop.
Here’s the rest of it, which doesn’t need a loop:
The first loops we had moved a number by steps to a target. This is sort of like that, but it will count up or down to 5. We figure out which way each time, inside the loop:
Checking (i!=5) makes it seem like we don’t know if we’re coming from above or
below, which we don’t. Clearly loops like these are especially easy to mess up and
make infinite – often bouncing between two numbers.
Checking for prime numbers is a traditional loop. Basically, try to divide by every number less than you. To speed it up, we’ll check for even numbers first, then test-divide by odds, up to half:
The actual loop is a boring 1, 3, 5 …counter. It abuses the early return trick – if anything divides our number, we can quit and say it’s not prime.
The special cases (1, 2, 3, and even #’s) took up half the function. That’s common, and often a good idea. It’s not as if we get a prize for writing a loop that handles everything.
It’s really easy to mistype, or just have an idea that’s wrong, and get a loop that
runs forever. Here are a few ways. Don’t try them, since they’ll freeze all of
Unity.
The most common is forgetting the go-to-the-next. This can happen in longer loops:
This spins forever with num=0, locking up the program.
In this one, I accidentally tried to double 0 over and over. num is always 0, so the loop will never quit:
Here’s another infinite loop, where I accidentally left in the stop condition for a dividing loop, in a doubling loop:
This runs forever, which we’ve seen, but it’s different since doubling num gets
very big, very fast. It will hit the maximum int (about 2 billion) quickly.
It might have a normal crash with a number too big, but don’t count on
it.
This one forgot the {}’s for the body, so only runs the first line, forever:
It seems like computers should be able to detect infinite loops and stop
themselves. The problem is, sometimes a loop really needs 5 minutes to run.
Some loops are even supposed to be infinite, with a parallel thread stopping
them.
This is a repeat, but a mistake can also cause a loop to never run. In this example, I’m counting down, but forgot to flip the <=:
This prints nothing. It looks like the computer is just skipping our loop for no reason.
I want to start with a mystery loop and puzzle out what it does:
This loop is adding 1 and also dividing by 2. Hmmm …. Dividing num by 2 is the last thing, so that’s a hint. And while(num>1) confirms it – num drives this loop. It’s a “cut in half until it hits 1” loop.
Adding 1 to count is what we do while we watch. So this loop counts how many
times 2 goes into a number. Starting it with num at 32 it would go 16, 8, 4, 2, 1 and
count would be 5.
In this next example, I’m curious about how many times it takes to roll a 6. I know it should average 6 times, but I’m wondering about the spread; or how rare it is to take 20 or more tries.
The loop will roll until we get a 6, counting how many times. Update is to make it run over and over:
I’m not even saving the die roll since I don’t care what it is unless it’s a 6. This is another one that’s very easy to mess up. If we accidentally used Random(1,6), we’d roll only 1-5, can never get a 6, and it always runs forever.
We’ve seen the trick where n%10 gives you the one’s place (divide by 10 and take the remainder). We’ve also seen the trick where dividing by 10 chops off the 1’s place: 327/10 is exactly 32. And the combined trick where (n/10)%10 gives the ten’s place.
A loop can use that to look at every digit in a number.
First let’s test the idea. This divides and prints until we hit 0:
To make it useful, let’s take that same loop and print the 1’s place, using the n%10 trick. This only has that one line changed:
This happens to print the number backwards: 5 2 7 5 0 4 (on separate lines.)
A trick to flip them around is using a string and adding to the front. A quick test of front-adding:
Here’s the each digit loop changed to add each digit to the front of a string, putting them in order:
A cute thing we could do with this is add the numToWord function. Change the
adding line to: result=numToWord(dd)+","+result; to get “four, zero, five, seven,
two, five.”
We can change the digit-using line to count how many 7’s our number has:
Even easier, just count how many digits the number has. This is written as a math function:
The function destroys input n as it runs, which is fine, since it’s short and n is
only a local variable.
A sort of interesting non-loop comment: this gives the wrong answer for 0 and negative numbers (-251 counts as 3 digits, right?) It’s another example of a nice loop made ugly by needing extra if’s in front for special cases.
Counting how many times a loop runs can easily be off by 1. Take a look at this loop that prints 13 to 16, and, without thinking too hard, figure out how many times it runs.
My first instinct is that 16-13 is 3, so it runs three times. But if we count the numbers: 13, 14, 15, 16, it really runs four times.
We think of a number-line as a fence. Each number is a post, with a section of fence stretched between. The part that fools people is a length three fence needs four posts: 13 to 16 are three apart and also four numbers.
Whenever you’re doing number math, figure out whether you want to count the
posts, or the fence sections. If someone tells you to fill 13 to 16 with size one Cubes,
you need three – each Cube is like a fence section, running between two numbers.
But if someone asks you to place markers on 13 to 16, you need four – the markers
are like posts. If you rent storage lockers 13 to 16, that’s four, with three adjoining
walls.
The most common off-by-one error is like that first example. An obvious one: 10
to 20 are clearly ten apart, but are eleven numbers. high-low+1 is a common
formula. A loop that prints 10 to 20 runs 20-10+1=11 times,
Being off by one because of this problem is most often called a fence-post error.
A typical counting while loop has the “driving” part spread across three lines. We write so many of these loops, that it might be worth it to combine i=0, i<10 and i++ into one line. It would make the loop just a little easier to read, and maybe avoid a few mistakes (like forgetting i++ and getting an infinite loop).
A for loop does this. It’s just a shortcut, and can’t do anything that a while loop
can’t do, but it’s very common. They were invented way back and most languages
have copied them.
Here’s a for loop that prints 0 to 9, with the explanation later:
The inside of the ()’s is a pile of special for-loop rules. Declaring i and setting it to 0 was moved inside. The same i<10 is there. And our i++ has been moved from the end of the loop, to inside the parens.
Here are the official rules:
We like for loops because they let us gather the loop driving lines. We can look at
the top of the loop and easily see how it moves.
Some more typical for loops:
The top part has the exact three lines we’d use in a while loop, even the i>=0 to
show we’re counting down to 0. But they’re grouped up there, making it easy to see
the loop is really one print statement.
This is a basic move-by number loop printing 5, 15 up to 105:
The print-power-of-2 loop looks the same. Since the body now only has one line, we can leave off the curly-braces:
Floats are nothing special, but it’s nice to see an example with them:
Usually we like how we can insta-declare the variable inside the for-loop, and how it vanishes when the loop ends. But we don’t have to. This finds the first power of 2 past cows by declaring n before the loop, the way a while does:
After the loop, we still have n, with whatever the loop doubled it to.
Most people use a for loop for simple sequences, then the odder stuff is a while. But you can use either for anything. Here’s “how many digits” rewritten using a for (I tweaked it a little. It stops dividing at one digit, and starts the count at 1 to account for that):
While-not-done loops look nicer as whiles, but some people just love for’s. Here’s a for-loop to roll 1-6 but not 5:
That one is really freaky. The semi-colon means it has no body. It doesn’t need one. It rolls 1-6 and keeps going until it gets a non-5. The starting num=5 makes it run the first time. You’d have to love for loops to think this is a good way, but it works.
When you want to run through a sequence of floats, it’s often nicer to use an int loop.
For example, suppose you want 5 numbers that start at 12.5 and go up by 0.8. Use a 0 to 4 counting loop with a formula inside:
It’s easy to see this runs 5 times. The formula looks pretty good written on one line, and is easy to change. Start i at 0 makes it easy to see how 12.5 is the first number.
Even better, we can easily have it count down. Change the formula to
12.5f-0.8f*i, and that’s it.
A little different version, suppose we don’t know how many we want. We want to go from 12.5 to at most 20. The int plan still works, using a flag:
A do-while loop is the same as a while loop, but the test is at the end. The body always runs once, before checking the test the first time. Other than that, it’s the same as a while. It looks like this:
The only difference between this and a while loop is when it would run 0 times.
do-while loops always run at least once. For n=20, this loop prints 20 then quits (a
while loop would just quit).
You never need to use one of these. While loops are all you ever need, and for loops are a nice shortcut. do-while’s are one of the “shortcuts” we tried back in the old days that weren’t all that useful.
If you get a chance, look up a repeat-until loop.