~2 мин чтения

Prefab и ScriptableObject

Переиспользуемые объекты и data-only ассеты — без них проект разваливается.

Prefab — шаблон GameObject

Prefab — ассет, описывающий GameObject со всеми компонентами и значениями. Из одного prefab вы можете создать сколько угодно экземпляров (instances). Изменили prefab — изменились все экземпляры (кроме явных override’ов на конкретных).

Веб

React-компонент: один файл <Card />, который вы рендерите много раз с разными props. Изменили компонент — обновились все его использования.

Unity

Prefab “Enemy.prefab” с HealthComponent, MeshRenderer, AI-скриптом. Спавните его в 30 точек — получаете 30 врагов. Поправили базовый prefab — все 30 обновились.

Создание prefab

  1. Соберите GameObject в сцене с нужными компонентами.
  2. Перетащите его из Hierarchy в папку в Project. Появится .prefab ассет, а в сцене объект станет синим (instance of prefab).
  3. Дальнейшие изменения базового prefab — двойной клик на ассете (войдёте в “Prefab Mode”) или через “Open” в инспекторе.

Overrides и пропагация

В сцене на конкретном экземпляре вы можете изменить значение поля — оно станет “override” (жирным шрифтом в Inspector). Override живёт только на этом экземпляре.

Prefab.health = 100
   ├── Instance1.health = 100   (наследует)
   ├── Instance2.health = 50    (override, жирный шрифт)
   └── Instance3.health = 100   (наследует)

Если измените Prefab.health = 150, то Instance1 и Instance3 станут 150, а Instance2 останется 50.

Prefab Variant

Variant — это prefab, унаследованный от базового. Полезно, когда нужны “виды”:

  • Enemy (базовый) → RedEnemy (variant с красным цветом) → RedEliteEnemy (variant с большим здоровьем).
  • Изменение базового Enemy распространяется на варианты, если они не override-нули поле.

Nested Prefabs

Prefab внутри prefab. Например, основной Character.prefab содержит Weapon.prefab как ребёнка. Можно править отдельно, изменения видны во всех контейнерах.

Spawn из кода

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 принимает prefab (как GameObject), позицию и поворот. Возвращает созданный GameObject. Если префаб содержит компонент, можно сразу типизировать: Instantiate<Enemy>(enemyPrefab).

Object Pool вместо Instantiate каждый раз

Создание и уничтожение объектов — дорогая операция (особенно с физикой). Для частых сущностей (пули, частицы) используйте пулинг. В Unity есть встроенный UnityEngine.Pool.ObjectPool<T>.

ScriptableObject — data-only ассет

ScriptableObject — это data-контейнер, не привязанный к GameObject. Хранится как ассет в Project, сериализуется, отлично подходит для:

  • Конфигов (статы оружия, рецепты крафта, типы врагов).
  • Базы данных (список всех предметов в инвентаре).
  • Event-каналов (одна шина, не привязанная к конкретной сцене).
// Описание оружия как данных
[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;
}

После создания такого класса в Project → Create → Game → Weapon вы получите меню для создания ассетов “Pistol.asset”, “Shotgun.asset” — каждый со своими значениями. Удобно: дизайнер настраивает оружие, программисту не нужно лезть в код.

Использование

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);
    }
}

В Inspector перетащите Pistol.asset — слот стреляет пистолетом. Заменили на Shotgun.asset — стреляет дробовиком. Никаких изменений в коде.

Веб

JSON-конфиг + zod-схема: описали структуру, грузите JSON, типизированно используете. ScriptableObject — то же самое, но интегрировано в редактор и без парсинга на лету.

Unity

WeaponData сериализуется один раз при импорте, дальше Unity загружает его как готовый объект. Никакого JSON.parse в рантайме, всё уже в памяти как типизированный объект.

Event-шина через ScriptableObject

Популярный паттерн: глобальные события (например, “враг умер”, “игра окончена”) хранятся как ScriptableObject. Любой код может подписаться или вызвать событие, не знакомясь с конкретными объектами:

[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);
}

Создаёте ассет “OnEnemyDied.asset”, и HUD/Audio/Score подписываются на него независимо. Это loose coupling без надстройки Singleton’ов.

ScriptableObject и состояние

В редакторе ScriptableObject хранит изменения между запусками Play Mode — если в Play изменили weapon.damage, после Stop значение сохранится в ассете. Это неочевидно и может привести к “почему мой prefab сломался”. Для рантайм-состояния либо клонируйте SO через Instantiate, либо храните состояние отдельно.

В следующей главе соберём всё вместе и сделаем простой контроллер от первого лица.