Prefab and ScriptableObject
Reusable objects and data-only assets — without them a project falls apart.
Prefab — a GameObject template
A Prefab is an asset that describes a GameObject with all of its components and values. From a single prefab you can create any number of instances. Change the prefab — all instances change too (except for explicit overrides on specific ones).
A React component: a single <Card /> file that you render many times with different props. Change the
component — all of its usages update.
An “Enemy.prefab” prefab with a HealthComponent, MeshRenderer, and AI script. Spawn it at 30 points — you get 30 enemies. Tweak the base prefab — all 30 update.
Creating a prefab
- Assemble a GameObject in the scene with the components you need.
- Drag it from the Hierarchy into a folder in the Project. A
.prefabasset appears, and in the scene the object turns blue (instance of prefab). - To make further changes to the base prefab — double-click the asset (you’ll enter “Prefab Mode”) or use “Open” in the inspector.
Overrides and propagation
In the scene, on a specific instance, you can change the value of a field — it becomes an “override” (shown in bold in the Inspector). An override lives only on that instance.
Prefab.health = 100
├── Instance1.health = 100 (inherits)
├── Instance2.health = 50 (override, bold)
└── Instance3.health = 100 (inherits)
If you change Prefab.health = 150, then Instance1 and Instance3 become 150, while Instance2 stays at 50.
Prefab Variant
A Variant is a prefab inherited from a base one. Useful when you need “kinds”:
Enemy(base) →RedEnemy(variant with a red color) →RedEliteEnemy(variant with more health).- A change to the base
Enemypropagates to the variants, as long as they haven’t overridden that field.
Nested Prefabs
A prefab inside a prefab. For example, the main Character.prefab contains Weapon.prefab as a child.
It can be edited separately, and the changes are visible in all containers.
Spawning from code
public class EnemySpawner : MonoBehaviour
{
[SerializeField] private GameObject enemyPrefab;
[SerializeField] private Transform[] spawnPoints;
public void SpawnAll() {
foreach (var point in spawnPoints) {
Instantiate(enemyPrefab, point.position, point.rotation);
}
}
}
Instantiate takes a prefab (as a GameObject), a position, and a rotation. It returns the created GameObject.
If the prefab contains a component, you can type it directly: Instantiate<Enemy>(enemyPrefab).
Creating and destroying objects is an expensive operation (especially with physics). For frequently used entities
(bullets, particles) use pooling. Unity has a built-in UnityEngine.Pool.ObjectPool<T>.
ScriptableObject — a data-only asset
A ScriptableObject is a data container that is not tied to a GameObject. It’s stored as an asset in the Project,
is serialized, and is great for:
- Configs (weapon stats, crafting recipes, enemy types).
- Databases (a list of all items in the inventory).
- Event channels (a single bus, not tied to a specific scene).
// Describing a weapon as data
[CreateAssetMenu(fileName = "Weapon", menuName = "Game/Weapon")]
public class WeaponData : ScriptableObject
{
public string displayName;
public Sprite icon;
public int damage = 10;
public float fireRate = 0.5f;
public AudioClip fireSound;
public GameObject projectilePrefab;
}
After creating such a class, in Project → Create → Game → Weapon you get a menu for creating assets like “Pistol.asset”, “Shotgun.asset” — each with its own values. Convenient: the designer configures the weapon, and the programmer doesn’t need to dig into the code.
Usage
public class WeaponSlot : MonoBehaviour
{
[SerializeField] private WeaponData weapon;
[SerializeField] private Transform muzzle;
private float _nextFireTime;
public void TryFire() {
if (Time.time < _nextFireTime) return;
_nextFireTime = Time.time + weapon.fireRate;
AudioSource.PlayClipAtPoint(weapon.fireSound, muzzle.position);
Instantiate(weapon.projectilePrefab, muzzle.position, muzzle.rotation);
}
}
In the Inspector, drag in Pistol.asset — the slot fires a pistol. Swap it for Shotgun.asset — it fires a shotgun. No code changes.
A JSON config + a zod schema: you describe the structure, load the JSON, and use it in a typed way. A ScriptableObject is the same thing, but integrated into the editor and without parsing on the fly.
WeaponData is serialized once on import, after which Unity loads it as a ready-made object.
No JSON.parse at runtime, everything is already in memory as a typed object.
An event bus via ScriptableObject
A popular pattern: global events (for example, “enemy died”, “game over”) are stored as a ScriptableObject. Any code can subscribe to or raise the event without being aware of specific objects:
[CreateAssetMenu(menuName = "Events/Game Event")]
public class GameEvent : ScriptableObject
{
private readonly System.Collections.Generic.List<System.Action> _listeners = new();
public void Raise() {
for (int i = _listeners.Count - 1; i >= 0; i--) _listeners[i]();
}
public void Register(System.Action listener) => _listeners.Add(listener);
public void Unregister(System.Action listener) => _listeners.Remove(listener);
}
You create an “OnEnemyDied.asset” asset, and the HUD/Audio/Score subscribe to it independently. This is loose coupling without a Singleton superstructure.
In the editor, a ScriptableObject preserves changes between Play Mode runs — if during Play
you changed weapon.damage, after Stop the value will persist in the asset. This is non-obvious and can
lead to “why is my prefab broken.” For runtime state, either clone the SO via
Instantiate, or store the state separately.
In the next chapter we’ll put it all together and build a simple first-person controller.