Как стать автором
Обновить

Разработка игры-бесконечной гонки для iOS при помощи Cocos2D-iphone

Время на прочтение9 мин
Количество просмотров43K
Сегодня я хочу вам рассказать о создании игры для iOS на основе Cocos2D на примере недавно вышедшей игры «Пчелогонки» (анг. – Bee Race).
Геймплей не содержит в себе ничего сложного – это по сути бесконечный ранер, в котором нужно собирать поинты и уворачиваться от препятствий. Только вместо рыжей девочки или кладоискателя – здесь летает двухмерная пчелка.
Для заинтересовавшихся, прошу под кат (Ахтунг! Минен унд много буквирен).
Основные разделы для рассмотрения:
  1. Очень краткое введение в Cocos2D
  2. Используем Cocos2D одновременно с StoryBoard
  3. Краткое описание геймплея и структуры проекта
  4. Покупаем инструменты и что делать, если душит жаба
  5. Чем не пахнет приложение или подключаем in-app билинг
  6. Социализируем. Подключаем Game Center и создаем мультиплеерную версию на два игрока
  7. В чём промахнулся Акела
  8. Паблиш


Спойлер:




1. Очень краткое введение в Cocos2D.

Cocos2d-iphone – это свободный 2D движок для iOS, использующий OpenGL для аппаратного ускорения графики и поддерживающий Chipmunk или Box2D в качестве движков физики.
Собственно, зачем нужен движок? Для того, чтобы не было необходимости писать загрузчик спрайтов (в частности из спрайтшитов), корректно запускать/останавливать анимацию, игровые таймеры, движок физики. Ну и главное – это аппаратное ускорение графики, если создавать игру, пусть и с несложной графикой, на основе компонентов Cocoa Touch, то после N-го количества игровых элементов, вы будете активно получать эффект bullet-time к месту и не к месту, либо игра вообще почит в бозе.
Cocos2D хранит все спрайты в кеше, дубликаты одного спрайта являются относительно легкими, т.к. спрайт хранит ссылку на фрейм, но не саму графику. При этом рендеринг спрайта в OpenGL буфер — более быстрая операция, чем рендеринг Cocoa элемента в дереве других графических элементов.
Что касается физических объектов – напрямую со спрайтами они не связаны, поэтому, если хотите, чтобы спрайт, к примеру цветка, содержал соответствующих физический полигон, то нужно писать наследника SpriteFlower. Зато более гибкая система – цветку можно присандалить не один, а несколько полигонов, к тому же можно даже выбрать какой из движков физики использовать.
Теперь, когда мы разобрались во всех немыслимых тонкостях прочитали обещанное введение в Cocos2D, приступим к некой конкретике.


2. Используем Cocos2D одновременно с StoryBoard

После всех дифирамбов Cocos2D, выглядит немного нелогично. Однако в действительности для статичных окон намного проще использовать Interface Builder и механизмы Storyboard. Во-первых можно мышкой потягать все эти картинки, а во-вторых сам програмный интерфейс Storyboard слишком удобный, чтобы его не использовать. И да, я в курсе, что есть это cocosbuilder.com
Как оно выглядит в Interface Builder:

Все окна здесь описаны в Storyboard, а главное окно игры – просто содержит Cocos2D сцену в качестве фона, а поверх размещен HUD, сделанный также на основе стандартных элементов.

Обратите внимание, как выглядит экран – белый фон – это Cocos2D, который загрузит необходимую графику на старте игры, а индикатор жизней, строки и прочее – это сделано при помощи интерфейс билдера.
Не буду увеличивать и без того большую статью описанием, как это делается, просто приведу ссылку, откуда я копипастил код черпал вдохновение github.com/tinytimgames/CCViewController


3. Краткое описание геймплея и структуры проекта

Геймплей таков – пчела висит неподвижно, а по теории относительности, поле несется навстречу, неся различные опасности в виде пауков и ядовитых растений, а также разные плюшки в виде одуванов и одуванчиковых семян. В Cocos2D на самом деле есть объект camera, который может следовать за игроком, но на практике получилось проще двигать CCLayer на котором находится игровой мир.
Управление самое простое – тапнул на экран, пчела полетела вниз, тапнул еще раз – вверх. Тем не менее, оказалось, что игрокам интуитивно хочется делать слайд и поэтому по началу довольно не удобно.
Генерация мира идет не равномерно, а порциями по несложному алгоритму. В игре есть три специальных слоя для заполнения объектами. Во время старта игры заполняются объектами все три. Потом берется один из слоев и заполняется объектами на определенную длину. Через некоторое время заполняется следующий участок, но уже во второй слой, а затем третий. Когда пчела перелетает из второго участка в третий, первый слой очищается, а потом в него начинает заполнятся четвертый участок и так далее. Можно было обойтись и двумя слоями, но тогда было бы сложнее бороться с эффектом, когда исчезает объект, перелетевший из одного участка в другой (в моем случае такими объектами являются только летящие семена одувана). Это происходит из-за того, что объекты удаляются одним махом, а не покоординатно.
В проекте используется движок физики Chipmunk. Я взял его по двум причинам. Во-первых, он вроде бы как более простой, а во-вторых с Box2D я уже имел дело, когда использовал AndEngine, а захотелось чего-то нового. Движок нужен только для того, чтобы определять столкновения пчелы с игровыми объектами, физики, как в злых птицах, нет.


4. Покупаем инструменты и что делать, если душит жаба

В разработке казуалки главное не зацикливаться. Могу с уверенностью сказать, что главным убийцей Angry Birds мог быть только перфекционизм. Но даже, если нет возможности необходимости делать суперфреймворк, с базовым классом MyObject, который может редактироваться специально предназначенным редактором, это не значит, что нельзя автоматизировать многие вещи.
Прежде всего очень сильно упрощает жизнь Linux-way. К примеру, художник сбрасывает мне рисунки в большом разрешении в дропбокс. У меня есть скрипт, который конвертирует их в меньшее разрешение (mogrify, ага) + добавляет –hd версию (@2x в зависимости от ситуации).
Использование Cocos2D совместно с Interface builder – это тоже прежде всего оптимизация по времени.
Еще может быть очень полезным TexturePacker, который упакует все спрайты в один спрайтшит и таким образом уменьшит количество потребляемой памяти, если только вы и без этого не ухитритесь сделать все спрайты по размерам кратные двойке. TexturePacker платный, но он стоит того. Со спрайтшитом проще еще в том плане, что добавляя новый спрайт, нет необходимости добавлять новые файлы в проект.
Основная сложность для 2D физики – это по картинкам создать полигон, который будет загружен физическим движком. Вручную это делать нереально, поэтому нужно использовать какой-то инструмент. Есть различные предложения типа делать векторную структуру при помощи программы Inkspace, но лично я написал для этого утилиту AndengineVertexHelper code.google.com/p/andengine-vertex-helper/downloads/list
Времени на это ушло где-то один день, но зато эта штука оказалось очень полезной. По умолчанию в программе стоит шаблон для кодогенерации движка Andengine, подробности здесь www.andengine.org/forums/features/vertex-helper-t1370.html
Меняем шаблон на
<real>%.5f</real><real>%.5f</real>
и получаем форматирование в plist.

Далее создаем plist файл с описаниями объектов:
Развернуть пример
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>bee0</key>
	<dict>
		<key>vertices</key>
		<array>
            <real>-0.41786</real><real>-0.14844</real>
            <real>-0.40714</real><real>0.07031</real>
            <real>-0.03929</real><real>0.14453</real>
            <real>0.27143</real><real>0.15625</real>
            <real>0.46071</real><real>-0.02344</real>
            <real>0.45714</real><real>-0.29688</real>
            <real>0.26786</real><real>-0.46484</real>
            <real>0.02143</real><real>-0.46094</real>
            <real>-0.29643</real><real>-0.31250</real>
		</array>
	</dict>


И загрузчик объекта:
Развернуть пример
- (void)createBodyAtLocation:(CGPoint)location{
    float mass = 1.0;
    body = cpBodyNew(mass, cpMomentForBox(mass, self.sprite.contentSize.width*self.sprite.scale,
   		 self.sprite.contentSize.height*self.sprite.scale));
    body->p = location;
    body->data = self;
    cpSpaceAddBody(space, body);
    
    NSString *path =[[NSBundle mainBundle] pathForResource:@"obj _descriptions" ofType:@"plist"]; // <- load plist
    NSDictionary *objConfigs = [[[NSDictionary alloc] initWithContentsOfFile:path] autorelease];
    NSArray *vertices = [[objConfigs objectForKey:namePrefix] objectForKey:@"vertices"];
    shape = [ChipmunkUtil polyShapeWithVertArray:vertices
                                        withBody:body
                                           width:self.sprite.contentSize.width
                                          height:self.sprite.contentSize.height];
    shape->e = 0.7; 
    shape->u = 1.0;
    shape->data = self;
    shape->collision_type = OBJ_COLLISION_TYPE;
    cpSpaceAddShape(space, shape);
}

Чтобы визуально протестировать соответствие спрайтов и их физических представлений, очень советую воспользоваться вот этой штукой: github.com/nubbel/CPDebugLayer
Выглядеть будет намного понятней:

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


5. Чем не пахнет приложение или подключаем in-app билинг

Собирая в игре одуваны, игрок на самом деле получает внутри-игровые деньги. Их можно потратить на новых персонажей. По большому счету, персонажи не отличаются ничем, кроме внешнего вида – они ни более ловкие, ни еще какие-нибудь. Однако там где есть прогресс, там есть и чит. В игре есть возможность купить одуваны за реальные деньги. Т.к. персонажи ничем практически не отличаются, то и серверной проверки на валидность покупки нет.


6. Социализируем. Подключаем GameCenter и создаем мультиплеерную версию на два игрока

Для социализции приложения я добавил два лидерборда – счетчик пройденного расстояния и количество набранных очков. Хотя мне искренне интересно, а кто-нибудь этим вообще использует?
Более полезная фича гейм центра – это возможность создания мультиплеерной игры.
Порционность генерации карты я делал не зря – для того, чтобы буфер заполнялся достаточным количеством объектов «на будущее», если есть проблемы с сетью. Один из игроков будет сервером, другой – клиентом.
Чтобы определить кто есть кто, в начале с игры вместе с другой метаинформацией об игроке передается случайное число. Тот у кого оно больше – сервер.
И хотя размер буфера с объектами достаточно велик, чтобы сгладить неравномерность сети, задержка может привести к тому, что один игрок сильно опередит другого. В этом случае соперник будет показан стрелкой, т.е. будет показываться его высота.
Спрайт второго игрока также был неподвижным, однако с периодичностью делалась поправка его положения с учетом переданной им информацией и текущего положения позиции «мира». В идеале (при мгновенной передаче по сети), игрок «двигался» бы равномерно, при этом не передавая слишком много информации.
Однако на практике такое обновление вызывало побочные эффекты – игрок подергивался, т.к. глазу были заметны даже небольшие задержки между обновлениями. Поэтому сделал обновление не мгновенное, а в виде анимации движения за пол секунды. На практике это выглядит, как будто пчела плавно замедляется или ускоряется.


7. В чём промахнулся Акела

По итогам разработки всплыли некоторые проблемы:
1. Вышеупомянутая проблема управления — тап не интуитивен в данной игре

2. Графика немного запинается каждый перезапуск общего слоя.
При чем, если action зациклен, как repeatForever, то такое проблемы практически нет:
Развернуть пример
-(void) infiniteMove{
    id actionBy = [CCMoveBy actionWithDuration: BUFFER_DURATION position: ccp(-BUFFER_LENGTH, 0)];
    id actionCallFunc = [CCCallFunc actionWithTarget:self selector:@selector(requestFillingNextBuffer)];
    id actionSequence = [CCSequence actions: actionBy, actionCallFunc, nil];
    id repeateForever = [CCRepeatForever actionWithAction:actionSequence];
    [self.bufferContainer runAction:repeateForever];
}

Но я сделал, чтобы движение постепенно ускорялось, поэтому каждый цикл создается новые action. Это приводит к тому, что графика немного запинается:
Развернуть пример
-(void) infiniteMoveWithAccel{
    float duration = BUFFER_DURATION-BUFFER_ACCEL*self.lastBufferNumber;
    duration = max(duration, MIN_BUFFER_DURATION);
    id actionBy = [CCMoveBy actionWithDuration: duration position: ccp(-BUFFER_LENGTH, 0)];
    id restartMove = [CCCallFunc actionWithTarget:self selector:@selector(infiniteMoveWithAccel)];
    id fillBuffer = [CCCallFunc actionWithTarget:self selector:@selector(requestFillingNextBuffer)];
    id actionSequence = [CCSequence actions: actionBy, restartMove, fillBuffer, nil];
    [self.bufferContainer runAction:actionSequence];
}

В прочем, когда я тестировал «на кошках», никто из испытуемых не пожаловался на это, но если присмотреться, то видно.

3. Использование Game Center для мультиплеера с одной стороны избавило от необходимости авторизации, а также дало возможность сделать игру без серверной части (используется только эпловский Peer-to-Peer), но с другой стороны, лишило возможности написать бота.
А ведь вряд ли эта игра наберет столько пользователей, чтобы всегда было несколько человек онлайн. Бот в этом плане замечательное решение – человек побеждает AI, думая, что борется с реальным человеком и даже простейшая игра кажется в несколько раз интересней.


8. И, наконец, публикация.

На самом деле, в публикации не было ничего эдакого. Разве что дня четыре не мог понять почему приложение со статусом ready for sale не видно в iTunes. Оказалось, что я просто забыл сменить дату «Rights and Pricing -> Availability Date», которая стояла у меня аж на июль этого года.

Надеюсь, было интересно читать и кто-нибудь подчерпнул для себя полезное.
Теги:
Хабы:
+31
Комментарии45

Публикации

Изменить настройки темы

Истории

Работа

iOS разработчик
23 вакансии
Swift разработчик
32 вакансии

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн