Сколько помню разработку игр для мобилок, всегда приходилось изобретать какие-то ухищрения, чтобы все достойно работало. Сталкивались с этим 15 лет назад, когда писали игры еще под черно-белые телефоны, сталкиваемся и сейчас. Уверен, что огромный набор подобных трюков существует в разработке игр для десктопов и, тем более, консолей. Но я занимаюсь именно мобилками, поэтому речь пойдет о них.
J2ME
Самый грязный трюк в разработке под J2ME – поставить внутрь основного цикла игры один общий try-catch.
public void run()
{
while(isGame)
{
try()
{
gameField.Update();
}
catch (Exception ex)
{
}
}
}?
В результате любой exception перехватывается внутри цикла. Кадр обрабатывается не до конца, но игра не падает — как ни в чем не бывало начинает обсчитываться следующий кадр. Никаких вылетов.
Побочный эффект – зависание игры, если проблема совсем серьезная и возникает на каждом кадре.
Каюсь, один раз пришлось воспользоваться этим методом. Как у всех бывает, сроки поджимали, а баг отказывался находиться.
Трюк, к которому приходилось прибегать практически в каждом проекте – это вызов двух апдейтов логики на одну отрисовку.
Обновления объектов в зависимости от времени кадра мы старались не использовать, так как операции с флотами отнимали много процессорного времени. Поэтому в тот момент, когда сложность логики и количество объектов в кадре вырастали так, что телефон не справлялся, мы запускали два апдйта подряд. Чаще всего это происходило еще до финальной балансировки игры, так что ни на что особо не влияло.
Почему в таких случаях просто не менять баланс, увеличив расстояния, преодолеваемые объектами за кадр? В первую очередь, наверное, из-за коллизии. Для перемещений объектов на большие дистанции пришлось бы писать более сложную логику коллизий, что сказалось бы на производительности. Также логика при движении на большие расстояния иногда давала сбои, и ее пришлось бы усложнять. Ну и в целом, отрисовка занимала гораздо больше времени, чем обработка логики даже в двойном объеме, так что это было проще всего.?
Еще одна проблема, с которой приходилось сталкиваться на J2ME, – размер памяти. Причем как оперативной памяти, так и поставки самого билда. Экспериментальным путем было установлено, что достаточно много можно сэкономить, уменьшив количество классов. Как итог, вся игровая логика лежала в одном классе. Все данные были разложены по массивам. В одном из них лежал тип объекта, в другом – тип его логики, в третьем – стэйт логики, в четвертом и пятом – X и Y координаты и т.д. В комплекте шел цикл с большим свитчем по типу объекта, который все это крутил. Никаких динамических выделений и удалений, меньше промахов по кэшу. Работало достаточно шустро. Еще пару классов для меню, спрайтов, тайлового бэкграунда, основного цикла игры. В результате вся игра занимает меньше десяти java-классов. К счастью, ни один мой знакомый «правильный» Java-разработчик, никогда этот код не видел.
BREW
В ранних версиях BREW напрочь отсутствовало API для работы с экраном напрямую. А те функции для отрисовки, которые предоставляла система, были очень медленными. Пришлось найти буфер экрана самим. Для этого мы залили весь экран красным цветом, создали пустой спрайт (или как он там у них назывался) и от его адреса сканировали память в обе стороны. Перемещались на количество байт немного меньшее, чем площадь экрана. Если находили похожее сочетание, заливали экран другими цветами и при помощи пары вычислений находили формат пикселей, начало буфера экрана и есть ли отступ после каждой строки. Все занимало меньше секунды, игрок не успевал ничего заметить. Ясное дело, что обновление всего этого процесса на экран мы не включали.
iOS (WoT Blitz)
Самое жесткое, с чем пришлось столкнуться в iOS – это необходимость расчистки памяти во время запуска игры. В силу некоторых обстоятельств мы не стали использовать для всех аллокаций свой пул памяти с кастомными аллокаторами. Но наш расход памяти временами подбирался к отметке в 300 мб, что не очень хорошо сказывалось на стабильности приложения на слабых девайсах. Настоящие проблемы начались с выходом iOS 7 или 8. Ось стала иногда «ошибаться» и закрывать Blitz в моменты пиковой нагрузки на память, хотя было видно, что остаются висеть другие, менее приоритетные приложения (скайп или почтовый клиент). После дополнительного исследования мы выяснили, что iOS очень не любит выделения большого количества памяти за один тик. Но если выделять память постепенно, то можно перевалить далеко за наши лимиты без опасения быть закрытым.
Перераспределять выделение памяти на проекте, в который уже играют сотни тысяч игроков, долго и очень опасно. Поэтому мы приняли немного иное решение. На старте игры мы постепенно, кусками по 10 мб, выделяем память до объема, необходимого игре при пике нагрузки. А потом сразу всю ее и очищаем. Занимает это все меньше секунды. А в логе видно, как ось закрывает другие приложения. Таким вот хитрым способом мы исправили падения игры при пиковой нагрузке на память.
Одним из серьезных недостатков мобильных GPU является медленная отрисовка полупрозрачной геометрии. А в игре позарез нужны кусты и деревья. И как можно больше. Дело осложнялось и тем, что у нас есть снайперский режим, в котором игрок наблюдает все происходящее прямо из прицела танка, в хорошем приближении. Временами филлрейт в кустах и деревьях переваливал за 10 экранов полупрозрачной геометрии.
Для кустов решение было достаточно простым, и придумали его даже не программисты, а художники. Мы ввели ближний лод, который включался практически вплотную к кусту и состоял из одного билборда, плоскости всегда повернутой к игроку. Это дало возможность относительно безболезненно засесть в кустах и даже выцеливать вражеские танки.
С деревьями дело оказалось немного сложнее, так как они могут быть повалены на землю в любом направлении. Для них мы создали специальный шейдер, который включался, когда игрок находился очень близко к дереву. Этот шейдер-садовник обрезал все лишние ветки дерева, оставляя только три ближайшие к камере. Из-за того, что ближние ветки заслоняют практически весь обзор, игрок не замечает отсутствие остальных. А вот GPU очень даже замечает.
Я бы не сказал, что данные решения полностью избавили нас от проблем, но они однозначно позволили добавлять на карту хоть какую-то растительность в приемлемом количестве.
С той же проблемой в снайперском режиме мы столкнулись при создании эффектов. Снайперский режим плюс какие-то партикловые эффекты вблизи камеры – и счетчик FPS гарантированно замрет на отметке 10 (меньше просто не позволит движок). Решение было сходное с кустами, хоть технически далось намного сложнее. Мы ввели уровни детализации для частиц. И на самом ближнем лоде избавились от всего лишнего (например, выстрел выглядит как просто вспышка). В результате при максимальном приближении остаются только самые необходимые эффекты. Как не парадоксально, но наш дальний лод эффектов более детализирован, чем ближний.
Интересная история вышла с сервисом регистрации пользователей. Он был написан под старшего брата и без зазрений совести отправлял куски JavaScript в надежде, что клиент это все запустит, а уже этот JavaScript сформирует следующий http-запрос. Оказалось, мобильный web view для этих целей не очень подходит. В нашем случае его нужно делать скрытым, а на внутренних тестовых сервисах он спрашивал у пользователя подтверждение на подключение. Вдобавок это все необходимо было крутить в отдельном потоке. Времени было мало, а заказать переделку сервиса регистрации в требуемые сроки – практически нереально. И нам пришлось на своей стороне парсить приходящие от сервера скрипты, формировать на их основании следующие запросы, отправлять в сервис. Позже было пару фиксов, которые позволили немного стандартизировать этот процесс. Звучит странно, но до сих пор работает.
Android (WoT Blitz)
Работая над Android-версией нам пришлось искать много нетривиальных решений, но вряд ли они достойны статьи. Есть одна действительно забавная штука, которую нам пришлось применить для проведения тестов производительности. Дело в том, что в процессе боя, или нескольких боев, многие Android-устройства успевают нагреться, частота процессора автоматически снижается. Что ведет к понижению FPS. А результаты ежедневных тестов производительности нам очень хотелось видеть более-менее стабильными. После исследования ситуации мы пришли к выводу, что после каждого теста необходимо перезагружать устройство. Телефон успевает остыть и одуматься, а мы получаем достаточно предсказуемые результаты. Это никоим образом не отменяет плейтесты, на которых QA проверяет играбельность и качество сборок, но дает возможность заметить какие-то изменения в производительности устройства и принять меры.
А еще у нас есть свой скайп-бот, который собирает билды, обновляет сервера, следит за состоянием транка и юнит тестов, и назначает программистов на ревью кода. Но это уже совсем другая история.
Если у вас есть какие-то вопросы, либо вы готовы поделиться своими мобильными лайфхаками – пишите комментарии, обсудим.
Комментарии (32)
XaveScor
20.06.2016 20:07А под виндой как WOT Blitz делали? Хаки были?
Hottych
20.06.2016 21:08Из того что приходит на ум:
- На телефонах дали возможность уменьшить разрешение рендера в два раза. Так-как некоторые аппараты откровенно не тянули необходимый филлрейт. Но это было еще до официального релиза платформы, возможно что-то изменилось.
- На десктопах, кроме развлечений с мшкой и клавиатурой, вроде все спокойно.
YoungSkipper
20.06.2016 20:17+3Про j2me можно сильно, сильно больше рассказать. О запихивании всего приложения в 3-5 классов — чтобы отыграть нужные сотни байт. Про первые IDEN моторолы с поддержкой j2me где товарищи разрабочики прешили, что если в стандарте написанно что поддержка прозрачности в png опциональная — то ее не нужно делать. И портирование игр под эти телефон — из серии режем картинку на куски по 4x4 пикселей, если там 2 непрозрачных пикселя и больше — то рисуем целиком. Этакий пиксель арт в пиксель арте.
Про BREW — ну это не грязный хак, но на части телефонов флешка и память была столь медленная, а процессоры по тем временам вполне быстрые — что держать в и на диске и в памяти все данные в zip-е и распаковывать при обращении на лету было быстрее, чем напрямую.
Или про первый мобильный телефон с 3д ускорителем и с j2me — уж не помню какая Моторола — которой мы радовались, пока не выяснилось, что заодно просто fillrate за счет большого экрана отнимал 30% от всего рендеринга, сводя на ноль пользу 3д ускорителя по сравнению с другими телефонами.
seregamorph
20.06.2016 23:04В Motorola RAZR другой глюк еще был. Долго не могли понять, почему приложение не может нормально распарсить HTTP-ответ сервера, хотя на эмуляторе и других устройствах было ок. В итоге решили вывести ответ сервера на экран телефона, а там «HTTP/1.0 200 OK\r\n» и далее по списку, т.е. в потоке все тело ответа вместе с шапкой.
YoungSkipper
20.06.2016 23:22Ну это легко детектиться, а вот помнится когда iOS решило кэшировать результаты POST запросов — вот это была радость понять, что не так.
dkv
21.06.2016 11:51В мотороллах вообще HTTP-протокол странно обрабатывался. К примеру, было что-то мутное с длинной буфера. Вот выжимка из исходников моего древнего SMS Send:
// Motorola implementation of connection class is something very
// strage having no relation to standard J2ME connections behavoir
boolean bMotor = («com.mot.cldc.io.j2me.http.Protocol».equals(m_httpConn.getClass().getName()));
if (bMotor)
body = new byte[len+4];
else
body = new byte[len];
kAIST
20.06.2016 23:39Или про первый мобильный телефон с 3д ускорителем и с j2me — уж не помню какая Моторола
До моторолы точно была nokia 3410 — она была с монохромным экраном, но с 3D графикойYoungSkipper
20.06.2016 23:51Ну все таки подобная графика, и наличие, на телефоне 3д ускорителя (по моему это был где-то 2004 год) и поддержка оным jst-184 сильно различные вещи. Если говорить о подобном 3д — я так 3д танчики (с видом из башни) писал еще для https://ru.wikipedia.org/wiki/Cybiko — что было раньше чем эта Нокия появилась на свет
kAIST
20.06.2016 23:55Не знаю, насчет jsr-184, но API там было, и использовалось не только в этой java игре, а еще и в других java играх, а так же в самой системе, в роли 3D «хранителей экрана» и пр.
YoungSkipper
20.06.2016 23:59+1API то было, но по сути это был software rendering — что на BREW телефонах того же времени делалось просто в коде самой игры. А тут просто они реализовали это в прошивке и прокинули из j2me вызов функций. Т.е. ни о каком хардварном ускорении речи не шло.
kAIST
21.06.2016 00:06Тут вы правы конечно же. Задумался, а ведь в смартфонах nokia (symbian 6-8.1), тоже не было никакого аппаратного ускорения, включая легендарный n gage
Hottych
20.06.2016 21:01+2Про 3-5 классов расказал же :)
С Моторолкой этой не работали. Но с графикой была другая штука: пару игр выпустили на Nokia S60 с отрисовкой через нокиевский API, и все понять не могли почему же так педалит. Потом доперло рисовать png через родные методы j2me.
В BREW мы LZO вроде все упаковывали и в один файл. Но «на лету» вроде не стримили.
3D на j2me — миновала меня чаша сия.
AllexIn
20.06.2016 22:31вызов двух апдейтов логики на одну отрисовку.
это решение выглядит… странно.
почему не считать сколько прошло времени и не вызывать соответствующее количество апдейтов?
С двумя фиксированными на выходе должна быть полная непредсказуемая ахинея.
Хотя нормальное решение выглядит весьма просто:
const int UPDATE_PERIOD = 100;//ms int TotalTime = 0; while (!finished){ //Render TotalTime += deltaTime; while (TotalTime>=UPDATE_PERIOD){ //Update TotalTime-=UPDATE_PERIOD; } }
Hottych
21.06.2016 01:02Ну я и не писал, что это идеальное решение на все случаи жизни. Все, само-собой, не настолько однозначно, как я описал. Просто, если расписывать все варианты, то можно смело делать из этого отдельную статью.
Когда все было совсем туго со временем апдейта, приходилось писать что-то, что позволяло вычислять сколько же апдейтов надо запустить перед отрисовкой (если быть более точным, то мы пропускали вызов отрисовки). Бывало, что логика разделялась на две части, и одна обновлялась несколько раз за кадр, а другая только один. Но чаще отрисовка, работающая в другом потоке, занимала больше времени чем даже три апдейта. Поэтому можно было не стесняться.
Кроме всего прочего, игры часто адаптировались под каждый конкретный телефон, и там этот цикл настраивался индивидуально.
vintage
21.06.2016 08:50-2Распарсить JS проще и быстрее, чем добавить упрощённый метод в бэкенде? На чём у вас бэкенд был написан? На брейнфаке? :-)
Hottych
21.06.2016 11:57Главный вопрос не на чем, а кем. Писалось это все командой которая поддерживала в первую очередь WoT, а во вторую WoWP и не вышедший еще тогда WoWS. А Blitz был больше эксперементальным проектом. И ясное дело, что ресурсы на это ни кто в срочном порядке выделять не стал.
barker
21.06.2016 10:26+2Под J2ME ещё могу вспомнить обильное использование глобальных и статических объектов, и явное зануление указателей у уже ненужных объектов (чего-то там с GC не всего было предсказуемо). Уменьшение количества классов — это да, ну а какие-нибудь анонимные классы (например в качестве слушателей) никому в голову не приходило даже использовать.
Hottych
21.06.2016 12:05Статические объекты — да, было такое. А GC помню, на каком-то телефоне нужно было принудительно дергать.
hssergey
21.06.2016 11:45Про J2ME можно вспомнить много веселого. Там каждая модель телефона глючила по-своему. Нынче с андроидами это тоже есть, но не так заметно.
Был плагин к ProGuard, который позволял мержить классы при обфускации. Выручало при сборке для всяких нокий S40 с ограничением размера джарника в 1 мегабайт. Правда работало это только на определенной версии прогварда и далеко не всегда стабильно (иногда падало, иногда выдавало нерабочий код), приходилось шаманить с настройками и тщательно тестировать после сборки.
Про зануление указателей тут уже писали — «помощь» GC, из-за чего все работало быстрее.
Еще помню были проблемы со статической инициализацией переменных — после обфускации и мержинга она могла пропасть. Грубо говоря, код
public class MyClass { private static int variable = 5; ... }
такого лучше было избегать, потому что внезапно оказывалось, что там ноль. И если нужна была именно начальная инциализация статических переменных, то лучше было ее выносить в отдельный метод, который дергать при запуске приложения.
Еще были «особенности» управления подсветкой на разных моделях, на тех же нокиях, если нужно было, чтобы подсветка не гасла во время работы приложения, надо было в цикле постоянно вызывать ее включение. А так же работы с звуком, где-то надо было заранее префетчить плееры и держать наготове в таком состоянии.
Еще помню были проблемы с отрисовкой, в частности, нативная заливка области была жутко тормозной, и реализация ее своими же средствами выходила значительно быстрее.Hottych
21.06.2016 12:29Мы классы мержили обычно в процессе написания. Ограничение на jar для первых Nokia S40 было 64k :)
Ну а в плане звука каждый телефон был очень яркой индивидуальностью. Даже тот же префетч нужно было вызывать в определенном порядке с другими методами. Где-то для каждого звука необходимо было создать отдельный объект плеера. Иногда на подбор правильного решения, чтобы звук заиграл на новом телефоне, могла уйти неделя.
Plague
28.06.2016 00:37Ради экономии делали поддержку dxt сжатия, что было эффективнее чем png, который на малых объемах файлов не так эффективен чем dxt+jar дожатие, да и качество экранов позволяло. Единственной проблемой оказалось, что приложение распаковывается при запуске и занимало много памяти в таком виде до загрузки ресурсов.
Кроме того, использовали rar для подкручивания размеров словаря zip-сжатия, с последующим преобразованием в jar. Увеличение словаря, пока запускалось на телефонах, давало приличное дожатие.
Creator3D
21.06.2016 16:13Спасибо, понастальгировал :)
p.s. ещё до всяких WoT, были у нас танчики на J2ME с сетевым режимом игры по Bluetooth до 8ми игроков… ну и работа с Синезубом была не менее весёлой, чем со звуком и графикой — производители не спешили поддерживать все варианты работы протокола, а то что поддерживали — работало как всё на J2ME + «нестабильность радио-волн» — проблемы начинались уже с поиска устройств по-соседству…
Кто знает, будь технология стабильной, может быть мультиплеер и раньше бы стрельнул — в исполнении Bluetooth, закрепился бы с появлением Wi-Fi Direct — всё-равно школьники рядышком тусовались ;) С текущими скоростями мобильных сетей, не актуально теперь, конечно — всё-бы всё-равно перетекло в глобальную сеть.
inakrinat
22.06.2016 13:00Под J2ME было много приколов с использованием одной и той же графики под разные нужды, чтобы экономить место. А еще сборки под Самсунг частенько перерабатывались и урезались в функционале, по сравнению с СониЭриксонами и Нокиями, потому что у них был какой-то свой взгляд на степень поддержки MIDP2.0. С тех пор прошло 10 лет, а доверие к телефонам от Самсунга так и не восстановилось — психологическая травма, можно сказать
VovanZ
> А еще у нас есть свой скайп-бот
Скайп? Неожиданное решение :)
Неужели, у вас вся внутренняя коммуникация в скайпе? Почему бы не взять какой-нибудь слак или хипчат, которые:
1. куда удобнее для этого кейса
2. предоставляют хорошее API для ботов
Hottych
Смотрели на них. Но так уж исторически сложилось, что изначально был скайп. Так-как команда была всего 8 человек. А сейчас преходить особого смысле нет.
Подключить к боту скайп заняло около двух дней. Подключить все остальное — больше месяца, точно. Получается, что и с этой стороны переход бы нам ничего не дал.
Fedcomp
Полагаю у вас нет ни одного linux программиста раз все завязано на скайп?
Hottych
Пока такой необходимости действительно не возникало :)
Да и было бы странно разрабатывать игру для iOS, Andriod, Win10 и MacOS силами linux программистов.
На скайп, кстати, завязана только рутинная коммуникация. Вся важная коммуникация идет, как и положено, через почту и JIRA.