Розробка систем обнаруження столкновень та триггерів
OnTriggerEnter сработав двічі підряд — і гравець отримав предмет два рази. OnCollisionEnter не срабатувает взагалі — тому що на одному об'єкті забули поставити Rigidbody. Trigger-зона реагує на снаряди, мусор та NPC, хоча повинна реагувати тільки на гравця. Це не баги рушія — це закономірні наслідки роботи з коллізіями без розуміння їхної архітектури.
Як працюють коллізії у Unity: основи без упрощень
Unity PhysX розділяє взаємодії на два типи: 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 може викликатися кілька разів для одного входу, якщо об'єкт має кілька коллайдерів (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 тик.
Архітектура trigger-системи для рівнів
Монолітний 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 зброї з товщиною (удар мечем — не точка, а об'єм). Продуктивніше за OverlapSphere в кінці шляху + raycast на початку.
Physics.OverlapSphere / Physics.OverlapBox — повертають усі Collider[] у зоні без інформації про контакт. Для вворогів у зоні вибуху, збору предметів, AI perception. Результат записується у перераспределяємий буфер через Physics.OverlapSphereNonAlloc(center, radius, results, mask) — варіант без GC алокації, критичний при вимозі на кадр.
Оптимізація: QueryTriggerInteraction
За замовчуванням фізичні запити (Raycast, OverlapSphere) можуть попадати у триггери. Контролюється параметром QueryTriggerInteraction:
-
UseGlobal— слідує настройціPhysics.queriesHitTriggers -
Collide— попадає у триггери -
Ignore— ігнорує триггери
Для куль, які повинні попадати у коллайдери-стіни, але не у trigger-зони інтерактивних об'єктів: Physics.Raycast(ray, out hit, dist, mask, QueryTriggerInteraction.Ignore).
Орієнтовні строки
| Задача | Строк |
|---|---|
| Базові trigger-зони для рівня | 1–2 дня |
| Система событійних триггерів (TriggerZone + UnityEvent) | 2–4 дня |
| Hitbox/hurtbox система для боювання | 3–7 днів |
| Повна система detection (FOV + OverlapSphere + Raycast) | 1–2 тижні |
Типічні помилки
Не кешувати результат LayerMask.NameToLayer() — це строковий пошук, дорогий при вимозі в Update(). Кешувати в Awake().
Використовувати tag замість layer для фільтрації у фізичних запитах — теги не фільтруються на рівні PhysX, перевіряються уже після збору всіх результатів.
OnTriggerStay кожен кадр без Time.deltaTime — джерело непередбачуваної поведінки зон урону: урон наносится в залежності від fps, а не від ігрового часу. Завжди damage * Time.deltaTime або тик через InvokeRepeating.





