SpriteKit — Имитация физики — Столкновения и контакты — Часть 13

Итак, рано или поздно, два спрайта будут пытаться занять одно и то же место на сцене. Вам придется решать как будут вести себя спрайты. В Sprite Kit используется два вида взаимодействий между физическими телами:
Контакт используется, когда вы должны знать, что два тела соприкасаются друг с другом. В большинстве случаев, контакты используются если вам нужно внести изменения в геймплей, когда происходит столкновение.
Столкновение используется для предотвращения проникновения спрайтов друг в друга. Когда одно тело сталкивается с другим телом, Sprite Kit автоматически вычисляет результаты столкновения и применяет импульс к спрайтам. Например при столкновении астероиды будут разлетаться в разные стороны.

На сцене вы сами конфигурируете что с чем сталкиваться должно, а что не должно. В Sprite Kit есть ограничения на количество взаимодействий, это позволяет добиться хорошей производительности. Итак, Sprite Kit использует два механизма для ограничения количества взаимодействий в каждом кадре:
— Тела-границы(edge) никогда между собой не взаимодействуют.
— Каждое физическое тело относится к определенной категории. Каждая сцена не может иметь более 32 категорий. При настройке, вы задаете к какой категории принадлежит тело и с какими категориями оно будет взаимодействовать. Контакты и столкновения задаются отдельно.

Пример:
Контакты и столкновения легче гораздо легче понять на примере. Дуэль двух космических кораблей в космосе. В космосе есть планеты и астероиды, с которыми корабли могут столкнуться. И т.к. это дуэль, корабли вооружены ракетами и могут стрелять друг в друга.
1. Сначала определимся с категориями
У нас есть 4 типа физических тел: Ракеты, Корабли, Астероиды и Планеты. Понятно, что в этой сцене мы ограничимся четырьмя категориями. Каждое тело может принадлежать только одной категории.
2. Следующим шагом является определение взаимодействий, которые разрешены между этими физическими телами. Напоминаю, контакты и столкновения конфигурируются отдельно. Удобнее всего такие вещи делать в таблицах. Поэтому сначала таблица контактов:
contacts
Все эти взаимодействия основаны на игровой логике. То есть, когда любой из этих контактов происходит, спрайт кит должен уведомить наш класс об этом, для того чтобы мы могли поменять чего нибудь на сцене.
Вот то, что должно произойти:
— Ракета взрывается, когда происходит контакт с кораблем, астероидом или планетой. Если ракета поражает корабль, корабль получает повреждения.
— Если корабль контактирует с кораблем, астероидом или планетой, он получает повреждения.
— Астероид при контакте с планетой разрушается.

3. Следующим шагом является определение столкновений между физическими телами. Тоже таблица
collis

При работе со столкновениями, важную роль играют физические свойства тел. По таблице:
— Ракета при любых взаимодействиях уничтожается, поэтому ракета игнорирует все столкновения. А еще, ракеты имеют слишком малую массу, чтобы двигать другие тела в случае столкновения.
— Корабль отслеживает столкновения с кораблями, астероидами и планетами.
— Астероид игнорирует столкновения с планетами, потому что по нашим правилам астероид уничтожается при столкновении с планетой.
— Планеты отслеживают только столкновения с другими планетами. У всего остального слишком малая масса, чтобы переместить планету, так что игра игнорирует эти столкновения и предотвращает расчеты связанные с ними.

Реализация
1. Когда вы определили свои категории и взаимодействия, необходимо написать для всего этого код ) Категории задаются 32-битной маской:

static const uint32_t missileCategory =  0x1 << 0;
static const uint32_t shipCategory =  0x1 << 1;
static const uint32_t asteroidCategory =  0x1 << 2;
static const uint32_t planetCategory =  0x1 << 3;

Теперь при иницилизации ваших спрайтов, необходимо задать им свойства categoryBitMask, collisionBitMask и contactTestBitMask. Из названия должно быть понятно что это за свойства, но на всякий случай Маска Категории, Маска Столкновений и Маска Контакта. Эти свойсва задаются в соответствии с таблицами сверху.
2. Например космический корабль:

SKSpriteNode *ship = [SKSpriteNode spriteNodeWithImageNamed:@"spaceship.png"];
ship.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:ship.size];

ship.physicsBody.categoryBitMask = shipCategory;
ship.physicsBody.collisionBitMask = shipCategory | asteroidCategory | planetCategory;
ship.physicsBody.contactTestBitMask = shipCategory | asteroidCategory | planetCategory;

Сравните коллизии и контакты корабля с таблицами сверху и поймете как прописываются маски. Таблицы читать надо по строкам)
3. При иницилизации сцены, не забудьте прописать делегат physicsWorld, делается это так:

self.physicsWorld.contactDelegate = self;

4. Реализуйте метод didBeginContact добавьте туда игровую логику. Как видно из названия, метод выполняется при начале взаимодействия.

- (void)didBeginContact:(SKPhysicsContact *)contact
{
         SKPhysicsBody *firstBody, *secondBody;
         if (contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask)
         {
             firstBody = contact.bodyA;
             secondBody = contact.bodyB;
         }
         else {
             firstBody = contact.bodyB;
             secondBody = contact.bodyA;
         }
         //и дальнейшии действия при столкновении
}

Этот пример показывает несколько важных концепций, которые следует учитывать при реализации:
- В didBeginContact передается объект SKPhysicsContact, который показывает, какие тела были вовлечены в столкновение.
- Тела которые вступили во взаимодействие, могут появляться в любом порядке. И т.к. наши категории сделаны в отсортированном порядке, с помощью условия выше, мы можем понять кто в кого прилетел. Если же у вас не отсортирован список категорий или он более сложный, можете просто проверять bodyA и bodyB на соответствие вашим маскам и работать с ними.
- bodyA и bodyB являются полноценными ссылками на ваши спрайты. Поэтому можете смело с ними работать.

Высокая точность столкновений для маленьких или быстродвижущихся тел
При обнаружении столкновений, SpriteKit сначала определяет расположение всех физических тел на сцене. После этого он определяет, произошло ли столкновение или контакт. Этот метод работает быстро, но иногда может привести к пропуску столкновений. Например, небольшое тело может двигаться так быстро, что оно полностью пролетает через другое физическое тело, быстрее чем за один кадр анимации. Если у вас есть такие физические тела, вы можете указать SpriteKit использовать более точную модель столкновений для проверки взаимодействий.
Эта модель использует гораздо больше ресурсов, так что следует использовать ее с осторожностью. Делается это так:

ship.physicsBody.usesPreciseCollisionDetection = YES;

Соединение физических тел между собой
Иногда необходимо чтобы одно тело следовало за другим с определенным алгоритмом. Можно сделать это придав второму телу определенные импульсы и свойства. Но можно проще - SKPhysicsJoint. Итак типы соединений:
joint

Небольшое описание:
SKPhysicsJointFixed - Фиксированное соединение двух тел. Используется обычно для создания сложных форм, которые могут быть разбиты на части позже.
SKPhysicsJointSliding - Скольжение двух тел вдоль выбранной оси. Относительно друг друга.
SKPhysicsJointSpring - Пружина между двумя телами.
SKPhysicsJointLimit - Тела как бы связаны веревкой, при этом устанавливается максимальное расстояние между телами.
SKPhysicsJointPin - Тела вращаться независимо вокруг точки привязки.

При создании соединения, точки в нем всегда задаются в системе координат сцены. Это может потребовать от вас преобразования координат. Чтобы создать соединение надо:
1. Создать два физических тела.
2. Прикрепите физические тела к спрайтам.
3. Создать нужный объект SKPhysicsJoint...
4. При необходимости настроить свойства объекта, чтобы определить, как соединение должно работать.
5. Добавить к SKPhysicsWorld соединение addJoint:
И соединение будет работать.

Поиск физических тел
Иногда, надо найти физические тела на сцене. Например, вам может понадобиться:
- Определить, находится ли физическое тело в определенной области сцены
- Определить какое тело перешло определенную точку на сцене
- Определить какие тела находятся на линии прямой видимости

В некоторых случаях, вы можете реализовать эти взаимодействия, используя столкновения и контакты. Например, чтобы узнать, когда физическое тело входит в определенную область, можно создать тело и прикрепить его к невидимому спрайту на сцене. Затем настроить столкновения тела так, чтобы оно никогда не сталкивалось ни с чем и т.д. и при контакте вы будете знать о том что нужное вам физтело в определенной области. Но затея с невидимым спрайтом не оптимальна и сложна в исполнении. А еще сложнее реализовать концепцию с прямой видимостью. Небольшой пример, если вражеский корабль находится на линнии прямой видимости от пушки, происходит выстрел.

- (BOOL) isTargetVisibleAtAngle:(CGFloat)angle distance:(CGFloat) distance
{
    CGPoint rayStart = CGPointZero; //начало луча прямой видимости
    CGPoint rayEnd = CGPointMake(distance*cosf(angle), distance*sinf(angle)); //конец луча прямой видимости
    SKPhysicsBody *body = [self.physicsWorld bodyAlongRayStart:rayStart end:rayEnd]; 
    return (body && body.categoryBitMask == targetCategory); //если враг то YES, если какой нибудь другой спрайт, то NO
}
- (void) attackTargetIfVisible
{
    if ([self isTargetVisibleAtAngle: self.cannon.zRotation distance: 512])
    {
        [self shootCannon]; //выстрел
    }
}

Еще вы можете использовать методы bodyAtPoint: и bodyInRect:. Из названия понятно для чего они)

На этом все. Статья получилась длинная, но надеюсь полезная и понятная. В следующий раз будет пример с силами и физикой. И будет много кода.

  • Ярослав

    Уроки просто супер =) Когда будет продолжение?

    • floMaster

      на днях будет

  • Тимофей

    Отличные уроки! Автору респект и уважуха! На днях это сколько?:)

  • Доброго времени суток! Такой вопрос: разлетаются астероиды просто замечательно, корпус кораблика получает вмятины. Все супер, но вот от столкновения кораблик отлетает каждый раз и отлетает в разные стороны, может даже за пределы сцены вылететь, если астероидов много. Подскажите, как быть? В сущности, кораблик должен двигаться по одной траектории, а от столкновений траектория меняется. Как зафиксирован?