Skip to main content

3.2 - Switch Statement

The switch statement provides an elegant way to handle multiple conditions based on a single value. It's an alternative to using multiple if-else if statements when you're comparing a single variable against different possible values.

Why Use Switch Statements?

Switch statements offer several advantages over equivalent if-else if chains:

  1. Readability: They make the code more readable when comparing a single value against multiple possible values
  2. Performance: They can be more efficient than equivalent if-else if chains (especially for string comparisons in modern C#)
  3. Structure: They clearly express the intent to select one option from many based on a value

Basic Switch Syntax

The basic syntax of a switch statement is:

switch (expression)
{
case value1:
// Code to execute if expression equals value1
break;
case value2:
// Code to execute if expression equals value2
break;
// More cases...
default:
// Code to execute if expression doesn't match any case
break;
}

The expression is evaluated once, and its value is compared with each case. If a match is found, the corresponding code block is executed.

A Simple Example

int playerClass = 2;

switch (playerClass)
{
case 1:
Console.WriteLine("You selected the Warrior class.");
break;
case 2:
Console.WriteLine("You selected the Mage class.");
break;
case 3:
Console.WriteLine("You selected the Rogue class.");
break;
default:
Console.WriteLine("Invalid class selection.");
break;
}

In this example, the output would be "You selected the Mage class." because playerClass is 2.

The break Statement

Each case section typically ends with a break statement, which exits the switch block. Without a break (or other jump statement like return or goto), execution would "fall through" to the next case, which is usually not what you want.

// Example of what NOT to do (missing break statements)
switch (playerClass)
{
case 1:
Console.WriteLine("You selected the Warrior class.");
// Missing break - execution falls through to the next case!
case 2:
Console.WriteLine("You selected the Mage class.");
// Missing break
case 3:
Console.WriteLine("You selected the Rogue class.");
break;
}

If playerClass is 1 in the above example, all three messages would be displayed due to the missing break statements.

Fall-Through Behavior (When It's Useful)

While fall-through is usually avoided, it can be useful when you want multiple cases to execute the same code:

string weaponType = "Sword";

switch (weaponType)
{
case "Sword":
case "Dagger":
case "Axe":
Console.WriteLine("Physical damage weapon");
break;
case "Fire Staff":
case "Flame Wand":
Console.WriteLine("Fire damage weapon");
break;
case "Ice Scepter":
case "Frost Wand":
Console.WriteLine("Ice damage weapon");
break;
default:
Console.WriteLine("Unknown weapon type");
break;
}

In this example, "Sword", "Dagger", and "Axe" all result in the same output.

The default Case

The default case is executed when none of the other cases match the expression. It's optional but recommended to handle unexpected values:

int enemyType = 5;

switch (enemyType)
{
case 1:
SpawnGoblin();
break;
case 2:
SpawnOrc();
break;
case 3:
SpawnTroll();
break;
default:
Console.WriteLine($"Unknown enemy type: {enemyType}");
SpawnDefaultEnemy();
break;
}

Switch with String Values

Switch statements work well with string comparisons:

string command = "attack";

switch (command.ToLower())
{
case "attack":
PlayerAttack();
break;
case "defend":
PlayerDefend();
break;
case "heal":
PlayerHeal();
break;
case "run":
PlayerRun();
break;
default:
Console.WriteLine("Unknown command. Type 'help' for a list of commands.");
break;
}

Note that string comparisons are case-sensitive by default. In the example above, we use ToLower() to make the comparison case-insensitive.

Switch with Enums

Switch statements are particularly useful with enums, as they provide compile-time checking that you've handled all possible enum values:

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

GameState currentState = GameState.Playing;

switch (currentState)
{
case GameState.MainMenu:
DisplayMainMenu();
break;
case GameState.Playing:
UpdateGameplay();
break;
case GameState.Paused:
DisplayPauseMenu();
break;
case GameState.GameOver:
DisplayGameOverScreen();
break;
case GameState.Victory:
DisplayVictoryScreen();
break;
default:
// This code is unreachable if all enum values are handled
// but it's good practice to include it anyway
throw new ArgumentException($"Unhandled game state: {currentState}");
}

Pattern Matching in Switch Statements (C# 7.0+)

C# 7.0 introduced pattern matching in switch statements, allowing for more powerful and flexible conditions.

Unity Compatibility Note

Pattern matching in switch statements was introduced in C# 7.0 and has been supported in Unity since Unity 2018.3. The examples below should work in most modern Unity versions, but if you're using an older version, you might need to use traditional switch statements or if-else chains instead.

Type Patterns

You can check the type of an object and cast it in one step:

object item = GetRandomItem();

switch (item)
{
case Weapon weapon:
Console.WriteLine($"You found a {weapon.Name} with {weapon.Damage} damage!");
EquipWeapon(weapon);
break;
case Potion potion:
Console.WriteLine($"You found a {potion.Type} potion!");
AddToInventory(potion);
break;
case Gold gold:
Console.WriteLine($"You found {gold.Amount} gold coins!");
AddGold(gold.Amount);
break;
case null:
Console.WriteLine("You found nothing.");
break;
default:
Console.WriteLine($"You found an unknown item: {item}");
break;
}

Property Patterns (C# 8.0+)

C# 8.0 added property patterns, allowing you to match on properties of an object:

Unity Compatibility Warning

Property patterns are a C# 8.0 feature that requires Unity 2020.2 or later for full support. If you're using an earlier Unity version, you should use traditional switch statements with type patterns or if-else statements instead.

Enemy enemy = GetNearestEnemy();

switch (enemy)
{
case { Health: <= 0 }:
Console.WriteLine("This enemy is already defeated.");
break;
case { IsBoss: true, IsAggressive: true }:
Console.WriteLine("Warning! Aggressive boss enemy detected!");
SoundAlarm();
break;
case { IsBoss: true }:
Console.WriteLine("Boss enemy detected!");
PrepareForBossFight();
break;
case { IsAggressive: true, DetectionRange: > 10 }:
Console.WriteLine("Aggressive enemy with long detection range!");
Sneak();
break;
case { IsAggressive: true }:
Console.WriteLine("Aggressive enemy detected!");
PrepareForCombat();
break;
case { IsAggressive: false }:
Console.WriteLine("Passive enemy detected.");
break;
default:
Console.WriteLine("Unknown enemy type.");
break;
}

Tuple Patterns (C# 8.0+)

You can also match on tuples, which is useful for checking multiple values at once:

(bool hasKey, bool hasLockpick, bool isDoorLocked) = (player.HasKey, player.HasLockpick, door.IsLocked);

switch (hasKey, hasLockpick, isDoorLocked)
{
case (true, _, true):
Console.WriteLine("You unlock the door with your key.");
door.Unlock();
break;
case (false, true, true):
Console.WriteLine("You pick the lock with your lockpick.");
door.Unlock();
player.LockpickSkill++;
break;
case (false, false, true):
Console.WriteLine("The door is locked and you have no way to open it.");
break;
case (_, _, false):
Console.WriteLine("The door is already unlocked.");
break;
}

The _ is a discard pattern that matches any value.

When Guards

You can add additional conditions to case patterns using when:

int playerLevel = 5;
int enemyLevel = 8;

switch (enemyLevel - playerLevel)
{
case var difference when difference > 5:
Console.WriteLine("This enemy is far too powerful! Run away!");
break;
case var difference when difference > 2:
Console.WriteLine("This enemy is stronger than you. Proceed with caution.");
break;
case var difference when difference >= 0:
Console.WriteLine("This enemy is a fair match for your skills.");
break;
case var difference when difference > -3:
Console.WriteLine("You have the upper hand against this enemy.");
break;
default:
Console.WriteLine("This enemy is no match for your power!");
break;
}

Switch Expressions (C# 8.0+)

C# 8.0 introduced switch expressions, which provide a more concise syntax for switch statements that compute a value:

Unity Compatibility Warning

Switch expressions are a C# 8.0 feature that requires Unity 2020.2 or later for full support. If you're using an earlier Unity version, you should use traditional switch statements instead. The examples below are provided for learning purposes and for use with compatible Unity versions.

// Traditional switch statement
string GetWeaponType(Weapon weapon)
{
switch (weapon.Category)
{
case WeaponCategory.Sword:
case WeaponCategory.Dagger:
case WeaponCategory.Axe:
return "Melee";
case WeaponCategory.Bow:
case WeaponCategory.Crossbow:
return "Ranged";
case WeaponCategory.Staff:
case WeaponCategory.Wand:
return "Magic";
default:
return "Unknown";
}
}

// Equivalent switch expression
string GetWeaponTypeExpression(Weapon weapon) => weapon.Category switch
{
WeaponCategory.Sword or WeaponCategory.Dagger or WeaponCategory.Axe => "Melee",
WeaponCategory.Bow or WeaponCategory.Crossbow => "Ranged",
WeaponCategory.Staff or WeaponCategory.Wand => "Magic",
_ => "Unknown"
};

The _ is a discard pattern that matches any value, similar to the default case in a traditional switch statement.

Property Patterns in Switch Expressions

You can use property patterns in switch expressions for even more concise code:

string GetEnemyThreatLevel(Enemy enemy) => enemy switch
{
{ IsBoss: true, Health: > 1000 } => "Extreme Threat",
{ IsBoss: true } => "High Threat",
{ IsAggressive: true, AttackDamage: > 50 } => "Significant Threat",
{ IsAggressive: true } => "Moderate Threat",
{ IsAggressive: false } => "Low Threat",
_ => "Unknown Threat Level"
};

Tuple Patterns in Switch Expressions

Tuple patterns are particularly elegant in switch expressions:

string GetDoorStatus(bool hasKey, bool hasLockpick, bool isDoorLocked) => (hasKey, hasLockpick, isDoorLocked) switch
{
(true, _, true) => "You can unlock the door with your key.",
(false, true, true) => "You can pick the lock with your lockpick.",
(false, false, true) => "The door is locked and you have no way to open it.",
(_, _, false) => "The door is already unlocked."
};

Practical Examples in Game Development

Example 1: Player Input Handling

void ProcessInput(string input)
{
switch (input.ToLower())
{
case "w":
case "up":
MovePlayer(Direction.North);
break;
case "s":
case "down":
MovePlayer(Direction.South);
break;
case "a":
case "left":
MovePlayer(Direction.West);
break;
case "d":
case "right":
MovePlayer(Direction.East);
break;
case "i":
case "inventory":
DisplayInventory();
break;
case "m":
case "map":
DisplayMap();
break;
case "h":
case "help":
DisplayHelp();
break;
case "q":
case "quit":
QuitGame();
break;
default:
Console.WriteLine("Unknown command. Type 'help' for a list of commands.");
break;
}
}

Example 2: Damage Calculation Based on Armor Type and Attack Type

float CalculateDamage(AttackType attackType, ArmorType armorType, float baseDamage)
{
float damageMultiplier = (attackType, armorType) switch
{
(AttackType.Slash, ArmorType.Cloth) => 1.5f,
(AttackType.Slash, ArmorType.Leather) => 1.2f,
(AttackType.Slash, ArmorType.Mail) => 0.8f,
(AttackType.Slash, ArmorType.Plate) => 0.5f,

(AttackType.Pierce, ArmorType.Cloth) => 1.2f,
(AttackType.Pierce, ArmorType.Leather) => 1.0f,
(AttackType.Pierce, ArmorType.Mail) => 1.2f,
(AttackType.Pierce, ArmorType.Plate) => 0.8f,

(AttackType.Blunt, ArmorType.Cloth) => 1.0f,
(AttackType.Blunt, ArmorType.Leather) => 0.9f,
(AttackType.Blunt, ArmorType.Mail) => 1.2f,
(AttackType.Blunt, ArmorType.Plate) => 1.5f,

(AttackType.Magic, ArmorType.Cloth) => 0.5f,
(AttackType.Magic, ArmorType.Leather) => 0.8f,
(AttackType.Magic, ArmorType.Mail) => 1.0f,
(AttackType.Magic, ArmorType.Plate) => 1.2f,

_ => 1.0f // Default multiplier for any other combination
};

return baseDamage * damageMultiplier;
}

Example 3: State Machine for Enemy AI

void UpdateEnemyAI()
{
switch (currentState)
{
case EnemyState.Idle:
// Check if player is in detection range
if (IsPlayerInDetectionRange())
{
currentState = EnemyState.Alert;
PlayAlertAnimation();
}
else
{
// Occasionally look around or patrol
if (Random.value < 0.01f)
{
currentState = Random.value < 0.5f ? EnemyState.LookAround : EnemyState.Patrol;
}
}
break;

case EnemyState.Alert:
// Look for player
LookForPlayer();

if (IsPlayerVisible())
{
currentState = EnemyState.Chase;
PlayChaseSound();
}
else if (alertTimer > maxAlertTime)
{
currentState = EnemyState.LookAround;
alertTimer = 0;
}
else
{
alertTimer += Time.deltaTime;
}
break;

case EnemyState.Chase:
if (IsPlayerInAttackRange())
{
currentState = EnemyState.Attack;
PrepareAttack();
}
else if (!IsPlayerVisible() && !IsPlayerInDetectionRange())
{
currentState = EnemyState.Search;
lastKnownPlayerPosition = playerTransform.position;
searchTimer = 0;
}
else
{
ChasePlayer();
}
break;

case EnemyState.Attack:
if (!IsPlayerInAttackRange())
{
currentState = EnemyState.Chase;
}
else if (attackCooldown <= 0)
{
PerformAttack();
attackCooldown = attackRate;
}
else
{
attackCooldown -= Time.deltaTime;
}
break;

case EnemyState.Search:
MoveToPosition(lastKnownPlayerPosition);

if (IsPlayerVisible())
{
currentState = EnemyState.Chase;
}
else if (HasReachedPosition(lastKnownPlayerPosition) || searchTimer > maxSearchTime)
{
currentState = EnemyState.LookAround;
searchTimer = 0;
}
else
{
searchTimer += Time.deltaTime;
}
break;

case EnemyState.LookAround:
LookAround();

if (IsPlayerVisible())
{
currentState = EnemyState.Chase;
}
else if (lookAroundTimer > maxLookAroundTime)
{
currentState = EnemyState.Patrol;
lookAroundTimer = 0;
}
else
{
lookAroundTimer += Time.deltaTime;
}
break;

case EnemyState.Patrol:
Patrol();

if (IsPlayerVisible() || IsPlayerInDetectionRange())
{
currentState = EnemyState.Alert;
}
else if (HasReachedPatrolPoint())
{
SetNextPatrolPoint();
}
break;

case EnemyState.Stunned:
if (stunnedTimer > stunnedDuration)
{
currentState = EnemyState.Alert;
stunnedTimer = 0;
}
else
{
stunnedTimer += Time.deltaTime;
}
break;

default:
Debug.LogError($"Unhandled enemy state: {currentState}");
currentState = EnemyState.Idle;
break;
}
}

Switch vs. If-Else: When to Use Each

Both switch statements and if-else chains can handle multiple conditions, but they have different strengths:

Use Switch When:

  • You're comparing a single variable against multiple constant values
  • You have many possible values to check (more than 2-3)
  • You're working with enums
  • You want to express "pick exactly one option based on this value"
  • Pattern matching would simplify your code (C# 7.0+)

Use If-Else When:

  • You're checking multiple different variables or expressions
  • Your conditions involve ranges or complex logic
  • You have only a few conditions to check
  • Your conditions aren't based on equality comparisons
  • You need to check conditions in a specific order

Best Practices for Switch Statements

  1. Always include a default case to handle unexpected values, even if you think all possible values are covered.

  2. Don't forget the break statement at the end of each case (unless you intentionally want fall-through behavior).

  3. Consider using enums instead of magic numbers to make your code more readable and maintainable.

  4. Keep case blocks short or call methods from them to maintain readability.

  5. Use pattern matching (C# 7.0+) for more expressive and concise code.

  6. Consider switch expressions (C# 8.0+) when computing a value based on a switch.

  7. Be mindful of fall-through behavior - use it intentionally and document it when you do.

Conclusion

The switch statement is a powerful tool for handling multiple conditions based on a single value. With the addition of pattern matching and switch expressions in modern C#, it has become even more versatile and expressive.

In this section, we've covered:

  • Basic switch syntax and behavior
  • Fall-through behavior and when to use it
  • Working with strings and enums in switch statements
  • Pattern matching in switch statements
  • Switch expressions for more concise code
  • Practical examples in game development
  • When to use switch vs. if-else

In the next section, we'll explore looping constructs, which allow you to repeat blocks of code multiple times.

Unity Relevance

In Unity development, switch statements are commonly used for:

  • State machines for character or enemy AI
  • Handling different types of items or power-ups
  • Processing user input commands
  • Determining effects based on collision types
  • Selecting animations based on character state

Switch statements with enums are particularly useful in Unity for creating clear, type-safe state machines that are easy to debug and extend.