Сегодня я хочу вам рассказать о создании игры для iOS на основе Cocos2D на примере недавно вышедшей игры «Пчелогонки» (анг. – Bee Race).
Геймплей не содержит в себе ничего сложного – это по сути бесконечный ранер, в котором нужно собирать поинты и уворачиваться от препятствий. Только вместо рыжей девочки или кладоискателя – здесь летает двухмерная пчелка.
Для заинтересовавшихся, прошу под кат (Ахтунг! Минен унд много буквирен).
Основные разделы для рассмотрения:
- Очень краткое введение в Cocos2D
- Используем Cocos2D одновременно с StoryBoard
- Краткое описание геймплея и структуры проекта
- Покупаем инструменты и что делать, если душит жаба
- Чем не пахнет приложение или подключаем in-app билинг
- Социализируем. Подключаем Game Center и создаем мультиплеерную версию на два игрока
- В чём промахнулся Акела
- Паблиш
Спойлер:
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», которая стояла у меня аж на июль этого года.
Надеюсь, было интересно читать и кто-нибудь подчерпнул для себя полезное.
Автор: Nepherhotep