Разработка систем обнаружения столкновений и триггеров
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 аллокации, критически важен при вызове каждый кадр.
Optimization: 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.





