Так случилось, что я стал продюсером игры под iOS, разработанной нами на Unreal Development Kit от Epic Games. Игра представляет собой arcade combat racing и является интерпретацией на тему легендарной Rock’n’Roll Racing. В этом посте я расскажу о технических трудностях, с которыми мы столкнулись в процессе разработки, и о методах их преодоления.
Пост не рекламный, поэтому никаких ссылок приводить не буду. Если кому-нибудь будет интересно, на интересующие вопросы я с удовольствием отвечу в личке.
Для начала стоит сказать, что делать гонки, даже аркадные, на UDK — далеко не лучшая идея. Это прекрасный движок, очень удобный и мощный, он отлично подходит для разработки FPS/TPS, сайдскроллеров, адвенчур, неплохо — для RTS и файтингов, но никак не для гоночных игр!
Первой трудностью, с которой мы столкнулись, была чрезмерная для наших целей комплексность физической подсистемы транспортных средств. Наша игра — аркадная, поведение машинок в ней по задумке должно быть максимально приближено к таковому в Rock’n’Roll Racing. Около месяца мы потратили на изучение влияния значений всяческих параметров на поведение машинок. В итоге, мы решили проблему выставлением многих параметров, отвечающих “реалистичное” поведение (параметры подвески, обработки столкновений, сцепления с дорогой и прочие подобные) в 0, а дампинги (подавляющие силы, действующие на машинку в разных ситуациях), наоборот, завысили до предела.
Приведенные ниже параметры, наряду с остальными отвечающими за поведение машинок, находятся в классах, наследуемых от UTVehicle:
LockSuspensionStiffness
Default value: 62.5
Our value: 9000
Отвечает за “жесткость” подвески. Влияет на устойчивость машинки. Чем больше, тем жестче подвеска и тем устойчивее машинка.
LockSuspensionTravel
Default value: 37.0
Our value: 0
Отвечает за амплитуду подвески. Также влияет на устойчивость машинки.
AirSpeed
Default value: 1100
Our value: 4500
Максимальная скорость машинки в воздухе (например, после прыжка с трамплина). Если значение будет меньше, чем GroundSpeed, то машинка, оторвавшись всеми колесами от поверхности, моментально сбросит скорость до заданного в этом параметре значения.
COMOffset
Координаты центра масс. Сильно влияет на поведение машинки и требует внимательной и тщательной настройки.
COMOffset
Максимальная скорость на земле.
MaxSpeed
Предельная скорость машинки в общем случае. Если GroundSpeed или AirSpeed больше значения этого параметра, скорость машинки все равно будет лимитирована именно им.
В итоге, нам удалось добиться того, что машинка стала вести себя предсказуемо на поворотах, подъемах, спусках, в воздухе, при столкновениях и приземлении. Были практически убраны заносы, колебания подвески, упрощена обработка столкновений.
Группа параметров UprightConstraint
Параметры этой группы овтечают за применение к машинке сил для того, чтобы она оставалась правильно ориентированной, то есть за противодействие переворотам.
Второй проблемой стал огромный вес билда. После оптимизации ассетов, которые и так создавались с прицелом на экономию, игра весила 1.8 Гб. Контента в игре довольно много — 55 уникальных трасс в пяти сеттингах, 8 машин и около десяти видов оружия. Но все равно, такой размер билда не был для нас позволительным.
Первым шагом на пути к оптимизации размера стало сжатие лайтмап. Трассы в нашей игре, как и в её идейном вдохновителе, расположены на платформах высоко над землей, поэтому на лайтмапах бэкграундов можно было сэкономить без заметной потери качества картинки. Сжав лайтмапы с 1024 до 64, мы уменьшили вес билда на 250 Мб.
Последовавие за этим технические брейнстормы принесли нам рефакторинг системы расположения трасс по “уровням”. До этого каждая трасса в целях ускорения загрузки представляла собой отдельный файл (уровень), в котором хранилась вся геометрия, скрипты и источники света.
Одна из основных фишек эпиковской технологии состоит в реиспользовании повторяющихся ассетов в пределах одного уровня. В скомпилированном билде 1000 одинаковых объектов, расположенных на одном уровне, будут весить гораздо меньше, чем 1000 тех же объектов, расположенных в нескольких уровнях.
Поэтому мы переработали систему так: вся геометрия и мастер-скрипт (основной кисмет, обеспечавающий игровой цикл) одного сеттинга стала одним большим уровнем-файлом, загружающимся при старте гонки этого сеттинга;
специфические для отдельно взятой гонки скрипты стали отдельными уровнями, подгружающимися к основному уровню сеттинга при старте конкретной гонки.
Отсутствие необходимости для движка хранить большие объемы повторяющейся геометрии в множестве уровней сэкономило нам 900 МБ.
Есть у движка понятие startup packages — пэккейджи, загружающиеся в память при старте приложения и хранящиеся там до прекращения его работы. Так вот, пока мы не положили в эти стартап паки всё, что касается машинок, оружия, и UI фронтенда (14 экранов), движок собирал и хранил все эти элементы для каждого уровня. Когда он перестал делать эту гадость, вес билда снизился еще на 180 Мб.
Выбор контента, который будет храниться в памяти игры постоянно, обоснован архитектурой взаимодействия игрока с игрой — в гараже игрок может просмотреть все имеющиеся в игре машинки и оружие, а также свободно перемещаться между экранами. Подгрузка машинок, оружия и экранов “на лету” была бы, ко всему прочему, заметна. Все-таки на i-девайсы делаем, а не на современные PC. Таким образом мы сэкономили не только размер билда, но и обеспечили шуструю работу меню.
До выхода ноябрьского UDK в 2012 г. настройки startup packages хранились в файле BaseEngine.ini, а в ноябрьской версии перекочевали в BaseSystemSettings.ini, в раздел с соответствующим названием.
Также отличным решением является перенос 14 фронтенд-экранов, по одному flash-файлу на каждый, в один комплексный файл с отдельными слоями под экраны. Это тоже экономит вес.
Прочие методы оптимизации размера ipa-файла касаются принципов работы с контентом, их описывать здесь я не стану.
В итоге, после всех этих манипуляций, мы получили ипаху размером в примерно 600 Мб, а собрав её в distribution-варианте, наслаждались красивым значением в 240 Мб.
Следующая проблема — скорость загрузки приложения. Долгое время она составляла около четырех с половиной минут, что не то что неприемлемо, но вообще адски ужасно.
Не буду говорить, сколько нашей молодой команде потребовалось времени и усилий, чтобы выяснить в чем дело и решить проблему, но дело оказалось в шейдерах.
При каждом запуске игры происходило компилирование шейдеров. При этом кэш шейдеров на эпловских устройствах не сохраняется. Поэтому единичным пересчетом при первом запуске дело не ограничилось, шейдера компилировались при каждом запуске приложения.
Результатом нашей битвы с этим “нюансом” стало нахождение булевой переменной, отвечающей за компилирование шейдеров по требованию. Переменная по дефолту была выставлена в false, что, в целом, логично — Unreal и его младший брат UDK предназначены в первую очередь для больших игр.
Поставив переменную в true, мы сразу же получили время загрузки 24 секунды и тормоза при запуске первой гонки каждого сеттинга. Но это уже не стало проблемой — упростили шейдера, первели их на instance-based идеологию, и проблема сгладилась. Полностью не ушла, все еще чувствуется некоторое проседание FPS в начале гонок, но это уже не критично.
Нужную переменную можно найти все в том же файле BaseSystemSettings.ini, а называется она MobileWarmUpPreprocessedShaders.
Поговорим теперь о производительности. Здесь могу сказать немногое, и все рекомендации будут типичными, но могут оказаться кому-нибудь полезными. Игра у нас никогда не работала слишком медленно, но и не всегда работала так плавно, как того требуют нынешние стандарты рынка.
Оптимизация производительности со стороны контента свелась к отключению динамических теней у статической геометрии и переносу skeletal mesh’ей энвайронмента в отдельные уровни трасс (чтоб не грузить динамические акторы со всех трасс при запуске одной трассы).
С точки зрения кода — к использованию грамотных алгоритмов сортировки и тщательному вычищению наследия UT’шных ботов (функции “слуха”, пасфайндинга и прочие).
Огромной препоной на пути к завершению разработки стал серьезнейший баг в движке. Причем баг, известный эпикам уже довольно давно, и до сих пор не пофикшеный.
Инструментом, снискавшим мою горячую любовь, как и любовь тысяч других девелоперов, в UDK является Kismet — визуальный редактор скриптов. Эта штука была очень сильно вовлечена в разработку с первых дней, так как позволяет ощутимо разгрузить программистов, отдав часть их задач геймдизайнерам. В итоге кисметовских скриптов у нас оказалось много.
Баг, который я упоминал выше, заключается в том, что при копипасте скриптов всегда отваливаются случайные связи между объектами.
То есть, даже если просто нажать в редакторе скриптов ctrl+c или ctrl+v, то, к примеру, такая структура может лишиться нескольких линий:
Причем связи, визуализируемые этими линиями, пропадут сразу, а вот сами линии — только при обновлении экрана с этим скриптом. Более того, линки рвутся не только на текущем скриптовом уровне, но и на некоторых других. Иногда даже на всех
.
UI Kismet’а не обладает кнопками или какими-то иными средствами копирования выделенного фрагмента скрипта, кроме клавиатурного сочетания. А вот вставить фрагмент из буфера обмена можно при помощи пункта в контекстном меню.
Это и натолкнуло нас на шаманское, но все же решение: фрагмент скрипта, который необходимо скопировать и затем вставить, копируется посредством ctrl+c, UDK закрывается без сохранения изменений, запускается снова, после чего фрагмент из буфера вставляется при помощи контекстного меню.
В нашем проекте много схожих кисметовских последовательностей (секвенций), которые нужно было копипастить на множество карт и затем модифицировать (например, считалки наград за прохождение трасс). В целях удобства балансирования была создана большая сиквенса с вынесенными наружу переменными, которую нужно было размножить на каждую трассу. В условиях невозможности нормально копипастить у нас ушла как минимум неделя на то, чтобы только вставить этот скрипт в каждую из 98 гонок игры. Когда решение было найдено, дело пошло быстрее :)
Теперь об интерфейсе. Для его реализации использовали Scaleform. Отрисовали, собрали флашки, интегрировали в игру, запустили, протестили, пофиксили.
Залили на девайс — вместо элементов UI черные прямоугольники. Полтора дня ресерча принесло нам знание о том, что исходные картинки обязательно должны иметь разрешение в степени двойки и быть квадратными. Поправили, и еще одной проблемой стало меньше.
Однако, элементы интерфейса, хоть уже и не черны как ночь, но страдают от странного артефакта, которого нет при просмотре на PC: по границам изображений, там, где должен быть прозрачный участок, красуются непонятные полосы. Пристально смотрели на исходные картинки и в глаза художнику. И там и там чисто. А все дело в компрессии текстур для iOS-устройств. Оказалось, при компрессии движок смещает координаты исходника, и, к примеру. перемещает полосу шириной в пару пикселей с “низа” изображения на его “верх” или с левого края на правый. От этого и непонятные полосы.
Решили проблему расширением прозрачных участков по краям изображений. При компрессии таким образом смещается прозрачный участок, что совершенно безобидно.
И последняя проблема, затянувшая нам релиз (который состоится в январе 2013 г.) и испившая литры нашей разработческой крови: наше приложение периодически, с завидной регулярностью, роняло i-устройства. Нормальные приложения, если крашатся, вываливаются в ось, а наше роняло саму iOS. iPad2 перезагружается, iPad3 намертво виснет. Никакие логи при этом не пишутся.
Мы долго оптимизировали производительность (стабильные 30 fps и выше), профайлили десятки разных показателей (все показатели в пределах нормы), проверяли самые безумные варианты. Мы даже полностью отключали рендеринг и смотрели, как черный экран радостно сменяется серебристым яблочком в момент краша.
Написали в Epic Games. Их техподдержка (ребята классно работают, у эпиков отличный саппорт) сказала, что никогда с таким не сталкивалась, пообещала сделать все возможное для решения, и порекомендовала написать в Apple. Мы так и поступили. С эпловской поддержкой переписывались около месяца. Безрезультатно.
За это время мы выяснили, что проблема кроется в физической подсистеме, причем актуальна только для ботов — игрок волен кататься на своей машинке сколь угодно долго, игра будет работать стабильно. А вот даже один бот способен закрашить игру.
Все очевидные и, после, даже самые неочевидные варианты вероятных причин этой проблемы мы убрали. Думали уже писать в NVidia, чье детище (PhysX) используется Unreal в качестве физического ядра.
Но тут вышел ноябрьский билд UDK, в котором эта проблема была решена. Мы вздохнули свободно. Ничто больше не закрывало нам путь к релизу.
P.S. В качестве бонуса, который будет полезен всем Unreal-разработчикам, прикладываю ссылку на замечательную таблицу типичных проблем UDK и способов их решения.
Автор: 13branniy