libGDX: Часть 6.2. Использование фильтров в Box2D

Box2D Filter

В прошлой статье рассматривалась работа с ContactListener. Вот только примеры, которые я использовал, были не совсем верно выбраны. В Box2D есть намного более удобные средства для фильтрации столкновений, а именно — фильтры. О них и напишу в этот раз.

В прошлой статье о ContactListener рассматривалось, как переопределить метод preSolve, чтобы сделать контакты неактивными. Метод имеет право на существование, но с использованием фильтров эту же задачу решать намного удобнее. И тут дело не просто в удобстве, а в оптимизации. В чём же преимущество?

Фильтры обрабатываются перед обработкой коллизий. То есть, если на уровне фильтров мы укажем, чтобы какие-то объекты не сталкивались, то в дальнейшем обработки коллизий между этими объектами не будет. В случае же с использование ContactListener, не будут срабатывать его методы для таких объектов. Оптимизация на лицо. А теперь поподробнее рассмотрим.

Категории и маски

Категории и маски — самый мощный способ для фильтрования столкновений, но также и самым сложный (для новичков). Идея состоит в том, чтобы определить категорию для типов объектов, и использовать маски для фильтрования столкновений между этими типами объектов. Для начала определим категории.

// 0000000000000001 in binary
final public static short CATEGORY_PLAYER = 0x0001;
  
// 0000000000000010 in binary
final public static short CATEGORY_BALOOM = 0x0002; 

// 0000000000000100 in binary
final public static short CATEGORY_RUNNER = 0x0004; 

// 0000000000001000 in binary
final public static short CATEGORY_SCENERY = 0x0008; 

Затем зададим их нашим объектам.

//игроку
f.categoryBits = MyWorld.CATEGORY_PLAYER;

//блокам и платформе
f.categoryBits = MyWorld.CATEGORY_SCENERY;

//для Baloom'а
f.categoryBits = MyWorld.CATEGORY_BALOOM;

//для Runner'а
f.categoryBits = MyWorld.CATEGORY_RUNNER;

Вы должны были заметить, что нумерация 0×001, 0×002, 0×004 и 0×008. Почему? Дело в том, что категории и маски — битовые поля (закодированы в 16 битах). Это означает, что возможные категории являются степенью 2 (в десятичной системе счисления: 1, 2, 4, 8, 16, 32, 64, 128 …, или в шестнадцатеричном: 0×1, 0×2, 0×4, 0×8, 0×10, 0×20, 0×40, 0×80 …). 16 битов означают, что есть 16 возможных категорий от 0×0001 до 0×8000.

Теперь определим маски.

final public static short MASK_PLAYER = CATEGORY_RUNNER | CATEGORY_SCENERY; // или ~MASK_PLAYER
final public static short MASK_BALOOM =  CATEGORY_SCENERY ; 
final public static short MASK_RUNNER = CATEGORY_PLAYER | CATEGORY_SCENERY ; 
final public static short MASK_SCENERY = -1;

Обычная булева алгебра с типичными операциями над числами. Стоит лишь остановиться на маске и категории для пейзажных объектов. Почему -1? -1 означает, что объект будет контактировать со всеми другими объектами. Если же надо, чтобы объект не контактировал ни с кем, то установите значение 0.

Затем маски зададим нашим объектам.

//игроку
f.maskBits = MyWorld.MASK_PLAYER;

//блокам и платформе
f.maskBits = MyWorld.MASK_SCENERY;

//для Baloom'а
f.maskBits = MyWorld.MASK_BALOOM;

//для Runner'а
f.maskBits = MyWorld.MASK_RUNNER;

Если кто-то не понял, по маскам и категориям куски из кода представлены. Целиком назначения фильтра приведу на всякий случай, как пример для игрока:

Filter f = new Filter();
f.categoryBits = MyWorld.CATEGORY_PLAYER;
f.maskBits = MyWorld.MASK_PLAYER;
playerSensorFixture.setFilterData(f);
playerPhysicsFixture.setFilterData(f);

В итоге игрок будет взаимодействовать только с пейзажными объектами и с Runner’ом. Ballom только с пейзажем. Runner с игроком и пейзажем.

Пара дополнений по поводу фильтров для различных объектов:

  • Фикстуры на статическом теле могут столкнуться только с динамическим телом.
  • Фикстуры на кинематическом теле могут столкнуться только с динамическим телом.
  • Фикстуры на том же теле никогда не сталкиваются друг с другом.

Фильтрация на уровне групп

Вы можете работать с категориями и масками, но порой нет необходимости их использовать, считать битовые маски и т.д. Группы специально были придуманы, чтобы отключить/включить обработку коллизий для каких-то связанных объектов. В мануале по Box2D найдём:

Collision groups let you specify an integral group index. You can have all fixtures with the same group index always collide (positive index) or never collide (negative index).

Группы должны использоваться вместо категорий и масок, когда необходимо отключить столкновения между объектами одной группы. Как пример, мы хотим, чтобы игроки не сталкивались. Для начала, добавим ещё одного игрока.

BodyDef def2 = new BodyDef();
def.type = BodyType.DynamicBody;
Body boxP2 = world.createBody(def2);
player2 = new Player(boxP2);		
player2.getBody().setTransform(5.0f, 1.0f, 0);
player2.getBody().setFixedRotation(true);

В классе Player зададим группу.

Filter f = new Filter();
f.groupIndex = -1;
playerSensorFixture.setFilterData(f);
playerPhysicsFixture.setFilterData(f);

Так же необходимо задать группы для всех остальных объектов. Пускай для платформы groupIndex будет 3, а для блоков 2. В принципе, платформе и блокам можно группу не назначать, тогда будет установлено значение по умолчанию — 0. Теперь, если запустить игру, персонаж будет контактировать со всеми объектами кроме других персонажей.

Вы должны были обратить внимание на то, что индекс имеет отрицательное значение. Если индекс положителен — объекты всегда контактируют, если отрицателен — никогда не контактируют.

ContactFilter

Есть ещё один способ фильтрации, но он глобальный и похож на ContactListener. Я говорю о ContactFilter. Если написать свой глобальный фильтр, реализующий этот интерфейс, то можно включить/отключить обработку коллизий. Плюс этого метода всё тот же, если использовать ContactFilter, то не будет в дальнейшем обработки коллизий для объектов, которые не должны контактировать. В следствии чего будет выигрыш в скорости по сравнению с ContactListener. Просто, как пример, сделаем так, чтобы не контактировали только игрок и Runner. Наш класс будет таким:

public class MyFilter implements ContactFilter{
  public boolean shouldCollide(Fixture fixtureA, Fixture fixtureB) {
    Filter filterA = fixtureA.getFilterData();
    Filter filterB = fixtureB.getFilterData();
    if((filterA.categoryBits == MyWorld.CATEGORY_PLAYER && filterB.categoryBits == MyWorld.CATEGORY_RUNNER) ||
				(filterB.categoryBits == MyWorld.CATEGORY_PLAYER && filterA.categoryBits == MyWorld.CATEGORY_RUNNER ))
    return false; 


  return true;
  }
}

Если запустите игру, то игрок будет контактировать со всеми объектами кроме Runner. ContactFilter конечно довольно удобен, но при его использовании придётся самому прописать все ситуации для всех объектов. Я бы рекомендовал использовать всё же маски и категории.

Исходники

Можете скачать исходники этого урока Libgdxtutorial-lesson6.2.rar.