Chapter 4

Controlling the Flow of the Code

Up to this point, our code has run in a straight line. Every line executes, one after another, from top to bottom, without exception. That's fine for a program that does exactly the same thing every time, but it's not much good for a game. Games need to make decisions. Is the player touching the enemy? If yes, lose a life. Did the player collect the key? If yes, open the door. Is the score high enough for a bonus? If yes, award an extra life. If no, carry on.

This chapter is all about giving our code the power to make choices. We'll learn how to compare values, combine comparisons into more complex questions, and branch our code down different paths depending on what those questions answer. By the end of this chapter, our programs will finally be able to react to what's happening instead of just plowing on regardless.

In this chapter, we will:

Let's start with the tool that underpins everything else — comparing values.

Comparison Operators

To make decisions in code, we first need to ask questions that have yes-or-no answers. Is this number bigger than that number? Are these two values the same? Is this value different from that one? C++ gives us a set of comparison operators for exactly this purpose.

Every comparison operator takes two values, compares them, and gives back a result of type bool — either true or false. That's it. Ask a question, get a yes or no.

Here are the six comparison operators you'll use constantly:

int playerScore = 100;
int bossScore = 250;

bool result1 = (playerScore == bossScore);   // false — are they equal?
bool result2 = (playerScore != bossScore);   // true  — are they different?
bool result3 = (playerScore < bossScore);    // true  — is player less than boss?
bool result4 = (playerScore > bossScore);    // false — is player greater than boss?
bool result5 = (playerScore <= bossScore);   // true  — less than or equal?
bool result6 = (playerScore >= bossScore);   // false — greater than or equal?

In the preceding code, we take two int variables and compare them six different ways. Each comparison gives us a bool. The parentheses around each comparison are not strictly required, but they make the code easier to read. Get in the habit of using them.

There is one comparison operator that causes more beginner bugs than all the others combined, and it's ==.

When you want to check whether two values are equal, you use two equals signs: ==. When you want to assign a value to a variable, you use one equals sign: =. These two things look almost identical but do completely different jobs.

int lives = 3;               // Assignment — put 3 into lives
bool isDead = (lives == 0);  // Comparison — is lives equal to 0?

Mix these up and your code might compile but behave in ways you never expected. Writing if (lives = 0) instead of if (lives == 0) doesn't check whether lives is zero — it sets lives to zero and then asks whether zero is true or false. The compiler will usually warn you about this, but not always. Read your comparisons carefully, especially when things aren't working.

Now that we can ask single questions, let's learn how to ask several at once.

Logical Operators

A lot of game logic isn't just "is this one thing true?" It's "is this thing AND that thing true?" or "is this thing OR that other thing true?" C++ gives us three logical operators to combine comparisons into bigger, more useful questions.

The three logical operators are:

Here they are in action:

int playerScore = 150;
int playerLives = 2;
bool hasKey = true;

// AND: both must be true
bool canUnlockBonus = (playerScore > 100) && (playerLives > 0);   // true

// OR: at least one must be true
bool canContinue = (playerLives > 0) || hasKey;   // true

// NOT: flips the value
bool isGameOver = !canContinue;   // false

In the preceding code, we combine comparisons to ask bigger questions. The canUnlockBonus line asks "is the score over 100 AND are there lives remaining?" The canContinue line asks "are there lives remaining OR does the player have the key?" The isGameOver line just flips canContinue — if the player can continue, the game is not over.

A quick look at the logic in words:

These operators can be chained together to build up quite complex conditions:

bool canAccessSecretRoom = hasKey && (playerLives > 0) && (playerScore >= 500);

That condition is true only if the player has the key AND has lives remaining AND has a score of at least 500. All three conditions must hold. If any one of them fails, the whole thing is false.

One piece of advice: when your conditions start to get complicated, use parentheses generously to make the grouping obvious. (A && B) || C and A && (B || C) are not the same thing, and a reader of your code (including future you) shouldn't have to work out which one you meant.

if and else

Now we can compare values and combine those comparisons. Time to actually do something with the answers. This is where if statements come in.

The if statement is the most fundamental decision-making tool in C++. The structure looks like this:

if (condition) {
    // Code in here runs only if condition is true
}

The condition inside the parentheses must evaluate to a bool — true or false. If it's true, the code inside the curly braces runs. If it's false, the code is skipped.

Here's a real example:

int playerHealth = 0;

if (playerHealth <= 0) {
    // Player has died — handle game over
    std::cout << "Game Over!" << std::endl;
}

In the preceding code, we check whether playerHealth is zero or less. If it is, we print a game over message. If the player still has health, the code inside the braces is skipped entirely. You'll notice I've used std::cout here — it's the standard way to print text to the console in C++, and we'll see more of it throughout the book.

if / else

Often we want to do one thing if a condition is true, and a different thing if it's false. For that, we add an else block:

int playerScore = 150;

if (playerScore >= 100) {
    std::cout << "You qualify for the bonus round!" << std::endl;
} else {
    std::cout << "Keep trying — you need 100 points." << std::endl;
}

In the preceding code, exactly one of the two messages will be printed. If the score is 100 or higher, the first message appears. Otherwise, the second one does. It's impossible for both to run, and it's impossible for neither to run.

if / else if / else

Sometimes we have more than two possible paths. Maybe the player's score puts them in one of several tiers. For that we chain else if blocks together:

int playerScore = 750;

if (playerScore >= 1000) {
    std::cout << "Gold rank!" << std::endl;
} else if (playerScore >= 500) {
    std::cout << "Silver rank!" << std::endl;
} else if (playerScore >= 100) {
    std::cout << "Bronze rank!" << std::endl;
} else {
    std::cout << "Keep practicing!" << std::endl;
}

In the preceding code, C++ checks each condition in order from top to bottom. As soon as one of them is true, that block runs and the rest are skipped. If none of them are true, the final else block runs as a catch-all. In this example, the score of 750 isn't at least 1000, so we move on. It is at least 500, so "Silver rank!" prints and the rest of the chain is skipped.

The order matters. If I'd put the 100 check first, the player would be labeled Bronze even if their score was 5000 — the first true condition wins. Write your else if chains from most specific to most general, or most demanding to least demanding.

Nested if Statements

You can put an if statement inside another if statement. This is called nesting, and it's sometimes exactly what you need:

bool hasKey = true;
int playerLives = 3;

if (hasKey) {
    if (playerLives > 0) {
        std::cout << "You can open the door!" << std::endl;
    } else {
        std::cout << "You have the key, but you're dead." << std::endl;
    }
}

In the preceding code, the inner if only runs when the outer condition is true. We first check whether the player has the key. If they do, we then check whether they have any lives left.

Nesting is useful, but it can quickly get out of hand. Two levels deep is usually fine. Three is pushing it. Four or more and your code starts to look like the side of a pyramid. When things get deeply nested, it's often a sign that a single combined condition would read better:

if (hasKey && playerLives > 0) {
    std::cout << "You can open the door!" << std::endl;
}

That version is cleaner and says the same thing. Use nesting when the separate levels genuinely mean different things, not just to avoid learning &&.

Ternary Operator

There's a compact shorthand for if/else when you're just assigning one of two values to a variable. It's called the ternary operator, and it looks like this:

int playerLives = 3;
std::string status = (playerLives > 0) ? "Alive" : "Dead";

In the preceding code, we set the status string to either "Alive" or "Dead" depending on whether playerLives is greater than zero. The structure is:

condition ? value_if_true : value_if_false

The ? comes right after the condition, then the value to use if the condition is true, then a colon, then the value to use if it's false.

That same code written with a regular if/else would look like this:

std::string status;
if (playerLives > 0) {
    status = "Alive";
} else {
    status = "Dead";
}

Both versions do the same thing. The ternary version is five lines shorter and, for simple cases like this, arguably clearer.

A word of warning, though. The ternary operator is great when the condition and values are all short and easy to read. It becomes awful when it gets complicated. If you find yourself writing a ternary operator that runs over a line and a half, or if you're nesting ternaries inside ternaries, stop. Rewrite it as a regular if/else. Cleverness is not the goal here. Clarity is.

switch

When you have a single value that could match many possible cases — a menu selection, a game state, a direction — you can certainly use a long if/else if chain. But for these situations, C++ offers a specialized tool called switch, and it's often cleaner.

Here's the structure:

int menuChoice = 2;

switch (menuChoice) {
    case 1:
        std::cout << "Start New Game" << std::endl;
        break;
    case 2:
        std::cout << "Load Game" << std::endl;
        break;
    case 3:
        std::cout << "Options" << std::endl;
        break;
    case 4:
        std::cout << "Quit" << std::endl;
        break;
    default:
        std::cout << "Invalid choice" << std::endl;
        break;
}

In the preceding code, we check menuChoice against each case value in turn. When we find a match, the code for that case runs. The default block at the bottom runs if none of the cases match — it's the switch equivalent of a final else.

The break keyword at the end of each case is important. It tells C++ "we're done, get out of the switch statement." If you forget it, something unexpected happens, and we'll get to that in a moment.

switch has one significant limitation: it only works with integer-like types. That means int, char, and enumerations (which we'll meet later). You can't switch on a float, a double, or a std::string. For those, you'll need to use if/else if chains.

So when should you pick switch over if/else if? When you have one value being checked against many specific possibilities. Menu selections, game states, keyboard keys, and discrete categories are all perfect for switch. When your conditions involve ranges, multiple variables, or complex logic, stick with if.

Fall-Through

Here's a subtle feature of switch that catches beginners out. When a case matches, C++ starts executing code from that point onward, and it doesn't stop until it hits a break or the end of the switch. If you forget a break, execution "falls through" into the next case:

int difficulty = 1;

switch (difficulty) {
    case 1:
        std::cout << "Easy mode" << std::endl;
        // No break! Falls through to case 2
    case 2:
        std::cout << "Normal mode" << std::endl;
        break;
    case 3:
        std::cout << "Hard mode" << std::endl;
        break;
}

In the preceding code, because case 1 has no break, the player ends up with both "Easy mode" AND "Normal mode" printed. That's almost certainly a bug. Forgetting a break is one of the most common switch mistakes.

However, fall-through isn't always a mistake. Sometimes you genuinely want several cases to do the same thing, and leaving out the break is a neat way to do it:

char key = 'w';

switch (key) {
    case 'w':
    case 'W':
    case 'i':
    case 'I':
        std::cout << "Moving up" << std::endl;
        break;
    case 's':
    case 'S':
    case 'k':
    case 'K':
        std::cout << "Moving down" << std::endl;
        break;
}

In the preceding code, we deliberately let the first four cases fall through to the same block of code. Pressing w, W, i, or I all move the player up. This is intentional fall-through, and it's perfectly fine. Just be aware that a reader can't tell at a glance whether a missing break is deliberate or a bug, so it's good practice to add a comment like // fall through when you do it on purpose.

Short-Circuit Evaluation

Here's a useful detail about how && and || actually work.

When C++ evaluates a condition like A && B, it doesn't always bother to check B. If A is false, then the whole expression must be false no matter what B is, so C++ doesn't waste time checking. This is called short-circuit evaluation.

The same thing happens with ||. If A is true, the whole expression must be true regardless of B, so B is never checked.

Most of the time this is just a quiet performance win you never notice. But occasionally it's a very useful feature. Look at this:

std::string* namePointer = nullptr;   // A null pointer (we'll cover pointers in Chapter 10)

if (namePointer != nullptr && namePointer->length() > 0) {
    // Safe to use namePointer here
}

Don't worry about the pointer syntax yet — we'll cover it properly in Chapter 10. The important thing is the logic. The first condition checks whether the pointer is valid. The second condition uses the pointer. Thanks to short-circuit evaluation, if the first check fails, the second check is never attempted. This keeps our program safe from trying to use a pointer that isn't valid.

Without short-circuit evaluation, this pattern wouldn't work. You'd have to write a nested if, which is clunkier. Short-circuit evaluation is why the idiom if (pointer != nullptr && pointer->something) is safe and common.

It's also why the order of your conditions can matter. Put cheap or protective checks first, expensive or risky ones second. That way, short-circuiting does the right work in the right order.

goto (and Why Not to Use It)

C++ has a keyword called goto that lets you jump directly to a labeled point elsewhere in your code. It exists. It works. It's a historical leftover from older languages where it was genuinely useful.

In modern C++, you should essentially never use it. Every legitimate use case for goto is handled better by if, switch, loops (which we'll meet in Chapter 6), and functions (Chapter 8). Code that uses goto tends to become tangled and difficult to reason about — a reader has to follow a web of jumps to understand what's going on, rather than a clean top-to-bottom flow.

We're mentioning it here only so that if you ever see goto in someone else's code, you know what it is and why it's probably a red flag. That's all. Let's move on.

AI Exercise (Optional)

If you'd like to explore decision-making code a little further, here's an AI exercise to try.

Open your AI chatbot of choice. Paste in this prompt:

"I'm a C++ beginner. I've just learned about if, else, switch, and the logical operators &&, ||, and !. Please give me a small code snippet (10–20 lines) that uses all of these to model a simple game scenario — something like a player's attack that deals different damage based on multiple factors. Include comments explaining each decision. Then, at the end, point out one subtle bug or potential improvement in your own code so I can learn to spot issues myself."

Notice what the preceding prompt is doing. It tells the AI who you are, what you've just learned, the size and kind of example you want, and — crucially — asks the AI to critique its own output. That last part is clever. It flips the AI from "code generator" mode into "code reviewer" mode, which is often more useful for learning.

When you get the response, read it carefully. Can you follow every decision the code makes? Do you understand why each if and switch was used instead of the other? Does the bug or improvement the AI identified actually make sense to you? If any of it doesn't click, ask a follow-up question. "Why did you use switch here instead of else if?" is a perfectly good question, and the AI will usually give a thoughtful answer.

The goal isn't to get working code. The goal is to sharpen your intuition for when each tool is the right one.

Summary

You've just picked up the core decision-making vocabulary of C++. You can compare values, combine comparisons with logical operators, and branch your code using if, else if, else, switch, and the ternary operator. You understand short-circuit evaluation, which will save you from some genuinely nasty bugs later on. And you know about goto — mostly so you can avoid it.

Decision-making is half of what makes code interactive. The other half is repetition — doing something over and over until a condition changes. That's what loops are for, and they're coming up in Chapter 6. But first, in the next chapter, we'll put everything you've just learned to work in a real SDL project where decisions actually drive what appears on the screen.