Skip to main content

📝 Lesson 2.3: Instantiating and Destroying Objects

So far every object existed before you pressed Play. Real games create objects on the fly — bullets, enemies, particles, loot — and remove them when they're done. This lesson covers prefabs, Instantiate to spawn them, and Destroy to clean them up.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain what a prefab is and why it's the template for runtime spawning
  • Spawn objects with Instantiate, including position, rotation, and parent overloads
  • Use the generic Instantiate<T>() to get a typed component back without casting
  • Remove objects with Destroy, and delay destruction with the timed overload
  • Configure a freshly spawned object (e.g. give a bullet its velocity)
  • Recognize why constant spawning/destroying motivates object pooling (Lesson 5.1)

Estimated Time: 60 minutes

Project: A spawner that fires bullet prefabs on a key press, each self-destructing after a few seconds.

In This Lesson

Prefabs: Reusable Templates

You can't spawn a GameObject from nothing at runtime — you spawn copies of a template. That template is a prefab.

📖 Definition

A prefab is a saved GameObject — with all its components, values, and children — stored as an asset in your project. Think of it as a blueprint or a class: you author it once, then stamp out as many instances as you like. Editing the prefab asset updates every instance.

You make one by dragging a configured GameObject from the Hierarchy into the Project window; it turns blue and becomes a reusable asset. The analogy to C# is exact:

graph LR P["Bullet prefab (asset)
the 'class' / blueprint"] -->|"Instantiate()"| I1["Bullet instance #1"] P -->|"Instantiate()"| I2["Bullet instance #2"] P -->|"Instantiate()"| I3["Bullet instance #3"] style P fill:#eff6ff,stroke:#3b82f6,stroke-width:2px style I1 fill:#e8f5e9,stroke:#4CAF50,stroke-width:2px style I2 fill:#e8f5e9,stroke:#4CAF50,stroke-width:2px style I3 fill:#e8f5e9,stroke:#4CAF50,stroke-width:2px
💡 Prefab ≈ class, instance ≈ object. Just as new Enemy() creates an object from a class, Instantiate(enemyPrefab) creates a GameObject from a prefab. The difference: a prefab also carries all the scene data (meshes, components, tuned Inspector values) a plain class can't.

To spawn a prefab from a script, the script needs a reference to it — which is exactly the Inspector-wiring skill from Lesson 1.3: a [SerializeField] field you drag the prefab into.

Instantiate

Instantiate creates a copy of a prefab (or any object) in the scene. It has many overloads; these are the ones you'll use daily:

// 1) Copy at the prefab's own default position/rotation:
Instantiate(bulletPrefab);

// 2) Copy at a specific position and rotation (the most common form):
Instantiate(bulletPrefab, spawnPoint.position, spawnPoint.rotation);

// 3) Copy and immediately parent it under another Transform:
Instantiate(bulletPrefab, spawnPoint.position, spawnPoint.rotation, container);

📖 Definition: Quaternion

Rotations in Unity are Quaternions, not plain angles. You rarely build one by hand — use Quaternion.identity ("no rotation"), copy another object's transform.rotation, or Quaternion.Euler(x, y, z) to convert from degrees. For a bullet, spawnPoint.rotation aims it the way the spawn point faces.

The generic Instantiate<T>() — get a component directly

Plain Instantiate returns the type you passed in. When you spawn a prefab typed as a component, the generic overload returns that component directly, so you can configure it without a GetComponent call:

// If bulletPrefab is declared as a Bullet, this returns a Bullet — no cast, no GetComponent:
Bullet bullet = Instantiate(bulletPrefab, spawnPoint.position, spawnPoint.rotation);
bullet.speed = 20f;   // configure the fresh instance right away

✅ Tip: type your prefab field as the component you'll use

Declaring [SerializeField] private Bullet bulletPrefab; (instead of GameObject) still lets you drag the prefab in, and makes Instantiate hand you a Bullet ready to configure. Use GameObject only when you don't need a specific component off it.

⚠️ Important: The value Instantiate returns is your handle to the new object. If you don't capture it (var b = Instantiate(...)), you can't easily configure or track that instance afterward. Grab the return value whenever you need to touch the spawned object.

Configuring a Spawned Object

A spawned object usually needs setup the prefab can't know in advance — a bullet's direction, an enemy's target, a pickup's value. You do that right after instantiating, using the returned reference. Here's a spawner that fires bullets forward:

using UnityEngine;

public class Gun : MonoBehaviour
{
    [SerializeField] private Bullet bulletPrefab;   // drag the prefab here (Lesson 1.3)
    [SerializeField] private Transform muzzle;      // where bullets appear
    [SerializeField] private float bulletSpeed = 20f;

    void Update()
    {
        // Fire on the frame the key goes down (Lesson 1.2 / 2.1).
        if (Input.GetKeyDown(KeyCode.Space))
            Fire();
    }

    void Fire()
    {
        // Spawn at the muzzle, facing the muzzle's direction.
        Bullet bullet = Instantiate(bulletPrefab, muzzle.position, muzzle.rotation);

        // Configure the new instance: push it forward.
        bullet.Launch(muzzle.forward * bulletSpeed);
    }
}
using UnityEngine;

[RequireComponent(typeof(Rigidbody))]
public class Bullet : MonoBehaviour
{
    private Rigidbody body;

    void Awake() => body = GetComponent<Rigidbody>();

    public void Launch(Vector3 velocity)
    {
        body.linearVelocity = velocity;   // Unity 6 name (Lesson 2.2)
    }
}

💡 transform.forward

muzzle.forward is the muzzle's local +Z direction in world space — a unit Vector3 pointing "the way it faces." Multiplying by a speed gives a velocity vector. This is how you fire in whatever direction the gun is aimed, without hardcoding an axis.

Destroy: Cleaning Up

Every spawned object that isn't removed lives forever, piling up and eventually tanking performance. Destroy removes an object from the scene. You met it in Lesson 1.1; here are its important forms:

Destroy(gameObject);            // destroy the whole GameObject now (end of this frame)
Destroy(gameObject, 3f);        // destroy it after 3 seconds — great for timed cleanup
Destroy(this);                  // destroy just THIS component, leaving the GameObject
Destroy(GetComponent<Rigidbody>());   // remove one specific component

The timed overload is the idiomatic way to make short-lived objects clean up after themselves. A bullet that should vanish after 3 seconds does it in one line, in Start:

public class Bullet : MonoBehaviour
{
    [SerializeField] private float lifetime = 3f;

    void Start()
    {
        Destroy(gameObject, lifetime);   // self-cleanup — no manual bookkeeping
    }

    void OnCollisionEnter(Collision collision)
    {
        // Or destroy on impact, whichever comes first.
        Destroy(gameObject);
    }
}

⚠️ Destroy is deferred, and the reference goes "null"

Destroy doesn't remove the object instantly — it happens after the current Update loop finishes, so the object still exists for the rest of this frame. Afterward, any reference to it compares equal to null via Unity's overloaded == (Lesson 1.3). Always guard with if (obj != null) before using a reference that might have been destroyed. (There's also DestroyImmediate — avoid it at runtime; it's for editor tools only.)

💡 gameObject vs this: Destroy(gameObject) removes the entire object and everything on it; Destroy(this) removes only the script component. Ninety percent of the time you want Destroy(gameObject) — a common bug is destroying just the component and wondering why the object is still there.

The Spawn/Destroy Lifecycle

A runtime-created object runs the same lifecycle you learned in Lesson 1.2 — it just begins mid-game instead of at scene load. When you Instantiate, Unity immediately runs the new object's Awake and OnEnable; its Start fires just before its first Update. When destroyed, OnDisable then OnDestroy run.

graph TD S["Instantiate(prefab)"] --> A["Awake() + OnEnable()
run immediately"] A --> ST["Start()
before first Update"] ST --> U["Update() / FixedUpdate()
while it lives"] U --> D["Destroy(gameObject[, delay])"] D --> OD["OnDisable() ➜ OnDestroy()"] style S fill:#eff6ff,stroke:#3b82f6,stroke-width:2px style U fill:#e8f5e9,stroke:#4CAF50,stroke-width:2px style OD fill:#fdecea,stroke:#c0392b,stroke-width:2px

This is why bullet setup goes in Awake/Start and cleanup (like unsubscribing) goes in OnDestroy — exactly the rules from Module 1, now applied to objects born at runtime. There's nothing new to learn about the lifecycle; spawning just triggers it on demand.

💡 Keep the Hierarchy tidy

Spawning many objects clutters the scene root. Passing a parent to Instantiate (overload 3) nests them under a container object — e.g. an empty "Bullets" object — so the Hierarchy stays readable and you can clear them all by destroying the container.

Why Pooling Exists

Instantiate and Destroy aren't free. Creating an object allocates memory and runs its Awake/Start; destroying it produces garbage that the C# garbage collector must later clean up. Do this a few times and no one notices. Do it hundreds of times per second — a bullet-hell shooter, a particle-heavy effect — and you get frame hitches from GC spikes.

graph LR subgraph Naive["❌ Spawn & destroy constantly"] N1["Instantiate"] --> N2["use briefly"] N2 --> N3["Destroy ➜ garbage"] N3 --> N1 end subgraph Pool["✅ Object pool (reuse)"] P1["Take inactive object
from pool"] --> P2["use"] P2 --> P3["Deactivate &
return to pool"] P3 --> P1 end style N3 fill:#fdecea,stroke:#c0392b,stroke-width:2px style P3 fill:#e8f5e9,stroke:#4CAF50,stroke-width:2px

The fix is object pooling: instead of destroying an object, you deactivate it and stash it; next time you'd spawn, you reactivate one from the pool. No allocation, no garbage. It's the single most important optimization for spawn-heavy games.

✅ For now, spawn and destroy freely

Pooling is an optimization — premature pooling adds complexity you don't need while learning. Use Instantiate/Destroy until profiling shows a problem. We build a proper pool (and Unity's built-in ObjectPool<T>) in Lesson 5.1: Unity Performance and Memory. Just know why it exists.

Exercise & Quiz

🏋️ Exercise: Build a Bullet Spawner

Objective: Fire self-destructing bullet prefabs from a gun on a key press.

Instructions:

  1. Create a small Sphere with a Rigidbody (turn off Use Gravity so it flies straight). Add the Bullet script. Drag it into the Project window to make a prefab, then delete it from the scene.
  2. Create an empty Muzzle child on your player/gun, positioned at the barrel tip and facing forward.
  3. Write Gun with [SerializeField] fields for the bullet prefab and the muzzle; on Space, Instantiate a bullet at the muzzle and launch it forward.
  4. In Bullet, self-destruct after lifetime seconds using the timed Destroy overload.
  5. Play: each press should fire a bullet that flies off and disappears a few seconds later. Confirm bullets don't accumulate forever in the Hierarchy.

Starter Code:

using UnityEngine;

public class Gun : MonoBehaviour
{
    [SerializeField] private Bullet bulletPrefab;
    [SerializeField] private Transform muzzle;
    [SerializeField] private float bulletSpeed = 20f;

    void Update()
    {
        // TODO: on Space, spawn a bullet at the muzzle and launch it forward.
    }
}
💡 Hint

Instantiate(bulletPrefab, muzzle.position, muzzle.rotation) returns a Bullet directly (generic overload). Call your Launch(muzzle.forward * bulletSpeed) on it. In Bullet.Start, call Destroy(gameObject, lifetime) so each bullet cleans itself up.

✅ Solution
using UnityEngine;

public class Gun : MonoBehaviour
{
    [SerializeField] private Bullet bulletPrefab;
    [SerializeField] private Transform muzzle;
    [SerializeField] private float bulletSpeed = 20f;

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
            Fire();
    }

    void Fire()
    {
        Bullet bullet = Instantiate(bulletPrefab, muzzle.position, muzzle.rotation);
        bullet.Launch(muzzle.forward * bulletSpeed);
    }
}
using UnityEngine;

[RequireComponent(typeof(Rigidbody))]
public class Bullet : MonoBehaviour
{
    [SerializeField] private float lifetime = 3f;
    private Rigidbody body;

    void Awake() => body = GetComponent<Rigidbody>();

    void Start()
    {
        Destroy(gameObject, lifetime);   // self-cleanup
    }

    public void Launch(Vector3 velocity)
    {
        body.linearVelocity = velocity;
    }
}

The prefab is the template; each press stamps out a configured instance; the timed Destroy keeps the scene from filling with dead bullets. This exact pattern powers guns, spell-casting, spawners, and particle bursts.

🎯 Quick Quiz

Question 1: What is a prefab, in one sentence?

Question 2: You want a spawned bullet to disappear after 3 seconds with the least code. What do you write?

Question 3: Why do spawn-heavy games eventually switch from Instantiate/Destroy to object pooling?

Summary

🎉 Key Takeaways

  • A prefab is a saved GameObject template — the "class" you stamp instances from at runtime.
  • Instantiate(prefab, position, rotation) spawns a copy; capture the return value to configure it. The generic Instantiate<T>() hands you a typed component with no cast.
  • Give a script a prefab via a [SerializeField] reference (Lesson 1.3), and configure the new instance right after spawning (e.g. bullet.Launch(...)).
  • Destroy(gameObject) removes an object; Destroy(gameObject, seconds) delays it — ideal for self-cleaning, short-lived objects.
  • Destroy is deferred (end of frame) and the reference then reads as null — guard before use; avoid DestroyImmediate at runtime.
  • Spawned objects run the normal AwakeOnDestroy lifecycle; heavy spawning motivates object pooling (Lesson 5.1).

📚 Additional Resources

🚀 What's Next?

That wraps Module 2 — Gameplay Scripting: things move, collide, spawn, and disappear. Module 3 turns to structure. Lesson 3.1, ScriptableObjects for Data, shows a cleaner way to hold shared data (like bullet stats) as assets — no MonoBehaviour required.

🎉 Module 2 complete!

Input, physics, and now spawning — you have the full gameplay toolkit. From here we focus on organizing all this into clean, maintainable systems.