8.8 - Enums
In game development, you'll often need to represent a set of related named constants—like weapon types, enemy states, or difficulty levels. Enumerations, or enums for short, provide an elegant way to define these sets of named values, making your code more readable, maintainable, and type-safe.
What Are Enums?
An enum is a distinct value type that declares a set of named constants. Enums provide several benefits:
- Readability: Using named constants instead of magic numbers makes code more understandable
- Type safety: The compiler ensures you only use valid enum values
- Maintainability: Adding, removing, or changing enum values is easier than updating raw numbers throughout your code
- IntelliSense support: IDEs can show you available enum values as you type
Basic Enum Syntax
To define an enum, use the enum
keyword:
// Basic enum definition
public enum GameState
{
MainMenu,
Loading,
Playing,
Paused,
GameOver
}
// Using the enum
GameState currentState = GameState.MainMenu;
// Checking enum values
if (currentState == GameState.Playing)
{
// Handle playing state
}
// Switching on enum values
switch (currentState)
{
case GameState.MainMenu:
ShowMainMenu();
break;
case GameState.Loading:
LoadGameResources();
break;
case GameState.Playing:
UpdateGameplay();
break;
case GameState.Paused:
ShowPauseMenu();
break;
case GameState.GameOver:
ShowGameOverScreen();
break;
}
Enum Underlying Types
By default, enum values are stored as integers, starting from 0 and incrementing by 1. You can specify a different underlying type using a colon after the enum name:
// Enum with byte as underlying type (saves memory if you have many instances)
public enum ItemRarity : byte
{
Common, // 0
Uncommon, // 1
Rare, // 2
Epic, // 3
Legendary // 4
}
The available underlying types for enums are: byte
, sbyte
, short
, ushort
, int
, uint
, long
, and ulong
.
Explicit Enum Values
You can explicitly assign values to enum members:
public enum DamageType
{
Physical = 0,
Fire = 1,
Ice = 2,
Lightning = 3,
Poison = 4
}
You can also assign the same value to multiple enum members, creating aliases:
public enum MovementDirection
{
North = 0,
East = 1,
South = 2,
West = 3,
// Aliases
Up = North,
Right = East,
Down = South,
Left = West
}
Non-Sequential Enum Values
Enum values don't have to be sequential. You might use specific values for bit flags (which we'll cover later) or to match external systems:
public enum HttpStatusCode
{
OK = 200,
Created = 201,
Accepted = 202,
BadRequest = 400,
Unauthorized = 401,
Forbidden = 403,
NotFound = 404,
InternalServerError = 500,
NotImplemented = 501,
BadGateway = 502
}
Converting Between Enums and Integers
You can explicitly cast between enums and their underlying type:
// Integer to enum
int stateValue = 2;
GameState state = (GameState)stateValue; // state = GameState.Playing
// Enum to integer
GameState currentState = GameState.Paused;
int stateNumber = (int)currentState; // stateNumber = 3
Be careful when casting integers to enums—if the integer doesn't correspond to a defined enum value, you'll get an undefined enum value that might cause unexpected behavior.
Checking for Valid Enum Values
To check if an integer corresponds to a defined enum value, you can use Enum.IsDefined
:
int userInput = GetUserInput();
if (Enum.IsDefined(typeof(GameState), userInput))
{
GameState state = (GameState)userInput;
// Use the valid enum value
}
else
{
Console.WriteLine("Invalid game state!");
}
Enum Methods and Properties
The Enum
class provides several useful methods for working with enumerations:
// Get all defined values of an enum
Array values = Enum.GetValues(typeof(GameState));
foreach (GameState state in values)
{
Console.WriteLine(state);
}
// Get all defined names of an enum
string[] names = Enum.GetNames(typeof(GameState));
foreach (string name in names)
{
Console.WriteLine(name);
}
// Parse a string to an enum value
if (Enum.TryParse<GameState>("Paused", out GameState result))
{
Console.WriteLine($"Parsed value: {result}");
}
Enum Flags
One powerful feature of enums is the ability to use them as bit flags, allowing multiple values to be combined into a single value. This is useful for representing sets of options or states that can be combined.
To create a flags enum, you:
- Apply the
[Flags]
attribute - Define values as powers of 2 (1, 2, 4, 8, 16, etc.)
- Optionally define common combinations
[Flags]
public enum PlayerStatus
{
None = 0,
Poisoned = 1,
Burning = 2,
Frozen = 4,
Stunned = 8,
Invulnerable = 16,
// Common combinations
MovementImpaired = Frozen | Stunned,
DamageOverTime = Poisoned | Burning
}
// Using flags
PlayerStatus status = PlayerStatus.None;
// Add flags
status |= PlayerStatus.Poisoned;
status |= PlayerStatus.Stunned;
// Check if a flag is set
if ((status & PlayerStatus.Poisoned) != 0)
{
ApplyPoisonDamage();
}
// Check if all flags in a combination are set
if ((status & PlayerStatus.MovementImpaired) == PlayerStatus.MovementImpaired)
{
// Player has all movement impairment statuses
}
// Remove a flag
status &= ~PlayerStatus.Poisoned;
// Toggle a flag
status ^= PlayerStatus.Invulnerable;
// Clear all flags
status = PlayerStatus.None;
The HasFlag
method provides a more readable way to check for flags:
if (status.HasFlag(PlayerStatus.Burning))
{
ApplyBurningDamage();
}
However, be aware that HasFlag
is slightly less efficient than the bitwise operations shown earlier.
Practical Example: Character Controller
Let's see how enums can be used in a character controller for a game:
// Character states
public enum CharacterState
{
Idle,
Walking,
Running,
Jumping,
Falling,
Attacking,
Blocking,
Stunned,
Dead
}
// Input actions (using flags for combinations)
[Flags]
public enum InputAction
{
None = 0,
MoveLeft = 1,
MoveRight = 2,
MoveForward = 4,
MoveBackward = 8,
Jump = 16,
Attack = 32,
Block = 64,
Crouch = 128,
// Common combinations
Movement = MoveLeft | MoveRight | MoveForward | MoveBackward
}
// Damage types (using flags to allow multiple types)
[Flags]
public enum DamageType
{
None = 0,
Physical = 1,
Fire = 2,
Ice = 4,
Lightning = 8,
Poison = 16,
Magic = 32,
// Common combinations
Elemental = Fire | Ice | Lightning | Poison,
All = Physical | Elemental | Magic
}
// Character controller using these enums
public class CharacterController
{
public CharacterState CurrentState { get; private set; } = CharacterState.Idle;
public InputAction CurrentInput { get; private set; } = InputAction.None;
// Character stats
public float Health { get; private set; } = 100f;
public float Stamina { get; private set; } = 100f;
// Resistances to different damage types (0-1 range)
private Dictionary<DamageType, float> _resistances = new Dictionary<DamageType, float>();
public CharacterController()
{
// Initialize resistances
_resistances[DamageType.Physical] = 0.2f;
_resistances[DamageType.Fire] = 0.1f;
_resistances[DamageType.Ice] = 0.3f;
_resistances[DamageType.Lightning] = 0.0f;
_resistances[DamageType.Poison] = 0.5f;
_resistances[DamageType.Magic] = 0.1f;
}
public void ProcessInput(InputAction input)
{
CurrentInput = input;
// Update character state based on input
if (CurrentState == CharacterState.Dead)
return;
if (input.HasFlag(InputAction.Jump) && CanJump())
{
CurrentState = CharacterState.Jumping;
}
else if (input.HasFlag(InputAction.Attack) && CanAttack())
{
CurrentState = CharacterState.Attacking;
Stamina -= 10f;
}
else if (input.HasFlag(InputAction.Block) && CanBlock())
{
CurrentState = CharacterState.Blocking;
}
else if ((input & InputAction.Movement) != 0)
{
if (input.HasFlag(InputAction.Crouch))
CurrentState = CharacterState.Walking;
else
CurrentState = CharacterState.Running;
}
else
{
CurrentState = CharacterState.Idle;
}
}
public void TakeDamage(float amount, DamageType type)
{
if (CurrentState == CharacterState.Dead)
return;
// Calculate damage reduction based on resistances
float totalResistance = 0f;
float resistanceCount = 0;
// For each damage type flag that is set, apply the corresponding resistance
foreach (DamageType damageType in Enum.GetValues(typeof(DamageType)))
{
if (damageType != DamageType.None && type.HasFlag(damageType))
{
totalResistance += _resistances[damageType];
resistanceCount++;
}
}
// Calculate average resistance if multiple damage types
float effectiveResistance = resistanceCount > 0 ? totalResistance / resistanceCount : 0;
// Apply damage with resistance
float actualDamage = amount * (1 - effectiveResistance);
// If blocking, reduce damage further
if (CurrentState == CharacterState.Blocking && type.HasFlag(DamageType.Physical))
{
actualDamage *= 0.5f;
}
Health -= actualDamage;
// Check if dead
if (Health <= 0)
{
Health = 0;
CurrentState = CharacterState.Dead;
}
else if (actualDamage > 20)
{
// Heavy hit causes stun
CurrentState = CharacterState.Stunned;
}
}
private bool CanJump() => CurrentState != CharacterState.Jumping &&
CurrentState != CharacterState.Falling &&
CurrentState != CharacterState.Stunned;
private bool CanAttack() => CurrentState != CharacterState.Attacking &&
CurrentState != CharacterState.Stunned &&
Stamina >= 10f;
private bool CanBlock() => CurrentState != CharacterState.Stunned &&
Stamina > 0;
}
This example demonstrates how enums can make your code more readable and maintainable when dealing with states, input combinations, and type systems.
Enum Best Practices
-
Use meaningful names: Choose clear, descriptive names for both the enum type and its values.
-
Follow naming conventions: Use PascalCase for enum types and values.
-
Consider ordering: Arrange enum values in a logical order (e.g., from least to most powerful for item rarities).
-
Document special values: If certain enum values have special meaning or behavior, document them with comments.
-
Use flags appropriately: Only use the
[Flags]
attribute when values are meant to be combined. -
Be consistent with values: If you explicitly set some enum values, consider setting all of them for clarity.
-
Consider extensibility: Leave gaps in enum values if you anticipate adding more values in the future.
Enums in Unity
Unity makes extensive use of enums for various purposes:
- Layer masks: Unity's layer system uses a flags enum internally.
- Animation states: Controlling character animations.
- Rendering options: Configuring how objects are rendered.
- Input handling: Defining input actions and button states.
- Physics interactions: Configuring collision detection.
Unity's Inspector can display enum fields with dropdown menus, making them particularly useful for exposing options to designers. You can also use the [EnumFlags]
attribute in Unity to display a flags enum with checkboxes in the Inspector.
Advanced Enum Techniques
Enum Extension Methods
You can create extension methods for enums to add functionality:
public static class DamageTypeExtensions
{
public static Color GetColor(this DamageType damageType)
{
return damageType switch
{
DamageType.Physical => Color.gray,
DamageType.Fire => Color.red,
DamageType.Ice => Color.cyan,
DamageType.Lightning => Color.yellow,
DamageType.Poison => Color.green,
DamageType.Magic => Color.magenta,
_ => Color.white
};
}
public static string GetDescription(this DamageType damageType)
{
return damageType switch
{
DamageType.Physical => "Blunt or sharp physical damage",
DamageType.Fire => "Burning damage that may ignite targets",
DamageType.Ice => "Freezing damage that may slow targets",
DamageType.Lightning => "Electrical damage that may stun targets",
DamageType.Poison => "Toxic damage that persists over time",
DamageType.Magic => "Arcane damage that bypasses physical resistance",
_ => "Unknown damage type"
};
}
}
Enum Attributes
You can use attributes to add metadata to enum values:
public enum WeaponType
{
[Description("Basic melee weapon with balanced stats")]
Sword,
[Description("Slow but powerful two-handed weapon")]
Axe,
[Description("Fast dual-wielded weapons")]
Daggers,
[Description("Long-range weapon requiring ammunition")]
Bow,
[Description("Magical weapon that consumes mana")]
Staff
}
To retrieve these descriptions, you'd need to use reflection:
public static string GetEnumDescription(Enum value)
{
var field = value.GetType().GetField(value.ToString());
var attribute = field.GetCustomAttribute<DescriptionAttribute>();
return attribute != null ? attribute.Description : value.ToString();
}
// Usage
WeaponType weapon = WeaponType.Axe;
string description = GetEnumDescription(weapon);
Conclusion
Enums are a powerful feature in C# that allow you to define named constants, making your code more readable, maintainable, and type-safe. In game development, they're particularly useful for representing states, types, options, and flags.
By using enums effectively, you can create more expressive and self-documenting code, reduce errors from invalid values, and make your game systems more flexible and extensible.
In the next section, we'll put many of the concepts we've learned into practice with an exercise focused on creating an event system using delegates, events, and LINQ.