Control Flow: If Statements and Loops

Control flow is how your program decides what to do and when to do it. It's the foundation of all game logic—from checking if a player has enough health to attack, to looping through enemies to update their AI, to creating complex decision trees for game mechanics.

In this chapter, you'll learn how to make your code make decisions and repeat actions, which are essential skills for any game developer. By the end, you'll be able to create conditional logic, handle different game states, and write efficient loops that power your game's core mechanics.

What You'll Learn

  • If statements - Making decisions based on conditions
  • Switch statements - Handling multiple conditions elegantly
  • For loops - Repeating code a specific number of times
  • While loops - Repeating code while a condition is true
  • Foreach loops - Iterating through collections
  • Nested control structures - Combining control flow for complex logic
  • Practical game examples - Real-world applications in Unity

Prerequisites


Understanding Control Flow

Control flow determines the order in which your code executes. Without control flow, your program would run from top to bottom, line by line, with no way to make decisions or repeat actions.

Why Control Flow Matters in Games:

  • Decision Making: Should the player take damage? Is the enemy in range? Can the player afford this item?
  • Repetition: Update all enemies every frame, check all collectibles, process all particles
  • State Management: Handle different game states (menu, playing, paused, game over)
  • Event Handling: Respond to player input, collisions, and game events

If Statements: Making Decisions

If statements let your code make decisions based on conditions. They're the most fundamental control structure in programming.

Basic If Statement

The simplest form checks a condition and executes code if it's true:

int playerHealth = 100;

if (playerHealth > 0)
{
    Debug.Log("Player is alive!");
}

Syntax:

if (condition)
{
    // Code to execute if condition is true
}

If-Else Statements

Add an alternative path when the condition is false:

int playerHealth = 50;

if (playerHealth > 0)
{
    Debug.Log("Player is alive!");
}
else
{
    Debug.Log("Player is dead!");
}

If-Else If-Else Chains

Handle multiple conditions:

int playerHealth = 75;

if (playerHealth >= 100)
{
    Debug.Log("Player is at full health!");
}
else if (playerHealth >= 50)
{
    Debug.Log("Player is healthy.");
}
else if (playerHealth >= 25)
{
    Debug.Log("Player is wounded.");
}
else
{
    Debug.Log("Player is critically injured!");
}

Comparison Operators

Use these operators to create conditions:

int score = 1000;
int highScore = 500;

// Equality
if (score == highScore) { }  // Equal to
if (score != highScore) { } // Not equal to

// Comparison
if (score > highScore) { }  // Greater than
if (score < highScore) { }  // Less than
if (score >= highScore) { } // Greater than or equal
if (score <= highScore) { } // Less than or equal

Logical Operators

Combine multiple conditions:

int playerHealth = 75;
bool hasKey = true;

// AND operator - both conditions must be true
if (playerHealth > 0 && hasKey)
{
    Debug.Log("Player can open the door!");
}

// OR operator - at least one condition must be true
if (playerHealth <= 0 || !hasKey)
{
    Debug.Log("Player cannot proceed.");
}

// NOT operator - reverses the condition
if (!hasKey)
{
    Debug.Log("Player needs a key!");
}

Game Example: Health System

Here's a practical example using if statements for a health system:

using UnityEngine;

public class PlayerHealth : MonoBehaviour
{
    public int maxHealth = 100;
    private int currentHealth;

    void Start()
    {
        currentHealth = maxHealth;
    }

    public void TakeDamage(int damage)
    {
        currentHealth -= damage;

        if (currentHealth <= 0)
        {
            currentHealth = 0;
            Die();
        }
        else if (currentHealth < maxHealth * 0.3f)
        {
            Debug.Log("Low health warning!");
            // Play low health sound effect
        }
    }

    public void Heal(int amount)
    {
        if (currentHealth < maxHealth)
        {
            currentHealth += amount;

            if (currentHealth > maxHealth)
            {
                currentHealth = maxHealth;
            }
        }
    }

    void Die()
    {
        Debug.Log("Player has died!");
        // Handle death logic
    }
}

Switch Statements: Multiple Conditions

Switch statements are perfect when you have many possible values to check. They're cleaner than long if-else chains.

Basic Switch Statement

int gameState = 1;

switch (gameState)
{
    case 0:
        Debug.Log("Main Menu");
        break;
    case 1:
        Debug.Log("Playing");
        break;
    case 2:
        Debug.Log("Paused");
        break;
    case 3:
        Debug.Log("Game Over");
        break;
    default:
        Debug.Log("Unknown state");
        break;
}

Syntax:

switch (variable)
{
    case value1:
        // Code for value1
        break;
    case value2:
        // Code for value2
        break;
    default:
        // Code if no case matches
        break;
}

Switch with Strings

Switch works with strings too:

string weaponType = "sword";

switch (weaponType)
{
    case "sword":
        Debug.Log("Slashing attack!");
        break;
    case "bow":
        Debug.Log("Ranged attack!");
        break;
    case "staff":
        Debug.Log("Magic attack!");
        break;
    default:
        Debug.Log("Unknown weapon!");
        break;
}

Switch Expressions (C# 8.0+)

Modern C# allows switch expressions for cleaner code:

string weaponType = "sword";
int damage = weaponType switch
{
    "sword" => 10,
    "bow" => 8,
    "staff" => 12,
    _ => 5  // Default case
};

Game Example: Game State Manager

Here's a practical game state manager using switch:

using UnityEngine;

public enum GameState
{
    MainMenu,
    Playing,
    Paused,
    GameOver,
    Victory
}

public class GameStateManager : MonoBehaviour
{
    private GameState currentState = GameState.MainMenu;

    public void ChangeState(GameState newState)
    {
        // Exit current state
        ExitState(currentState);

        // Enter new state
        currentState = newState;
        EnterState(currentState);
    }

    void EnterState(GameState state)
    {
        switch (state)
        {
            case GameState.MainMenu:
                Debug.Log("Entering Main Menu");
                // Show menu UI
                break;
            case GameState.Playing:
                Debug.Log("Starting Game");
                // Hide menu, start gameplay
                Time.timeScale = 1f;
                break;
            case GameState.Paused:
                Debug.Log("Game Paused");
                // Show pause menu
                Time.timeScale = 0f;
                break;
            case GameState.GameOver:
                Debug.Log("Game Over");
                // Show game over screen
                break;
            case GameState.Victory:
                Debug.Log("Victory!");
                // Show victory screen
                break;
        }
    }

    void ExitState(GameState state)
    {
        // Handle state exit logic if needed
    }
}

For Loops: Repeating Code

For loops repeat code a specific number of times. They're perfect when you know exactly how many iterations you need.

Basic For Loop

// Count from 0 to 9
for (int i = 0; i < 10; i++)
{
    Debug.Log("Count: " + i);
}

Syntax:

for (initialization; condition; increment)
{
    // Code to repeat
}

Parts:

  • Initialization: Runs once at the start (e.g., int i = 0)
  • Condition: Checked before each iteration (e.g., i < 10)
  • Increment: Runs after each iteration (e.g., i++)

Common For Loop Patterns

// Count up
for (int i = 0; i < 10; i++)
{
    Debug.Log(i);
}

// Count down
for (int i = 10; i > 0; i--)
{
    Debug.Log(i);
}

// Count by 2s
for (int i = 0; i < 10; i += 2)
{
    Debug.Log(i); // 0, 2, 4, 6, 8
}

// Count with custom step
for (int i = 100; i >= 0; i -= 10)
{
    Debug.Log(i); // 100, 90, 80, ..., 0
}

Game Example: Spawning Enemies

Here's how to use a for loop to spawn multiple enemies:

using UnityEngine;

public class EnemySpawner : MonoBehaviour
{
    public GameObject enemyPrefab;
    public int enemyCount = 5;
    public float spawnRadius = 10f;

    void Start()
    {
        SpawnEnemies();
    }

    void SpawnEnemies()
    {
        for (int i = 0; i < enemyCount; i++)
        {
            // Calculate random position
            Vector2 randomPosition = Random.insideUnitCircle * spawnRadius;
            Vector3 spawnPosition = new Vector3(randomPosition.x, 0, randomPosition.y);

            // Instantiate enemy
            Instantiate(enemyPrefab, spawnPosition, Quaternion.identity);

            Debug.Log("Spawned enemy " + (i + 1));
        }
    }
}

Nested For Loops

You can put loops inside loops for 2D operations:

// Create a grid of objects
for (int x = 0; x < 10; x++)
{
    for (int y = 0; y < 10; y++)
    {
        Vector3 position = new Vector3(x, 0, y);
        // Create object at position
        Debug.Log($"Grid position: ({x}, {y})");
    }
}

While Loops: Conditional Repetition

While loops repeat code as long as a condition is true. Use them when you don't know how many iterations you'll need.

Basic While Loop

int count = 0;

while (count < 5)
{
    Debug.Log("Count: " + count);
    count++; // Important: Update the condition!
}

Syntax:

while (condition)
{
    // Code to repeat
    // Must update condition to avoid infinite loop!
}

Do-While Loops

Do-while loops always execute at least once:

int count = 0;

do
{
    Debug.Log("Count: " + count);
    count++;
} while (count < 5);

Difference: The condition is checked after the first execution.

Game Example: Health Regeneration

Here's a practical example using a while loop for health regeneration:

using UnityEngine;
using System.Collections;

public class HealthRegeneration : MonoBehaviour
{
    public int maxHealth = 100;
    private int currentHealth = 50;
    public int regenRate = 1;
    public float regenInterval = 1f;

    void Start()
    {
        StartCoroutine(RegenerateHealth());
    }

    IEnumerator RegenerateHealth()
    {
        while (currentHealth < maxHealth)
        {
            currentHealth += regenRate;

            if (currentHealth > maxHealth)
            {
                currentHealth = maxHealth;
            }

            Debug.Log($"Health: {currentHealth}/{maxHealth}");
            yield return new WaitForSeconds(regenInterval);
        }

        Debug.Log("Health fully regenerated!");
    }
}

Infinite Loops Warning

Never create infinite loops accidentally:

// BAD - This will freeze your game!
while (true)
{
    Debug.Log("This runs forever!");
}

// GOOD - Always have an exit condition
bool isRunning = true;
while (isRunning)
{
    // Do something
    if (someCondition)
    {
        isRunning = false; // Exit the loop
    }
}

Foreach Loops: Iterating Collections

Foreach loops iterate through collections like arrays and lists. They're simpler than for loops when you just need to process each item.

Basic Foreach Loop

string[] weapons = { "sword", "bow", "staff", "dagger" };

foreach (string weapon in weapons)
{
    Debug.Log("Weapon: " + weapon);
}

Syntax:

foreach (type variableName in collection)
{
    // Code to execute for each item
}

Game Example: Processing Enemies

Here's how to use foreach to update all enemies:

using UnityEngine;
using System.Collections.Generic;

public class EnemyManager : MonoBehaviour
{
    public List<GameObject> enemies = new List<GameObject>();

    void Update()
    {
        // Update all enemies
        foreach (GameObject enemy in enemies)
        {
            if (enemy != null)
            {
                // Update enemy AI, health, etc.
                EnemyAI enemyAI = enemy.GetComponent<EnemyAI>();
                if (enemyAI != null)
                {
                    enemyAI.UpdateAI();
                }
            }
        }
    }

    public void RemoveDeadEnemies()
    {
        // Remove null or destroyed enemies
        enemies.RemoveAll(enemy => enemy == null);
    }
}

Foreach Limitations

You can't modify the collection while iterating:

// BAD - This will cause an error!
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
foreach (int number in numbers)
{
    if (number % 2 == 0)
    {
        numbers.Remove(number); // ERROR!
    }
}

// GOOD - Create a new list or use a for loop backwards
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
for (int i = numbers.Count - 1; i >= 0; i--)
{
    if (numbers[i] % 2 == 0)
    {
        numbers.RemoveAt(i); // Safe!
    }
}

Combining Control Structures

You can combine control structures to create complex logic:

Nested If Statements

int playerLevel = 15;
int playerGold = 500;

if (playerLevel >= 10)
{
    if (playerGold >= 1000)
    {
        Debug.Log("Player can buy premium item!");
    }
    else
    {
        Debug.Log("Player needs more gold.");
    }
}
else
{
    Debug.Log("Player level too low.");
}

Loops with Conditions

// Find first enemy with low health
GameObject[] enemies = GameObject.FindGameObjectsWithTag("Enemy");

for (int i = 0; i < enemies.Length; i++)
{
    EnemyHealth enemyHealth = enemies[i].GetComponent<EnemyHealth>();

    if (enemyHealth != null && enemyHealth.currentHealth < 20)
    {
        Debug.Log("Found weak enemy!");
        // Attack this enemy
        break; // Exit loop early
    }
}

Game Example: Inventory System

Here's a complete example combining multiple control structures:

using UnityEngine;
using System.Collections.Generic;

public class Inventory : MonoBehaviour
{
    public List<Item> items = new List<Item>();
    public int maxInventorySize = 20;

    public bool AddItem(Item newItem)
    {
        // Check if inventory is full
        if (items.Count >= maxInventorySize)
        {
            Debug.Log("Inventory is full!");
            return false;
        }

        // Check if item already exists (stackable items)
        foreach (Item item in items)
        {
            if (item.itemName == newItem.itemName && item.isStackable)
            {
                item.quantity += newItem.quantity;
                Debug.Log($"Added {newItem.quantity} {newItem.itemName}");
                return true;
            }
        }

        // Add new item
        items.Add(newItem);
        Debug.Log($"Added {newItem.itemName} to inventory");
        return true;
    }

    public void UseItem(string itemName)
    {
        foreach (Item item in items)
        {
            if (item.itemName == itemName)
            {
                if (item.quantity > 0)
                {
                    item.quantity--;
                    Debug.Log($"Used {itemName}");

                    // Remove item if quantity is 0
                    if (item.quantity <= 0)
                    {
                        items.Remove(item);
                    }
                }
                return;
            }
        }

        Debug.Log($"Item {itemName} not found in inventory");
    }

    public void DisplayInventory()
    {
        Debug.Log("=== INVENTORY ===");

        if (items.Count == 0)
        {
            Debug.Log("Inventory is empty");
        }
        else
        {
            for (int i = 0; i < items.Count; i++)
            {
                Item item = items[i];
                if (item.isStackable)
                {
                    Debug.Log($"{i + 1}. {item.itemName} x{item.quantity}");
                }
                else
                {
                    Debug.Log($"{i + 1}. {item.itemName}");
                }
            }
        }
    }
}

[System.Serializable]
public class Item
{
    public string itemName;
    public int quantity = 1;
    public bool isStackable = false;
}

Control Flow Best Practices

Follow these best practices to write clean, efficient control flow code.

1. Use Meaningful Conditions

// BAD - Unclear what this checks
if (x) { }

// GOOD - Clear and descriptive
if (playerHealth > 0) { }
if (isGamePaused) { }
if (enemyCount > 0) { }

2. Avoid Deep Nesting

// BAD - Too nested, hard to read
if (condition1)
{
    if (condition2)
    {
        if (condition3)
        {
            // Code
        }
    }
}

// GOOD - Use early returns or combine conditions
if (!condition1) return;
if (!condition2) return;
if (!condition3) return;
// Code

3. Use Switch for Multiple Values

// BAD - Long if-else chain
if (weaponType == "sword") { }
else if (weaponType == "bow") { }
else if (weaponType == "staff") { }
// ... many more

// GOOD - Clean switch statement
switch (weaponType)
{
    case "sword": break;
    case "bow": break;
    case "staff": break;
}

4. Avoid Magic Numbers

// BAD - What does 10 mean?
if (playerHealth < 10) { }

// GOOD - Use named constants
const int LOW_HEALTH_THRESHOLD = 10;
if (playerHealth < LOW_HEALTH_THRESHOLD) { }

5. Break Out of Loops Early

// GOOD - Exit loop when condition is met
for (int i = 0; i < enemies.Length; i++)
{
    if (enemies[i].IsDead())
    {
        AttackEnemy(enemies[i]);
        break; // Found target, exit loop
    }
}

Common Mistakes and How to Avoid Them

Mistake 1: Forgetting Break in Switch

// BAD - Falls through to next case
switch (state)
{
    case 0:
        DoSomething();
        // Missing break!
    case 1:
        DoSomethingElse();
        break;
}

// GOOD - Always include break
switch (state)
{
    case 0:
        DoSomething();
        break;
    case 1:
        DoSomethingElse();
        break;
}

Mistake 2: Infinite Loops

// BAD - Condition never changes
int count = 0;
while (count < 10)
{
    Debug.Log(count);
    // Forgot to increment count!
}

// GOOD - Always update loop condition
int count = 0;
while (count < 10)
{
    Debug.Log(count);
    count++; // Update condition
}

Mistake 3: Off-by-One Errors

// BAD - Goes one too far
for (int i = 0; i <= array.Length; i++) // Should be <
{
    Debug.Log(array[i]); // Will crash on last iteration
}

// GOOD - Correct bounds
for (int i = 0; i < array.Length; i++)
{
    Debug.Log(array[i]);
}

Mistake 4: Modifying Collection During Iteration

// BAD - Modifies collection while iterating
foreach (GameObject enemy in enemies)
{
    if (enemy.IsDead())
    {
        enemies.Remove(enemy); // ERROR!
    }
}

// GOOD - Iterate backwards or create new list
for (int i = enemies.Count - 1; i >= 0; i--)
{
    if (enemies[i].IsDead())
    {
        enemies.RemoveAt(i); // Safe!
    }
}

Practice Exercises

Try these exercises to reinforce your understanding:

Exercise 1: Health Check System

Create a system that checks player health and displays appropriate messages:

  • Full health (100%): "Perfect condition!"
  • Healthy (50-99%): "Feeling good"
  • Wounded (25-49%): "Take care"
  • Critical (1-24%): "Danger!"
  • Dead (0%): "Game Over"

Exercise 2: Score Multiplier

Create a score system that applies multipliers based on combo count:

  • Combo 0-4: 1x multiplier
  • Combo 5-9: 2x multiplier
  • Combo 10-19: 3x multiplier
  • Combo 20+: 5x multiplier

Exercise 3: Enemy Spawner

Create a system that spawns enemies in a grid pattern:

  • Use nested for loops
  • Spawn 5x5 grid of enemies
  • Each enemy should have a unique position

Exercise 4: Item Finder

Create a function that searches through an inventory:

  • Use foreach to iterate items
  • Find items with quantity less than 5
  • Return list of items that need restocking

Troubleshooting

Issue: Loop Runs Forever

Problem: Your while loop never exits.

Solution: Ensure you update the condition variable inside the loop.

// Make sure to update the condition
int count = 0;
while (count < 10)
{
    count++; // Don't forget this!
}

Issue: Switch Statement Executes Multiple Cases

Problem: Multiple cases execute when only one should.

Solution: Add break; after each case.

switch (value)
{
    case 1:
        DoSomething();
        break; // Don't forget break!
    case 2:
        DoSomethingElse();
        break;
}

Issue: Array Index Out of Bounds

Problem: Trying to access array element that doesn't exist.

Solution: Always check bounds before accessing.

// Check bounds first
if (index >= 0 && index < array.Length)
{
    Debug.Log(array[index]);
}

Summary

Control flow is essential for creating interactive, dynamic games. You've learned:

  • If statements - Make decisions based on conditions
  • Switch statements - Handle multiple conditions elegantly
  • For loops - Repeat code a specific number of times
  • While loops - Repeat code while condition is true
  • Foreach loops - Iterate through collections easily
  • Nested structures - Combine control flow for complex logic

Key Takeaways:

  • Use if statements for simple decisions
  • Use switch for multiple related values
  • Use for loops when you know the iteration count
  • Use while loops when iteration count is unknown
  • Use foreach for iterating collections
  • Always update loop conditions to avoid infinite loops
  • Break out of loops early when possible

Next Steps: Now that you understand control flow, you're ready to learn about functions and methods, which will help you organize your code and make it reusable. Functions are the building blocks of clean, maintainable game code.


Next Chapter

Ready to organize your code better? In the next chapter, you'll learn about Functions and Methods - how to create reusable code blocks that make your game development more efficient and your code easier to maintain.

Continue to Chapter 3: Functions and Methods →


Additional Resources

Related Guides:

Practice:

  • Try modifying the examples in this chapter
  • Create your own game systems using control flow
  • Experiment with different loop types
  • Build a simple game mechanic using if statements and loops

Keep practicing, and soon control flow will become second nature!