An Artist's Guide to C# in Unity3D: Flow Control
This article is part of an ongoing series for code-leaning artists who want to learn Unity 3D. In this article we learn about mechanisms for controlling the execution flow of your programs.
This article is part of an ongoing series for code-leaning artists who want to learn Unity 3D. In this article we learn about mechanisms for controlling the execution flow of your programs.
Read Previous: Collections
We’ve learned quite a bit already about coding in C-sharp--variables, data types, operators and collections. However, we still can’t do much in our programs without a way to:
Make branching decisions
Conditionally repeat an action or block of actions
Iterate through items of a collection
For example, say you have a list of 100 city temperatures in Fahrenheit and wish to convert all 100 values to Celsius. How would you do that? You need a way to run through your entire list and perform the conversion equation on each value.
Or say you have a player character running around in your video game world. You’ve placed several different types of objects around the world for the character to run into. Some objects may be coins to add to his wallet, health tokens to give him extra health points or poisonous bushes that reduce his health points. You would need a way to make decisions based on what type of object your player has collided with.
These are all examples of flow control in a program’s structure.
Making branching decisions
The primary device for making branching decisions is the logical if-else statement. It tells your program to execute one block of code if a set of conditions is true, but execute a different block of code if those conditions are false. The basic form looks like this:
if ( expression_is_true) { // Run all the code in this block } else { // ...run the code here instead }
The expression_is_true should be replaced with an expression that the compiler can evaluate into a boolean value. This likely means using logic or comparison operators.
Here’s an example. Your character stumbles on a clover and you would like to print different messages depending on the number of leaves found on the clover.
int cloverLeaves = 4; // set to 4 or some other number if(cloverLeaves == 4) { Debug.Log("Yippee! Today is my lucky day!"); } else { Debug.Log("Shucks! No luck today!"); }
In this example, the statement in the if-clause checks to see if cloverLeaves
is equal to 4
. It then proceeds down one of two paths, depending on whether that statement is true or not. This is the most basic form of branching you’ll create.
You can also have the if-clause alone, if your logic doesn’t require an else-clause.
if(cloverLeaves == 4) { Debug.Log("Yippee! Today is my lucky day!'); }
There is also an if-else-if statement you can use. This adds an extra evaluation clause and code block to the entire construct. For example, what if we also want to check if the clover has 5
or more leaves? Our code would look like this
if(cloverLeaves == 4) { Debug.Log("Yippee! Today is my lucky day!"); } else if (cloverLeaves >= 5) { Debug.Log("Today is an unusual day!"); } else { Debug.Log("Shucks! No luck today!"); }
We now have three possible branches our code could take depending on the value of cloverLeaves
. It's important to note that when our running program encounters this code, it will only select one of these branches to run, before continuing through the rest of your program.
At this point, you may be wondering if you can add more else-if clauses to this structure. The answer is, “yes!”
You can add as many else-if clauses as you like. Let's add another one to handle a clover with three leaves or fewer. I’ll rearrange the order of the clauses so the conditional numbers are arranged incrementally--it's just easier to read and understand.
if (cloverLeaves <= 3) { Debug.Log("Meh! Nothing special today!"); } else if (cloverLeaves == 4) { Debug.Log("Yippee! Today is my lucky day!"); } else if (cloverLeaves >= 5) { Debug.Log("Hmm, today is an unusual day!"); } else { Debug.Log("Shucks! No luck today!"); }
Ok, now I would like to add yet another clause to handle exactly two leaves. I could add it to my existing stack of clauses, but at this point it's starting to get a bit long for my liking. If you have more than four or five branches to string together, there is another feature we can use called a switch statement.
Switch statements take an expression and then execute the code block that exactly matches the expression value. Each code block in a switch statement is labeled with a case clause which must be a literal data value and NOT a variable. There is no limit to the number of cases allowed in a switch statement block.
The syntax for a switch statement looks like this:
switch(expression) { case literal_value1: // this code runs if expression == literal_value1 break; case literal_value2: case literal_value3: // this code runs if expression == literal_value2 or // expression == literal_value3 break; case literal_value4: // this code runs if expression == literal_value4 break; default: // this code runs if expression does NOT match any of // the literal values above break; }
Additionally, every case clause must be terminated with a break
statement. The last clause is simply called default
and will run if none of the previous case clauses match the expression.
Default clauses are optional.
Let's refactor our clover leaf branching logic to use a switch statement instead of a string of if-else statements. We’ll also add the extra code to handle clovers with two leaves.
int cloverLeaves; // assign this to any number between 2 and 5 switch(cloverLeaves) { case 2: Debug.Log("Ooh, you better go ring shopping!"); break; case 3: Debug.Log("Meh! Nothing special today!"); break; case 4: case 5: Debug.Log("Yay! It’s a lucky day!"); break; default: Debug.Log("Dude! Are you sure it's even a clover leaf??"); // this clause will run if cloverLeaves is set to anything // other than 2, 3, 4 or 5 }
Conditionally repeat a block of actions
Sometimes you may need to repeat an action or group of actions several times. We’ll start by learning two ways of implementing loops in C-sharp.
for loops
while loops
These loops accomplish the same thing--repeating a particular block of code one or more times. However they accomplish this in different ways depending on the scenario.
for loops
for
loops are best used when you know how many times you want your code block to run. The basic syntax is:
for (initialize counter; termination expression; increment counter) { // this code will run multiple times until // the termination expression is false }
So what’s going on here? The for statement has three parts to it.
A counting variable (usually an integer) must be initialized to a starting value like
0.
Before each attempt to execute the code block, the program checks the termination expression. If the expression evaluates to true, then it executes the code block. If the termination expression evaluates to false, it will terminate the loop and continue with the rest of your program.
After every execution of the code block, the program increments the counter by a specified amount—usually
1
.
Let’s look at a trivial example.
Remember when you had to write lines in school for punishment? (Or am I the only one?)
I once had to write the following line 100 times:
“I will not pour glue on my neighbors chair!”
If I knew how to code back then, I could’ve saved myself a lot of time. Using a for
loop I could’ve done it this way:
for (int i = 0; i < 100; i++) { Debug.Log("I will not pour glue on my neighbor’s chair!"); }
That’s all there is to it.
We initialize a variable (the convention is to use an i
) to 0
. The termination expression is a check to see if i
is less than 100
. After each loop, we increment i
by 1
using the ++
operator. There is nothing special about incrementing by 1
. If your logic required it, you could increment by any number like 4
— i += 4
. But then it wouldn’t repeat 100 times, right? And I’d still be in detention.
while Loops
In some other scenarios you may not want to loop your code a fixed number of times. Perhaps the type of loop you would like to create doesn’t depend on a strict counter.
Let’s consider the example of continuously outputting an “S.O.S” message until it has been received by someone. You could create a received
variable and set it to false. Then repeatedly print the SOS until received
is set to true.
while
loops look like this:
while (termination expression is true) {
// run this code ..
}
Simple enough, right? The thing with while
loops, is that it’s up to you to make sure your termination expression evaluates to true at some point, otherwise the loop will run forever. Or at least until your machine crashes. Seriously.
Following this syntax, our example would look like this:
bool received = false; while (received == false) { Debug.Log("S.O.S"); // This message would print continuously, // until confirmation is set to true }
Note: If you’re running this program in your Fundamentals.cs
file from lesson two, you’ll need a way to set the received
variable to true otherwise this loop will run forever. Let's modify our code so that received
is flipped to true after 20 loops.
bool received = false; int counter = 0; while (received == false) { Debug.Log("S.O.S"); if (counter > 19) { received = true; } counter++; }
Iterate through items of a collection
Ok, now the last set of flow controls we’ll discuss is how to loop through all those fancy collections you learned about in the previous lesson.
We can use for
loops and while
loops to accomplish this. Let’s start with a for
loop.
Given this array:
String[] turtles = new string[4] {"Leonardo", "Michelangelo", "Raphael", "Donatello"}
We can write a simple iteration loop to pull each name from our array and print it out.
Let’s recall the standard loop syntax for this:
for (initialize counter; termination expression; increment counter) { // this code will run multiple times until // the termination expression is
false
}
We’ll set our initial counter variable to
0
.We want the looping to stop once the counter reaches the end of our array.
We will increment the counter by
1
.
Putting that all together, our loop will look like this:
for (int i = 0; i < turtles.Length; i++) { // we use i as the index of the item we want to retrieve // since i will be incremented from 0 through to the end // of the array we can use turtles[i] // to get the array item Debug.Log($"Here comes {turtles[i]} to kick Shredder's butt!"); }
We can achieve the same thing with a while
loop. We just have to provide and increment our own counter. Given the same array, here’s what a while
loop would look like:
int index = 0; while (index < turtles.Length) { Debug.Log($"Here comes {turtles[i]} to kick Shredder’s butt!"); index++; // increments our counter at the end of each loop }
Looping through arrays and looping through lists are almost identical. The only real difference is using .Count
to get the size of the list instead of .Length
for arrays.
Here’s a for
loop:
List<string> turtles = new List<string>() {"Leonardo", "Michelangelo", "Raphael", "Donatello"}; for (int i = 0; i < turtles.Count; i++) { Debug.Log($"Here comes {turtles[i]} to kick Shredder’s butt!"); }
And the while
loop:
int index = 0; while (index < turtles.Count) { Debug.Log($"Here comes {turtles[i]} to kick Shredder’s butt!"); index++; // increments our counter at the end of each loop }
There is another type of loop called the foreach
loop which is commonly used for iterating through collections. Rather than use an incrementing index for referencing the items in the collection, foreach
gives you the actual item as a variable in each loop iteration.
The basic syntax is:
foreach ( data_type variable in collection) { // code goes here to do something with
variable
; }
Using our collection of turtles, a foreach
loop would look like this:
foreach (string turtle in turtles) { Debug.Log($"Here comes {turtle} again!"); // the variable,
turtle
, is the actual value retrieved // from the collection or turtles--no indices needed }
The foreach
loop is particularly useful for looping through collections like dictionaries which aren’t indexed by number. Looping through a dictionary requires the use of a new data type we haven’t encountered yet called a KeyValuePair
. This is a special data type for storing key-value pairs of a dictionary item.
The syntax for the key value pair looks like this: KeyValuePair<data_type, data_type>
, where the data_types match the dictionary to loop through.
First, let’s create our dictionary:
Dictionary<string,string> ninjaTurtles = new Dictionary<string,string>() { {“Leonardo”, “Leader”}, {“Donatello”, “Hacker”}, {“Raphael”, “Brawler”}, {“Michelangelo”, “Party Boy”} };
Now we’ll use a KeyValuePair<string, string>
to match the data types of our dictionary.
foreach ( KeyValuePair<string, string> item in ninjaTurtles) { // Now we can access the key with item.Key // and the value with item.Value Debug.Log($"{item.Key} is the {item.Value}!"); }
This will print out:
Leonardo is the Leader! Donatello is the Hacker! Raphael is the Brawler! Michelangelo is the Party Boy!
Try it out
We’ve covered a lot of territory in this lesson. Understanding and using proper flow control is a fundamental aspect of writing code. The good thing is that these are just syntactic concepts to represent the way we naturally reason as human beings.
Let's get a little more practice before we move on to the next lesson.
You’re writing code for a quality assurance robot at a toy factory. The robot receives a list of toys each with a quality score assigned. Quality scores range between 0.0
(if it's utter crap) and 10.0
(perfect). It's the robot’s job to sort the toys into the following bins according to their quality scores.
Discard bin:
0 to 2.50
Re-fabrication bin:
2.51 to 6.50
Refinishing bin:
6.51 to 8.0
Shipping bin:
8.01 to 10.0
Here is your quality test data. Copy-and-paste this into your script.
Dictionary<string, float> toyScores = new Dictionary<string, float> { {"Z00M56R", 6.9}, {"Z00M14R", 7.8}, {"Z00M91R", 1.33}, {"Z00M28R", 9.93}, {"Z00M66R", 3.97}, {"Z00M72R", 6.5}, {"Z00M39R", 8.55}, {"Z00M87R", 2.509}, {"Z00M43R", 0.248}, {"Z00M15R", 4.41}, {"Z00M07R", 9.01}, }
Create suitable collections to represent the four different bins. All they need to store is the toy’s serial id.
Iterate through the toy scores and place each toy in its correct bin.
Once the toys are sorted, print out the contents of each bin. How many toys are in each bin? Print that out too.
Next, we’ll learn about functions and methods—the last topic of this introductory series to the fundamentals of C-sharp.
An Artist's Guide to C# in Unity3D