Грязные трюки мобильной разработки от J2ME до Android

Сколько помню разработку игр для мобилок, всегда приходилось изобретать какие-то ухищрения, чтобы все достойно работало. Сталкивались с этим 15 лет назад, когда писали игры еще под черно-белые телефоны, сталкиваемся и сейчас. Уверен, что огромный набор подобных трюков существует в разработке игр для десктопов и, тем более, консолей. Но я занимаюсь именно мобилками, поэтому речь пойдет о них.


bc35e1faf5514dc9b707c32806217dd3.jpg

Самый грязный трюк в разработке под J2ME — поставить внутрь основного цикла игры один общий try-catch.

public void run()
{
    while(isGame)
    {
        try()
        {
            gameField.Update();
        }
        catch (Exception ex)
        {
        }
    }
}


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


bd6c1068c8ad4cce8432a2711425845d.png

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



c71fba5d657e4ad2bc4818a6218a2508.png

Еще одна проблема, с которой приходилось сталкиваться на J2ME, — размер памяти. Причем как оперативной памяти, так и поставки самого билда. Экспериментальным путем было установлено, что достаточно много можно сэкономить, уменьшив количество классов. Как итог, вся игровая логика лежала в одном классе. Все данные были разложены по массивам. В одном из них лежал тип объекта, в другом — тип его логики, в третьем — стэйт логики, в четвертом и пятом — X и Y координаты и т.д. В комплекте шел цикл с большим свитчем по типу объекта, который все это крутил. Никаких динамических выделений и удалений, меньше промахов по кэшу. Работало достаточно шустро. Еще пару классов для меню, спрайтов, тайлового бэкграунда, основного цикла игры. В результате вся игра занимает меньше десяти java-классов. К счастью, ни один мой знакомый «правильный» Java-разработчик, никогда этот код не видел.


555fbdd0bfec4bbfa05a531ffd9945b6.png

В ранних версиях BREW напрочь отсутствовало API для работы с экраном напрямую. А те функции для отрисовки, которые предоставляла система, были очень медленными. Пришлось найти буфер экрана самим. Для этого мы залили весь экран красным цветом, создали пустой спрайт (или как он там у них назывался) и от его адреса сканировали память в обе стороны. Перемещались на количество байт немного меньшее, чем площадь экрана. Если находили похожее сочетание, заливали экран другими цветами и при помощи пары вычислений находили формат пикселей, начало буфера экрана и есть ли отступ после каждой строки. Все занимало меньше секунды, игрок не успевал ничего заметить. Ясное дело, что обновление всего этого процесса на экран мы не включали.


1054d324181d497285d467f5cb0e44f6.jpg

Самое жесткое, с чем пришлось столкнуться в iOS — это необходимость расчистки памяти во время запуска игры. В силу некоторых обстоятельств мы не стали использовать для всех аллокаций свой пул памяти с кастомными аллокаторами. Но наш расход памяти временами подбирался к отметке в 300 мб, что не очень хорошо сказывалось на стабильности приложения на слабых девайсах. Настоящие проблемы начались с выходом iOS 7 или 8. Ось стала иногда «ошибаться» и закрывать Blitz в моменты пиковой нагрузки на память, хотя было видно, что остаются висеть другие, менее приоритетные приложения (скайп или почтовый клиент). После дополнительного исследования мы выяснили, что iOS очень не любит выделения большого количества памяти за один тик. Но если выделять память постепенно, то можно перевалить далеко за наши лимиты без опасения быть закрытым.
Перераспределять выделение памяти на проекте, в который уже играют сотни тысяч игроков, долго и очень опасно. Поэтому мы приняли немного иное решение. На старте игры мы постепенно, кусками по 10 мб, выделяем память до объема, необходимого игре при пике нагрузки. А потом сразу всю ее и очищаем. Занимает это все меньше секунды. А в логе видно, как ось закрывает другие приложения. Таким вот хитрым способом мы исправили падения игры при пиковой нагрузке на память.


da9abdbbf6d64b009099b691f27f8ea4.jpg

Одним из серьезных недостатков мобильных GPU является медленная отрисовка полупрозрачной геометрии. А в игре позарез нужны кусты и деревья. И как можно больше. Дело осложнялось и тем, что у нас есть снайперский режим, в котором игрок наблюдает все происходящее прямо из прицела танка, в хорошем приближении. Временами филлрейт в кустах и деревьях переваливал за 10 экранов полупрозрачной геометрии.
Для кустов решение было достаточно простым, и придумали его даже не программисты, а художники. Мы ввели ближний лод, который включался практически вплотную к кусту и состоял из одного билборда, плоскости всегда повернутой к игроку. Это дало возможность относительно безболезненно засесть в кустах и даже выцеливать вражеские танки.
С деревьями дело оказалось немного сложнее, так как они могут быть повалены на землю в любом направлении. Для них мы создали специальный шейдер, который включался, когда игрок находился очень близко к дереву. Этот шейдер-садовник обрезал все лишние ветки дерева, оставляя только три ближайшие к камере. Из-за того, что ближние ветки заслоняют практически весь обзор, игрок не замечает отсутствие остальных. А вот GPU очень даже замечает.

Я бы не сказал, что данные решения полностью избавили нас от проблем, но они однозначно позволили добавлять на карту хоть какую-то растительность в приемлемом количестве.

С той же проблемой в снайперском режиме мы столкнулись при создании эффектов. Снайперский режим плюс какие-то партикловые эффекты вблизи камеры — и счетчик FPS гарантированно замрет на отметке 10 (меньше просто не позволит движок). Решение было сходное с кустами, хоть технически далось намного сложнее. Мы ввели уровни детализации для частиц. И на самом ближнем лоде избавились от всего лишнего (например, выстрел выглядит как просто вспышка). В результате при максимальном приближении остаются только самые необходимые эффекты. Как не парадоксально, но наш дальний лод эффектов более детализирован, чем ближний.


c52fa11966df412886a10c9d009d4563.jpg

Интересная история вышла с сервисом регистрации пользователей. Он был написан под старшего брата и без зазрений совести отправлял куски JavaScript в надежде, что клиент это все запустит, а уже этот JavaScript сформирует следующий http-запрос. Оказалось, мобильный web view для этих целей не очень подходит. В нашем случае его нужно делать скрытым, а на внутренних тестовых сервисах он спрашивал у пользователя подтверждение на подключение. Вдобавок это все необходимо было крутить в отдельном потоке. Времени было мало, а заказать переделку сервиса регистрации в требуемые сроки — практически нереально. И нам пришлось на своей стороне парсить приходящие от сервера скрипты, формировать на их основании следующие запросы, отправлять в сервис. Позже было пару фиксов, которые позволили немного стандартизировать этот процесс. Звучит странно, но до сих пор работает.


ccd602971c914448ab3293e77383f3b7.jpg

Работая над Android-версией нам пришлось искать много нетривиальных решений, но вряд ли они достойны статьи. Есть одна действительно забавная штука, которую нам пришлось применить для проведения тестов производительности. Дело в том, что в процессе боя, или нескольких боев, многие Android-устройства успевают нагреться, частота процессора автоматически снижается. Что ведет к понижению FPS. А результаты ежедневных тестов производительности нам очень хотелось видеть более-менее стабильными. После исследования ситуации мы пришли к выводу, что после каждого теста необходимо перезагружать устройство. Телефон успевает остыть и одуматься, а мы получаем достаточно предсказуемые результаты. Это никоим образом не отменяет плейтесты, на которых QA проверяет играбельность и качество сборок, но дает возможность заметить какие-то изменения в производительности устройства и принять меры.


А еще

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

Если у вас есть какие-то вопросы, либо вы готовы поделиться своими мобильными лайфхаками — пишите комментарии, обсудим.

© Habrahabr.ru