Группы и маски столкновений

Система фильтрации столкновений в Ammo.js основана на механизме collision groups (группы) и collision masks (маски). Этот механизм позволяет управлять тем, какие физические тела могут взаимодействовать друг с другом, а какие должны игнорироваться движком. В основе лежит логика побитовых операций, унаследованная от физического движка Bullet.

Каждое физическое тело в мире:

  • принадлежит одной группе;
  • содержит маску, определяющую, с какими группами оно может сталкиваться.

Столкновение между двумя телами происходит только в том случае, если выполняется условие:

(groupA & maskB) !== 0 && (groupB & maskA) !== 0

Иначе говоря, оба тела должны “разрешать” столкновение друг с другом.


Стандартные группы Bullet

Ammo.js, как порт Bullet Physics в WebAssembly/JavaScript, использует предопределённые группы, аналогичные оригинальному движку:

Группа Значение Назначение
Default 1 Обычные динамические объекты
Static 2 Статическая геометрия
Kinematic 4 Кинематические тела
Debris 8 Обломки
SensorTrigger 16 Сенсоры
Character 32 Персонажи
All -1 Все группы

В реальной практике чаще используются собственные битовые флаги.


Побитовая природа групп

Группы определяются степенями двойки:

const GROUP_STATIC      = 1 << 0; // 1
const GROUP_DYNAMIC     = 1 << 1; // 2
const GROUP_PLAYER      = 1 << 2; // 4
const GROUP_ENEMY       = 1 << 3; // 8
const GROUP_PROJECTILE  = 1 << 4; // 16

Каждое тело может принадлежать только одной группе, но его маска может включать несколько групп через побитовое OR:

const MASK_PLAYER = GROUP_STATIC | GROUP_ENEMY;

Это означает, что игрок будет сталкиваться только со статическими объектами и врагами.


Добавление тела в мир с группой и маской

При добавлении тела в btDiscreteDynamicsWorld используется метод:

world.addRigidBody(body, group, mask);

Пример:

world.addRigidBody(
    playerBody,
    GROUP_PLAYER,
    GROUP_STATIC | GROUP_ENEMY
);

Если параметры group и mask не указаны, используется группа Default и маска All.


Пример архитектуры фильтрации

1. Статическая геометрия

world.addRigidBody(
    groundBody,
    GROUP_STATIC,
    GROUP_DYNAMIC | GROUP_PLAYER | GROUP_ENEMY | GROUP_PROJECTILE
);

Статика взаимодействует со всеми динамическими объектами.


2. Игрок

world.addRigidBody(
    playerBody,
    GROUP_PLAYER,
    GROUP_STATIC | GROUP_ENEMY
);

Игрок не сталкивается с собственными снарядами.


3. Снаряды

world.addRigidBody(
    projectileBody,
    GROUP_PROJECTILE,
    GROUP_STATIC | GROUP_ENEMY
);

Снаряды не сталкиваются друг с другом.


Принцип симметричности

Фильтрация работает в обе стороны. Если одна сторона разрешает столкновение, а другая — нет, контакта не будет.

Пример:

  • Игрок разрешает столкновение с врагом.
  • Враг не включает группу игрока в свою маску.

Результат — столкновение отсутствует.

Поэтому проектирование масок должно учитывать симметричность логики.


Сенсоры и триггеры

Ammo.js поддерживает объекты, которые участвуют в проверке пересечений, но не генерируют физический отклик. Для этого используется флаг:

body.setCollisionFlags(
    body.getCollisionFlags() | Ammo.btCollisionObject.CF_NO_CONTACT_RESPONSE
);

Такие объекты:

  • проходят фильтрацию по группам и маскам;
  • создают контактные события;
  • не участвуют в разрешении коллизии.

Типичный пример — зона активации:

world.addRigidBody(
    triggerBody,
    GROUP_SENSOR,
    GROUP_PLAYER
);

Исключение самостолкновений

Для составных объектов (например, ragdoll или транспорт) можно распределять части по разным группам, чтобы исключить нежелательные самостолкновения.

Пример:

const GROUP_RAGDOLL = 1 << 5;

world.addRigidBody(limb1, GROUP_RAGDOLL, GROUP_STATIC | GROUP_DYNAMIC);
world.addRigidBody(limb2, GROUP_RAGDOLL, GROUP_STATIC | GROUP_DYNAMIC);

Маска не включает GROUP_RAGDOLL, поэтому части не сталкиваются друг с другом.


Производительность и фильтрация

Группы и маски участвуют в broadphase-фильтрации — самой первой стадии проверки столкновений. Это означает:

  • уменьшение числа потенциальных пар;
  • снижение нагрузки на narrowphase;
  • уменьшение количества расчётов контактных точек.

Грамотная настройка групп может существенно снизить нагрузку в сценах с большим количеством объектов.


Динамическое изменение масок

Изменение фильтрации после добавления тела в мир требует:

  1. Удаления тела из мира.
  2. Повторного добавления с новой группой и маской.
world.removeRigidBody(body);
world.addRigidBody(body, newGroup, newMask);

Прямого метода изменения маски “на лету” нет, поскольку фильтрация кэшируется в broadphase.


Частые архитектурные схемы

Разделение по слоям логики

  • Статика
  • Динамика
  • Персонажи
  • Триггеры
  • Снаряды
  • Декорации

PvP-фильтрация

В многопользовательских проектах создаются группы команд:

const GROUP_TEAM_A = 1 << 6;
const GROUP_TEAM_B = 1 << 7;

Маски исключают дружественный огонь.


Отладка фильтрации

Проблемы чаще всего возникают из-за:

  • несовпадения масок;
  • отсутствия симметрии;
  • использования одинаковых битов для разных сущностей;
  • значения 0 в маске (объект ни с кем не сталкивается).

Полезная практика — централизованное определение всех групп в одном модуле и строгий контроль распределения битов.


Ограничения системы

  • Максимальное количество независимых групп ограничено размером 16-битной маски (в Bullet обычно используется 16 бит).
  • Каждое тело может принадлежать только одной группе.
  • Маска не определяет направление взаимодействия — проверка всегда двусторонняя.

Рекомендации по проектированию

  1. Резервировать старшие биты под расширение.
  2. Чётко документировать назначение каждой группы.
  3. Минимизировать число групп — избыточная детализация усложняет архитектуру.
  4. Использовать маски как механизм логического слоя, а не как замену игровой логики.

Связь с движком Bullet

Ammo.js является JavaScript/WebAssembly-портом C++ движка Bullet Physics. Механизм групп и масок полностью повторяет оригинальную реализацию btBroadphaseProxy::m_collisionFilterGroup и m_collisionFilterMask.

Понимание принципов Bullet позволяет применять документацию C++ версии напрямую при работе с Ammo.js, включая расширенную фильтрацию через кастомные broadphase callback-объекты.


Расширенная фильтрация

В сложных проектах возможно внедрение собственного callback-фильтра:

  • наследование от btOverlapFilterCallback;
  • переопределение метода needBroadphaseCollision.

В Ammo.js это требует работы с низкоуровневыми биндингами и Emscripten-интерфейсом, но позволяет:

  • динамически разрешать/запрещать столкновения;
  • учитывать состояние объектов;
  • реализовывать сложные игровые правила.

Группы и маски столкновений являются базовым инструментом оптимизации и архитектурного разделения физического взаимодействия в Ammo.js. Их грамотное использование позволяет создавать масштабируемые физические сцены с предсказуемым поведением и контролируемой производительностью.