Интеграция физического движка Box2D в UIKit-приложение для iOS

Привет!


Сегодня мы покажем, насколько легко встроить физический движок Box2D в любое игровое приложение, написанное на стандартных фреймворках Apple. Примером послужит интерактивная книга, выпущенная нашей студией полгода назад. Эта книга была нашим первым приложением для детей, и когда мы начинали работу над ней, у нас было мало опыта в создании анимаций, поэтому мы выбрали знакомые нам мощные и отлично документированные стандартные фреймворки Apple – так было проще на тот момент. Книга была готова уже через два месяца. Однако некоторые задумки реализованы не были. Из этих пожеланий был оставлен список на будущее, чтобы, когда будет время и знания, вернуться к проекту.

Физика


Одним из пунктов была симуляция физического мира, чтобы у пользователя была возможность играть с предметами: создавать их, бросать, перекидывать из угла в угол средствами акселерометра и так далее. Для реализации этой возможности требовалась интеграция в проект физического движка. И вот, когда на новом проекте было освоены Cocos2D и Box2D, возник резонный вопрос: если Box2D по своей сути не зависим от графической реализации программы, то почему бы не использовать его в самой первой книге? Недолгие поиски на просторах Сети привели к замечательной и лаконичной статье в блоге http://www.cocoanetics.com, просто и доступно объясняющей, как использовать Box2D в стандартном приложении на UIKit. И мы принялись за дело. Больше всего опасений у нас вызывал тот факт, что работа движка, написанного на C++, потребует масштабных изменений в текущем коде проекта. Но, к счастью, обошлось всего лишь парой небольших изменений — тип файлов классов страниц были изменен на Objective-C++, да была проведена легкая оптимизация кода. На данные изменения ушло примерно 4 часа.

Принцип работы


Принцип интеграции движка прост (смотри указанную статью). При загрузке страницы создается физический мир. Далее ему назначаются требуемые границы (в данном случае — весь экран), и создаются требуемые тела. Так как созданные тела не видны пользователю, им в соответствие с помощью свойства тела userData назначаются необходимые объекты UIKit, например, картинки класса UIImageView. Далее, при вызове метода viewDidAppear запускается таймер с нужной частотой, который, обрабатывая положения всех тел в физическом мире, перемещает на нужные позиции связанные с телами соответствующие картинки. Тем самым создается иллюзия того, что сталкиваются непосредственно сами картинки. При вызовет метода viewDidDissapear таймер останавливается, все созданные пользователем тела и соответствующие им изображения — удаляются из мира и из self.view. Вопреки опасениям, что эта схема не даст приемлемой частоты кадров, рендеринг оказался быстрым даже на старых устройствах типа iPhone 3G, на глаз не уступая рендерингу в приложении на основе Cocos2D.

Тела


Описанный в исходной статье способ позволял создавать только прямоугольные тела по размерам view, передаваемых в сообщении о создания тела. Для нас же это был недостаточно гибкий способ, так как тела у нас могли быть любой формы. В работе над проектом на основе Cocos2D мы задействовали удобную программу для Mac OS — SpriteHelper от индивидуального разработчика Bogdan Vladu. Платная лицензия позволяет делать текстурные карты из заготовленных картинок и задавать параметры физическим телам: форму, плотность, коэффициент трения, упругость и т.д. — всё что надо. Для работы с получаемыми файлами автор написал класс SpriteHelperLoader, который позволяет в одно сообщение создать нужное тело в нужном физическом мире и слое Cocos2D. Одна беда — этот класс был жестко ориентирован на совместную работу с Cocos2D. Пришлось потратить некоторое время и «вырезать» из него все упоминания «кокоса». По сути нам он нужен был лишь для получения одного параметра тела — формы (текстурные карты в данном случае не используются). Теперь у нас в руках оказался удобный, а главное привычный способ, чтобы добавить физику в наши первые книги, дать им второе дыхание и, надеемся, прибавить положительных отзывов. Оригинальный метод добавления тел из исходной статьи был переписан:
- (void) addPhysicalBodyForView:(UIImageView *)physicalImageView ofType:(NSString *)type
{
    // get image's center coordinates
    CGPoint position = physicalImageView.center;

    // Define the dynamic body.
    b2BodyDef bodyDef;
    bodyDef.type = b2_dynamicBody;    
    bodyDef.position.Set(position.x/PTM_RATIO, (screenSize.height - position.y)/PTM_RATIO); // convert into Box2D coordinates
    bodyDef.userData = physicalImageView;
    
    // Tell the physics world to create the body
    b2Body *body = world->CreateBody(&bodyDef);
    
    position = CGPointMake(bodyDef.position.x, bodyDef.position.y);
    
    // use modified SpriteHelperLoader to get shape
    tempBody = [bodyLoader bodyWithUniqueName:type atPosition:position world:world];
    
    b2Fixture* fixture = tempBody->GetFixtureList();
    b2Shape *shape = fixture->GetShape();
    
    // Define the dynamic body fixture.
    b2FixtureDef fixtureDef;
    fixtureDef.shape = shape;
    fixtureDef.density = fixture->GetDensity();
    fixtureDef.friction = fixture->GetFriction();
    fixtureDef.restitution = fixture->GetRestitution();
    body->CreateFixture(&fixtureDef);
    
    // a dynamic body reacts to forces right away
    body->SetType(b2_dynamicBody);

    world->DestroyBody(tempBody);

    fixture = nil;
    shape = nil;
}


Обработка касаний


Для обработки касаний мы воспользовались встроенным механизмом Box2D и стандартными методами регистрации прикосновений UIKit. При регистрации касания на главном view происходит трансформация координат точки касания из координатной системы UIKit в координатную систему Box2D. Далее происходит проверка на попадание в какое-либо тело в физическом мире. И если попадание есть, то между данным телом создается физическая связь с телом-землей (groundBody), которая позволяет таскать тело пальцем по экрану. При завершении касания тело отправляется в свободный полет (путем уничтожения связи) согласно приобретенному в процессе перемещения импульсу. Выглядит это довольно натурально. Стоит отметить, что для того, чтобы ваши перетаскиваемые тела не вылетали за границы экрана, надо разрешить расчет столкновений у создаваемой связи между данными телами. Для этого надо свойству связи collideConnected присвоить значение YES.
class QueryCallback : public b2QueryCallback
{
public:
	QueryCallback(const b2Vec2& point)
	{
		m_point = point;
		m_fixture = NULL;
	}    
	bool ReportFixture(b2Fixture* fixture)
	{
		b2Body* body = fixture->GetBody();
		if (body->GetType() == b2_dynamicBody)
		{
			bool inside = fixture->TestPoint(m_point);
			if (inside)
			{
				m_fixture = fixture;                
				// We are done, terminate the query.
				return false;
			}
		}        
		// Continue the query.
		return true;
	}
	b2Vec2 m_point;
	b2Fixture* m_fixture;
};

[...]

#pragma mark - Drag and Drop
// source - http://iphonedev.net/2009/08/05/how-to-grab-a-sprite-with-cocos2d-and-box2d/
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch* myTouch = [touches anyObject];                
    CGPoint location = [myTouch locationInView: [myTouch view]];
    location = CGPointMake(location.x, screenSize.height-location.y);
    m_mouseWorld.Set(location.x/PTM_RATIO, location.y/PTM_RATIO);
    if (m_mouseJoint != NULL)
    {
        NSLog(@"m_mouseJoint != NULL");
        return;
    }
    b2AABB aabb;
    b2Vec2 d;
    d.Set(0.001f, 0.001f);
    aabb.lowerBound = m_mouseWorld - d;
    aabb.upperBound = m_mouseWorld + d;
    
    // Query the world for overlapping shapes.
    QueryCallback callback(m_mouseWorld);
    world->QueryAABB(&callback, aabb);

    b2Body* nbody = NULL;
    
    if (callback.m_fixture)
    {
        nbody = callback.m_fixture->GetBody();        
    }

    if (nbody)
    {
        b2MouseJointDef md;
        md.bodyA = groundBody; // 
        md.bodyB = nbody;
        md.target = m_mouseWorld;
        md.collideConnected = YES;
		#ifdef TARGET_FLOAT32_IS_FIXED
        md.maxForce = (nbody->GetMass() < 16.0)? (1000.0f * nbody->GetMass()) : float32(16000.0);
		#else
        md.maxForce = 1000.0f * nbody->GetMass();
		#endif
        m_mouseJoint = (b2MouseJoint*)world->CreateJoint(&md);
        nbody->SetAwake(YES);
    }
}

- (void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch* myTouch = [touches anyObject];    
    CGPoint location = [myTouch locationInView: [myTouch view]];
	// translate uikit coordinates into box2d coordinates
    location = CGPointMake(location.x, screenSize.height-location.y);    
    m_mouseWorld.Set(location.x/PTM_RATIO, location.y/PTM_RATIO);
    
    if (m_mouseJoint)
    {
        m_mouseJoint->SetTarget(m_mouseWorld);
    }
}

- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"touches ended");
    if (m_mouseJoint)
    {
        AudioServicesPlaySystemSound (soundThrust);
        world->DestroyJoint(m_mouseJoint);
        m_mouseJoint = NULL;
    }
}

- (void) touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self touchesEnded:touches withEvent:event];
}


Акселерометр


При попытке создать реалистичную коробочку с предметами надо не забыть про гравитацию, и тут есть несколько подводных камней. Можно, конечно, пойти по простому пути и получать компоненты вектора гравитации из показаний акселерометра, взяв значения по двум осям — X и Y, как это сделано в исходной статье:

- (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration
{
	b2Vec2 gravity;
	gravity.Set( acceleration.x * 9.81,  acceleration.y * 9.81 );
	world->SetGravity(gravity);
}

Но данный способ позволяет случайно создать нежелательную невесомость в случае расположения устройства на горизонтальной поверхности, так как проекции земной силы тяжести на оси X и Y устройства станут почти равны нулю. В случае такой виртуальной невесомости предметы будут неестественно парить в воздухе. Более натурально будет, если вектор тяжести всегда будет равен 1 и предметы будут находиться под действием силы постоянной величины независимо от расположения устройства: на коленях или столе, вертикально в руках или вообще над головой экраном вниз. Мы реализовали этот несколько более сложный алгоритм расчета направления силы тяжести, но оставим его в секрете, как свое маленькое ноу-хау. Так же стоит упомянуть, что для более точного задания вектора тяжести и более тонкого управления им на новых девайсах можно задействовать акселерометр, а от простого использования протокола UIAccelerometerDelegate (который уже не стоит использовать в iOS 5) стоит перейти к комплексному CMMotionManager из фреймворка CoreMotion.

Видеодемонстрация результата




Вместо заключения


На интеграцию физического движка на 4 страницы нашей книги ушло всего несколько дней, включая придумывание маленького игрового сюжета на каждой странице, создание соответствующей графики и кодинг — итоговые затраты небольшие, а новых возможностей много. Дерзайте и вы!
Спасибо за внимание.
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 18

    0
    У меня английский интерфейс айпада и поиск в Апсторе «Волк и семеро козлят» показывает совсем другое приложение.
    Как скачать именно ваше и на русском языке? Менять интерфейс?
      0
      Если вы заходите с iPad по ссылке и попадаете сразу в App Store, то не стоит волноваться, что описание приложения на английском и скриншоты тоже — оно поддерживает два языка без необходимости смены локали устройства. На главном экране надо просто тапнуть по кубику с надписью «Ru».
        0
        Поиск показывает только «Волк и семеро козлят» by Anton Mishin.
          0
          Простите, невнимательно прочитал первый комментарий. Пройдите по ссылке из последнего абзаца статьи.
            0
            Возможно, у вас не только интерфейс на английском, но и аккаунт заграничный? Тогда там стоит искать по названию «The Wolf and the little goats». Кстати, а как вы узнали название книги, не ходя по ссылке внизу статьи?
              0
              Я ходил по ссылке в конце статьи, в том и дело. Захотелось купить ваше приложение, но на айпаде поиск по-русски его не нашел. Пришлось искать по имени разработчика.

              Судя по всему, в заграничных айпсторах вашу книжку не найти по «Волк и семеро козлят», а это отсечение доброй трети потенциальных клиентов.
                0
                Вы имеете ввиду, что треть русскоязычных пользователей сидит через американские и другие не российские аккаунты? Вообще, в вопросе выбора названия мы руководствовались обычным подходом — когда название общее на английском, а локализованные названия — на соответствующих языках. То есть можно сказать, что изначально это наше приложение ориентировано на западный рынок (как и все), а для России оно локализовано. Просто для удобства детей и родителей переключение языка сделано в самом приложении. Всё-таки большая часть продаж (в идеале) это США и Канада. Ну, и я думаю, что русскоязычные пользователи, выбирая не Россию как страну для привязки аккаунта, понимают, что могут не встретить русское название.
                  0
                  Русскоязычные живут не только в России, поверьте.
                  Просто добавьте «Волк и семеро козлят» на русском в конце описания, делов-то.
                    0
                    Вообще, именно потому, что есть много эммигрантов, мы сделали выбор языка внутри приложения. Да и вообще, для игр так правильнее. Но мы всё-таки не видим проблемы в данном случае. К тому же название уже нельзя изменить, но можно добавить ключевые слова «волк и семеро козлят» в английском описании – возможно это решит проблему, которая возникла у вас.
                      +1
                      Русскоязычные это не только эмигранты. В Казахстане, к примеру, 15 миллионов русскоязычных.

                      Да, добавьте «волк и семеро козлят» в конце английского описания, об этом я и говорил.
                      0
                      Хоть у нас точки зрения по данному вопросу не совпадают — спасибо вам за ваше мнение!
          0
          Видео бы показали. Всё же это важно с учётом фишек вашего приложения.
            0
            К вечеру попробуем выложить демонстрацию на Ютубе, спасибо.
              0
              Добавлено видео.
              0
              Вот как раз полезный перевод статьи про Objective-C и С++: Способы встраивания C++ в Objective-C проекты
                0
                Спасибо, очень познавательно.

                Сам когда-то подумал, что на UIKit в общем можно собрать нормальную игру вполне. Уж для всяких текстовых квестов или простых арканоидов даже опенгл или кокос не нужен.
                  0
                  Как вы делали собственный run loop для анимации? Насколько я понимаю в Box2D его нет (как и в AppKit).
                    0
                    Извините, что на месяц позже отвечаю )
                    Не до конца понял ваш вопрос. Но видимо вы спрашиваете про то, что у меня описано в части «Принцип работы»? Или я не прав?

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое