Box2d — это кроссплатформенный двумерный физический движок, который часто используется для создания двумерных игр. Для разработки под iOS часто используют Box2d на совместно с Cocos2d. Также Box2d может быть использован вместе с Microsoft XNA под WP7.

Фильтр (filter) в Box2d это свойство у фиксчи (fixture). Это свойство позволяет настраивать коллизии так, чтобы нужные нам фиксчи создавали контакты, а не нужные — не создавали. На самом деле не всякое приложение требует использования фильтров, иногда вполне можно обойтись и без них, однако бывают случаи, когда фильтры просто незаменимы.

Как работают фильтры

Представить себе работу фильтров можно на простом примере: допустим в Box2d приложении есть два бильярдных шара, которые представлены динамическими (b2_dynamicBody) телами с круглыми (b2CircleShape) фиксчами. Фиксчи у шаров несенсорные (NO == fixture->IsSensor()). Если фильтры настроены так, чтобы шары контактировали (коллайдили), то при их столкновении будет создан контакт и, в соответствие с физическими законами, шары изменят свои скорости и направления движения. Если фильтры настроены так, чтобы шары не контактировали, то при столкновении они просто не заметят друг друга, контакта не будет создано и изменения скоростей и направлений движения не произойдет, — они просто пройдут друг через друга и будут двигаться дальше.

В Box2d фильтр описан как структура (b2Fixture.h):

struct b2Filter

{
b2Filter()
{
categoryBits = 0x0001;
maskBits = 0xFFFF;
groupIndex = 0;
}
uint16 categoryBits;
uint16 maskBits;
int16 groupIndex;
};

Где categoryBits — 16 бит категории (биты категории), определяющих категорию, к которой относится фиксча. В комментарии к этому полю в коде Box2d написано, что обычно вам нужно устанавливать всего один бит. Иногда полезно бывает устанавливать более одного бита, но это отдельный разговор;

maskBits — 16 бит маски (биты маски), определяющих те категории, с которыми будет контактировать наша фиксча;

groupIndex — идентификатор группы (collision group). Если положителен — объекты всегда контактируют, если отрицателен — никогда не контактируют. Чуть дальше будет показано, как именно это работает. Я до сих пор не нашел контекста, в котором удобно было бы применять группы, если знаете — подскажите.

Если вы в своем приложении не используете фильтры, то для всех фиксчей выставятся параметры фильтров по умолчанию — т.е. категория 0x0001, контактирует со всеми категориями (maskBits = 0XFFFF) и группа 0.

Код Box2d, который применяет фильтры, выглядит так (b2WorldCallbacks.cpp):

bool b2ContactFilter::ShouldCollide(b2Fixture* fixtureA, b2Fixture* fixtureB)

{
const b2Filter& filterA = fixtureA->GetFilterData();
const b2Filter& filterB = fixtureB->GetFilterData();
if (filterA.groupIndex == filterB.groupIndex && filterA.groupIndex != 0)
{
return filterA.groupIndex > 0;
}
bool collide = (filterA.maskBits & filterB.categoryBits) != 0 && (filterA.categoryBits & filterB.maskBits) != 0;
return collide;
}

Пройдемся по коду. Если идентификаторы групп совпадают, и, если они положительны, то фиксчи контактируют. Если идентификаторы групп совпадают и они отрицательны, то фиксчи не контактируют. Если идентификаторы групп не совпадают или равны 0, то применяются биты категории и маски следующим образом: если в фиксче A хоть один бит в маске совпадает с битом категории фикчи B и (тут «именно» и, а не «или») если в фиксче B хоть один бит в маске совпадает с битом категории фиксчи A, то фиксчи контактируют. В противном случае — не контактируют. Если все это вам было понятно по коду, этот абзац можно было и не читать.

Далее в примерах будут использованы биты маски и категории. Идентификатор группы использован не будет в том смысле, что его значение всегда будет 0. Настройка фильтра, по сути, сводится к заданию бит маски и категории. Далее речь пойдет о том, как написать код, который будет настраивать фильтры.

Плохой код с использованием фильтров

/*hero.mm — герой*/

...
heroFixtureFilter.categoryBits = 0x08;
heroFixtureFilter.maskBits = 0x07;
...
/*villain.mm — злодей*/
...
villainFixtureFilter.categoryBits = 0x04;
villainFixtureFilter.maskBits = 0x0А;
...
/*obstacle.mm — препятствие для всех*/
...
obstacleFixtureFilter.categoryBits = 0x02;
obstacleFixtureFilter.maskBits = 0x0C;
...
/*heroOnlyObstacle.mm — препятствие только для героя*/
...
heroOnlyObstacleFixtureFilter.categoryBits = 0x01;
heroOnlyObstacleFixtureFilter.maskBits = 0x08;
...

Совсем необязательно за такой код отрывать человеку руки — он еще вполне может исправиться. Хотя недостатки и очевидны:

  1. Нечитабельно: при взгляде на 'maskBits = 0x0C' сразу не совсем понятно, что эта фиксча контактирует с героем и со злодеем. Даже при наличии всего четырех фиксчей, разобраться можно не сразу. А если бы фиксчей (в смысле видов, а не экземпляров) было бы 16? А если 30?!
  2. Трудно модифицируемо: если мы хотим поменять бит категории герою — нужно лезть и менять все фильтры, где раньше присутствовал герой, и все фильтры где присутствует новый бит категории героя. Забудешь где-то поменять — начнутся приколы в игре;
  3. Трудно расширяемо: при создании нового типа игровых объектов, нужно будет лезть во все фильтры объектов, которые должны контактировать с новым, и превращать 0x07, например, в 0x17. Безусловно это хорошее упражнение для ума в плане шестнадцатеричной арифметики, но пользы для проекта от этого упражнения не очень много.

Хороший код с использованием фильтров

Создаем пару файлов (Contacts.h, Contacts.mm) — назвать их можно и по-другому, это не суть важно. Расширение .mm можно заменить на .cpp, это как кому удобнее. В дальнейшем коде будет использованы приемы по работе с перечислениями, описанные здесь. Итак,

Contacts.h:

#import "b2Fixture.h" 

typedef enum _FixtureType
{
kFixtureTypeHero = 0,
kFixtureTypeVillain = 1,
kFixtureTypeObstacle = 2,
kFixtureTypeHeroOnlyObstacle = 3
} FixtureType;
#define ftFitst kFixtureTypeHero
#define ftLast kFixtureTypeHeroOnlyObstacle
#define ftEnd (FixtureType)(ftLast + 1)
#define ftNext(ft) ((ft <= ftLast) ? (FixtureType)(ft + 1) : ftEnd)
//Установить фильтр для фикчи в соответствие с типом
void SetupFilter(b2Fixture *fixture, FixtureType fixtureType);
//Установить фильтр для определения фиксчи (fixtureDef) в соответствие с типом
void SetupFilter(b2FixtureDef *fixtureDef, FixtureType fixtureType);
//Проверить тип фиксчи. Удобно использовать в if конструкциях
BOOL FixtureIsOfType(const b2Fixture *fixture, FixtureType fixtureType);

Здесь мы объявили типы фиксчей, добавили макросы для удобной работы с ними, объявили методы для работы с фиксчами и типами фиксчей. Как можно заметить, объявлено два метода SetupFilter, причина этого в том, что иногда бывает удобно задавать фильтр при создании фиксчи (у fixtureDef), а иногда удобно его менять, когда фиксча уже создана (у самой fixture). Также примечательно, что в объявлении у элементов enum’а указана цифра. Вообще говоря, этого можно и не делать, т.к. по умолчанию им будут присвоены именно такие цифры. Но лучше, все же, пронумеровать, т.к. потом будет легче ориентироваться, да и при дебаге это поможет. Имплементация выглядит следующим образом:

Contacts.mm:

#include "Contacts.h"

//Объявление вспомогательных функций, в хидере не объявлены, а в исполняемом файле используются
// Возвращает биты категории для заданного типа фиксчи
unsigned short CategoryBitsFor(FixtureType fixtureType);
// Возвращает биты маски для заданного типа фиксчи
unsigned short MaskBitsFor(FixtureType fixtureType);
// Вспомогательные дефайны, нужны для того, чтобы лучше визуально воспринимать ниже описанную матрицу
// Наличие колизии
#define __YES__ YES
// Отсутствие коллизии
#define _______ NO
// Неопределенное значение. По факту, оно совпадает с отсутствием коллизии. Можно создать для этих целей enum, а не
// использовать тип BOOL, но скорее всего это будет излишним, обычно вполне хватает типа BOOL
#define ___U___ NO
// Матрица контактов, описывает все возможные коллизии между типами фиксчей. В ней присутствуют 4 зарезервированных типа фиксчей — они помечены Empty.
// Матрицу следует читать так — для данного типа по горизонтали читаем строку, пока не дойдем до самого себя
// (это произойдет на главной диагонали матрицы), затем читать по вертикали (по столбцу).
static const BOOL s_contactMatrix[8][8] =
{
/*----------Hero Villain Obstacl HeOnObs Empty Empty Empty Empty
{/*Hero*/ _______,___U___,___U___,___U___,___U___,___U___,___U___,___U___},
{/*Villain*/__YES__,_______,___U___,___U___,___U___,___U___,___U___,___U___},
{/*Obstacl*/__YES__,__YES__,_______,___U___,___U___,___U___,___U___,___U___},
{/*HeOnObs*/__YES__,_______,_______,_______,___U___,___U___,___U___,___U___},
{/*Empty*/ ___U___,___U___,___U___,___U___,___U___,___U___,___U___,___U___},
{/*Empty*/ ___U___,___U___,___U___,___U___,___U___,___U___,___U___,___U___},
{/*Empty*/ ___U___,___U___,___U___,___U___,___U___,___U___,___U___,___U___},
{/*Empty*/ ___U___,___U___,___U___,___U___,___U___,___U___,___U___,___U___}
};
// Определяем вспомогательные функции, объявленные выше
unsigned short CategoryBitsFor(FixtureType fixtureType)
{
assert((fixtureType >= ftFirst) && (fixtureType <= ftLast));
return (1 << fixtureType);
}
unsigned short MaskBitsFor(FixtureType fixtureType)
{
unsigned short result = 0x0000;
unsigned short categoryBits = CategoryBitsFor(fixtureType);
for(FixtureType ftIt = ftFirst; ftIt != ftEnd; ftIt = ftNext(ftIt))
{
unsigned short mask = CategoryBitsFor(ftIt);
if (categoryBits & mask)
{
//просматриваем строку для текущего типа
for(FixtureType colIt = ftFirst; colIt <= ftIt; colIt = ftNext(colIt))
if (__YES__ == s_contactMatrix[ftIt][colIt])
result |= CategoryBitsFor(colIt);
//просматриваем колонку для текущего типа
for(FixtureType rowIt = ftNext(ftIt); rowIt != ftEnd; rowIt = ftNext(rowIt))
if (__YES__ == s_contactMatrix[rowIt][ftIt])
result |= CategoryBitsFor(rowIt);
}
}
return result;
}
#undef ___U___
#undef _______
#undef __YES__
// Определяем функции, объявленные в хидере
void SetupFilter(b2Fixture *fixture, FixtureType fixtureType)
{
b2Filter filter;
filter.categoryBits = CategoryBitsFor(fixtureType);
filter.maskBits = MaskBitsFor(fixtureType);
fixture->SetFilterData(filter);
fixture->SetUserData((void*) fixtureType);
}
void SetupFilter(b2FixtureDef *fixtureDef, FixtureType fixtureType)
{
b2Filter filter;
filter.categoryBits = CategoryBitsFor(fixtureType);
filter.maskBits = MaskBitsFor(fixtureType);
fixtureDef->filter = filter;
fixtureDef->userData = (void*) fixtureType;
}
BOOL FixtureIsOfType(const b2Fixture *fixture, FixtureType fixtureType)
{
return (fixture->GetFilterData().categoryBits & CategoryBitsFor(fixtureType)) == CategoryBitsFor(fixtureType);
}

Вспомогательные дефайны в описании матрицы использованы не случайно, а для того, чтобы визуально проще было ее (матрицу) воспринимать.

Давайте рассмотрим подробнее матрицу, описывающую контакты. По сути, информативной частью матрицы является лишь левая нижняя половина, а правая верхняя всегда будет помечена как ___U___. Можно было бы, конечно, дублировать информацию из левой нижней половины в правую верхнюю, но зачем? Рекомендуется выделять сразу матрицу 16х16 (по размерности битов категории и маски — 16), чтобы потом без труда добавлять новые типы фиксчей. Для примера выбран размер 8х8 — просто чтобы влезло в экран. После слов, которые говорят о типе фиксчи (Hero, Villain и т.д.), рекомендуется вставлять табуляции, а не пробелы (в HTML разметке этой статьи по известным причинам вставлены пробелы), т.к. потом удобнее будет изменять эти самые слова. К слову, подобный способ описания константных матриц мне лично никогда раньше не встречался, и я пока не нашел, где бы его еще можно было применить. Если есть мысли — подсказывайте.

В методах SetupFilter в поле userData сохраняется тип фиксчи. Сделано это для того, чтобы при дебаге можно было быстро понять, с какой фиксчей мы имеем дело. Для работы программы это не имеет никакого значения, поэтому, если вам userData нужна для чего-то другого — не записывайте туда fixtureType, а используйте для своих целей.

Преимущества такого подхода:

  1. Читабельно: при взгляде на код, которой вызывает SetupFilter, сразу видно, какой тип фиксчи устанавливается. По матрице контактов легко можно проследить все контакты данной фиксчи;
  2. Модифицируемо: если нужно поменять бит категории — нужно просто переупорядочить элементы в enum‘е FixtureType. Если нужно перенастроить контакты, нужно просто модифицировать матрицу;
  3. Расширяемо: при добавлении нового типа фиксчей потребуется лишь добавление элемента в enum FixtureType ( не забывайте модифицировать макросы ftFirst и ftLast при необходимости) и дозаполнение матрицы контактов.

Пример использования

В имплементации класса, описывающего главного героя, создание тела(createBody) и обработка контакта(proceedContact:) может выглядеть примерно так:

Hero.mm:

#import "Contacts.h"

// ..
- (void) createBody
{
// в хидере у объекта Hero объявлено поле b2Body *_body
// создаем _body
// ..
b2FixtureDef fixtureDef;
// устанавливаем, если нужно, shape, userData, friction, restitution, density, isSensor
// ..
SetupFilter(&fixtureDef, kFixtureTypeHero);
_body->CreateFixture(&fixtureDef);
}
- (void) proceedContact: (b2Contact*) contact
{
b2Fixture *foreignFixture = (c->GetFixtureA()->GetBody() == _body) ? c->GetFixtureB() : c->GetFixtureA();
if (YES == FixtureIsOfType(foreighFixture, kFixtureTypeVillain))
{
// Обрабатываем контакт со злодеем
}
}
// ..

Материал предоставлен компанией Softeq Development, FLLC

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *