Collections
| Types | T[], List<T>, Dictionary<K,V>, HashSet<T>, Queue<T>, Stack<T> |
|---|---|
| Namespace | System.Collections.Generic |
| In Unity | Inventories, enemy lists, spawn points, lookup tables |
Collections
Collections hold many values in one variable. Picking the right one makes your code simpler and faster. Add using System.Collections.Generic; for everything except arrays.
Which one do I need?
| Collection | Think of it as | Use when | Inspector? |
|---|---|---|---|
Array T[] | An egg carton: fixed number of slots | The size doesn't change: spawn points, waypoints | Yes |
List<T> | A shopping list: add and cross off items | The size changes: inventory, active enemies | Yes |
Dictionary<K,V> | A phone book: look up a value by its key | Find things by ID or name: items by ID, stats by name | No |
HashSet<T> | A guest list: each name at most once, fast "are you on the list?" | Uniqueness and fast "contains": enemies already hit by this attack | No |
Queue<T> | A line at the shop: first in, first out | Process in order: dialogue lines, spawn queue, pathfinding | No |
Stack<T> | A stack of plates: last in, first out | Undo history, menu back-navigation | No |
Arrays
[SerializeField] private Transform[] spawnPoints; // set the size and fill it in the Inspector
int[] scores = new int[5]; // 5 slots, all 0
string[] names = { "Ana", "Ben" }; // created with values
Debug.Log(spawnPoints.Length); // number of slots
Transform first = spawnPoints[0]; // index starts at 0
Transform last = spawnPoints[^1]; // ^1 = last element (C# 8)
List<T>
List<Item> inventory = new List<Item>();
inventory.Add(sword); // add to the end
inventory.Insert(0, potion); // add at a position
inventory.Remove(potion); // remove first match (returns true if found)
inventory.RemoveAt(0); // remove by index
bool hasSword = inventory.Contains(sword);
int count = inventory.Count; // Count for lists, Length for arrays
inventory.Clear(); // empty it, keep the memory for reuse
// Remove all matching items
inventory.RemoveAll(item => item.isBroken);
// Find the first match (or null)
Item key = inventory.Find(item => item.id == "gold_key");
Dictionary<TKey, TValue>
Dictionary<string, int> ammo = new Dictionary<string, int>
{
{ "Pistol", 12 },
{ "Shotgun", 6 },
};
ammo["Rifle"] = 30; // add or overwrite
ammo["Pistol"] -= 1; // change existing
// Safe lookup: no exception if the key is missing
if (ammo.TryGetValue("Rocket", out int rockets))
{
Debug.Log($"Rockets: {rockets}");
}
foreach (KeyValuePair<string, int> pair in ammo)
{
Debug.Log($"{pair.Key}: {pair.Value}");
}
Dictionaries in the InspectorUnity can't show dictionaries. A common workaround: serialize a
List of small [Serializable] entries, then build the dictionary from it in Awake.[System.Serializable]
public class ItemEntry { public string id; public ItemData data; }
[SerializeField] private List<ItemEntry> entries; // edited in the Inspector
private Dictionary<string, ItemData> byId;
void Awake()
{
byId = new Dictionary<string, ItemData>();
foreach (var e in entries) byId[e.id] = e.data;
}
HashSet, Queue, Stack
// A sword swing should hit each enemy only once, even with several colliders
HashSet<Enemy> alreadyHit = new HashSet<Enemy>();
if (alreadyHit.Add(enemy)) // Add returns false if it was already there
enemy.TakeDamage(10);
// Dialogue shown in order
Queue<string> lines = new Queue<string>();
lines.Enqueue("Hello!");
lines.Enqueue("Nice weather.");
string next = lines.Dequeue(); // "Hello!"
// Menu back button
Stack<GameObject> menuHistory = new Stack<GameObject>();
menuHistory.Push(optionsMenu);
GameObject previous = menuHistory.Pop();
Looping
// foreach: simplest, read-only
foreach (Enemy e in enemies) e.Alert();
// for: when you need the index, or to change items
for (int i = 0; i < enemies.Count; i++) enemies[i].Alert();
// Removing while looping: go BACKWARDS
for (int i = enemies.Count - 1; i >= 0; i--)
{
if (enemies[i] == null) enemies.RemoveAt(i);
}
What happens at runtime
- A
List<T>is an array inside. When it's full, it creates a bigger array (about double) and copies everything over. If you know the size,new List<T>(100)avoids those copies. ContainsandRemoveon a List check every item one by one. With thousands of items, aHashSetorDictionaryis far faster, because it jumps straight to the right slot using the item's hash.- Creating a new collection allocates memory; creating one every frame creates garbage. Keep collections as fields and
Clear()them.
Common Mistakes
InvalidOperationException: Collection was modified: you added/removed items inside aforeachover the same collection. Loop backwards withfor, or collect changes and apply them after.- Off-by-one:
for (int i = 0; i <= list.Count; i++)goes one past the end. Use<. dictionary[key]for a missing key throwsKeyNotFoundException. UseTryGetValue.- Forgetting to create the collection.
private List<Enemy> enemies;is null until you write= new List<Enemy>(). (Serialized lists are created by Unity automatically.)
See also
This page is a simplified, AI-assisted interpretation. For the complete and authoritative reference, see the official Microsoft C# documentation.