3.4 - Loop Control Statements
Loop control statements give you fine-grained control over how loops execute. They allow you to skip iterations, exit loops early, or jump to different parts of your code. These tools are essential for writing efficient and responsive code, especially in game development where you often need to handle complex conditions and early exits.
Why Loop Control Statements Matter
In game development, you frequently encounter situations where:
- You need to exit a loop early when a specific condition is met (e.g., finding the first enemy in attack range)
- You want to skip certain iterations based on conditions (e.g., processing only active game objects)
- You need to exit both a loop and its containing method when an error occurs
- You want to restart a loop from the beginning when certain conditions change
Loop control statements provide elegant solutions for these scenarios, making your code more efficient and readable.
The break Statement
The break statement immediately exits the innermost loop or switch statement that contains it. Execution continues with the statement following the terminated loop or switch.
Basic Syntax
while (condition)
{
// Some code
if (exitCondition)
{
break; // Exit the loop immediately
}
// More code
}
// Execution continues here after break
Examples
// Find the first even number in an array
int[] numbers = { 7, 3, 5, 8, 1, 9, 2, 4 };
int firstEven = -1;
for (int i = 0; i < numbers.Length; i++)
{
if (numbers[i] % 2 == 0)
{
firstEven = numbers[i];
break; // Exit the loop once we find an even number
}
}
Console.WriteLine($"First even number: {firstEven}");
// Process user input until they enter "quit"
while (true) // Infinite loop
{
Console.Write("Enter a command (or 'quit' to exit): ");
string input = Console.ReadLine().ToLower();
if (input == "quit")
{
break; // Exit the loop
}
ProcessCommand(input);
}
Game Development Examples
// Find the closest interactable object
Interactable closestInteractable = null;
float closestDistance = float.MaxValue;
foreach (Collider collider in Physics.OverlapSphere(transform.position, interactionRadius))
{
Interactable interactable = collider.GetComponent<Interactable>();
if (interactable != null)
{
float distance = Vector3.Distance(transform.position, interactable.transform.position);
if (distance < closestDistance)
{
closestInteractable = interactable;
closestDistance = distance;
if (distance < 0.5f)
{
// If we're very close to an interactable, no need to check others
break;
}
}
}
}
// Check if any enemy can see the player
bool playerDetected = false;
foreach (Enemy enemy in activeEnemies)
{
if (enemy.CanSeePlayer())
{
playerDetected = true;
TriggerAlarm();
break; // No need to check other enemies once player is detected
}
}
The continue Statement
The continue statement skips the rest of the current iteration and moves directly to the next iteration of the innermost loop that contains it.
Basic Syntax
while (condition)
{
// Some code
if (skipCondition)
{
continue; // Skip to the next iteration
}
// This code is skipped when continue is executed
}
Examples
// Print only odd numbers from 1 to 10
for (int i = 1; i <= 10; i++)
{
if (i % 2 == 0)
{
continue; // Skip even numbers
}
Console.WriteLine(i);
}
// Process non-null items in an array
string[] items = { "Sword", null, "Shield", null, "Potion" };
foreach (string item in items)
{
if (item == null)
{
continue; // Skip null items
}
Console.WriteLine($"Processing item: {item}");
}
Game Development Examples
// Update only active enemies
foreach (Enemy enemy in allEnemies)
{
if (!enemy.gameObject.activeInHierarchy)
{
continue; // Skip inactive enemies
}
enemy.UpdateAI();
}
// Apply damage to vulnerable targets only
foreach (Damageable target in GetTargetsInArea())
{
if (target.IsInvulnerable || target.IsDead)
{
continue; // Skip invulnerable or dead targets
}
float distance = Vector3.Distance(explosionPosition, target.transform.position);
float damageMultiplier = 1f - (distance / explosionRadius);
int damage = Mathf.RoundToInt(baseDamage * damageMultiplier);
target.TakeDamage(damage);
}
The return Statement
The return statement exits the current method and optionally returns a value to the caller. When used inside a loop, it not only exits the loop but also the entire method.
Basic Syntax
returnType MethodName()
{
// Some code
while (condition)
{
// Loop code
if (exitCondition)
{
return value; // Exit both the loop and the method
}
}
// This code is skipped if return is executed in the loop
return defaultValue;
}
Examples
// Check if an array contains a specific value
bool Contains(int[] array, int value)
{
for (int i = 0; i < array.Length; i++)
{
if (array[i] == value)
{
return true; // Exit the method immediately when value is found
}
}
return false; // Only reached if the value is not found
}
// Find the index of an element
int FindIndex(string[] array, string element)
{
for (int i = 0; i < array.Length; i++)
{
if (array[i] == element)
{
return i; // Return the index when element is found
}
}
return -1; // Return -1 if element is not found
}
Game Development Examples
// Find the first enemy in attack range
Enemy FindTargetInRange()
{
foreach (Enemy enemy in GetVisibleEnemies())
{
float distance = Vector3.Distance(transform.position, enemy.transform.position);
if (distance <= attackRange)
{
return enemy; // Return the first enemy in range
}
}
return null; // Return null if no enemies are in range
}
// Check if player has line of sight to target
bool HasLineOfSight(Transform target)
{
Vector3 direction = target.position - transform.position;
float distance = direction.magnitude;
// Check for obstacles between player and target
RaycastHit[] hits = Physics.RaycastAll(transform.position, direction.normalized, distance);
foreach (RaycastHit hit in hits)
{
if (hit.transform != transform && hit.transform != target)
{
// Found an obstacle
return false;
}
}
// No obstacles found
return true;
}
The goto Statement
The goto statement transfers control to a labeled statement elsewhere in the method. While generally discouraged in modern programming due to its potential to create "spaghetti code," it can be useful in specific scenarios, such as breaking out of nested loops.
Basic Syntax
// Define a label
labelName:
// Some code
// Jump to the label
goto labelName;
Examples
// Breaking out of nested loops
bool found = false;
for (int i = 0; i < 10; i++)
{
for (int j = 0; j < 10; j++)
{
if (matrix[i, j] == targetValue)
{
Console.WriteLine($"Found at position [{i}, {j}]");
found = true;
goto LoopExit; // Exit both loops
}
}
}
LoopExit:
if (!found)
{
Console.WriteLine("Value not found");
}
// Implementing a simple state machine
string state = "Start";
ProcessState:
switch (state)
{
case "Start":
Console.WriteLine("Starting process...");
state = "Processing";
goto ProcessState;
case "Processing":
Console.WriteLine("Processing data...");
if (IsProcessingComplete())
{
state = "Finished";
}
else
{
state = "Error";
}
goto ProcessState;
case "Error":
Console.WriteLine("An error occurred");
state = "Cleanup";
goto ProcessState;
case "Finished":
Console.WriteLine("Processing complete");
state = "Cleanup";
goto ProcessState;
case "Cleanup":
Console.WriteLine("Cleaning up resources...");
state = "End";
goto ProcessState;
case "End":
Console.WriteLine("Process ended");
break;
}
The goto statement should be used sparingly, as it can make code harder to understand and maintain. In most cases, there are cleaner alternatives using other control structures.
Game Development Examples
// Breaking out of nested loops in a pathfinding algorithm
bool pathFound = false;
for (int x = 0; x < gridWidth; x++)
{
for (int y = 0; y < gridHeight; y++)
{
if (grid[x, y].isBlocked)
{
continue;
}
List<Node> path = FindPath(startNode, grid[x, y]);
if (path != null && path.Count > 0)
{
// Found a valid path
currentPath = path;
pathFound = true;
goto PathComplete; // Exit both loops
}
}
}
PathComplete:
if (pathFound)
{
FollowPath(currentPath);
}
else
{
HandleNoPathFound();
}
Breaking Out of Nested Loops Without goto
While goto can be used to break out of nested loops, there are cleaner alternatives:
1. Using a Flag Variable
bool found = false;
for (int i = 0; i < 10 && !found; i++)
{
for (int j = 0; j < 10 && !found; j++)
{
if (matrix[i, j] == targetValue)
{
Console.WriteLine($"Found at position [{i}, {j}]");
found = true;
}
}
}
2. Extracting to a Method and Using return
void FindValue()
{
for (int i = 0; i < 10; i++)
{
for (int j = 0; j < 10; j++)
{
if (matrix[i, j] == targetValue)
{
Console.WriteLine($"Found at position [{i}, {j}]");
return; // Exit both loops by returning from the method
}
}
}
Console.WriteLine("Value not found");
}
Combining Loop Control Statements
You can use multiple loop control statements together to create more complex control flow:
// Process items until we find a special item or process all items
bool specialItemFound = false;
for (int i = 0; i < items.Length; i++)
{
if (items[i] == null)
{
continue; // Skip null items
}
ProcessItem(items[i]);
if (items[i].IsSpecial)
{
specialItemFound = true;
break; // Exit the loop when a special item is found
}
}
if (specialItemFound)
{
HandleSpecialItem();
}
else
{
HandleNormalCompletion();
}
Practical Examples in Game Development
Example 1: Enemy Detection System
public class PlayerDetection : MonoBehaviour
{
[SerializeField] private float detectionRadius = 10f;
[SerializeField] private float fieldOfViewAngle = 90f;
[SerializeField] private LayerMask obstacleLayer;
private Transform player;
private void Start()
{
player = GameObject.FindGameObjectWithTag("Player").transform;
}
public bool CanSeePlayer()
{
// First, check if player is within detection radius
float distanceToPlayer = Vector3.Distance(transform.position, player.position);
if (distanceToPlayer > detectionRadius)
{
return false; // Player is too far away
}
// Check if player is within field of view
Vector3 directionToPlayer = (player.position - transform.position).normalized;
float angleToPlayer = Vector3.Angle(transform.forward, directionToPlayer);
if (angleToPlayer > fieldOfViewAngle / 2)
{
return false; // Player is outside field of view
}
// Finally, check if there are obstacles blocking the view
if (Physics.Raycast(transform.position, directionToPlayer, distanceToPlayer, obstacleLayer))
{
return false; // View is blocked by an obstacle
}
// All checks passed, player is visible
return true;
}
public void ScanForPlayer()
{
// Get all colliders in detection radius
Collider[] colliders = Physics.OverlapSphere(transform.position, detectionRadius);
foreach (Collider collider in colliders)
{
// Skip if not the player
if (!collider.CompareTag("Player"))
{
continue;
}
// Check if player is within field of view
Vector3 directionToCollider = (collider.transform.position - transform.position).normalized;
float angleToCollider = Vector3.Angle(transform.forward, directionToCollider);
if (angleToCollider > fieldOfViewAngle / 2)
{
continue; // Outside field of view
}
// Check for obstacles
float distance = Vector3.Distance(transform.position, collider.transform.position);
if (Physics.Raycast(transform.position, directionToCollider, distance, obstacleLayer))
{
continue; // View is blocked
}
// Player detected!
OnPlayerDetected(collider.transform);
return; // Exit the method once player is detected
}
// If we get here, player was not detected
OnPlayerLost();
}
private void OnPlayerDetected(Transform playerTransform)
{
Debug.Log("Player detected!");
// Alert other enemies, change state, etc.
}
private void OnPlayerLost()
{
Debug.Log("Player lost or not detected");
// Return to patrol state, etc.
}
}
Example 2: Inventory Search System
public class InventoryManager : MonoBehaviour
{
[SerializeField] private List<Item> inventory = new List<Item>();
[SerializeField] private int maxInventorySize = 20;
public bool HasItem(string itemName, int quantity = 1)
{
int count = 0;
foreach (Item item in inventory)
{
if (item.Name == itemName)
{
count += item.Quantity;
if (count >= quantity)
{
return true; // Found enough of the item
}
}
}
return false; // Not enough of the item found
}
public Item FindItem(string itemName)
{
foreach (Item item in inventory)
{
if (item.Name == itemName)
{
return item; // Return the first matching item
}
}
return null; // Item not found
}
public bool UseItem(string itemName)
{
for (int i = 0; i < inventory.Count; i++)
{
if (inventory[i].Name == itemName)
{
// Use the item
inventory[i].Use();
// If the item is consumed, reduce quantity or remove it
if (inventory[i].IsConsumable)
{
inventory[i].Quantity--;
if (inventory[i].Quantity <= 0)
{
inventory.RemoveAt(i);
}
}
return true; // Item used successfully
}
}
return false; // Item not found
}
public void SortInventory()
{
// Sort by category, then by name
for (int i = 0; i < inventory.Count - 1; i++)
{
for (int j = i + 1; j < inventory.Count; j++)
{
// Skip null items
if (inventory[i] == null || inventory[j] == null)
{
continue;
}
// Compare categories
int categoryComparison = inventory[i].Category.CompareTo(inventory[j].Category);
if (categoryComparison > 0)
{
// Swap items
Item temp = inventory[i];
inventory[i] = inventory[j];
inventory[j] = temp;
}
else if (categoryComparison == 0)
{
// Same category, compare by name
if (string.Compare(inventory[i].Name, inventory[j].Name) > 0)
{
// Swap items
Item temp = inventory[i];
inventory[i] = inventory[j];
inventory[j] = temp;
}
}
}
}
}
public bool AddItem(Item newItem)
{
// Check if inventory is full
if (inventory.Count >= maxInventorySize)
{
Debug.Log("Inventory is full!");
return false;
}
// Check if item is stackable and we already have it
if (newItem.IsStackable)
{
for (int i = 0; i < inventory.Count; i++)
{
if (inventory[i].ID == newItem.ID)
{
// Stack with existing item
inventory[i].Quantity += newItem.Quantity;
Debug.Log($"Added {newItem.Quantity} {newItem.Name} to existing stack.");
return true;
}
}
}
// Add as new item
inventory.Add(newItem);
Debug.Log($"Added {newItem.Name} to inventory.");
return true;
}
}
Example 3: Wave-Based Enemy Spawner with Loop Control
public class WaveSpawner : MonoBehaviour
{
[System.Serializable]
public class Wave
{
public string waveName;
public List<EnemyGroup> enemyGroups;
public float timeBetweenGroups = 2f;
public int creditsReward = 100;
}
[System.Serializable]
public class EnemyGroup
{
public GameObject enemyPrefab;
public int count = 5;
public float timeBetweenSpawns = 1f;
}
[SerializeField] private List<Wave> waves;
[SerializeField] private Transform[] spawnPoints;
[SerializeField] private float timeBetweenWaves = 5f;
[SerializeField] private GameObject bossWarningUI;
private int currentWaveIndex = 0;
private bool isSpawning = false;
private void Start()
{
StartCoroutine(StartNextWave());
}
private IEnumerator StartNextWave()
{
// Show wave announcement
ShowWaveAnnouncement(currentWaveIndex);
yield return new WaitForSeconds(3f); // Wait for announcement
if (currentWaveIndex >= waves.Count)
{
Debug.Log("All waves completed!");
GameManager.Instance.WinGame();
yield break; // Exit the coroutine
}
Wave currentWave = waves[currentWaveIndex];
isSpawning = true;
// Check if this is a boss wave
bool isBossWave = currentWave.waveName.Contains("Boss");
if (isBossWave)
{
// Show boss warning
bossWarningUI.SetActive(true);
yield return new WaitForSeconds(5f); // Dramatic pause
bossWarningUI.SetActive(false);
}
// Spawn each group in the wave
for (int groupIndex = 0; groupIndex < currentWave.enemyGroups.Count; groupIndex++)
{
EnemyGroup group = currentWave.enemyGroups[groupIndex];
// Check if player died during spawning
if (GameManager.Instance.IsGameOver)
{
isSpawning = false;
yield break; // Exit if game is over
}
// Spawn all enemies in this group
for (int i = 0; i < group.count; i++)
{
// Select a random spawn point
Transform spawnPoint = spawnPoints[Random.Range(0, spawnPoints.Length)];
// Spawn the enemy
GameObject enemy = Instantiate(group.enemyPrefab, spawnPoint.position, spawnPoint.rotation);
// If this is the last enemy of the last group in a boss wave, tag it as the boss
if (isBossWave && groupIndex == currentWave.enemyGroups.Count - 1 && i == group.count - 1)
{
enemy.tag = "Boss";
enemy.GetComponent<Enemy>().SetAsBoss();
}
// Wait between spawns
yield return new WaitForSeconds(group.timeBetweenSpawns);
// Check if player died during spawning
if (GameManager.Instance.IsGameOver)
{
isSpawning = false;
yield break; // Exit if game is over
}
}
// Wait between groups
yield return new WaitForSeconds(currentWave.timeBetweenGroups);
}
isSpawning = false;
// Wait until all enemies are defeated
yield return StartCoroutine(WaitForEnemiesDefeated());
// Award credits for completing the wave
GameManager.Instance.AddCredits(currentWave.creditsReward);
// Move to the next wave
currentWaveIndex++;
// Wait between waves
yield return new WaitForSeconds(timeBetweenWaves);
// Start the next wave
StartCoroutine(StartNextWave());
}
private IEnumerator WaitForEnemiesDefeated()
{
// Keep checking until all enemies are defeated
while (true)
{
// Find all active enemies
GameObject[] enemies = GameObject.FindGameObjectsWithTag("Enemy");
GameObject boss = GameObject.FindGameObjectWithTag("Boss");
// If there's a boss, we only care about defeating it
if (boss != null)
{
yield return new WaitForSeconds(1f);
continue; // Keep waiting if boss is alive
}
// If no regular enemies either, wave is complete
if (enemies.Length == 0)
{
break; // Exit the loop
}
// Check again after a delay
yield return new WaitForSeconds(1f);
// Check if player died while waiting
if (GameManager.Instance.IsGameOver)
{
yield break; // Exit if game is over
}
}
Debug.Log("All enemies defeated!");
}
private void ShowWaveAnnouncement(int waveIndex)
{
if (waveIndex < waves.Count)
{
string waveName = waves[waveIndex].waveName;
UIManager.Instance.ShowWaveAnnouncement(waveIndex + 1, waveName);
}
}
// Public method to check if enemies are currently spawning
public bool IsSpawning()
{
return isSpawning;
}
// Public method to get current wave info
public int GetCurrentWave()
{
return currentWaveIndex + 1;
}
public int GetTotalWaves()
{
return waves.Count;
}
}
Best Practices for Loop Control
-
Use
breakwhen you need to exit a loop early based on a condition. -
Use
continueto skip iterations that don't need processing, rather than nesting the processing code in anifstatement. -
Use
returnwhen you've found what you're looking for and don't need to execute any code after the loop. -
Avoid
gotowhen possible, using the alternatives mentioned earlier. -
Add comments to explain complex loop control logic, especially when using multiple control statements together.
-
Be careful with loop control in nested loops - make sure you're affecting the intended loop level.
-
Consider extracting complex loop logic into separate methods for better readability and maintainability.
Conclusion
Loop control statements are powerful tools that give you fine-grained control over how your loops execute. By using break, continue, return, and occasionally goto, you can write more efficient and elegant code that handles complex conditions and early exits.
In this section, we've covered:
- The
breakstatement for exiting loops early - The
continuestatement for skipping iterations - The
returnstatement for exiting both loops and methods - The
gotostatement and its alternatives - Practical examples of loop control in game development
With these tools in your programming toolkit, you can write more efficient and responsive code for your games and applications.
In the next section, we'll put all the control flow concepts we've learned into practice with a mini-project.
In Unity development, loop control statements are particularly useful for:
- Optimizing performance by exiting loops early when conditions are met
- Implementing AI behavior that needs to respond to changing conditions
- Processing collections of game objects efficiently
- Handling user input and game events
- Implementing game mechanics like wave spawners, item systems, and more
Understanding loop control is essential for writing efficient game code that responds appropriately to the dynamic nature of games.