Collections
TypesT[], List<T>, Dictionary<K,V>, HashSet<T>, Queue<T>, Stack<T>
NamespaceSystem.Collections.Generic
In UnityInventories, 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?

CollectionThink of it asUse whenInspector?
Array T[]An egg carton: fixed number of slotsThe size doesn't change: spawn points, waypointsYes
List<T>A shopping list: add and cross off itemsThe size changes: inventory, active enemiesYes
Dictionary<K,V>A phone book: look up a value by its keyFind things by ID or name: items by ID, stats by nameNo
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 attackNo
Queue<T>A line at the shop: first in, first outProcess in order: dialogue lines, spawn queue, pathfindingNo
Stack<T>A stack of plates: last in, first outUndo history, menu back-navigationNo

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

  1. 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.
  2. Contains and Remove on a List check every item one by one. With thousands of items, a HashSet or Dictionary is far faster, because it jumps straight to the right slot using the item's hash.
  3. Creating a new collection allocates memory; creating one every frame creates garbage. Keep collections as fields and Clear() them.

Common Mistakes

See also