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:
- Uses a List to store items (dynamic size)
- Uses a Dictionary to look up items by ID (fast access)
- Supports adding, removing, and finding items
- 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.