Summary

A prefab is a reusable GameObject asset stored in the project. Any number of identical instances can be created from it at runtime using Instantiate. This is the standard pattern for pickups, enemies, projectiles, and any object that appears more than once in a scene. The spawning script holds a reference to the prefab (assigned in the Inspector), calls Instantiate to create instances, and tracks them in a List<GameObject> so they can be reconfigured or destroyed later.


Key ideas

Prefab: A saved GameObject asset in the Project window. Editing the prefab asset updates all scene instances simultaneously. Instances can be overridden individually without affecting the prefab.

Instantiate(prefab, position, rotation): Creates a new copy of the prefab in the scene at the given position and rotation. Returns a GameObject reference to the new instance. Quaternion.identity means no rotation (the object spawns upright).

List<GameObject>: A resizable list from System.Collections.Generic. Better than an array for spawning because the size is not known in advance. Use list.Add(obj) to append, list.Remove(obj) to remove by reference, list.RemoveAt(i) to remove by index, list.Count for the current size.

transform.SetParent(container): Parents the spawned object under a container transform. Keeps the Hierarchy organised — all spawned coins live under a single ”--- Coins ---” folder rather than cluttering the root.

Iterating backwards to remove: When looping through a list and removing by index (RemoveAt(i)), loop from the last index down to zero. Iterating forwards while removing causes elements to be skipped because the indices shift after each removal.

Remove before Destroy: Remove the item from the tracking list before calling Destroy. This prevents the list from holding a dead reference to a destroyed object.


In practice

Inspector-assigned prefab field:

[Header("Prefab to spawn")]
[SerializeField] private GameObject coinPrefab;
 
[Header("Spawn settings")]
[SerializeField] private int   spawnCount = 5;
[SerializeField] private float spacingX   = 1.5f;

Spawning a row of prefab instances:

using System.Collections.Generic;
 
private List<GameObject> spawnedCoins = new List<GameObject>();
private Transform coinContainer;
 
private void Start()
{
    // Create a container object to keep the Hierarchy tidy
    coinContainer = new GameObject("--- Coins ---").transform;
}
 
public void SpawnAll()
{
    if (coinPrefab == null)
    {
        Debug.LogError("[PrefabSpawner] coinPrefab not assigned!");
        return;
    }
 
    float startX = transform.position.x - (spawnCount - 1) * spacingX * 0.5f;
 
    for (int i = 0; i < spawnCount; i++)
    {
        Vector3 spawnPos = new Vector3(startX + i * spacingX, transform.position.y, 0f);
 
        // Create an instance; Quaternion.identity = no rotation
        GameObject coin = Instantiate(coinPrefab, spawnPos, Quaternion.identity);
 
        coin.transform.SetParent(coinContainer);  // keep Hierarchy tidy
        coin.name = $"Coin_{i:D2}";               // descriptive name for debugging
 
        spawnedCoins.Add(coin);                   // track it
    }
 
    Debug.Log($"[PrefabSpawner] Spawned {spawnedCoins.Count} coins.");
}

Reading SpawnAll:

  1. coinPrefab is not created in code. It is assigned in the Inspector, so a null check comes before Instantiate.
  2. startX calculates where the first coin should appear so the full row is centred around the spawner.
  3. The for loop runs once per coin. i is both the loop counter and part of the coin name.
  4. Instantiate(coinPrefab, spawnPos, Quaternion.identity) creates the scene instance and returns a reference to it.
  5. SetParent(coinContainer) keeps the Hierarchy readable without changing which prefab was spawned.
  6. spawnedCoins.Add(coin) stores the new instance so later code can destroy or recolour it.

The important distinction is prefab asset versus scene instance. coinPrefab points to the reusable asset. coin points to one spawned copy in the current scene.

Despawning all tracked instances:

public void DespawnAll()
{
    // Iterate backwards — safe when removing by index
    for (int i = spawnedCoins.Count - 1; i >= 0; i--)
    {
        Destroy(spawnedCoins[i]);
        spawnedCoins.RemoveAt(i);
    }
    Debug.Log("[PrefabSpawner] All coins despawned.");
}

Removing a single coin on collection:

public void OnCoinCollected(GameObject coin)
{
    if (gameManager != null)
        gameManager.AddScore(coinValue);
 
    spawnedCoins.Remove(coin);  // remove from list BEFORE Destroy
    Destroy(coin);
 
    if (spawnedCoins.Count == 0)
        Debug.Log("[PrefabSpawner] Wave complete.");
}

Batch-configuring spawned instances via GetComponent:

public void BatchSetColour(Color colour)
{
    foreach (GameObject coin in spawnedCoins)
    {
        SpriteRenderer sr = coin.GetComponent<SpriteRenderer>();
        if (sr != null)
            sr.color = colour;
    }
}

Gotchas

  • Always check if (coinPrefab == null) in Start or before Instantiate. An unassigned prefab field only fails when Instantiate is called (Unity reports that the object you want to instantiate is null), not when the scene loads — so the error can be hard to trace back to the empty Inspector field.
  • Instantiate has generic overloads that return the same type you pass in: “You can use generic types to instantiate objects so you don’t have to cast the result to a specific type” (Unity Technologies, Scripting API: Object.Instantiate). Passing a GameObject prefab therefore returns a GameObject, which is why the code above needs no cast. Older tutorials that write as GameObject were using the non-generic overload, which returns Object. (This gotcha previously said Instantiate always returns Object; corrected 2026-10-25.)
  • Never hold a list reference to a destroyed object. Call list.Remove(obj) or list.RemoveAt(i) before Destroy(obj), or the list will contain a null entry that throws on the next iteration.
  • When a prefab is edited in the Project window, all scene instances update automatically — unless an instance has been overridden in the Inspector (shown in bold). Be aware of this when debugging unexpected behaviour on a single instance.
  • SetParent(parent, true) preserves world position. SetParent(parent, false) treats the object position as local to the parent. Use the default carefully when a spawned object appears in the wrong place after parenting.

Worked example

The beginner 2D route spawns coins at hand-placed spawn points rather than in a row:

  • PickupSpawner.cs — loops over a Transform[] of spawn points, skips any that are missing with a warning, instantiates the coin prefab at each, parents it under a pickupParent, and passes each new coin the scene’s GameManager
  • CoinPickup.cs — the script on the spawned coins; its SetGameManager method receives that reference, because a prefab asset cannot store a reference to an object in the scene (scene set-up in the folder’s README)

Practice

Build a PrefabSpawner from the snippets on this page and drive it from the keyboard:

  • S calls SpawnAll() to make a centred row of five coins
  • D calls DespawnAll()
  • C calls BatchSetColour with a colour of your choice

Success test:

  • the row is centred on the spawner and the coins are named Coin_00 to Coin_04
  • every spawned coin sits under the --- Coins --- container in the Hierarchy
  • after D, the Hierarchy has no coins and spawnedCoins.Count logs 0
  • pressing S with the prefab field empty logs your error message instead of crashing

Extension:

  • give the coin a trigger collider that calls OnCoinCollected(gameObject) on the spawner, and log “Wave complete” when the last coin is collected

Self-test

  1. What does Instantiate(coinPrefab, spawnPos, Quaternion.identity) return, and why does the spawner keep it?
  2. Why is a List<GameObject> a better fit than an array for spawned objects?
  3. Why does DespawnAll loop from the last index down to zero?
  4. Why should a coin be removed from the list before it is destroyed?
  5. What does Quaternion.identity mean?
  6. A coin appears in the wrong place after SetParent. What does the second argument of SetParent control?

Answers

  1. A reference to the new scene instance. The spawner adds it to spawnedCoins so it can recolour or destroy that coin later.
  2. The number of spawned objects is not known in advance, and a list can grow and shrink with Add, Remove and RemoveAt.
  3. Removing by index shifts the later items down; looping forwards would skip elements, looping backwards does not.
  4. So the list never holds a reference to a destroyed object, which would throw on the next loop.
  5. No rotation: the object spawns upright.
  6. Whether the world position is kept (true) or the object’s position is treated as local to the new parent (false).