Как создать Infinite Runner на iOS: Cocos2D, автоматизация и многое другое

Опубликовано: 2022-03-11

Разработка игр для iOS может быть полезным опытом как с точки зрения личного, так и финансового роста. Ранее в этом году я разместил в App Store основанную на Cocos2D игру Bee Race. Его игровой процесс прост: бесконечный раннер, в котором игроки (в данном случае пчелы) набирают очки и избегают препятствий. Смотрите здесь для демонстрации.

В этом уроке я объясню процесс разработки игр для iOS, от Cocos2D до публикации. Для справки, вот краткое содержание:

  • Спрайты и физические объекты
  • Краткое введение в Cocos2D
  • Использование Cocos2D с раскадровками
  • Геймплей и (краткое) описание проекта
  • Автоматизируйте рабочие места. Используйте инструменты. Будь крутым.
  • Биллинг в приложении
  • Многопользовательский геймплей с Game Center
  • Возможности для совершенствования
  • Заключение

Спрайты и физические объекты

Прежде чем мы углубимся в мелкие детали, будет полезно понять разницу между спрайтами и физическими объектами.

Для любого заданного объекта, который появляется на экране бесконечной игры-раннера, графическое представление этого объекта называется спрайтом , а полигональное представление этого объекта в физическом движке называется физическим объектом .

Таким образом, спрайт рисуется на экране вместе с соответствующим физическим объектом, который затем обрабатывается вашим физическим движком. Эту настройку можно визуализировать здесь, где спрайты отображаются на экране, а их физические многоугольные аналоги обведены зеленым:

В бесконечных бегунках для iOS сосуществуют спрайты и физические объекты.

Физические объекты по умолчанию не связаны со своими соответствующими спрайтами, а это означает, что вы, как разработчик iOS, можете выбирать, какой физический движок использовать и как соединять спрайты и тела. Самый распространенный способ — создать подкласс спрайта по умолчанию и добавить к нему конкретное физическое тело.

С этим в мыслях…

Краткое руководство по разработке игр Cocos2D для iOS

Cocos2D-iphone — это платформа с открытым исходным кодом для iOS, которая использует OpenGL для аппаратного ускорения графики и поддерживает физические движки Chipmunk и Box2D.

Во-первых, зачем нам такая структура? Ну, для начала, фреймворки реализуют часто используемые компоненты разработки игр. Например, Cocos2D может загружать спрайты (в частности, листы спрайтов (почему?)), запускать или останавливать физический движок и правильно обрабатывать синхронизацию и анимацию. И все это делается с кодом, который был тщательно проверен и протестирован — зачем тратить собственное время на переписывание кода, который, вероятно, будет хуже?

Однако, возможно, самое главное — разработка игр Cocos2D использует аппаратное ускорение графики . Без такого ускорения любая игра с бесконечным раннером для iOS даже с умеренным количеством спрайтов будет работать с заметно низкой производительностью. Если мы попытаемся сделать более сложное приложение, то, вероятно, начнем видеть на экране эффект «замедления», т. е. несколько копий каждого спрайта, когда он пытается анимироваться.

Наконец, Cocos2D оптимизирует использование памяти, поскольку кэширует спрайты. Таким образом, любые дублированные спрайты требуют минимальной дополнительной памяти, что, очевидно, полезно для игр.

Использование Cocos2D с раскадровками

После всех похвал, которые я получил от Cocos2D, может показаться нелогичным предлагать использовать раскадровки. Почему бы просто не манипулировать вашими объектами с помощью Cocos2D и т. д.? Ну, если честно, для статических окон часто удобнее использовать Xcode's Interface Builder и его механизм Storyboard.

Во-первых, это позволяет мне перетаскивать и позиционировать все мои графические элементы для моей бесконечной игры-раннера с помощью мыши. Во-вторых, Storyboard API очень и очень полезен. (И да, я знаю о Cocos Builder).

Вот краткий обзор моей раскадровки:

Чтобы научиться делать бесконечную игру, начните с хорошей раскадровки.

Контроллер основного вида игры просто содержит сцену Cocos2D с некоторыми элементами HUD поверх:

Начало нашего руководства по Cocos2D находится на контроллере представления.

Обратите внимание на белый фон: это сцена Cocos2D, которая загрузит все необходимые графические элементы во время выполнения. Другие представления (индикаторы в реальном времени, одуванчики, кнопки и т. д.) являются стандартными представлениями Cocoa, добавляемыми на экран с помощью Interface Builder.

Не буду останавливаться на деталях — если вам интересно, примеры можно найти на GitHub.

Геймплей и (краткое) описание проекта

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

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

Cocos2D имеет объект камеры, который был разработан, чтобы следовать за персонажем; на практике было проще манипулировать CCLayer, содержащим игровой мир.

Управление простое: нажатие на экран перемещает пчелу вверх, а другое нажатие перемещает ее вниз.

Сам мировой слой фактически имеет два подслоя. Когда игра начинается, первый подслой заполняется от 0 до BUF_LEN и отображается изначально. Второй подуровень заполняется заранее от BUF_LEN до 2*BUF_LEN. Когда пчела достигает BUF_LEN, первый подуровень очищается и мгновенно заменяется с 2*BUF_LEN на 3*BUF_LEN, и представляется второй подуровень. Таким образом, мы чередуем слои, никогда не сохраняя устаревшие объекты, что является важной частью предотвращения утечек памяти.

Моя бесконечная игра-раннер состоит из многослойного мира.

Что касается физических движков, я использовал Chipmunk по двум причинам:

  1. Он написан на чистом Objective-C.
  2. Я работал с Box2D раньше, поэтому я хотел сравнить их.

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

Автоматизируйте рабочие места. Используйте инструменты. Будь крутым.

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

Но иногда автоматизация также может быть компромиссом между перфекционизмом и соблюдением сроков. В этом смысле перфекционизм может стать убийцей Angry Birds.

Например, в другой игре для iOS, которую я сейчас разрабатываю, я построил фреймворк для создания макетов с помощью специального инструмента (доступен на GitHub). У этого фреймворка есть свои ограничения (например, у него нет красивых переходов между сценами), но его использование позволяет мне делать свои сцены в десятую часть времени.

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

Перфекционизм может быть убийцей Angry Birds. Время — важнейший ресурс в разработке игр для iOS.
Твитнуть

При создании этого бесконечного раннера ключевую роль снова сыграла автоматизация. Например, мой художник присылал мне графику высокого разрешения через специальную папку Dropbox. Чтобы сэкономить время, я написал несколько скриптов для автоматического создания наборов файлов для различных целевых разрешений, требуемых App Store, а также добавил -hd или @2x (упомянутые скрипты основаны на ImageMagick).

Что касается дополнительных инструментов, я считаю, что TexturePacker очень полезен — он может упаковывать спрайты в листы спрайтов, чтобы ваше приложение потребляло меньше памяти и загружалось быстрее, поскольку все ваши спрайты будут считываться из одного файла. Он также может экспортировать текстуры практически во все возможные форматы фреймворков. (Обратите внимание, что TexturePacker не является бесплатным инструментом, но я думаю, что он стоит своей цены. Вы также можете попробовать бесплатные альтернативы, такие как ShoeBox.)

Основная сложность, связанная с игровой физикой, заключается в создании подходящих полигонов для каждого спрайта. Другими словами, создание многоугольного изображения какой-то пчелы или цветка неясной формы. Даже не пытайтесь сделать это вручную — всегда пользуйтесь специальными приложениями, которых очень много. Некоторые из них даже довольно… экзотичны — например, создание векторных масок в Inkspace, а затем их импорт в игру.

Для разработки своей собственной игры в жанре бесконечный раннер я создал инструмент для автоматизации этого процесса, который я назвал Andengine Vertex Helper. Как следует из названия, изначально он был разработан для платформы Andengine, хотя в наши дни он будет работать с рядом форматов.

В нашем случае нам нужно использовать шаблон plist:

 <real>%.5f</real><real>%.5f</real>

Далее мы создаем файл 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>jet_ant</key> <dict> <key>vertices</key> <array> <real>-0.18262</real><real>0.08277</real> <real>-0.14786</real><real>-0.22326</real> <real>0.20242</real><real>-0.55282</real> <real>0.47047</real><real>0.41234</real> <real>0.03823</real><real>0.41234</real> </array> </dict> </dict> </plist>

И загрузчик объектов:

 - (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; 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->collision_type = OBJ_COLLISION_TYPE; cpSpaceAddShape(space, shape); }

Чтобы проверить, как спрайты соответствуют их физическим телам, см. здесь.

Гораздо лучше, правда?

Таким образом, всегда автоматизируйте, когда это возможно. Даже простые сценарии могут сэкономить вам массу времени. И что важно, это время можно использовать для программирования, а не для щелчков мышью. (Для дополнительной мотивации вот токен XKCD.)

Биллинг в приложении

Собранные шарики в игре действуют как валюта в приложении, позволяя пользователям покупать новые скины для своей пчелы. Однако эту валюту также можно купить за реальные деньги. Важный момент, который следует отметить в отношении выставления счетов в приложении, заключается в том, нужно ли вам выполнять проверки на стороне сервера для действительности покупки. Поскольку все покупаемые товары практически одинаковы с точки зрения игрового процесса (только изменение внешнего вида пчелы), нет необходимости выполнять проверку сервера на действительность покупки. Однако во многих случаях вам обязательно нужно это сделать.

Кроме того, у Рэя Вендерлиха есть идеальное руководство по выставлению счетов в приложении.

Многопользовательский геймплей с Game Center

В мобильных играх общение — это больше, чем просто добавление кнопки «Мне нравится» в Facebook или настройка списков лидеров. Чтобы сделать игру более увлекательной, я реализовал многопользовательскую версию.

Как это работает? Во-первых, два игрока соединяются с помощью системы подбора игроков iOS Game Center в режиме реального времени. Поскольку игроки действительно играют в одну и ту же игру с бесконечным раннером, должен быть только один набор игровых объектов. Это означает, что экземпляр одного игрока должен генерировать объекты, а экземпляр другого игрока будет их считывать. Другими словами, если бы устройства обоих игроков генерировали игровые объекты, было бы сложно синхронизировать опыт.

Имея это в виду, после установления соединения оба игрока отправляют друг другу случайное число. Игрок с большим номером действует как «сервер», создавая игровые объекты.

Вы помните обсуждение порционной генерации мира? Где у нас было два подслоя, один от 0 до BUF_LEN, а другой от BUF_LEN до 2*BUF_LEN? Эта архитектура была использована не случайно — нужно было обеспечить плавную графику в сетях с задержкой. Когда часть объектов сгенерирована, она упаковывается в plist и отправляется другому игроку. Буфер достаточно большой, чтобы позволить второму игроку играть даже с сетевой задержкой. Оба игрока отправляют друг другу свою текущую позицию с периодом в полсекунды, также немедленно отправляя свои движения вверх-вниз. Чтобы сгладить впечатление, положение и скорость корректируются каждые 0,5 секунды с плавной анимацией, поэтому на практике кажется, что другой игрок движется или ускоряется постепенно.

Безусловно, в отношении многопользовательского бесконечного геймплея необходимо учитывать и другие соображения, но, надеюсь, это даст вам представление о типах связанных с ним задач.

Возможности для совершенствования

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

  1. Проблемы с управлением: постукивание часто является неинтуитивным жестом для игроков, предпочитающих скользить.
  2. Слой мира перемещается с помощью действия CCMoveBy. Это было нормально, когда скорость мирового слоя была постоянной, так как действие CCMoveBy циклически повторялось с CCRepeatForever:

     -(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]; }

    Но позже я добавил увеличение скорости мира, чтобы сделать игру сложнее:

     -(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 или запуска собственного игрового сервера. С другой стороны, создание ботов стало невозможным, что я хотел бы изменить.

Заключение

Создание собственной инди-игры в жанре «бесконечный раннер» может стать отличным опытом. И как только вы дойдете до этапа публикации, вы можете испытать прекрасное чувство, когда вы выпускаете свое собственное творение в дикую природу.

Процесс рассмотрения может длиться от нескольких дней до нескольких недель. Кроме того, здесь есть полезный сайт, который использует данные из краудсорсинга для оценки текущего времени проверки.

Кроме того, я рекомендую использовать AppAnnie для изучения различной информации обо всех приложениях в App Store, а также может быть полезной регистрация в некоторых аналитических службах, таких как Flurry Analytics.

И если эта игра вас заинтриговала, обязательно загляните в магазин Bee Race.