В статье GameDev и канделябр мы описали процесс портирования игры Марьяж под Android. Игра вышла достаточно успешной и практически сразу после релиза мы начали планировать кроссплатформенную версию с дальнейшим прицелом на онлайн. В качестве платформы была выбрана unity3d. Процесс разработки занял около шести месяцев.
Интересно? Заходите под кат!
Задание
Итак, платформа выбрана, исполнители тоже, вроде все просто и понятно, никаких видимых проблем не видно на горизонте.
Разработку условно можно разделить на 2 части — backend (далее — логику) и frontend (GUI с особенностями платформы, далее — клиент). В качестве платформы была выбрана unity3d (именно так будет дальше по тексту, чтобы там не говорили про ребрендинг и отсутствие “3d” сами юнитеки). Логика уже была написана и прекрасно функционировала. Единственной проблемой было то, что языком, на котором была написана логика, была java, а для выбранной платформы подходил только C#, либо JS. Соответственно, главной задачей стало портирование существующего кода в валидный код на C#, причем с сохранением работоспособности один в один.
Клиентская часть так же уже была реализована, но как и логика — была жестко завязана на реализацию GUI в Android. Соответственно, клиента пришлось писать с нуля, по технически грамотному ТЗ в виде фразы “чтобы было так же как в Android-версии” :)
Портирование логики
Задача стояла сделать это как можно быстрее с минимальным переписыванием. Но с целым рядом проблем все-таки пришлось столкнуться.
В C# нету расширяемого типа enum, а в нашем Преферансе этот тип использовался очень широко: масти и ранги карт, сами карты, конвенции, ставки и висты, состояния игры. В некоторых случаях удалось обойтись и стандартным enum-ом в C#. Но в большинстве случаев enum-ы были больше, чем просто перечислимыми типами: к ним были привязаны разнообразные данные и имелись вспомогательные методы. Все это не хотелось терять в портированном коде. В результате был сделан минимально подобный джавовскому класс Enum для C#. Все оригинальные enum-ы стали отдельными классами с сохранением прежних данных и функционала, к ним привязывались обычные enum-ы из C#, которые задавали последовательность (метод Enum.ordinal()) и имя (Enum.name()) перечислимого типа, а также использовались в switch..case.
Тяжелое наследие Delphi дало о себе знать и при портирования на C#. Большая часть логики была написана во внутренних процедурах с общими переменными. В Java это решалось сравнительно просто — внутренними классами. В C# внутренние классы хоть и есть, но аналогичны статическим классам в Java, то есть не могут использовать поля и методы класса-контейнера. Пришлось добавлять дополнительные ссылки.
Не смотря на более богатый выбор массивов в C#, аналогичного Java фунционала там не оказалось. Проблема в том, что массив вида int[][] не может быть создан одним оператором вроде new int[10][20]. Перейти на прямоугольные массивы удалось не везде, в некоторых случаях отдельно использовались подмассивы. Пришлось писать вспомогательные методы, полностью создающие многомерный массив.
Были и другие неприятные неудобства:
- switch..case, требующий break даже в самом последнем условии, включая default;
- массовая замена boolean на bool;
- зачистка final, либо замена на readonly/sealed;
- отсутствие беззнакового сдвига вправо (>>>) ввиду наличия беззнаковых типов;
- оператор goto, который в Java управляет переходами во вложенных циклах, а в C# сохраняет свое древнее предназначение.
Но это все выявлял компилятор. А вот выпиливать знаковый Java тип byte и менять его на аналог sbyte из С# пришлось без посторонней помощи.
Реализация клиента
Весь графический контент был полностью (с небольшими фиксами) позаимствован из Android-версии. В качестве GUI-фреймворка был выбран NGUI — тут вариантов не было в принципе: работа с атласами, оптимизация по отрисовке с минимизацией DrawCalls, вменяемая система сообщений, использующая систему сообщений unity3d. Не зря главный разработчик этого фреймворка сейчас пилит стандартный GUI для unity3d.
Весь контент был рассортирован по логическим группам и собран в атласы исходя из ограничения 2048х2048 точек на атлас:
Атлас иконок городов (еще есть место минимум под 6 городов).
Атлас основных контролов, использующихся на всех “экранах”.
Подобная организация графического контента позволила снизить количество DrawCalls в пике до 15 (например, при открытии скролируемой панели ставок и открытии меню с визуальными настройками).
Одной из проблем стала необходимость “сделать все так же, как на Android”, а именно — подстраивание контролов под разрешение экрана. Обычно все контролы “приклеиваются” к краям экрана и раздвигаются / сдвигаются вместе с ними, в данном случае такой вариант был неприемлем. В результате было написано решение, работающее примерно по следующей схеме:
- выставляем эталонное разрешение (было выбрано 1280х800);
- размещаем контролы как они должны выглядеть правильно;
- вешаем на контролы хелперы для пропорционального масштабирования / перепозиционирования;
- профит.
При запуске на любом разрешении высота экрана всегда считается равной 800 виртуальным точкам, а ширина меняется в зависимости от аспекта. Хелперы при старте считают соотношение между старым аспектом и новым, по результатам выполняют перепозиционирование и изменение масштаба.
Эталонное разрешение (1280х800).
Более “широкий” аспект (1136х640) — видны полосы по краям.
То же разрешение (1136х640), результат отработки хелперов — видно, что контролы размещены пропорционально шире, а фон отмасштабирован.
В результате все делалось на эталонном разрешении + тестировалось на самом узком аспекте (4:3).
Потребовалось так же реализовать нестандартное поведение контролов, что, благодаря унифицированной системе сообщений NGUI, получилось сделать относительно просто. Так были реализованы LongPress-кнопки, “бегущая строка” с клипированием и Grid с анимированным перепозиционированием элементов. Так же пришлось пропатчить пару мест стандартных компонентов для адаптации под проект (UIPopup получил возможность показа всплывающей части в указанном родителе, а не в текущем — решились проблемы с клипированием всплывающего контента UIPanel-компонентами; центрирование скролящейся панели по дочернему элементу научилось игнорировать отключенные элементы и прочие мелкие фиксы).
Вся работа клиента была разбита на “экраны” с подвязкой локальной логики поведения на конкретном “экране”: экран главного меню, экран настроек “учебки”, экран выбора города в “турнире”, экран самой игры и т.д. Это позволило снизить нагрузку на графическую часть в плане потребления памяти в каждый момент времени — при загрузке следующего “экрана” все загруженные объекты уничтожались и движок мог почистить освобожденную память или использовать ее для загрузки новых ресурсов. Между сценами “катаются” всего несколько неубиваемых объектов, обеспечивающих доступ к данным пользователя в плане настроек, последних выбранных элементов и т.п.
К моменту начала работ по клиентской части не было утверждено API взаимодействия с логикой, поэтому было принято решение разделить всю игровую механику на изолированные модули (управление набором карт каждого пользователя, управление “летанием” карт, управление ставками, управление текстовыми сообщениями для игроков и информационной панели, управление настройками игры, локальным состоянием и т.д.) с доступом к ним из любой точки игрового процесса через синглтоны. Параллельно писалось что-то типа тестов прямо внутри клиента, дергающих все доступные методы для проверки корректности работы всего этого хозяйства. Все анимации и прочие штуки работают в одном потоке, через сoroutine-ы, ничего особенного тут нет.
Внешние системы
Сюда можно отнести реализацию IAP-функционала, банерной рекламы, Tapjoy-клиента для получения внутриигровой валюты бесплатно за определенные действия рекламной площадки и GoogleAnalytics.
В качестве IAP-фреймворка изначально была выбрана soom.la: кроссплатформенное API, поддержка для двух основных мобильных платформ, бесшовная интеграция без костылей вокруг патчинга стартап-активити в Android и это все за даром. Достаточно сравнить ужасы интеграции поделий prime31, когда они принудительно перетирали манифесты исключительно под себя любимых + имели разное API на разных платформах, чтобы понять, что данный фреймворк — просто гениален. В дальнейшем (если будет потребность в winphone-версии и soomla не получит штатной поддержки) возможен переход на Unibill.
В качестве банерного провайдера выступал admob, а клиентом к нему был выбран продукт от http://www.neatplug.com. Причина та же — легкая интеграция, унифицированное API, поддержка для двух основных мобильных платформ, стоимость универсальной версии под 2 платформы — $56 (против $50 за каждую платформу у prime31 и косяков, описанных выше для IAP). Было найдено одно не совсем адекватное поведение фреймворка при абсолютном позиционировании банеров на разном DPI, поэтому в результате было решено ограничиться стандартной привязкой к границам экрана.
В качестве клиента к Tapjoy выступил стандартный фреймворк от самой площадки. Был немного допилен код для корректной работы в редакторе, общая интеграция не доставила каких-либо проблем, достаточно было 4-5 запусков билда в эмуляторе для проверки срабатывания событий по логам.
GoogleAnalytics самопальный, адаптированный с версии для Web-сайтов. В результате получилось кроссплатформенное решение через WWW, не требующее native-версий библиотек для разных платформ по $50.
Сопряжение логики и клиента
Через 4 месяца появилась первая версия логики и ее можно было начинать стыковать с клиентом. Сама логика разрабатывалась как независимое приложение, крутящееся в бесконечном цикле, позволяющее ставить в очередь команды и дергающее методы подписчика в случае наличия исходящих команд. В результате логика в unity3d была обернута в код, управляющий ее стартом в отдельном потоке и обеспечивающий транзит команд в обе стороны (не забываем, что типы данных, сигнатуры методов и т.п. полностью несовместимы, т.к. обе системы разрабатывались в изоляции друг от друга и двумя разными исполнителями). Как ни странно, все срослось на удивление просто и команды управления хорошо легли на модули клиента.
Проблемы, связанные с целевой платформой исполнения
С работой логики в отдельном потоке приключилась одна не очень хорошая история. Как известно, на iOS запрещено использования любого не native-кода, поэтому unity3d при экспорте проекта прогоняет managed-сборки через AOT, генерируя на выходе нейтивные библиотеки, которые уже линкуются с системными + добавляются всякие glue-врапперы на obj-c для стыковки всего этого добра. Причем для эмулятора делается отдельный билд, в котором есть JIT и все будет работать как работает в редакторе — это важно, ибо ошибки, возникающие на реальном железе, могут никогда не возникнуть ни в редакторе, ни в эмуляторе.
Т.е. фактически при запуске на реальном железе — никакой отладки проекта, только на ощупь по логам и колстеку.
Итак, возвращаясь к проблеме с потоком. Вроде все работало, а потом внезапно стало падать, роняя приложение полностью. По логам xcode получалось, что краш происходит глубоко в рантайме Mono после вызова Thread.Sleep() или Thread.Current.IsAlive если поток был прерван (но не каждый раз). Т.е. получали краш на вызове стандартных методов при вроде как бы стандартной попытке прерывания потока. Самое смешное, что на JIT-версии билда (например, Android, standalone, webplayer) все отрабатывало как надо. Методом тыка были определены указанные выше методы, приводящие к крашу. В результате было решено отказаться от принудительно прерывания потока, а вместо этого сигнализировать логике, что пора бы и завершиться, а потом ждать закрытия потока. Краши пропали и больше не возвращались.
Второй проблемой стал неверный расчет поведения ИИ при использовании типа double именно после обработки AOT и при запуске на реальном железе. Узнать это пришлось через сбор дикого количества логов, содержащих расчеты, используемые при анализе стратегии игры. Размер лога каждого тестовой игры превышел 40Мб plain-текста. Это все надо было как-то собирать внутри приложения, запущенного на реальном железе а так же извлекать наружу. Сначала была попытка сбора логов через GoogleDocs, но скорость и разрешенный объем удручали, поэтому по-быстрому был написан сервер на node.js, принимающий данные через post-запросы и склеивающий их в локальный текстовый файлик. Со стороны приложения все было реализовано в виде кеширующей очереди записей лога + скидывания при переполнении через стандартный WWW на самопальный http-сервер. Получилось быстро и относительно просто.
Тестирование
Весь процесс тестирования проходил с использованием платформы https://testflightapp.com, как для промежуточных Android-билдов, так и для всех стадий разработки под iOS. Были собраны UIID-номера устройств тестировщиков (автоматически, через GUI клиента после регистрации), обновлен provision-профиль и билды собирались уже с разрешениями на установку по перечисленным удаленным устройствам. После сборки ipa-билда он просто заливался через админку на тестфлайт и раздавался доступ с оповещениями по email для всех тестировщиков. Им было достаточно зайти в клиент и нажать кнопку установки билда, все происходило автоматически. Никакой дополнительной интеграции с сервисами тестфлайта не производилось, ибо это повлекло бы очередную интеграцию с native-библиотеками, а профита бы практически не принесло.
Хочешь сделать хорошо — сделай сам!
Мы не заказывали обзоров для продвижения Android версии игры, а вот для iOS решили заказать один обзор на популярном ресурсе. О его финансовой стороне говорить пока рано, но хочется поделиться впечатлениями об качестве обзора. После оплаты было получено письмо с подтверждением заказа и пожелания к обзору. Сам обзор на утверждение пришел вовремя, но лучше бы его было не открывать. Мало того что тот, кто писал обзор даже не удосужился нажать аж три(!) кнопки на главном экране и даже не пытался понять как функционирует игра (согласен, это не пасьянс косынка), но он вообще не умел писать. Помимо технических ляпов статья пестрила фразами типа «вам нужно набрать как можно больше очков, которые подсчитываются из очков». Копипаст википедии и нашего хелпа под соусом троечника из восьмого класса нас явно не устраивал и мы написали обзор сами. Стоит отметить, что после критики мы получили и вторую версию обзора, но она была так же очень далека от того, что можно показать пользователям.
Итоги
Unity3d — на данный момент лучшая платформа для создания кроссплатформенных игр. У нас не возникло особых проблем с созданием GUI, хотя интегрирование различных плагинов заняло намного больше времени, чем предполагалось изначально. Так же не стоит обновлять плагины или саму Unity3d, если у вас все работает и нет парочки лишних дней для приятного времяпровождения. И не стоит забывать, что гарантированной кроссплатформенности не будет, надо быть готовым к отладке логики на всех платформах.
Для Android мы пока что решили не использовать кросплатформенную версию, так как устройства на базе 2.2 и 2.3 (которые используют 4% и 10% пользователей соответственно) её просто не тянут, вываливаясь по OutOfMemory. Надеемся, что скоро эти устройства уйдут в прошлое, тем самым упростив разработку онлайн версии.
Автор: BrainFitness