Collections: Arrays, Lists, and Dictionaries

Collections are essential for managing groups of objects in your games. Whether you're tracking enemies, managing inventory items, or storing player scores, collections help you organize and manipulate data efficiently. In C#, you have several collection types to choose from, each with its own strengths and use cases.

In this chapter, you'll learn how to use arrays, lists, and dictionaries - the three most common collection types in C# game development. By the end, you'll be able to choose the right collection for your needs and use them effectively in your Unity games.

What You'll Learn

  • Arrays - Fixed-size collections for known quantities
  • Lists - Dynamic collections that grow and shrink
  • Dictionaries - Key-value pairs for fast lookups
  • When to use each collection type - Choosing the right tool for the job
  • Common operations - Adding, removing, searching, and iterating
  • Practical Unity examples - Real-world game development applications

Prerequisites

  • Completed Inheritance and Polymorphism
  • Understanding of variables, data types, and loops
  • Basic familiarity with Unity (optional, but helpful)

Understanding Collections

Collections are data structures that hold multiple values. Instead of creating separate variables for each item, you can store them all in one collection and access them efficiently.

Real-World Analogy:

  • Array = Fixed parking lot (exactly 10 spaces, numbered 0-9)
  • List = Expandable parking lot (starts small, adds spaces as needed)
  • Dictionary = Valet parking (give them your name, they find your car instantly)

In Game Development:

  • Array = Fixed number of inventory slots
  • List = Dynamic list of enemies (grows as enemies spawn)
  • Dictionary = Player database (look up player by ID instantly)

Why Collections Matter in Games

Organization:

  • Group related data together
  • Process multiple items efficiently
  • Reduce code duplication

Performance:

  • Fast access to data
  • Efficient memory usage
  • Optimized for common operations

Flexibility:

  • Handle varying amounts of data
  • Easy to add, remove, and modify items
  • Support different access patterns

Arrays

Arrays are fixed-size collections that store elements of the same type. Once created, their size cannot change.

Creating Arrays

// Array of integers
int[] scores = new int[5]; // Creates array with 5 elements

// Array with initial values
int[] levels = { 1, 2, 3, 4, 5 };

// Array of strings
string[] playerNames = new string[3];
playerNames[0] = "GamineAI Team";
playerNames[1] = "Bob";
playerNames[2] = "Charlie";

Key Points:

  • Arrays have fixed size (cannot grow or shrink)
  • Elements are accessed by index (starting at 0)
  • All elements must be the same type
  • Size is specified when creating the array

Accessing Array Elements

int[] scores = { 100, 200, 150, 300, 250 };

// Access by index
int firstScore = scores[0];  // 100
int lastScore = scores[4];   // 250

// Modify elements
scores[0] = 150; // Change first score to 150

// Get array length
int count = scores.Length; // 5

Iterating Through Arrays

int[] scores = { 100, 200, 150, 300, 250 };

// Using for loop
for (int i = 0; i < scores.Length; i++)
{
    Console.WriteLine($"Score {i}: {scores[i]}");
}

// Using foreach loop
foreach (int score in scores)
{
    Console.WriteLine($"Score: {score}");
}

Game Development Example: Inventory System

using UnityEngine;

public class Inventory : MonoBehaviour
{
    // Fixed-size inventory (10 slots)
    public Item[] items = new Item[10];

    public void AddItem(Item item, int slot)
    {
        if (slot >= 0 && slot < items.Length)
        {
            items[slot] = item;
        }
    }

    public Item GetItem(int slot)
    {
        if (slot >= 0 && slot < items.Length)
        {
            return items[slot];
        }
        return null;
    }

    public bool HasEmptySlot()
    {
        for (int i = 0; i < items.Length; i++)
        {
            if (items[i] == null)
            {
                return true;
            }
        }
        return false;
    }
}

Lists

Lists are dynamic collections that can grow and shrink as needed. They're more flexible than arrays but slightly slower.

Creating Lists

using System.Collections.Generic;

// Create empty list
List<int> scores = new List<int>();

// Create list with initial values
List<string> players = new List<string> { "GamineAI Team", "Bob", "Charlie" };

// Create list with initial capacity (optimization)
List<Enemy> enemies = new List<Enemy>(50);

Key Points:

  • Lists can grow and shrink dynamically
  • Elements are accessed by index (starting at 0)
  • All elements must be the same type
  • More flexible than arrays, slightly slower

Adding and Removing Elements

List<string> players = new List<string>();

// Add elements
players.Add("GamineAI Team");
players.Add("Bob");
players.Add("Charlie");

// Insert at specific position
players.Insert(1, "GamineAI Team"); // Insert at index 1

// Remove elements
players.Remove("Bob"); // Remove by value
players.RemoveAt(0);   // Remove by index

// Clear all elements
players.Clear();

Accessing List Elements

List<int> scores = new List<int> { 100, 200, 150 };

// Access by index
int firstScore = scores[0];  // 100
int lastScore = scores[scores.Count - 1]; // 150

// Modify elements
scores[0] = 150;

// Get count
int count = scores.Count; // 3

Common List Operations

List<int> scores = new List<int> { 100, 200, 150, 300, 250 };

// Check if contains value
bool hasHighScore = scores.Contains(300); // true

// Find index of value
int index = scores.IndexOf(150); // 2

// Sort list
scores.Sort(); // { 100, 150, 200, 250, 300 }

// Reverse list
scores.Reverse(); // { 300, 250, 200, 150, 100 }

// Find elements
int highScore = scores.Find(score => score > 200); // 300
List<int> highScores = scores.FindAll(score => score > 150); // { 200, 250, 300 }

Game Development Example: Enemy Manager

using UnityEngine;
using System.Collections.Generic;

public class EnemyManager : MonoBehaviour
{
    // Dynamic list of enemies (grows as enemies spawn)
    private List<Enemy> enemies = new List<Enemy>();

    public void SpawnEnemy(Enemy enemyPrefab, Vector3 position)
    {
        Enemy newEnemy = Instantiate(enemyPrefab, position, Quaternion.identity);
        enemies.Add(newEnemy);
    }

    public void RemoveEnemy(Enemy enemy)
    {
        if (enemies.Contains(enemy))
        {
            enemies.Remove(enemy);
            Destroy(enemy.gameObject);
        }
    }

    public Enemy FindNearestEnemy(Vector3 position)
    {
        Enemy nearest = null;
        float nearestDistance = float.MaxValue;

        foreach (Enemy enemy in enemies)
        {
            float distance = Vector3.Distance(position, enemy.transform.position);
            if (distance < nearestDistance)
            {
                nearestDistance = distance;
                nearest = enemy;
            }
        }

        return nearest;
    }

    public void RemoveAllEnemies()
    {
        foreach (Enemy enemy in enemies)
        {
            Destroy(enemy.gameObject);
        }
        enemies.Clear();
    }
}

Dictionaries

Dictionaries store key-value pairs, allowing you to look up values quickly using a key. They're perfect for fast lookups and associations.

Creating Dictionaries

using System.Collections.Generic;

// Create empty dictionary
Dictionary<string, int> playerScores = new Dictionary<string, int>();

// Create dictionary with initial values
Dictionary<int, string> levelNames = new Dictionary<int, string>
{
    { 1, "Forest" },
    { 2, "Cave" },
    { 3, "Castle" }
};

// Dictionary with different types
Dictionary<string, Player> players = new Dictionary<string, Player>();

Key Points:

  • Key-value pairs (each key maps to one value)
  • Keys must be unique
  • Very fast lookups by key
  • Keys and values can be different types

Adding and Accessing Dictionary Elements

Dictionary<string, int> playerScores = new Dictionary<string, int>();

// Add elements
playerScores.Add("GamineAI Team", 1000);
playerScores.Add("Bob", 1500);
playerScores["Charlie"] = 2000; // Alternative syntax

// Access by key
int aliceScore = playerScores["GamineAI Team"]; // 1000

// Check if key exists
if (playerScores.ContainsKey("GamineAI Team"))
{
    int score = playerScores["GamineAI Team"];
}

// Safe access with TryGetValue
if (playerScores.TryGetValue("GamineAI Team", out int score))
{
    Console.WriteLine($"Alice's score: {score}");
}

Removing Dictionary Elements

Dictionary<string, int> playerScores = new Dictionary<string, int>
{
    { "GamineAI Team", 1000 },
    { "Bob", 1500 },
    { "Charlie", 2000 }
};

// Remove by key
playerScores.Remove("Bob");

// Clear all elements
playerScores.Clear();

Iterating Through Dictionaries

Dictionary<string, int> playerScores = new Dictionary<string, int>
{
    { "GamineAI Team", 1000 },
    { "Bob", 1500 },
    { "Charlie", 2000 }
};

// Iterate through keys
foreach (string playerName in playerScores.Keys)
{
    Console.WriteLine($"Player: {playerName}");
}

// Iterate through values
foreach (int score in playerScores.Values)
{
    Console.WriteLine($"Score: {score}");
}

// Iterate through key-value pairs
foreach (KeyValuePair<string, int> pair in playerScores)
{
    Console.WriteLine($"{pair.Key}: {pair.Value}");
}

// Using var for cleaner syntax
foreach (var pair in playerScores)
{
    Console.WriteLine($"{pair.Key}: {pair.Value}");
}

Game Development Example: Player Database

using UnityEngine;
using System.Collections.Generic;

public class PlayerDatabase : MonoBehaviour
{
    // Dictionary: Player ID -> Player data
    private Dictionary<int, Player> players = new Dictionary<int, Player>();

    public void RegisterPlayer(int playerId, Player player)
    {
        if (!players.ContainsKey(playerId))
        {
            players[playerId] = player;
        }
    }

    public Player GetPlayer(int playerId)
    {
        if (players.TryGetValue(playerId, out Player player))
        {
            return player;
        }
        return null;
    }

    public bool IsPlayerRegistered(int playerId)
    {
        return players.ContainsKey(playerId);
    }

    public void RemovePlayer(int playerId)
    {
        if (players.ContainsKey(playerId))
        {
            players.Remove(playerId);
        }
    }

    public List<Player> GetAllPlayers()
    {
        return new List<Player>(players.Values);
    }
}

Choosing the Right Collection

Each collection type has its strengths. Choose based on your needs:

Use Arrays When:

  • You know the exact size beforehand
  • Size won't change
  • You need maximum performance
  • Memory usage is critical

Examples: Inventory slots, level layouts, fixed-size buffers

Use Lists When:

  • Size changes frequently
  • You need to add/remove items often
  • Order matters
  • You need flexibility

Examples: Enemy lists, inventory items, player queues, dynamic content

Use Dictionaries When:

  • You need fast lookups by key
  • You have unique identifiers
  • You need associations (key -> value)
  • Order doesn't matter

Examples: Player databases, item lookups, configuration settings, caches

Common Operations Comparison

Finding an Element

// Array/List: O(n) - must check each element
int[] scores = { 100, 200, 150, 300 };
int index = Array.IndexOf(scores, 150); // Linear search

// Dictionary: O(1) - instant lookup
Dictionary<string, int> playerScores = new Dictionary<string, int>();
int score = playerScores["GamineAI Team"]; // Instant lookup

Adding Elements

// Array: Cannot add (fixed size)
int[] scores = new int[5];
scores[0] = 100; // Must know index

// List: O(1) average, O(n) worst case
List<int> scores = new List<int>();
scores.Add(100); // Automatically grows

// Dictionary: O(1) average
Dictionary<string, int> scores = new Dictionary<string, int>();
scores["GamineAI Team"] = 100; // Instant add

Unity Game Development Examples

Example 1: Scoreboard System

using UnityEngine;
using System.Collections.Generic;
using System.Linq;

public class Scoreboard : MonoBehaviour
{
    // Dictionary: Player name -> Score
    private Dictionary<string, int> scores = new Dictionary<string, int>();

    public void AddScore(string playerName, int points)
    {
        if (scores.ContainsKey(playerName))
        {
            scores[playerName] += points;
        }
        else
        {
            scores[playerName] = points;
        }
    }

    public List<KeyValuePair<string, int>> GetTopScores(int count)
    {
        // Sort by score (descending) and take top N
        return scores.OrderByDescending(pair => pair.Value)
                     .Take(count)
                     .ToList();
    }

    public void DisplayScores()
    {
        var topScores = GetTopScores(10);
        foreach (var pair in topScores)
        {
            Debug.Log($"{pair.Key}: {pair.Value}");
        }
    }
}

Example 2: Item Registry

using UnityEngine;
using System.Collections.Generic;

public class ItemRegistry : MonoBehaviour
{
    // Dictionary: Item ID -> Item data
    private Dictionary<int, ItemData> items = new Dictionary<int, ItemData>();

    public void RegisterItem(int itemId, ItemData itemData)
    {
        items[itemId] = itemData;
    }

    public ItemData GetItem(int itemId)
    {
        if (items.TryGetValue(itemId, out ItemData item))
        {
            return item;
        }
        return null;
    }

    public ItemData GetItemByName(string itemName)
    {
        foreach (var item in items.Values)
        {
            if (item.name == itemName)
            {
                return item;
            }
        }
        return null;
    }
}

[System.Serializable]
public class ItemData
{
    public int id;
    public string name;
    public string description;
    public int value;
}

Example 3: Wave-Based Enemy Spawning

using UnityEngine;
using System.Collections.Generic;

public class WaveManager : MonoBehaviour
{
    // List of enemy prefabs for current wave
    private List<GameObject> currentWaveEnemies = new List<GameObject>();

    // Array of wave configurations
    private WaveConfig[] waves = new WaveConfig[10];

    private int currentWaveIndex = 0;

    public void SpawnWave()
    {
        if (currentWaveIndex >= waves.Length) return;

        WaveConfig wave = waves[currentWaveIndex];
        currentWaveEnemies.Clear();

        // Spawn enemies for this wave
        foreach (EnemySpawn spawn in wave.enemySpawns)
        {
            for (int i = 0; i < spawn.count; i++)
            {
                GameObject enemy = Instantiate(spawn.enemyPrefab, spawn.spawnPoint, Quaternion.identity);
                currentWaveEnemies.Add(enemy);
            }
        }

        currentWaveIndex++;
    }

    public bool IsWaveComplete()
    {
        // Remove destroyed enemies from list
        currentWaveEnemies.RemoveAll(enemy => enemy == null);
        return currentWaveEnemies.Count == 0;
    }
}

[System.Serializable]
public class WaveConfig
{
    public EnemySpawn[] enemySpawns;
}

[System.Serializable]
public class EnemySpawn
{
    public GameObject enemyPrefab;
    public Vector3 spawnPoint;
    public int count;
}

Best Practices

Initialize Collections Properly

// Good: Initialize with capacity if you know approximate size
List<Enemy> enemies = new List<Enemy>(50); // Pre-allocates space

// Good: Use initializers for known values
Dictionary<string, int> config = new Dictionary<string, int>
{
    { "maxHealth", 100 },
    { "maxMana", 50 }
};

// Avoid: Growing collections unnecessarily
List<int> scores = new List<int>(); // Fine for small lists

Use Appropriate Collection Types

// Good: Dictionary for fast lookups
Dictionary<int, Player> players = new Dictionary<int, Player>();

// Good: List for dynamic collections
List<Enemy> enemies = new List<Enemy>();

// Good: Array for fixed-size collections
Item[] inventory = new Item[10];

Handle Null and Empty Collections

List<Enemy> enemies = new List<Enemy>();

// Check before iterating
if (enemies != null && enemies.Count > 0)
{
    foreach (Enemy enemy in enemies)
    {
        // Process enemy
    }
}

// Or use null-conditional operator
enemies?.ForEach(enemy => enemy.Attack());

Common Mistakes to Avoid

Mistake 1: Array Index Out of Bounds

// Wrong: Accessing invalid index
int[] scores = new int[5];
int value = scores[10]; // ERROR: Index out of range

// Correct: Check bounds first
if (index >= 0 && index < scores.Length)
{
    int value = scores[index];
}

Mistake 2: Modifying Collection While Iterating

// Wrong: Modifying during iteration
List<Enemy> enemies = new List<Enemy> { /* ... */ };
foreach (Enemy enemy in enemies)
{
    if (enemy.IsDead())
    {
        enemies.Remove(enemy); // ERROR: Collection modified
    }
}

// Correct: Collect items to remove first
List<Enemy> toRemove = new List<Enemy>();
foreach (Enemy enemy in enemies)
{
    if (enemy.IsDead())
    {
        toRemove.Add(enemy);
    }
}
foreach (Enemy enemy in toRemove)
{
    enemies.Remove(enemy);
}

// Or use RemoveAll
enemies.RemoveAll(enemy => enemy.IsDead());

Mistake 3: Dictionary Key Not Found

// Wrong: Accessing non-existent key
Dictionary<string, int> scores = new Dictionary<string, int>();
int score = scores["GamineAI Team"]; // ERROR: Key not found

// Correct: Check if key exists
if (scores.ContainsKey("GamineAI Team"))
{
    int score = scores["GamineAI Team"];
}

// Or use TryGetValue
if (scores.TryGetValue("GamineAI Team", out int score))
{
    // Use score
}

Practical Exercise

Create a game inventory system that:

  1. Uses a List to store items (dynamic size)
  2. Uses a Dictionary to look up items by ID (fast access)
  3. Supports adding, removing, and finding items
  4. Displays all items in the inventory

Solution:

using System.Collections.Generic;
using UnityEngine;

public class GameInventory
{
    // List for ordered storage
    private List<Item> items = new List<Item>();

    // Dictionary for fast lookup by ID
    private Dictionary<int, Item> itemsById = new Dictionary<int, Item>();

    public void AddItem(Item item)
    {
        items.Add(item);
        itemsById[item.id] = item;
    }

    public void RemoveItem(int itemId)
    {
        if (itemsById.TryGetValue(itemId, out Item item))
        {
            items.Remove(item);
            itemsById.Remove(itemId);
        }
    }

    public Item GetItem(int itemId)
    {
        itemsById.TryGetValue(itemId, out Item item);
        return item;
    }

    public List<Item> GetAllItems()
    {
        return new List<Item>(items);
    }

    public bool HasItem(int itemId)
    {
        return itemsById.ContainsKey(itemId);
    }
}

Next Steps

Now that you understand collections, you're ready to learn about error handling and debugging:

  • Error Handling - Try-Catch blocks and exception handling
  • File I/O - Saving and loading game data
  • Advanced C# Features - LINQ, async/await, and more

Move on to Error Handling: Try-Catch and Debugging to learn how to handle errors gracefully in your games.

Summary

  • Arrays are fixed-size collections, best for known quantities
  • Lists are dynamic collections that grow and shrink
  • Dictionaries store key-value pairs for fast lookups
  • Choose the right collection based on your needs
  • Arrays: fixed size, maximum performance
  • Lists: dynamic size, flexible operations
  • Dictionaries: fast lookups, key-value associations
  • Collections are essential for managing groups of objects in games

Understanding collections is fundamental to game development. Whether you're managing enemies, tracking scores, or storing game data, collections help you organize and process information efficiently. Practice using each type to understand when to use arrays, lists, or dictionaries in your projects.