Prefab и ScriptableObject
Переиспользуемые объекты и data-only ассеты — без них проект разваливается.
Prefab — шаблон GameObject
Prefab — ассет, описывающий GameObject со всеми компонентами и значениями. Из одного prefab вы можете создать сколько угодно экземпляров (instances). Изменили prefab — изменились все экземпляры (кроме явных override’ов на конкретных).
React-компонент: один файл <Card />, который вы рендерите много раз с разными props. Изменили
компонент — обновились все его использования.
Prefab “Enemy.prefab” с HealthComponent, MeshRenderer, AI-скриптом. Спавните его в 30 точек — получаете 30 врагов. Поправили базовый prefab — все 30 обновились.
Создание prefab
- Соберите GameObject в сцене с нужными компонентами.
- Перетащите его из Hierarchy в папку в Project. Появится
.prefabассет, а в сцене объект станет синим (instance of prefab). - Дальнейшие изменения базового 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).
Создание и уничтожение объектов — дорогая операция (особенно с физикой). Для частых сущностей
(пули, частицы) используйте пулинг. В 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 — то же самое, но интегрировано в редактор и без парсинга на лету.
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 хранит изменения между запусками Play Mode — если в Play
изменили weapon.damage, после Stop значение сохранится в ассете. Это неочевидно и может
привести к “почему мой prefab сломался”. Для рантайм-состояния либо клонируйте SO через
Instantiate, либо храните состояние отдельно.
В следующей главе соберём всё вместе и сделаем простой контроллер от первого лица.