Boolean Operations and Conditionals - Digital Prototyping - Introduction to Game Design, Prototyping, and Development (2015)

Introduction to Game Design, Prototyping, and Development (2015)

Part II: Digital Prototyping

Chapter 20. Boolean Operations and Conditionals

Most people have heard that computer data is, at its base level, composed entirely of 1s and 0s, bits that are either true or false. However, only programmers really understand how much of programming is about boiling a problem down to a true or false value and then responding to it.

In this chapter, you learn about Boolean operations like AND, OR, and NOT; you learn about comparison statements like >, <, ==, and !=; and you come to understand conditionals like if and switch. These all lie at the heart of programming.

Booleans

As you learned in the previous chapter, a bool is a variable that can hold a value of either true or false. Bools were named after George Boole, a mathematician who worked with true and false values and logical operations (now known as “Boolean operations”). Though computers did not exist at the time of his research, computer logic is fundamentally based upon it.

In C# programming, bools are used to store simple information about the state of the game (for example, bool gameOver = false; ) and to control the flow of the program through the if and switch statements covered later in this chapter.

Boolean Operations

Boolean operations allow programmers to modify or combine bool variables in meaningful ways.

!—The NOT Operator

The ! (either pronounced “not” or “bang”) operator reverses the value of a bool. False becomes true, or true becomes false:

print( !true ); // Outputs: false
print( !false ); // Outputs: true
print( !(!true) ); // Outputs: true (the double negative of true is true)

! is also sometimes referred to as the logical negation operator to differentiate it from ~ (the bitwise not operator), which is explained in the “Bitwise Boolean Operators and Layer Masks” section of Appendix B, “Useful Concepts.”

&&—The AND Operator

The && operator returns true only if both operands are true:

print( false && false ); // false
print( false && true ); // false
print( true && false ); // false
print( true && true ); // true

||—The OR Operator

The || operator returns true if either operand is true as well as if both are true:

print( false || false ); // false
print( false || true ); // true
print( true || false ); // true
print( true || true ); // true

Shorting Versus Non-Shorting Boolean Operators

The standard forms of AND and OR (&& and ||) are shorting operators, which means that if the shorting operator can determine its return value from the first argument, it will not bother to evaluate the second argument. By contrast, a non-shorting operator (& and |) will always evaluate both arguments completely. The following code listing includes several examples that illustrate the difference. In the code listing, a double slash followed by a number (e.g., // 1) to the right of a line indicates that there is an explanation of that line following the code listing.

1 // This function prints "--true" and returns a true value.
2 bool printAndReturnTrue() {
3 print( "--true" );
4 return( true );
5 }
6
7 // This function prints "--false" and returns a false value.
8 bool printAndReturnFalse() {
9 print( "--false" );
10 return( false );
11 }
12
13 void ShortingOperatorTest() {
14 // The first half of this test uses the shorting && and ||
15 bool tfAnd = ( printAndReturnTrue() && printAndReturnFalse() ); // 1
16 print( "tfAnd: "+tfAnd );
17
18 bool tfAnd2 = ( printAndReturnFalse() && printAndReturnTrue() ); // 2
19 print( "tfAnd2: "+tfAnd2 );
20
21 bool tfOr = ( printAndReturnTrue() || printAndReturnFalse() ); // 3
22 print( "tfOr: "+tfOr );
23
24 bool tfOr2 = ( printAndReturnFalse() || printAndReturnTrue() ); // 4
25 print( "tfOr2: "+tfOr2 );
26
27
28 // The second half of this test uses the non-shorting & and |
29 bool tfAnd3 = ( printAndReturnFalse() & printAndReturnTrue() ); // 5
30 print( "tfAnd3: "+tfAnd3 );
31
32 bool tfOr3 = (printAndReturnTrue() | printAndReturnFalse() ); // 6
33 print( "tfOr3: "+tfOr3 );
34
35 }

The numbers in the list below refer to lines in the preceding code that are marked with // 1 and // 2 to the right of the line.

1. This line will print --true and --false before setting tfAnd to false. Because the first argument that the shorting && operator evaluates is true, it must also evaluate the second argument to determine that the result is false.

2. This line only prints --false before setting tfAnd2 to false. Because the first argument that the shorting && operator evaluates is false, it returns false without evaluating the second argument at all. On this line, printAndReturnTrue() is not executed.

3. This line only prints --true before setting tfOr to true. Because the first argument that the shorting || operator evaluates is true, it returns true without evaluating the second.

4. This line prints --false and --true before setting tfOr2 to true. Because the first argument that the shorting || operator evaluates is false, it must evaluate the second argument to determine which value to return.

5. The non-shorting & operator will evaluate both arguments regardless of the value of the first argument. As a result, this line prints --false and --true before setting tfAnd3 to false.

6. The non-shorting | operator will evaluate both arguments regardless of the value of the first argument. This line prints --true and --false before setting tfOr3 to true.

It is useful to know about both shorting and non-shorting operators when writing your code. Shorting operators (&& and ||) are much more commonly used because they are more efficient, but & and | can be used when you want to ensure that you evaluate all of the arguments of a Boolean operator.

If you want, I recommend entering this code into Unity and running the debugger to step through the behavior and really understand what is happening. To learn about the debugger, read Chapter 24, “Debugging.”

Bitwise Boolean Operators

| and & are also sometimes referred to as bitwise OR and bitwise AND because they can also be used to perform bitwise operations on integers. These are useful for a few esoteric things having to do with collision detection in Unity; and you can learn more about them in the “Bitwise Boolean Operators and Layer Masks” section of Appendix B.

Combination of Boolean Operations

It is often useful to combine various Boolean operations in a single line:

bool tf = true || false && true;

However, care must be taken when doing so because order of operations extends to Boolean operations as well. In C#, the order of operations for Boolean operations is as follows:

! - NOT
& - Non-shorting AND / Bitwise AND
| - Non-shorting OR / Bitwise OR
&& - AND
|| - OR

This means that the previous line would be interpreted by the compiler as:

bool tf = true || ( false && true );

The && comparision is executed before the || comparison every time.


Tip

Regardless of the order of operations, you should always use parentheses for clarity in your code as often as possible. Good readability is critical in your code if you plan to ever work with someone else (or even if you want to read the same code yourself months later). I code by a simple rule: If there’s any chance at all that something might be misunderstood later, I use parentheses and add comments to clarify what I am doing in the code and how it will be interpreted by the computer.


Logical Equivalence of Boolean Operations

The depths of Boolean logic are beyond the scope of this book, but suffice to say, some very interesting things can be accomplished by combining Boolean operations. In the examples that follow a and b are bool variables, and the rules hold true regardless of whether a and b are true or false, and they are true regardless of whether the shorting or non-shorting operators are used:

Image ( a & b ) is the same as !( !a | !b )

Image ( a | b ) is the same as !( !a & !b )

Image Associativity: ( a & b ) & c is the same as a & ( b & c )

Image Commutativity: ( a & b ) is the same as ( b & a )

Image Distributivity of AND over OR: a & ( b | c ) is the same as ( a & b ) | ( a & c )

Image Distributivity of OR over AND: a | ( b & c ) is the same as ( a | b ) & ( a | c )

If you’re interested in more of these equivalencies and how they could be used, you can find many resources about Boolean logic online.

Comparison Operators

In addition to comparing Boolean values to each other, it is also possible to create a Boolean result value by using comparison operators on any other values.

== (Is Equal To)

The equality comparison operator checks to see whether the values of any two variables or literals are equivalent to each other. The result of this operator is a Boolean value of either true or false.

1 int i0 = 10;
2 int i1 = 10;
3 int i2 = 20;
4 float f0 = 1.23f;
5 float f1 = 3.14f;
6 float f2 = Mathf.PI;
7
8 print( i0 == i1 ); // Outputs: True
9 print( i1 == i2 ); // Outputs: False
10 print( i2 == 20 ); // Outputs: True
11 print( f0 == f1 ); // Outputs: False
12 print( f0 == 1.23f ); // Outputs: True
13 print( f1 == f2 ); // Outputs: False // 1

1. The comparison in line 13 is false because Math.PI is far more accurate than 3.14f, and == requires that the values be exactly equivalent.


Warning

Don’t Confuse = And == There is sometimes confusion between the assignment operator (=) and the equality operator (==). The assignment operator (=) is used to set the value of a variable while the equality operator (==) is used to compare two values. Consider the following code listing:

1 bool f = false;
2 bool t = true;
3 print( f == t ); // prints: False
4 print( f = t ); // prints: True

On line 3, f is compared to t, and because they are not equal, false is returned and printed. However, on line 4, f is assigned the value of t, causing the value of f to now be true, and true is printed.

Confusion is also sometimes an issue when talking about the two operators. To avoid confusion, I usually pronounce i=5; as “i equals 5,” and I pronounce i==5; as “i is equal to 5.”


See the “Testing Equality by Value or Reference” sidebar for more detailed information about how equality is handled for several different variable types.


Testing Equality by Value or Reference

Unity’s version of C# will compare most simple data types by value. This means that as long as the values of the two variables are the same, they will be seen as equivalent. This works for all of the following data types:

Image bool

Image int

Image float

Image char

Image string

Image Vector3

Image Color

Image Quaternion

However, with more complex variable types like GameObject, Material, Renderer, and so on, C# no longer checks to see whether all the values of the two variables are equal but instead checks to see if the references of the two variables are equal. In other words, it checks to see whether the two variables are referencing (or pointing to) the same single object in the computer’s memory. (For the following example we’ll assume that boxPrefab is a preexisting variable that references a GameObject prefab.)

1 GameObject go0 = Instantiate( boxPrefab ) as GameObject;
2 GameObject go1 = Instantiate( boxPrefab ) as GameObject;
3 GameObject go2 = go0;
4 print( go0 == go1 ); // Output: false
5 print( go0 == go2 ); // Output: true

Even though the two instantiated boxPrefabs assigned to the variables go0 and go1 have the same values (they have the exact same default position, rotation, and so on) the == operator sees them as different because they are actually two different objects, and therefore reside in two different places in memory. go0 and go2 are seen as equal by == because they both refer to the exact same object. Let’s continue the previous code:

6 go0.transform.position = new Vector3( 10, 20, 30)
7 print( go0.transform.position); // Output: (10.0, 20.0, 30.0)
8 print( go1.transform.position); // Output: ( 0.0, 0.0, 0.0)
9 print( go2.transform.position); // Output: (10.0, 20.0, 30.0)

Here, the position of go0 is changed. Because go1 is a different GameObject instance, its position remains the same. However, since go2 and go0 reference the same GameObject instance, go2.transform.position reflects the change as well.


!= (Not Equal To)

The inequality operator returns true if two values are not equal and false if they are equal. It is the opposite of ==. (For the remaining comparisons, literal values will be used in the place of variables for the sake of clarity and space.)

print( 10 != 10 ); // Outputs: False
print( 10 != 20 ); // Outputs: True
print( 1.23f != 3.14f ); // Outputs: True
print( 1.23f != 1.23f ); // Outputs: False
print( 3.14f != Mathf.PI ); // Outputs: True

> (Greater Than) and < (Less Than)

> returns true if the value on the left side of the operator is greater than the value on the right:

print( 10 > 10 ); // Outputs: False
print( 20 > 10 ); // Outputs: True
print( 1.23f > 3.14f ); // Outputs: False
print( 1.23f > 1.23f ); // Outputs: False
print( 3.14f > 1.23f ); // Outputs: True

< returns true if the value on the left side of the operator is less than the value on the right:

print( 10 < 10 ); // Outputs: False
print( 20 < 10 ); // Outputs: True
print( 1.23f < 3.14f ); // Outputs: True
print( 1.23f < 1.23f ); // Outputs: False
print( 3.14f < 1.23f ); // Outputs: False

The characters < and > are also sometimes referred to as angle brackets, especially when they are used as tags in languages like HTML and XML or in generic functions in C#. However, when they are used as comparison operators, they are always called greater than and less than.

>= (Greater Than or Equal To) and <= (Less Than or Equal To)

>= returns true if the value on the left side is greater than or equivalent to the value on the right:

print( 10 >= 10 ); // Outputs: True
print( 10 >= 20 ); // Outputs: False
print( 1.23f >= 3.14f ); // Outputs: False
print( 1.23f >= 1.23f ); // Outputs: True
print( 3.14f >= 1.23f ); // Outputs: True

<= returns true if the value on the left side is less than or equal to the value on the right:

print( 10 <= 10 ); // Outputs: True
print( 10 <= 20 ); // Outputs: True
print( 1.23f <= 3.14f ); // Outputs: True
print( 1.23f <= 1.23f ); // Outputs: True
print( 3.14f <= 1.23f ); // Outputs: False

Conditional Statements

Conditional statements can be combined with Boolean values and comparison operators to control the flow of your programs. This means that a true value can cause the code to generate one result while a false value can cause it to generate another. The two most common forms of conditional statements are if and switch.

if Statements

An if statement will only execute the code inside its braces {} if the value inside its parentheses () evaluates to true.

if (true) {
print( "The code in the first if statement executed." );
}
if (false) {
print( "The code in the second if statement executed." );
}

// The output of this code will be:
// The code in the first if statement executed.

The code inside the braces {} of the first if statement executes, yet the code inside the braces of the second if statement does not.


Note

Statements enclosed in braces do not require a semicolon after the closing brace. Other statements that have been covered all require a semicolon at the end:

float approxPi = 3.14159f; // There's the standard semicolon

Compound statements (that is, those surrounded by braces) do not require a semicolon after the closing brace:

if (true) {
print( "Hello" ); // This line needs a semicolon.
print( "World" ); // This line needs a semicolon.
} // No semicolon required after the closing brace!

The same is true for any compound statement surrounded by braces.


Combining if Statements with Comparison Operators and Boolean Operators

Boolean operators can be combined with if statements to react to various situations in your game:

bool night = true;
bool fullMoon = false;

if (night) {
print( "It's night." );
}
if (!fullMoon) {
print( "The moon is not full." );
}
if (night && fullMoon) {
print( "Beware werewolves!!!" );
}
if (night && !fullMoon) {
print( "No werewolves tonight. (Whew!)" );
}

// The output of this code will be:
// It's night.
// The moon is not full.
// No werewolves tonight. (Whew!)

And, of course, if statements can also be combined with comparison operators:

if (10 == 10 ) {
print( "10 is equal to 10." );
}
if ( 10 > 20 ) {
print( "10 is greater than 20." );
}
if ( 1.23f <= 3.14f ) {
print( "1.23 is less than or equal to 3.14." );
}
if ( 1.23f >= 1.23f ) {
print( "1.23 is greater than or equal to 1.23." );
}
if ( 3.14f != Mathf.PI ) {
print( "3.14 is not equal to "+Mathf.PI+"." );
// + can be used to concatenate strings with other data types.
// When this happens, the other data type is converted to a string.
}

// The output of this code will be:
// 10 is equal to 10.
// 1.23 is less than or equal to 3.14.
// 1.23 is greater than or equal to 1.23.
// 3.14 is not equal to 3.141593.


Warning

Avoid Using = in an if Statement As was mentioned in the previous warning, == is a comparison operator that determines whether two values are equivalent. = is an assignment operator that assigns a value to a variable. If you accidentally use = in an if statement, the result will be an assignment instead of a comparison.

Sometimes Unity will catch this by giving you an error about not being able to implicitly convert a value to a Boolean. You will get that error from this code:

float f0 = 10f;
if ( f0 = 10 ) {
print( "f0 is equal to 10.");
}

Other times, Unity will actually give you a warning stating that it found an = in an if statement and asking if you meant to type ==.


if...else

Many times, you will want to do one thing if a value is true and another if it is false. At these times, an else clause is added to the if statement:

bool night = false;

if (night) {
print( "It's night." );
} else {
print( "It's daytime. What are you worried about?" );
}

// The output of this code will be:
// It's daytime. What are you worried about?

In this case, because night is false, the code in the else clause is executed.

if...else if...else

It’s also possible to have a chain of else clauses:

bool night = true;
bool fullMoon = true;

if (!night) { // Condition 1 (false)
print( "It's daytime. What are you worried about?" );
} else if (fullMoon) { // Condition 2 (true)
print( "Beware werewolves!!!" );
} else { // Condition 3 (not checked)
print( "It's night, but the moon is not full." );
}

// The output of this code will be:
// Beware werewolves!!!

Once any condition in the if...else if...else chain evaluates to true, all subsequent conditions are no longer evaluated (the rest of the chain is shorted). In the previous listing, Condition 1 is false, so Condition 2 is checked. Because Condition 2 is true, the computer will completely skip Condition 3.

Nesting if Statements

It is also possible to nest if statements inside of each other for more complex 'margin-top:6.0pt;margin-right:0cm;margin-bottom:6.0pt; margin-left:0cm;line-height:normal'>

bool night = true;
bool fullMoon = false;

if (!night) {
print( "It's daytime. What are you worried about?" );
} else {
if (fullMoon) {
print( "Beware werewolves!!!" );
} else {
print( "It's night, but the moon is not full." );
}
}

// The output of this code will be:
// It's night, but the moon is not full.

switch Statements

A switch statement can take the place of several if...else statements, but it has some strict limitations:

1. Switch statements can only compare for equality.

2. Switch statements can only compare a single variable.

3. Switch statements can only compare that variable against literals (not other variables).

Here is an example:

int num = 3;

switch (num) { // The variable in parentheses (num) is the one being compared
case (0): // Each case is a literal number that is compared against num
print( "The number is zero." );
break; // Each case must end with a break statement.
case (1):
print( "The number is one." );
break;
case (2):
print( "The number is two." );
break;
default: // If none of the other cases are true, default will happen
print( "The number is more than a couple." );
break;
} // The switch statement ends with a closing brace.

// The output of this code is:
// The number is more than a couple.

If one of the cases holds a literal with the same value as the variable being checked, the code in that case is executed until the break is reached. Once the computer hits the break, it exits the switch and does not evaluate any other cases.

It is also possible to have one case “fall through” to the next if there are no lines of code between the two:

int num = 4;

switch (num) {
case (0):
print( "The number is zero." );
break;
case (1):
print( "The number is one." );
break;
case (2):
print( "The number is a couple." );
break;
case (3):
case (4):
case (5):
print( "The number is a few." );
break;
default:
print( "The number is more than a few." );
break;
}

// The output of this code is:
// The number is a few.

In the previous code, if num is equal to 3, 4, or 5, the output will be The number is a few.

Knowing what you know about combining conditionals and if statements, you might question when switch statements are used, since they have so many limitations. They are used quite often to deal with the different possible states of a GameObject. For instance, if you made a game where the player could transform into a person, bird, fish, or wolverine, there might be a chunk of code that looked like this:

string species = "fish";
bool inWater = false;

// Each different species type will move differently
public function Move() {
switch (species) {
case ("person"):
Run(); // Calls a function named Run()
break;
case ("bird"):
Fly();
break;
case ("fish"):
if (!inWater) {
Swim();
} else {
FlopAroundPainfully();
}
break;
case ("wolverine"):
Scurry();
break;
default:
print( "Unknown species type: "+species );
break;
}
}

In the preceding code, the player (as a fish in water) would Swim(). It’s important to note that the default case here is used to catch any species that the switch statement isn’t ready for and that it will output information about any unexpected species it comes across. For instance, ifspecies were set to "lion", the output would be:

Unknown species type: lion

In the preceding code syntax, you also see the names of several functions that are not yet defined (e.g., Run(), Fly(), Swim()). Chapter 23, “Functions and Parameters,” covers the creation of your own functions.

Summary

Though Boolean operations may seem a bit dry, they form a big part of the core of programming. Computer programs are full of hundreds, even thousands, of branch points where the computer can do either one thing or another, and these all boil down in some way to Booleans and comparisons. As you continue through the book, you may want to return to this section from time to time if you’re ever confused by any comparisons in the code you see.