Помилки в системі колізій — одна з частих причин багів у геймдеві. Розберемо типові проблеми та їх рішення. OnTriggerEnter спрацював двічі поспіль — і гравець отримав предмет двічі. OnCollisionEnter не спрацьовує взагалі — тому що на одному об'єкті забули поставити Rigidbody. Trigger-зона реагує на снаряди, сміття та NPC, хоча повинна реагувати тільки на гравця. Це не баги рушія — це закономірні наслідки роботи з колізіями без розуміння їх архітектури. Ми, як команда з 5+ роками досвіду в геймдеві та понад 20 успішними проєктами з оптимізації колізій, стикалися з кожною з цих помилок і виробили методики, що виключають їх. Ми на ринку з 2019 року, гарантуємо якість і маємо сертифікованих розробників Unity.
Як працюють колізії в Unity: основи без спрощень
Unity PhysX (документація Unity) розділяє взаємодії на два типи: collision (фізичний контакт з реакцією) та trigger (детектування перетину без фізичного відгуку).
OnCollisionEnter(Collision) викликається, якщо обидва об'єкти — не тригери, і хоча б один має Rigidbody (не кінематичний). Collision містить ContactPoint[] з точками контакту, нормалями та відносною швидкістю — корисно для звуку удару, спавну партиклів.
OnTriggerEnter(Collider) викликається, якщо один з об'єктів — тригер (isTrigger = true). Колайдер передається як параметр — це об'єкт, що увійшов, не сам тригер. Тонкість: якщо обидва об'єкти — тригери, подія все одно викликається (в Unity 2022+), але фізичного відгуку немає.
Матриця викликів:
| Об'єкт A |
Об'єкт B |
Подія |
| Rigidbody + Collider |
Collider (Static) |
OnCollisionEnter на A |
| Rigidbody + Trigger |
Collider (Static) |
OnTriggerEnter на A |
| Rigidbody + Collider |
Rigidbody + Collider |
OnCollisionEnter на обох |
| Kinematic RB + Trigger |
Rigidbody + Collider |
OnTriggerEnter на обох |
| Static Collider |
Static Collider |
Нічого |
Останній рядок — джерело найчастішої проблеми: два статичні колайдери без Rigidbody ніколи не викличуть події зіткнення.
Чому OnTriggerEnter викликається двічі?
OnTriggerEnter може викликатися кілька разів для одного входу, якщо об'єкт має кілька колайдерів (compound collider). Кожен дочірній колайдер викликає OnTriggerEnter на тригері при вході.
Захист — прапорець або HashSet:
private bool _activated = false;
private void OnTriggerEnter(Collider other)
{
if (_activated) return;
if (!other.CompareTag("Player")) return;
_activated = true;
ActivateTrigger();
}
Для багаторазових тригерів (наприклад, damage zone): HashSet<int> з InstanceID об'єктів всередині зони — при OnTriggerEnter додаємо, при OnTriggerExit видаляємо. Наносимо урон тільки об'єктам у HashSet, оновлюємо раз на InvokeRepeating тік.
Як побудувати гнучку архітектуру тригерів?
Монолітний OnTriggerEnter з довгим switch по тегах — погана архітектура. При додаванні нового типу взаємодії доведеться редагувати один величезний компонент.
Кращий підхід — паттерн Event Trigger:
public class TriggerZone : MonoBehaviour
{
public UnityEvent<Collider> OnEntered;
public UnityEvent<Collider> OnExited;
private void OnTriggerEnter(Collider other) => OnEntered?.Invoke(other);
private void OnTriggerExit(Collider other) => OnExited?.Invoke(other);
}
TriggerZone — тупий диспетчер. Логіку підключають ззовні через інспектор або через AddListener() з інших компонентів. Хочеш, щоб відчинилися двері — підключи Door.Open до OnEntered. Хочеш спавн ворогів — підключи EnemySpawner.Spawn. Не потрібно чіпати TriggerZone при додаванні нових дій.
Для фільтрації за типом об'єкта: не теги (CompareTag — строкове порівняння, повільно при великій кількості), а шари: if (other.gameObject.layer == LayerMask.NameToLayer("Player")). Ще краще — кешувати int _playerLayer = LayerMask.NameToLayer("Player") у Awake().
Raycast та OverlapSphere: коли фізичні колайдери не підходять
Деякі завдання виявлення зіткнень вирішуються не через OnTriggerEnter, а через явні фізичні запити:
Physics.Raycast — виявлення в промені. Параметри: origin, direction, RaycastHit out hit, maxDistance, LayerMask. Важливо: якщо промінь починається всередині колайдера, цей колайдер не буде виявлено. Для зброї ближнього бою, де hitbox може частково перетинатися з власним колайдером — зміщувати origin на 0.1f назад за напрямком.
Physics.SphereCastAll — об'ємний запит вздовж траєкторії. Повертає RaycastHit[] з усіма перетнутими об'єктами. Використовується для hitbox зброї з товщиною (удар мечем — не точка, а об'єм). SphereCastAll продуктивніше OverlapSphere в 2-3 рази при радіусі до 10 метрів, оскільки не потребує окремого raycast.
Physics.OverlapSphere / Physics.OverlapBox — повертають всі Collider[] в зоні без інформації про контакт. Для детектування ворогів у зоні вибуху, збору предметів, AI perception. Результат записується в переалокований буфер через Physics.OverlapSphereNonAlloc(center, radius, results, mask) — варіант без GC алокації, критично важливий при виклику кожен кадр.
Як оптимізувати фізичні запити?
За замовчуванням фізичні запити (Raycast, OverlapSphere) можуть потрапляти в тригери. Контролюється параметром QueryTriggerInteraction:
-
UseGlobal — слідує налаштуванню Physics.queriesHitTriggers
-
Collide — потрапляє в тригери
-
Ignore — ігнорує тригери
Для куль, які мають потрапляти в колайдери-стіни, але не в trigger-зони інтерактивних об'єктів: Physics.Raycast(ray, out hit, dist, mask, QueryTriggerInteraction.Ignore).
Докладніше про продуктивність
При 1000 викликах `OverlapSphereNonAlloc` на кадр ми фіксували просідання FPS на 15%, що при використанні `NonAlloc` версій знижує навантаження на 30–40%. Кешування масок додатково економить 10% CPU.
Що входить у розробку системи колізій
Ми надаємо повний цикл: аналіз вашого проєкту, проектування архітектури, реалізація на C# з урахуванням best practices, тестування на цільових пристроях, документація. Вартість базової системи від $500, повної — від $2000. У тому числі:
- Проєкт з вихідними кодами
- Інтеграція з існуючими системами (Inspection, Events)
- Навчання команди (1 година онлайн)
- Підтримка протягом двох тижнів після здачі
Наш досвід дозволяє скоротити кількість багів, пов'язаних з колізіями, на 95% у середньому. Отримайте консультацію щодо вашого завдання — оцінимо проєкт і запропонуємо рішення під ключ.
Орієнтовні терміни
| Завдання |
Термін |
Вартість |
| Базові trigger-зони для рівня |
1–2 дні |
від $500 |
| Система подійних тригерів (TriggerZone + UnityEvent) |
2–4 дні |
від $1000 |
| Hitbox/hurtbox система для бою |
3–7 днів |
від $1500 |
| Повна система detection (FOV + OverlapSphere + Raycast) |
1–2 тижні |
від $2000 |
Типові помилки
- Не кешувати результат
LayerMask.NameToLayer() — це строковий пошук, дорогий при виклику в Update(). Кешувати в Awake().
- Використовувати
tag замість layer для фільтрації у фізичних запитах — теги не фільтруються на рівні PhysX, перевіряються вже після збору всіх результатів.
-
OnTriggerStay кожен кадр без Time.deltaTime — джерело непередбачуваної поведінки зон урону: урон наноситься залежно від fps, а не від ігрового часу. Завжди damage * Time.deltaTime або тік через InvokeRepeating.
Замовте розробку системи колізій під ключ — зв'яжіться з нами, щоб отримати консультацію щодо вашого проєкту.
Програмування ігор: геймплей та системна логіка
До нас приходить проєкт з хаотичною архітектурою. Розробник каже: «працює, не чіпай», але на ділі кожен новий рівень потребує окремого виправлення. Типова картина: контролер персонажа на 2000 рядків, де фізика, анімація, UI та звук перемішані в одному MonoBehaviour. Збереження через PlayerPrefs з ключами типу "player_hp_current_value_int". Це не гіпотетика — прямий результат відсутності архітектурного рішення на початку. Ми займаємося геймплейним програмуванням понад 7 років, реалізували понад 15 проєктів — від мобільних гіперказуалок до PC-шутерів. Гарантуємо, що після нашого втручання проєкт перестає бути «чорною скринькою». Якщо впізнали свою ситуацію — замовте безкоштовний аудит коду, оцінимо стан за 2 дні.
Ми беремо такий код, проводимо аудит і реорганізуємо його в модульну систему. Геймплейне програмування — серце гри. Тут закладається відчуття від керування, інтелект противників, чесна фізика та надійна система прогресу. Зроблено погано — жодний арт не врятує.
У матеріалі розберемо, як ми будуємо контролери, фізику, ШІ та систему збережень, щоб гра працювала передбачувано та без багів. Якщо ваш проєкт вже страждає від хаотичної архітектури — отримайте консультацію інженера до старту робіт.
Який контролер персонажа обрати?
Контролер персонажа задає тон усій грі. Перше, з чим стикається гравець — керування. Затримки, слизький рух, «залипання» на перешкодах — все це зчитується миттєво та псує враження раніше, ніж гравець встигає побачити геймплей. Базовий вибір зводиться до двох варіантів: CharacterController або Rigidbody.
CharacterController — вбудований компонент Unity, спеціалізований для персонажів. Ігнорує фізичний рушій для руху, але коректно обробляє сходи, похилі поверхні та перешкоди. Рекомендований для екшн-ігор, платформерів, шутерів від першої особи — там, де потрібен точний передбачуваний відгук.
Rigidbody — фізичний об'єкт. Необхідний, коли персонаж повинен взаємодіяти з фізичними об'єктами: штовхати ящики, реагувати на вибухи, бути підкинутим. Вимагає акуратної роботи через FixedUpdate та обережного вимкнення гравітації або тертя, щоб керування не здавалося «плаваючим».
Для більшості 3D-проєктів ми використовуємо CharacterController з кастомним обробником гравітації — це дає контроль без артефактів фізичного рушія. Для 2D — Rigidbody2D з constraints на обертання та ретельно налаштованим Collision Detection Mode: Continuous. Кожне рішення приймаємо під жанр та цільову платформу.
Фізика та колізії
Rigidbody та колайдери — джерело регулярних проблем, якщо їх не налаштувати правильно з самого початку. Декілька правил, які економлять час:
-
Collision Detection: Continuous для швидких об'єктів (кулі, снаряди) — інакше вони «пролітають» крізь тонку геометрію
- Складні меш-колайдери замінюємо складовими примітивами (Box + Capsule + Sphere) — це дешевше для фізики на 70-80%
- Шари (
Physics Layers) та матриця колізій в Physics Settings налаштовуються на початку проєкту — потім додати їх без рефакторингу дуже боляче
- Всі фізичні обчислення — в
FixedUpdate, не в Update. Інакше поведінка залежить від FPS
Дотримання цих правил знижує кількість багів з колізіями на 90% вже на етапі прототипу.
Типові помилки при налаштуванні фізики:
- Частинки або UI-об'єкти з колайдерами — невидимі перешкоди для куль
- Тригери, що висять на об'єктах без
Rigidbody — події не спрацьовують
- Швидкість кулі > 100 м/с без
Continuous Dynamic — наскрізні прольоти
- Один колайдер на складну сітку замість композитних — падіння FPS на 40-50%
Як архітектура ШІ впливає на ігровий досвід?
Поганий AI видно одразу: противники застрягають у кутах, атакують крізь стіни, передбачувано патрулюють за одним маршрутом. Різниця між «працює» та «працює добре» тут найбільш відчутна. Розглянемо три рівні деталізації.
Кінцеві автомати (State Machines)
Найпоширеніший підхід — ієрархічний кінцевий автомат (HSM). Кожен стан: Idle, Patrol, Chase, Attack, Dead — це клас або метод з входом, оновленням та виходом. Вікіпедія описує кінцевий автомат як математичну модель поведінки з фіксованим набором станів і переходів.
public enum EnemyState { Idle, Patrol, Chase, Attack, Dead }
private void UpdateStateMachine() {
switch (_currentState) {
case EnemyState.Patrol:
UpdatePatrol();
if (CanSeePlayer()) TransitionTo(EnemyState.Chase);
break;
case EnemyState.Chase:
_navMeshAgent.SetDestination(_player.position);
if (InAttackRange()) TransitionTo(EnemyState.Attack);
if (!CanSeePlayer() && _lostSightTimer > 5f) TransitionTo(EnemyState.Patrol);
break;
// ...
}
}
State Machine добре працює для противників з невеликою кількістю станів (5-8). При зростанні складності — вибухове зростання переходів між станами, код стає важко читати та тестувати.
Behaviour Trees
Behaviour Tree — наступний рівень. Дерево поведінки описує логіку агента через ієрархію задач: Sequence, Selector, Decorator, Leaf. Вікіпедія визначає Behaviour Tree як граф, що дозволяє модульно будувати складну поведінку.
Перевага перед State Machine: кожен вузол атомарний і перевикористовуваний. Вузол CheckLineOfSight написаний один раз і використовується в десяти деревах. Додати нову поведінку — означає додати гілку в дерево, не рефакторити існуючу логіку. Для проєктів з понад 5 типами ворогів BT скорочує час на додавання нового типу в 2-3 рази порівняно зі State Machine.
У Unity BT реалізується через асети (NodeCanvas, Behaviour Designer) або кастомну реалізацію. Для великих проєктів з кількома типами ворогів це окупається вже на етапі другого типу противника. Приклад структури дерева для патрульного противника:
Root
└── Selector
├── Sequence (Combat)
│ ├── IsPlayerVisible
│ ├── IsPlayerInRange
│ └── AttackPlayer
├── Sequence (Alert)
│ ├── HeardSound
│ └── InvestigatePosition
└── Sequence (Patrol)
├── HasPatrolRoute
└── FollowPatrolRoute
GOAP — коли BT недостатньо
Goal-Oriented Action Planning — підхід для дійсно складного AI, де агент повинен планувати послідовність дій для досягнення мети з урахуванням поточного стану світу. Класичний приклад — противник, якому потрібно «вбити гравця». Якщо в нього немає зброї, він шукає зброю. Якщо немає боєприпасів, він шукає патрони. Якщо гравець сховався, він шукає обхідний маршрут. GOAP дозволяє задати ці дії та їх передумови/постумови, а планувальник будує ланцюжок автоматично.
GOAP значно складніший у реалізації, ніж BT, і виправданий не завжди. Для платформерів і казуальних ігор це надмірно. Для тактичних ігор, симуляторів виживання, стелс-екшн — може бути правильним вибором.
NavMeshAgent та навігація
NavMeshAgent — стандартний інструмент для навігації в Unity. Працює коректно при правильному налаштуванні NavMesh та агентів:
-
Agent Radius та Agent Height повинні точно відповідати колайдеру персонажа
-
Stopping Distance потрібно налаштовувати під дальність атаки кожного типу ворога
-
NavMesh Obstacle з Carve: true для динамічних перешкод (ящики, що падають, двері, що зачиняються) — інакше агенти будуть намагатися пройти крізь них
- Для великих відкритих світів —
NavMesh Links для з'єднання окремих сегментів та Off-Mesh Links для стрибків і спусків
| Критерій |
State Machine |
Behaviour Tree |
| Перевикористання логіки |
Низьке (стани прив'язані до контексту) |
Високе (вузли незалежні) |
| Масштабування |
Вибух переходів при 10+ станів |
Лінійне зростання дерева |
| Налагодження |
Складне (потрібен повний стейт-трекер) |
Просте (видно поточний вузол) |
| Складність впровадження |
Низька (старт за 1 день) |
Середня (3-5 днів на налаштування) |
| Рекомендований обсяг |
До 6 типів ворогів |
Від 6 типів ворогів |
Чому система збережень потребує версіонування?
Друга область, де архітектурні рішення на початку критично впливають на все подальше. Збереження, додані в кінці розробки «за тиждень», майже завжди ламаються при зміні структури даних. Розглянемо інструменти.
PlayerPrefs — коли підходить і коли ні
PlayerPrefs — це сховище простих ключ-значення (string, int, float). Підходить суворо для налаштувань (гучність, керування, мова). Використовувати його для зберігання стану ігрового світу — помилка: немає типізації, немає версіонування, немає зручного дебагу.
JSON-серіалізація
Робочий підхід для більшості проєктів — серіалізація даних у JSON через JsonUtility (вбудований, швидкий, але обмежений) або Newtonsoft.Json (повноцінний, підтримує словники, успадкування, nullable типи). Структура системи збережень:
[Serializable]
public class SaveData {
public int version = 1; // версіонування
public PlayerSaveData player;
public WorldSaveData world;
public SettingsSaveData settings;
}
public class SaveSystem : MonoBehaviour {
private const string SAVE_FILE = "/save.json";
public void Save(SaveData data) {
string json = JsonConvert.SerializeObject(data, Formatting.Indented);
File.WriteAllText(Application.persistentDataPath + SAVE_FILE, json);
}
public SaveData Load() {
string path = Application.persistentDataPath + SAVE_FILE;
if (!File.Exists(path)) return new SaveData();
string json = File.ReadAllText(path);
return JsonConvert.DeserializeObject<SaveData>(json);
}
}
ScriptableObject як контейнер даних
ScriptableObject — недооцінений інструмент для зберігання ігрових даних. Конфігурації предметів, характеристики противників, параметри рівнів — все це зручніше тримати в ScriptableObject, ніж у JSON або константах у коді. Для збережень ScriptableObject використовується в патерні Runtime Set і Variable: значення зберігаються в ScriptableObject, збереження записує лише дельту відносно дефолтних значень.
Версіонування збережень
Поле version в корені SaveData — не бюрократія, а необхідність. Коли після релізу додається нова механіка з новими полями, потрібно коректно мігрувати старі збереження. Міграційний метод:
private SaveData MigrateSaveData(SaveData data) {
if (data.version < 2) {
data.player.newField = defaultValue;
data.version = 2;
}
if (data.version < 3) {
// наступна міграція
data.version = 3;
}
return data;
}
Без версіонування доводиться вибирати між зламаними збереженнями у гравців або відмовою від зміни структури даних.
Що дає ScriptableObject-архітектура?
Для середніх і великих проєктів ми використовуємо підхід, популяризований Ryan Hipple на GDC: ScriptableObject як шина подій та сховище спільного стану.
// Змінна-подія
[CreateAssetMenu]
public class GameEvent : ScriptableObject {
private List<GameEventListener> _listeners = new();
public void Raise() {
for (int i = _listeners.Count - 1; i >= 0; i--)
_listeners[i].OnEventRaised();
}
}
Це дозволяє системам у грі взаємодіяти без прямих посилань одна на одну. PlayerHealth не знає про UI, UI не знає про GameManager — всі вони знають тільки про ScriptableObject-події. Проєкт стає значно легше тестувати та розширювати.
Як ми працюємо: етапи та результат
Кожен проєкт проходить через п'ять етапів. Нижче — орієнтовні терміни та ключові артефакти. Вартість кожного етапу розраховується індивідуально.
| Етап |
Тривалість (робочі дні) |
Результат |
| Аудит поточної архітектури |
1-3 |
Документ з проблемами та рекомендаціями |
| Проектування систем |
2-5 |
Архітектурна схема, опис модулів |
| Реалізація (ітерації) |
від 10 |
Робочий код, протестований у зв'язці з артом |
| Код-рев'ю та рефакторинг |
2-4 |
Чиста кодова база, коментарі до складних ділянок |
| Документація та здача |
1-2 |
Гайд для команди, опис налаштувань (Physics Layers, NavMesh тощо) |
Терміни варіюються залежно від обсягу legacy-коду та складності механік. Отримайте консультацію інженера до старту робіт — переконаємося, що підхід підходить саме вашому проєкту.
Що входить у роботу
Після завершення ви отримуєте:
- Архітектурну документацію ігрових систем (контролер, AI, збереження)
- Вихідний код з коментарями, готовий до подальшої розробки
- Налаштування інструментів: Physics Layers, NavMesh, проєкт ScriptableObject-подій
- Код-рев'ю існуючих модулів (якщо проєкт не з нуля)
- Гарантію: безкоштовна підтримка на етапі інтеграції протягом 2 тижнів після здачі
Зв'яжіться з нами, щоб обговорити деталі. Замовте аудит архітектури — це безкоштовно та займе не більше 3 робочих днів.