Привет! Мы — небольшая команда из двух человек, которая пытается делать игры в свободное время. Совсем недавно мы наконец-то зарелизили свое первое скромное творение и решили поделиться опытом его создания с читателим.
“У всех есть свои раннеры, а чем мы хуже? Мы тоже сможем запилить за месяц крайне простую игру для телефонов”, — подумали мы немногим больше года назад, и все завертелось.
Мы нарисовали пару скетчей, написали диздок на 2 листа и взяли Unity, с которым у нас уже был небольшой опыт работы. Решив, что справимся с простым раннером примерно за месяц-два, приступили к работе.
Осторожно, под катом довольно много картинок!
Прототип
Итак, мы решили создать что-то очень простое, однокнопочное. Основная идея — персонаж, который бежит и на ходу разбивает камни. Игроку нужно в определенный момент тапнуть по экрану, чтобы уничтожить преграду.
Примерно за неделю создали прототип, который состоял из главного герой — Огра, преследующего его Динозавра и генератора камней, которые разлетались на куски даже с применением физики.
Графически все это обернули в простой и яркий flat-стиль. Для первых шагов — то, что надо.
Один из первых скетчей
Развитие прототипа
Мы, конечно, любим хардкорные игры, но не такие, когда после одного неверного нажатия вы проигрываете. Поэтому было решено, что за один раунд у игрока будет право на 3 ошибки, после чего Динозавр съест главного героя.
Поиграв немного в прототип, поняли, что нужно хоть какое-то разнообразие геймплея. Так у нас появились курочки. Их конечная реализация довольно сильно разнится с тем, что было изначально.
В первой реализации Огр швырял в курочек бумерангом. Игроку при этом необходимо было пальцем “разрезать” курочку, как режут фрукты в Fruit Ninja. Причем попасть по курочке нужно было дважды. Первый раз с нее слетала часть оперения, на второй она превращалась в несколько куриных ножек, которые летели к Динозавру в рот. Дино ел и добрел. Пропускали хоть одну курицу — Динозавр злился и подходил ближе. Но это не только звучит громоздко, но так же и играется. Курочек полностью переделали: вместо нескольких птиц и бумеранга — стая пернатых и, внезапно, двустволка. Не, ну в самом деле, это же игра, тут может происходить все, что угодно. Синий огр, убегающий от динозавра? ДА! Дробовик? Почему нет? Курица в небе? Да это не одна курица, а целая стая, и мы их сейчас изрешетим!
Теперь, чтобы убить курицу, достаточно было по ней тапнуть. Куриные ножки убрали, и если вовремя не убить пернатую, она врежется в динозаврову морду, он разозлится и подойдет ближе.
Черновая версия всех этих идей была готова примерно через 2 недели после начала разработки. Самое забавное, что геймплейно тот прототип мало чем отличался от текущего релиза.
Итого мы получили геймлей, состоящий из двух чередующихся частей: разбивание камней и расстрел стай курочек из дробовика.
После того, как с геймплеем было более-менее ясно, мы вырезали ненужный контент и стали добавлять необходимые элементы. Так за следующие пару месяцев появились: начальная заставка, несколько вариантов задних фонов, которые сменяли друг друга, другие декорации, такие, как кусты и цветочки, простенький туториал, а у Огра начали появляться синяки после удара о камень. После этого пришло время все привести в порядок, на что и ушла большая часть времени.
Куриные ножки и бумеранги почти сразу же были отправлены в мусорную корзину
Графика и анимация
Все персонажи претерпевали изменения по сравнению с первоначальными образами, добавлялось все больше деталей. Лишь курочки сразу получились отличными и графически не изменились вовсе.
Скриншот из релизной версии
В данной игре хотелось опробовать скелетную анимацию. В итоге решили попробовать плагин CCD, который был продемонстрирован на одной из конференций Unity. Слегка модифицированный, он дожил до релизной версии, и для этого проекта нам его в общем-то хватило. Однако в дальнейшем, если понадобится скелетная анимация, определенно будем использовать более продвинутые инструменты.
Так как геймплей обязывает игрока постоянно быть сосредоточенным на разбивании камней, анимацию хотелось сделать плавной и без лишних деталей. Но пару приятный мелочей мы добавили, такие, как игривые брови, прыгающее пузико и подвижный пучок волос.
Музыка и звук
Тут получилось довольно гладко. Для релизной озвучки мы купили несколько библиотек со звуками в магазине ассетов, парочку звуков записали самостоятельно. Музыку заказали у независимого композитора.
Единственным интересным фактов было то, что для черновой озвучки были использованы звуки ни много ни мало из первого Half-Life. В частности камень разбивался с характерным звуком смерти от попадания из Гауссгана. Заменив его в итоге на что-то более похожее на разбивание камней, мы какое-то время не могли привыкнуть к новому звуку после такого родного Гаусса.
Меню окончания раунда
Ну какое может быть меню в такой маленькой игре? Но не тут-то было, оказалось, что придется впихнуть немалое количество кнопок, чтобы был минимальный нынче набор: помощь, рекорды, оценка приложения, отключение звук. И еще нужно не забыть добавить возможность отключить рекламу. А также нужны очки, лучший результат и вознаграждение игрока за установление нового рекорда. Ну и, в конце концов, более приятное получение заработанных монет, чем просто скачок счетчика.
Такими вознаграждениями стали плавно увеличивающееся количество денег с характерным звоном монет и анимация кубка с аплодисментами в случае установления рекорда. Мелочи, а приятно.
Один из скетчей меню. Практически так же оно выглядит в релизной версии.
К слову, после Unity 4.6 создание UI стало гораздо более простым процессом, так что тут нам еще повезло.
Внутриигровой магазин
После того, как наш прототип все больше стал походить на игру, мы задумались, чем еще можно завлечь игрока возвращаться снова, кроме таблицы рекордов, которая у нас планировалась изначально.
За основу мы решили взять идею коллекционирования. Открываемые персонажи — круто, но долго. Нужно много арта, много анимаций, больше тестирования того, как все они ведут себя в игре. Также думали сделать открываемыми фоновые пейзажи, камни, но в итоге коллекционировать мы решили яйца.
Так мы добавили магазин яиц, в котором, однако, оказались самые разные трофеи.
Магазин “яиц”. До покупки можно лишь гадать, что скрывается за силуэтом
К слову, дался этот магазин не так легко, как казалось изначально. В итоге мы сделали кастомный шейдер для еще не купленных яиц и доработали стандартный ScrollRect.
Монетизация
Мы решили попробовать добавить какую-никакую монетизацию. Так сказать, попробовать это дело технически и, возможно, собрать немного чаевых.
В итоге выбор наш пал на один из самых простых вариантов — рекламу. Так как вставлять баннеры прямо в игру, тем самым уродуя игровую сцену, мы были не намерены, решили показывать межстраничную рекламу на весь экран в конце каждого третьего раунда. Вроде более-менее демократично. Мы также добавили возможность отключения рекламы за минимальный платеж (0.99 USD). Тут нам помог In-App Purchasing сервис от Unity. Все довольно тривиально, подробнее можно прочесть в официальном туториале. Работает как для iOS, так и для Android.
Также можно упомянуть, что UI для этого платежа немного отличается для iOS и Android. Разница вся в том, что в iOS нужно обязательно дать возможно пользователю руками восстановить его уже совершенный платеж при смене устройства или переустановке игры (Android, насколько нам известно, делает это автоматически).
Кроме того, мы добавили просмотр видео рекламы за монетки, но тут все строго по желанию игрока.
Из рекламы начали сперва подключать AdMob, но довольно быстро перешли на Appodeal. Из плюсов — довольно простая интеграция и неплохая тех. поддержка для бесплатного сервиса.
С рекламой, кстати, возникло несколько проблем, некоторые из которых до сих пор остались: игра подлагивает в начале на слабых и не очень устройствах, видео за вознаграждение отображается не у всех пользователей (лечится обычно сбросом рекламного ID, но кто из пользователей будет этим заниматься).
Подключение лидербордов
Для работы с ачивками и лидербордами в Unity существует интерфейс Social, который по умолчанию имеет реализацию для iOS. Для Android же реализация есть, например, в плагине от Google — play-games-plugin-for-unity. При сборке под Android нужно лишь засунуть инициализацию PlayGamesPlatform в #if UNITY_ANDROID блок, а сам код, связанный с Social, и для Android, и для iOS можно использовать один и тот же.
По умолчанию play-games-plugin-for-unity пытается билдиться и для iOS сборки. Чтобы исключить его и использовать реализацию от Unity, необходимо добавить в Scripting Define Symbols ключ NO_GPGS (находится в Player Setting → Other Setting):
Кстати, после добавления play-games-plugin-for-unity к проекту столкнулись с проблемой 65к при сборке под Android. Спаслись тем, что создали пустой проект и закинули в него только Appodeal и play-games-plugin-for-unity.
Оптимизация
Когда уже почти все было готово, несмотря на тесты на реальных девайсах, мы столкнулись с проблемой производительности. Забегая вперед, скажу, что оптимизация заняла ощутимое количество времени.
Вот сразу несколько ссылок на одни из самых полезных для нас статей: про все, про аллокации в C# часть 1, про аллокации в C# часть 2.
Батчи, батчи, батчи
Первой нашей проблемой было неприлично большое количество Draw Calls — около 50. И тут нам на помощь пришел FrameDebugger, который впервые появился в Unity 5.0. Посмотрев на пошаговое построение кадра, перепаковав текстурные атласы и настроив Order in Layer, мы получили в среднем 15 Draw Calls. Уже неплохо. Чуть позже мы снова вернулись к этом вопросу — объединили некоторые атласы (например, Огр, Динозавр, Курочка и гнездо у нас теперь хранятся в одной текстуре), уменьшили количество Sorting Layer, и в итоге в среднем у нас вышло 7-8 Draw Calls.
Кроме того, FrameDebugger позволяет найти графику, которая отрисовывается, но которой в итоге не видно на результирующей сцене. Например, мы не убрали из анимации бега облачко над головой Динозавра, которое необходимо только в заставке:
FrameDebugger в действии
Кроме FrameDebugger, Xcode также отлично показывает, что происходит с отрисовкой. Он тоже позволяет посмотреть построение кадра, причем оно может отличаться от того, что вам показывал FrameDebugger:
Профилирование с помощью Xcode
Также с помощью Xcode мы узнали о второй проблеме: неоправданно большое количество вершин. Возможно, некоторые спросят: “Спрайт — это ведь 2 треугольника, откуда там быть куче вершин?”. Дело в том, что на современных графических ускорителях дешевле нарисовать меш с разумным количеством вершин, чем обработать кучу прозрачных пикселей. Поэтому Unity строит меш по контуру спрайта, но делает это не всегда оптимально.
Тут нам на помощь пришел не бесплатный, но крайне полезный ассет SpriteSharp. После его применения количество вершин в сцене редко превышает тысячу.
Кстати, возможность эту можно отключить, и тогда спрайты будут строиться классически из двух треугольников. Настройки эти находятся в параметрах текстуры:
Количество Draw Call и количество вершин можно узнать и без применения профайлеров — достаточно заглянуть в Stats:
И еще немного советов:
- Оставьте финальное объединение текстурных атласов на потом, когда у вас действительно устаканится содержимое сцены.
- Внимательно смотрите за тем, как изменяете цвет спрайтов. SpriteRenderer.color не разбивает батч, а вот SpriteRenderer.material.color, как и любое другое изменение материала, разбивает.
- Помните, что динамический батчинг имеет ряд ограничений, например, меши, которые будут объединены, в сумме не могут превышать 900 вершин. Подробнее можно посмотреть на официальном сайте Unity.
Немного о шрифтах
Изначально счетчик очков у нас был сделан с использованием динамического шрифта и опции, что могло вызывать подлагивание при каждом успешном разбитии камня, так как счетчик увеличивался на единицу, а вместе с этим перестраивался меш надписи. Выход — использование Custom set в самих шрифтах и выключение Best Fit в UI.Text.
Оптимизации логики и физики
Про физику в Unity можно почерпнуть из хорошей статьи на Хабре. Про нас же скажем, что в релизной версии мы вообще ее не используем.
Например, сперва у нас были коллайдеры на всех камнях и Огре. Однако нет необходимости считать пересечение Огра со всеми камнями, достаточно только знать расстояние до ближайшего. Причем расстояние только по x — никаких квадратных корней не нужно.
Также у нас были коллайдеры в виде окружности на каждой курочке. Опять же нет смысла проверять пересечение координат тапа со всеми коллайдерами. Так как при одном тапе можно убить до двух курочек, достаточно найти первые два пересечения и прекратить дальнейшие расчеты.
Попадание мы искали, использовав расстояние от центра курочки до координат тапа, причем тут тоже можно обойтись без квадратный корней — достаточно использовать sqrMagnitude и квадрат радиуса. Немного подробнее тут.
Для камней же мы применили “запекание” физики в анимацию. На каждый камень было записано по 3 анимации для успешного разбивания кулаком и по 3, когда Огр врезается в камень. На деле выглядит почти так же, как и с применением симуляции физики. Скрипты, которые мы написали для запекания можно найти на bitbucket — вдруг кому-то пригодятся.
Еще мы перешли от подхода “каждый элемент сцены сам занимается своим обновлением” к схеме “менеджер и набор элементов, за которые он отвечает”.
Например, в случае с курочками можно было наделить каждую из них своим методом Update, в котором она бы искала остальных куриц и применяла логику поведения в стае. Однако скрипт курицы у нас содержит только базовые методы, такие как развернуться или умереть. За поведение всех пернатых в связке отвечает скрипт стаи.
Такой подход позволяет более гибко и централизованно управлять обновлением сцены. Например, элементы, не влияющие на геймплей, можно обновлять лишь несколько раз в секунду, вместо апдейта каждого кадра. Или, если коллекция очень велика, обновлять не все элементы за один Update, а разнести их обновления по кадрам: кадр N — обновление первой половины коллекции, кадр N+1 — обновление второй половины.
И всегда следует помнить, что каждый вызов метода MonoBehaviour, вроде Update или Start, имеет определенный overhead. Подробнее про это можно почитать в блоге Unity.
Пул объектов это “must have” при использовании Unity (да и любого другого движка в принципе). Благо у нас он был изначально.
Ну и никогда не следует забывать о “переполнении” float:
Метаморфозы в дальних мирах. Вверху справа значение координаты по оси X
В нашем случае при достижении значения 100000 по оси X сцена сдвигается на -100000. Другая техника, которую можно было применить — это имитировать движение. Огр и Динозавр стоят на месте, а все окружение движется справа налево.
Для поиска узких мест всегда используйте профайлеры, как самой Unity, так и Xcode. Причем делайте это на конкретном устройстве, а не в редакторе.
В заключение главы про оптимизацию хочется добавить, что, несмотря на все усилия, лаги все равно случаются: активности, связанные с рекламой, платежами, Google Play/Game Center и самой операционной системой.
Ну и, конечно, где-то мы сами недоглядели.
Размещение в App Store
Наверное все знают, что для того, чтобы зарелизить на iOS свое приложение, необходим какой-нибудь mac-девайс? Да, конечно, можно попробовать всяческие уловки типа “хакинтоша” или доставать друзей — счастливых обладателей макбука.
Неизвестно, как поведет себя Apple, если вдруг узнает, что приложение было залито с хакинтоша. А получить банхаммером и потерять все свои приложения в App Store как-то не хочется. От этого варианта отказались сразу.
Альтернатива постоянно доставать друзей, у которых есть мак, сомнительна, так как сборка, отладка, профилирование, а потом заливка приложения, аппрувы, дополнительное редактирование или исправления, выкат апдейтов и так далее — все это требует не один вечер работы.
В итоге мы обзавелись Mac mini средней конфигурации. Тут, к слову, неплохо работает схема: разрабатывать на мощном ПК, а слабенький мак использовать только для активностей, связанных с App Store.
Еще один интересный факт об App Store — описание и скриншоты вы можете залить с любой платформы, а вот видео только из Safari не ниже 8, работающем под управлением OS X не ниже 10.10. Причем на обработку видео после заливки отводится вплоть до 24 часов.
Ну и в целом правила публикации более суровые, нежеле в Google Play, так что, если получили аппрув от Apple, с Android точно разберетесь.
По времени получения аппрува — сейчас уходит около двух дней. Статья о подводных камнях маркетов не очень давно была на Хабре.
Ну и в официальном мануале тоже довольно неплохо описаны требования к размещаемым материалам.
Размещение в Google Play
Размещение в Google Play прошло гораздо легче. Видео и скриншоты для телефонов у нас уже были подготовлены, нам осталось только сделать версии для планшетов размерами 7 и 10 дюймов и залить видео на YouTube.
После этого подготовили релизный apk и проверили все на бета тесте. Далее нужно было отправить его на рассмотрение. И вот тут следует быть внимательным, если не хотите сразу зарелизить свое приложение. Мы вот знали, что это сделать проще простого, и все равно наступили на эти грабли, зарелизив версию для Android немного раньше, чем планировали.
По этому поводу можно сказать: всегда следите за тем, что выбрано в выпадающем списке на странице Production справа сверху:
И помните, что после каждой отложенной публикации этот параметр сбрасывается к стандартной публикации.
По времени получения аппрува — сейчас уходит обычно несколько часов.
Опять же больше деталей можно найти все в той же статье с Хабра и официальном мануале о подготовке материалов.
Заключение. О сроках
Ну, и на десерт — главный вопрос: “Почему так долго?!” (а с момента начала работы до релиза прошло почти полтора года).
На самом деле основной геймплей, который почти без изменений попал в релиз, был создан за первый месяц разработки.
А далее добавление магазина, меню, доработки, переделки, перерисовки, починка после обновления Unity и ожидание баг фиксов от них, оптимизация и еще куча технических сложностей, которые по неопытности делались очень медленно. Например, несколько вечеров мы потратили только на то, чтобы организовать процесс сборки и тестирования беты под iOS.
Были проблемы с рекламой, были проблемы со сборкой под Android, когда мы подключили все плагины.
Ну и конечно же “человеческий фактор”, он же лень. Потому как игрой мы занимались только в свободное время и скорее как хобби, не всегда после тяжелого трудового дня удавалось сделать
что-то полезное.
Однако главной целью для нас было пройти полный цикл разработки: от прототипа до релиза, и мы это сделали.
Автор: NilC