3.3 - Looping Constructs
Loops are essential programming constructs that allow you to execute a block of code repeatedly. They're crucial for tasks like processing collections of data, implementing game mechanics that repeat over time, or continuing actions until certain conditions are met.
Why Loops Matter in Game Development
In game development, loops are used for numerous tasks:
- Updating all game objects each frame
- Processing input from multiple players
- Spawning multiple enemies or objects
- Applying effects to all objects in an area
- Pathfinding and AI decision-making
- Particle systems and visual effects
- Loading and saving game data
Without loops, you'd need to write the same code multiple times, making your programs longer, harder to maintain, and less flexible.
Types of Loops in C#
C# provides four main types of loops:
forloop: Executes a block of code a specific number of timesforeachloop: Iterates through elements in a collectionwhileloop: Executes a block of code as long as a condition is truedo-whileloop: Executes a block of code at least once, then continues as long as a condition is true
Let's explore each type in detail.
The for Loop
The for loop is ideal when you know exactly how many times you want to execute a block of code.
Basic Syntax
for (initialization; condition; iteration)
{
// Code to execute in each iteration
}
- Initialization: Executed once before the loop starts
- Condition: Checked before each iteration; the loop continues if it's true
- Iteration: Executed after each iteration
Examples
// Count from 1 to 10
for (int i = 1; i <= 10; i++)
{
Console.WriteLine(i);
}
// Countdown from 10 to 1
for (int i = 10; i >= 1; i--)
{
Console.WriteLine(i);
}
// Process every other element in an array
int[] scores = { 85, 92, 78, 95, 88 };
for (int i = 0; i < scores.Length; i += 2)
{
Console.WriteLine($"Score at position {i}: {scores[i]}");
}
Game Development Examples
// Spawn 5 enemies at random positions
for (int i = 0; i < 5; i++)
{
Vector3 randomPosition = new Vector3(
Random.Range(-10f, 10f),
0,
Random.Range(-10f, 10f)
);
SpawnEnemy(randomPosition);
}
// Create a grid of tiles (2D)
for (int x = 0; x < gridWidth; x++)
{
for (int y = 0; y < gridHeight; y++)
{
Vector3 position = new Vector3(x * tileSize, 0, y * tileSize);
CreateTile(position, x, y);
}
}
// Apply damage over time for 5 seconds
float damageInterval = 1.0f;
float totalDuration = 5.0f;
for (float time = 0; time < totalDuration; time += damageInterval)
{
ApplyDamage(10);
yield return new WaitForSeconds(damageInterval);
}
Multiple Variables in a for Loop
You can use multiple variables in a for loop by separating them with commas:
// Count up with i while counting down with j
for (int i = 0, j = 10; i <= 10 && j >= 0; i++, j--)
{
Console.WriteLine($"i = {i}, j = {j}");
}
The foreach Loop
The foreach loop is designed specifically for iterating through collections like arrays, lists, and other enumerable objects.
Basic Syntax
foreach (type variable in collection)
{
// Code to execute for each element
}
Examples
// Iterate through an array
string[] weapons = { "Sword", "Bow", "Axe", "Staff" };
foreach (string weapon in weapons)
{
Console.WriteLine($"Available weapon: {weapon}");
}
// Iterate through a list
List<Enemy> enemies = GetAllEnemies();
foreach (Enemy enemy in enemies)
{
enemy.TakeDamage(10);
}
// Iterate through a dictionary
Dictionary<string, int> inventory = new Dictionary<string, int>
{
{ "Health Potion", 5 },
{ "Mana Potion", 3 },
{ "Antidote", 2 }
};
foreach (KeyValuePair<string, int> item in inventory)
{
Console.WriteLine($"{item.Key}: {item.Value}");
}
Game Development Examples
// Apply an effect to all players
foreach (Player player in GetAllPlayers())
{
player.ApplyBuff(BuffType.SpeedBoost, duration: 10f);
}
// Check if any enemy is in attack range
bool enemyInRange = false;
foreach (Enemy enemy in GetNearbyEnemies())
{
if (Vector3.Distance(transform.position, enemy.transform.position) < attackRange)
{
enemyInRange = true;
break; // Exit the loop early once we find one enemy in range
}
}
// Process all colliders in an area
Collider[] hitColliders = Physics.OverlapSphere(explosionPosition, explosionRadius);
foreach (Collider hitCollider in hitColliders)
{
Rigidbody rb = hitCollider.GetComponent<Rigidbody>();
if (rb != null)
{
rb.AddExplosionForce(explosionForce, explosionPosition, explosionRadius);
}
IDamageable damageable = hitCollider.GetComponent<IDamageable>();
if (damageable != null)
{
float distance = Vector3.Distance(explosionPosition, hitCollider.transform.position);
float damagePercent = 1f - (distance / explosionRadius);
int damage = Mathf.RoundToInt(maxDamage * damagePercent);
damageable.TakeDamage(damage);
}
}
Limitations of foreach
While foreach is convenient, it has some limitations:
- You cannot modify the collection while iterating through it
- You cannot access the index of the current element directly
- You cannot iterate through the collection in reverse order
If you need these capabilities, use a for loop instead.
The while Loop
The while loop executes a block of code as long as a specified condition is true. It's useful when you don't know in advance how many iterations you'll need.
Basic Syntax
while (condition)
{
// Code to execute as long as condition is true
}
Examples
// Count up to 10
int count = 1;
while (count <= 10)
{
Console.WriteLine(count);
count++;
}
// Process user input until they enter "quit"
string input = "";
while (input != "quit")
{
Console.Write("Enter a command (or 'quit' to exit): ");
input = Console.ReadLine().ToLower();
ProcessCommand(input);
}
// Generate random numbers until we get one greater than 90
Random random = new Random();
int number;
while ((number = random.Next(1, 101)) <= 90)
{
Console.WriteLine($"Generated: {number}");
}
Console.WriteLine($"Final number: {number}");
Game Development Examples
// Wait until player's health is restored
while (player.Health < player.MaxHealth)
{
player.Health += 1;
yield return new WaitForSeconds(0.5f);
}
// Keep trying to find a valid spawn position
Vector3 spawnPosition;
int attempts = 0;
while (attempts < 10)
{
spawnPosition = new Vector3(
Random.Range(-10f, 10f),
0,
Random.Range(-10f, 10f)
);
if (!Physics.CheckSphere(spawnPosition, 1.0f))
{
// Found a valid position with no collisions
SpawnEnemy(spawnPosition);
break;
}
attempts++;
}
// Implement a simple game loop
bool gameRunning = true;
while (gameRunning)
{
ProcessInput();
UpdateGameState();
RenderGame();
if (IsGameOver())
{
gameRunning = false;
}
}
The do-while Loop
The do-while loop is similar to the while loop, but it guarantees that the code block executes at least once, even if the condition is initially false.
Basic Syntax
do
{
// Code to execute at least once, then as long as condition is true
} while (condition);
Examples
// Ask for a positive number
int number;
do
{
Console.Write("Enter a positive number: ");
number = int.Parse(Console.ReadLine());
} while (number <= 0);
// Roll a die until we get a 6
int roll;
do
{
roll = random.Next(1, 7);
Console.WriteLine($"You rolled: {roll}");
} while (roll != 6);
Game Development Examples
// Menu system that always shows at least once
int choice;
do
{
DisplayMenu();
choice = GetPlayerChoice();
switch (choice)
{
case 1:
StartNewGame();
break;
case 2:
LoadGame();
break;
case 3:
ShowOptions();
break;
case 4:
// Exit the game
break;
default:
DisplayError("Invalid choice");
break;
}
} while (choice != 4);
// Implement a dialog system
string[] dialogLines = GetDialogLines();
int lineIndex = 0;
do
{
DisplayDialogLine(dialogLines[lineIndex]);
yield return WaitForPlayerInput();
lineIndex++;
} while (lineIndex < dialogLines.Length);
Nested Loops
You can place loops inside other loops to create nested loops. This is useful for working with multi-dimensional data or creating complex patterns.
Examples
// Print a multiplication table
for (int i = 1; i <= 10; i++)
{
for (int j = 1; j <= 10; j++)
{
Console.Write($"{i * j,4}"); // The ,4 formats to a width of 4 characters
}
Console.WriteLine(); // New line after each row
}
// Process a 2D grid
int[,] grid = new int[3, 3] {
{ 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 }
};
for (int row = 0; row < 3; row++)
{
for (int col = 0; col < 3; col++)
{
Console.WriteLine($"Value at [{row},{col}]: {grid[row, col]}");
}
}
Game Development Examples
// Generate a procedural terrain
for (int x = 0; x < terrainWidth; x++)
{
for (int z = 0; z < terrainLength; z++)
{
// Calculate height using Perlin noise
float xCoord = (float)x / terrainWidth * noiseScale;
float zCoord = (float)z / terrainLength * noiseScale;
float height = Mathf.PerlinNoise(xCoord, zCoord) * heightScale;
// Set the terrain height
terrain.SetHeight(x, z, height);
// Place objects based on height
if (height > waterLevel && height < mountainLevel)
{
if (Random.value < treeDensity)
{
PlaceTree(x, height, z);
}
}
else if (height >= mountainLevel)
{
if (Random.value < rockDensity)
{
PlaceRock(x, height, z);
}
}
}
}
// Pathfinding algorithm (simplified)
Queue<Node> openNodes = new Queue<Node>();
openNodes.Enqueue(startNode);
while (openNodes.Count > 0)
{
Node currentNode = openNodes.Dequeue();
if (currentNode == targetNode)
{
// Path found!
break;
}
foreach (Node neighbor in GetNeighbors(currentNode))
{
if (!visitedNodes.Contains(neighbor))
{
neighbor.Parent = currentNode;
visitedNodes.Add(neighbor);
openNodes.Enqueue(neighbor);
}
}
}
Infinite Loops
An infinite loop is a loop that continues indefinitely because its termination condition is never met. While usually a bug, infinite loops can be intentional in some cases, such as game loops that run until the application is closed.
Creating Intentional Infinite Loops
// Infinite loop with while
while (true)
{
// Code that runs forever
// (should include a break statement somewhere to exit under certain conditions)
}
// Infinite loop with for
for (;;)
{
// Code that runs forever
}
Game Development Examples
// Main game loop
void Start()
{
StartCoroutine(GameLoop());
}
IEnumerator GameLoop()
{
while (true)
{
UpdateGame();
yield return null; // Wait for the next frame
}
}
// Background music that loops forever
void PlayBackgroundMusic()
{
audioSource.clip = backgroundMusic;
audioSource.loop = true;
audioSource.Play();
}
Be careful with infinite loops! Always ensure there's a way to exit the loop (using break, return, or by changing the condition). Unintentional infinite loops can freeze your application.
Loop Performance Considerations
When writing loops, especially in performance-critical game code, consider these optimization tips:
-
Minimize work inside loops: Move calculations outside the loop if they don't change between iterations.
-
Cache collection lengths: For
forloops, cache the length of collections rather than recalculating it each iteration.// Less efficient - Length is checked each iteration
for (int i = 0; i < myArray.Length; i++)
// More efficient - Length is cached
int length = myArray.Length;
for (int i = 0; i < length; i++) -
Consider loop unrolling for very small, performance-critical loops:
// Normal loop
for (int i = 0; i < 4; i++)
sum += data[i];
// Unrolled loop
sum += data[0];
sum += data[1];
sum += data[2];
sum += data[3]; -
Use appropriate loop types:
foreachis convenient but can be slower thanforfor arrays and some collections. -
Exit early when possible using
breakorreturnonce you've found what you're looking for.
Choosing the Right Loop
Each loop type has its strengths and ideal use cases:
| Loop Type | Best Used When |
|---|---|
for | You know exactly how many iterations you need |
foreach | You need to process all elements in a collection |
while | You don't know how many iterations you need, and the condition should be checked before the first iteration |
do-while | You don't know how many iterations you need, but you want to ensure the loop executes at least once |
Practical Examples in Game Development
Example 1: Wave-Based Enemy Spawner
public class EnemySpawner : MonoBehaviour
{
[SerializeField] private GameObject[] enemyPrefabs;
[SerializeField] private Transform[] spawnPoints;
[SerializeField] private int wavesCount = 5;
[SerializeField] private int baseEnemiesPerWave = 3;
[SerializeField] private float timeBetweenWaves = 10f;
private int currentWave = 0;
private void Start()
{
StartCoroutine(SpawnWaves());
}
private IEnumerator SpawnWaves()
{
while (currentWave < wavesCount)
{
currentWave++;
// Calculate number of enemies for this wave
int enemiesToSpawn = baseEnemiesPerWave + currentWave;
Debug.Log($"Wave {currentWave} starting! Spawning {enemiesToSpawn} enemies.");
// Spawn enemies for this wave
for (int i = 0; i < enemiesToSpawn; i++)
{
SpawnEnemy();
// Small delay between spawning each enemy
yield return new WaitForSeconds(0.5f);
}
// Wait until all enemies are defeated
while (GameObject.FindGameObjectsWithTag("Enemy").Length > 0)
{
yield return new WaitForSeconds(1f);
}
Debug.Log($"Wave {currentWave} completed!");
if (currentWave < wavesCount)
{
Debug.Log($"Next wave in {timeBetweenWaves} seconds...");
yield return new WaitForSeconds(timeBetweenWaves);
}
}
Debug.Log("All waves completed! Game won!");
}
private void SpawnEnemy()
{
// Select random enemy prefab and spawn point
GameObject enemyPrefab = enemyPrefabs[Random.Range(0, enemyPrefabs.Length)];
Transform spawnPoint = spawnPoints[Random.Range(0, spawnPoints.Length)];
// Spawn the enemy
Instantiate(enemyPrefab, spawnPoint.position, spawnPoint.rotation);
}
}
Example 2: Inventory System
public class Inventory : MonoBehaviour
{
[SerializeField] private int maxItems = 20;
private List<Item> items = new List<Item>();
public bool AddItem(Item newItem)
{
// Check if inventory is full
if (items.Count >= maxItems)
{
Debug.Log("Inventory is full!");
return false;
}
// Check if item is stackable and we already have it
if (newItem.IsStackable)
{
foreach (Item item in items)
{
if (item.ID == newItem.ID)
{
// Stack with existing item
item.Quantity += newItem.Quantity;
Debug.Log($"Added {newItem.Quantity} {newItem.Name} to existing stack.");
return true;
}
}
}
// Add as new item
items.Add(newItem);
Debug.Log($"Added {newItem.Name} to inventory.");
return true;
}
public void RemoveItem(int itemID, int quantity = 1)
{
for (int i = 0; i < items.Count; i++)
{
if (items[i].ID == itemID)
{
if (items[i].Quantity <= quantity)
{
// Remove the entire item
Debug.Log($"Removed {items[i].Name} from inventory.");
items.RemoveAt(i);
}
else
{
// Reduce quantity
items[i].Quantity -= quantity;
Debug.Log($"Removed {quantity} {items[i].Name} from inventory.");
}
return;
}
}
Debug.LogWarning($"Item with ID {itemID} not found in inventory!");
}
public void DisplayInventory()
{
if (items.Count == 0)
{
Debug.Log("Inventory is empty.");
return;
}
Debug.Log("=== INVENTORY ===");
for (int i = 0; i < items.Count; i++)
{
Item item = items[i];
string quantityText = item.IsStackable ? $" x{item.Quantity}" : "";
Debug.Log($"{i+1}. {item.Name}{quantityText} - {item.Description}");
}
}
public Item GetItem(int index)
{
if (index >= 0 && index < items.Count)
{
return items[index];
}
return null;
}
public bool HasItem(int itemID, int quantity = 1)
{
foreach (Item item in items)
{
if (item.ID == itemID && item.Quantity >= quantity)
{
return true;
}
}
return false;
}
}
Example 3: Procedural Dungeon Generator
public class DungeonGenerator : MonoBehaviour
{
[SerializeField] private GameObject floorPrefab;
[SerializeField] private GameObject wallPrefab;
[SerializeField] private GameObject doorPrefab;
[SerializeField] private int width = 20;
[SerializeField] private int height = 20;
[SerializeField] private int roomCount = 5;
[SerializeField] private int minRoomSize = 3;
[SerializeField] private int maxRoomSize = 8;
private bool[,] floorMap;
private List<Room> rooms = new List<Room>();
private void Start()
{
GenerateDungeon();
}
private void GenerateDungeon()
{
// Initialize floor map (false = wall, true = floor)
floorMap = new bool[width, height];
// Generate rooms
for (int i = 0; i < roomCount; i++)
{
TryCreateRoom();
}
// Connect rooms with corridors
ConnectRooms();
// Create the actual GameObjects
InstantiateDungeon();
}
private void TryCreateRoom()
{
// Try up to 10 times to place a room
for (int attempt = 0; attempt < 10; attempt++)
{
// Random room size
int roomWidth = Random.Range(minRoomSize, maxRoomSize + 1);
int roomHeight = Random.Range(minRoomSize, maxRoomSize + 1);
// Random position
int roomX = Random.Range(1, width - roomWidth - 1);
int roomY = Random.Range(1, height - roomHeight - 1);
// Check if room overlaps with existing rooms
bool overlap = false;
// Add buffer around room for walls
for (int x = roomX - 1; x < roomX + roomWidth + 1; x++)
{
for (int y = roomY - 1; y < roomY + roomHeight + 1; y++)
{
if (x < 0 || x >= width || y < 0 || y >= height || floorMap[x, y])
{
overlap = true;
break;
}
}
if (overlap) break;
}
if (!overlap)
{
// Create the room
Room newRoom = new Room(roomX, roomY, roomWidth, roomHeight);
rooms.Add(newRoom);
// Mark floor tiles
for (int x = roomX; x < roomX + roomWidth; x++)
{
for (int y = roomY; y < roomY + roomHeight; y++)
{
floorMap[x, y] = true;
}
}
return;
}
}
}
private void ConnectRooms()
{
if (rooms.Count <= 1) return;
// Connect each room to the next one
for (int i = 0; i < rooms.Count - 1; i++)
{
ConnectTwoRooms(rooms[i], rooms[i + 1]);
}
}
private void ConnectTwoRooms(Room roomA, Room roomB)
{
// Get center points of rooms
Vector2Int pointA = roomA.GetCenter();
Vector2Int pointB = roomB.GetCenter();
// Randomly decide whether to go horizontal first or vertical first
if (Random.value < 0.5f)
{
// Horizontal then vertical
CreateHorizontalCorridor(pointA.x, pointB.x, pointA.y);
CreateVerticalCorridor(pointA.y, pointB.y, pointB.x);
}
else
{
// Vertical then horizontal
CreateVerticalCorridor(pointA.y, pointB.y, pointA.x);
CreateHorizontalCorridor(pointA.x, pointB.x, pointB.y);
}
}
private void CreateHorizontalCorridor(int x1, int x2, int y)
{
int start = Mathf.Min(x1, x2);
int end = Mathf.Max(x1, x2);
for (int x = start; x <= end; x++)
{
if (x >= 0 && x < width && y >= 0 && y < height)
{
floorMap[x, y] = true;
}
}
}
private void CreateVerticalCorridor(int y1, int y2, int x)
{
int start = Mathf.Min(y1, y2);
int end = Mathf.Max(y1, y2);
for (int y = start; y <= end; y++)
{
if (x >= 0 && x < width && y >= 0 && y < height)
{
floorMap[x, y] = true;
}
}
}
private void InstantiateDungeon()
{
// Create parent object for organization
Transform dungeonParent = new GameObject("Dungeon").transform;
// Create floors and walls
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
if (floorMap[x, y])
{
// Create floor
Instantiate(floorPrefab, new Vector3(x, 0, y), Quaternion.identity, dungeonParent);
// Check for walls (adjacent non-floor tiles)
CheckAndCreateWall(x + 1, y, 0, dungeonParent);
CheckAndCreateWall(x - 1, y, 180, dungeonParent);
CheckAndCreateWall(x, y + 1, 90, dungeonParent);
CheckAndCreateWall(x, y - 1, 270, dungeonParent);
}
}
}
}
private void CheckAndCreateWall(int x, int y, float rotation, Transform parent)
{
if (x < 0 || x >= width || y < 0 || y >= height || !floorMap[x, y])
{
// This is a wall position
Vector3 wallPos = new Vector3(x, 0, y);
Quaternion wallRot = Quaternion.Euler(0, rotation, 0);
Instantiate(wallPrefab, wallPos, wallRot, parent);
}
}
private class Room
{
public int X { get; private set; }
public int Y { get; private set; }
public int Width { get; private set; }
public int Height { get; private set; }
public Room(int x, int y, int width, int height)
{
X = x;
Y = y;
Width = width;
Height = height;
}
public Vector2Int GetCenter()
{
return new Vector2Int(X + Width / 2, Y + Height / 2);
}
}
}
Conclusion
Loops are fundamental programming constructs that allow you to execute code repeatedly. By understanding the different types of loops and when to use each one, you can write more efficient and elegant code for your games and applications.
In this section, we've covered:
- The four main types of loops in C#:
for,foreach,while, anddo-while - Nested loops for working with multi-dimensional data
- Infinite loops and their appropriate uses
- Performance considerations for loops
- Practical examples of loops in game development
In the next section, we'll explore loop control statements like break, continue, and return, which give you more fine-grained control over loop execution.
In Unity development, loops are used extensively:
- In
Update()methods to process game logic each frame - For iterating through collections of game objects, components, or data
- In coroutines for time-based operations
- For procedural generation of levels, terrain, or content
- In physics calculations and collision detection
- For AI behavior and pathfinding algorithms
Understanding loops is essential for implementing efficient game mechanics and systems in Unity.