Оптимизация игры на Unity и dev story Tap Tap Builder
В копилке каждого инди-разработчика должен быть свой сити-билдер, может быть поэтому я решился «сконструировать» свой велосипед. Конечно же, с квадратными колесами и креслом-качалкой вместо седушки. Работаю я один, поэтому никаких дизайнеров, художников, и тем более моделлеров, в проекте нет. Кроме того, в общем-то, это моя первая игра с трехмерной графикой. Дабы не утруждать себя изучением продвинутых инструментов для создания трехмерных моделей, я решил все сделать своими руками и средствами игровой среды Unity. Там есть только примитивы, вроде кубов да цилиндров, а также возможность их раскрасить. Что ж, следовало запастись терпением и начать «творить», погрузившись в роль архитектора. Полезной информацией для начинающих инди-разработчиков может оказаться мой опыт работы с издателем, а также способы оптимизации игры.
Основной особенностью Tap Tap Builder является симбиоз с кликером, а сей жанр крепко обосновался среди широкой публики. Итак, для возведения зданий от игрока потребуется, помимо некоторого количества ресурсов, изрядно понабивать пальцы, жмакая по экрану. Правда, можно построить строительный кран, который будет строить за вас, но весьма медленно. Большинство зданий можно улучшать, при этом через каждые 5 уровней изменяется их модель. Каждое здание в игре играет свою определенную роль.
В жилых домах живут люди. Без них население города расти не будет. Когда горожане обзаводятся жильем, они начинают искать работу. Например, могу пойти работать в офис. Офисы — основной источник поступления кредитов в городской бюджет в виде налогов. Кто-то может пойти работать за завод. Заводы производят ресурсы — металл и бетон. Для работы всех зданий нужна электроэнергия. Для этого придется построить электростанцию. Кроме того, в некоторых зданиях можно работать самому, при этом будет расходоваться энергия игрока, которая восстанавливается со временем. К примеру, игроку не хватает металла. Вместо того, чтобы ждать пару часов, пока он будет производиться на заводе, игрок может перейти в здание завода и поработать там сам. И конечно же, суть работы заключается в том, чтобы 100 раз кликнуть по кнопке с надписью «Работать»)
Если жители не смогут найти работу, придется платить им пособие по безработице. Однако, если же работников будет не хватать, то здания будут работать неэффективно. Поэтому важно постоянно следить за балансом рабочих мест и количеством жителей.
Зданий в игре довольно много. Есть отели и рестораны, полицейские участки и пожарные станции, банки, фитнес центры, биржы и т.д. За строительство и улучшение зданий игрок получает опыт и может повысить уровень города. При этом он получает 1 золотой ключ, который понадобится при строительстве специальных построек. Что-то вроде очков умения в RPG. Например, можно построить банк, тогда при выходе из игры все накопленные средства будут перечисляться на депозит, а игрок будет получать проценты за все время отсутствия в игре. Или можно построить налоговую инспекцию, которая увеличивает поступление налогов в городской бюджет на 10%. Кроме основных и специальных построек, есть еще служебные, уникальные и декоративные.
Еще в городе есть транспортные средства. Сделаны они только для красоты. Если построить городскую администрацию, по городу начнут ездить лимузины. Можно также построить шоу-рум, тогда по городу начнут гонять спорткары.
Среди служебных построек есть пивной паб и почта. Построив их, игроки смогут взаимодействовать друг с другом, например, обмениваться инвайтами и ресурсами. При этом не нужен доступ в сеть. Игроки будут обмениваться бит-кодами, это что-то вроде промо-кодов или купонов. Каждый город имеет уникальный индекс, который отображается в здании почты. Его нужно указывать в качестве получателя при отправке посылки. Кроме того, конечно же, нужно будет выбрать, что отправить. Например, можно отправить 1000 единиц металла, а полученный код надо будет сообщить другу. При этом этот код не получится отправить нескольким игрокам, поскольку в нем зашифрован индекс получателя.
Похожим образом генерируются промо-коды, которые вставляются в сообщения на стену в социальных сетях.
Насколько удачно это решение, покажет время. Буду надеяться, что игроки оценят оригинальную идею.
Проект успел принять участие в Games Jam Kanobu 2016 и оказаться в подборке интересных проектов. Увы, до финала не дошел. Зато удалось пообщаться с другими разработчиками и получить полезные отзывы. К сожалению, реальных игроков на таких мероприятиях практически не бывает. Но самое главное, игра наша издателя. Им стала российская компания Herocraft. И это круто! Кроме того, было еще 2 предложения, обсуждение которых закончилось без результата.
Итак, вкратце поделюсь поделюсь своими советами по поводу хакатонов (hackathon) и джемов (jam):
— ищите команду. Вероятность найти команду на хакатонах невелика. Обычно участвуют уже сформированные команды, а свободные художники на хакатоны заглядывают крайне редко. Пробуйте, но не рассчитывайте;
— смотрите на другие проекты и их результаты. Определяйте для себя наиболее популярные направления в разработке и «заимствуйте» хорошие идеи);
— ищите отзывы об игре;
— ищите издателя;
Первым делом составляется договор, в котором оговариваются платформы для выпуска игры, юридические и финансовые вопросы. До подписания договора стоит подготовить список вопросов и получить на них ответы. Например:
— какие доработки потребуются?
— на сколько может растянуться процесс доработок?
— какие условия расторжения договора?
После этого начинается процесс доработки игры. Издатель составляет список задач, которые нужно выполнить. Советую сразу определиться с удобной системой управления проектом. В моем случае это был GitHub. Там можно хранить исходники игры, билды и прочие файлы, а также заводить задачи и баги.
Касательно Tap Tap Builder основными задачами оказались:
— доработка системы обучения
— улучшение интерфейса
— интеграция инструментов аналитики
— увеличение производительности игры
— создание контента для игровых магазинов (иконка, баннер, описание, скриншоты, видео)
— разработка тестов
Замечу, что многие подзадачи пришлось отложить до релиза ввиду трудоемкости их выполнения. На этапе разработки тестов проходит итерация тестирования на стороне разработчика, ведь все переданные тесты должны быть как минимум корректными.
После доработки игры начинается итерация тестирования на стороне издателя и исправление обнаруженных багов.
И вот доведенный до кондиции и отлежавшийся билд отправляется на софтланч (soft launch). Софтланч это ограниченная публикация игры, например в какой-то одной стране и на одной платформе. Задача софтланча — получить отзывы реальных игроков и определить основные метрики игры (доход, удержание и прочие). На основе этих данных принимается решение о целесообразности мирового релиза. В случае провала софтланча, контракт расторгается, игру возвращают разработчику, и он может самостоятельно опубликовать ее.
Первые попытки запустить игру на мобильных устройствах выявили существенные проблемы с производительностью. Игра показывала 10–15 FPS на пустом острове, при этом устройства были далеко не самые слабые (Sony Z1 и Asus Transformer). Последующая терапия позволила увеличить скорость игры в несколько раз. Итак, по порядку.
Оптимизация скриптов
Не стоит лихорадочно начинать искать проблемы в игре и оптимизировать наиболее вероятные узкие места. Первым делом надо запустить Profiler. К сожалению, данная функция есть только в pro-версии юнити.
У меня сразу же обнаружились проблемы в скриптах. Исправление не заняло много времени, вся оптимизация свелась к кешированию информации и сокращению количества вызовов. Кстати, рекомендую шаблон проектирования Memoizer. Суть его состоит в запоминании результата работы функции для последующих вызовов с идентичными параметрами. Параметрами могут быть не только простые типы, но и структуры и классы. Ниже приведу пример использования этого шаблона.
public static readonly Func SumMemoized = Memoizer.Memoize(Sum);
private static int Sum(int a, int b)
{
return a + b;
}
public class Memoizer
{
public static Func Memoize(Func func)
{
object cache = null;
return () =>
{
if (cache == null) cache = func();
return (TReturn) cache;
};
}
public static Func Memoize(Func func)
{
var cache = new Dictionary();
return s =>
{
if (!cache.ContainsKey(s))
{
cache[s] = func(s);
}
return cache[s];
};
}
public static Func Memoize(Func func)
{
var cache = new Dictionary();
return (s1, s2) =>
{
var key = s1.GetHashCode() + "." + s2.GetHashCode();
if (!cache.ContainsKey(key))
{
cache[key] = func(s1, s2);
}
return cache[key];
};
}
}
Оптимизация трехмерных моделей
После того, как проблема со скриптами была решена, пришло время заняться рендерингом (rendering). Значительное влияние на производительность оказывал тот факт, что модели зданий были сделаны из примитивов, которые имели разные материалы (и цвета). Статистика показывала около 600 draw call (после работы автобатчинга) и 100.000 треугольников. Для сравнения — на текущий момент для сферической игры в вакууме рекомендуется иметь до 100 draw call и до 30.000 треугольников.
Первое решение — уменьшение количества поверхностей. Пришлось импортировать ассет для редактирования мешей. Многие здания содержали цилиндры, а стандартный цилиндр имеет 20 граней и 80 треугольников. Упрощение геометрии цилиндров до 12 граней дало ощутимый эффект в ущерб небольшой угловатости зданий.
Следующий шаг — удаление невидимых граней из примитивов. В результате существенное сокращение общего количества треугольников (около 20%) никакого прироста FPS не принесло. Вероятно, движок и так не тратит ресурсы на отрисовку невидимой геометрии, даже если она находится в пределах видимости камеры. Итог — бесполезный шаг.
Батчинг (batching)
Следующий шаг — попытка использования статического батчинга (batching). Вкратце расскажу про батчинг, для тех, кто не в курсе. Батчинг — это группировка мешей (mesh) в 1 общий меш перед вызовом перерисовки (draw call). Просто так получается, что видеокарте проще нарисовать 1 объект сразу, чем по частям за несколько вызовов. При этом есть несколько нюансов. Объединение возможно только для мешей, которые использую один материал. Во вторых, суммарное количество вершин в общем не должно превышать 900. И в третьих, изменение transform (позиция, поворот и масштаб) дочерних объектов невозможно. К примеру, у «сбатченой» модели автомобиля не будут крутиться колеса. Батчинг бывает статический и динамический. Динамический батчинг работает во время выполнения и не требует никаких действий со стороны разработчика. Результат работы динамического батчинга можно увидеть в окне Statistics.
Статический батчинг можно реализовать двумя способами — поставить галочку Static для объекта на сцене или префаба (prefab). Это могут быть статические игровые объекты, например элементы ландшафта, растительность и здания. Второй способ — использование StaticBatchingUtility во время выполнения.
Вполне логично было выполнить статический батчинг для зданий, что я и сделал. В результате игра стала работать еще медленнее! Как оказалось, принудительный статический батчинг нарушает работу динамического батчинга, который работает эффективнее и группирует мешы из разных логических объектов, например, принадлежащие разным зданиям. Кроме того, автоматически сгруппированные объекты могут двигаться относительно друг друга (при этом группировка отменяется). Итог — применение статического батчинга может быть полезно только при глубоком понимании логики его работы.
Оптимизация теней
Следующее наблюдение — отключение динамических теней сокращает количество треугольников ровно в 2 раза, что давало прирост FPS около 20%. Изменение качества теней видимого эффекта не дало. В данной ситуации есть простое стандартное решение — замена теней спрайтами. Такие тени часто называют статическими. Сразу же возникает вопрос — как получить тени объектов, ведь не рисовать же их вручную?! Самое простое решение — использовать размытые окружности. Это подошло бы для теней персонажей в простом раннере или стрелялке. Мне такое решение не подошло — тени должны повторять геометрию зданий. Что же, пришлось написать скрипт для дампа (dump) теней. Алгоритм работы простой — скрипт по очереди создает здания (из prefab) и делает скриншот здания с тенью, сохраняя его на диск. Затем второй скрипт производит элементарную обработку полученных изображений, выполняя заливку тени черным цветом и удаляя фон (по цвету). Увы, готовых решений я не нашел, поделитесь своим опытом в комментариях.
Итог — увеличение скорости на 20% при появлении незначительных артефактов (при наложении и наслоении теней). Кстати, спрайтам с тенями нужно установить Packing Tag, например, Shadow. Тогда Unity создаст для них атлас (atlas), и отрисовка ВСЕХ теней будет выполняться за 1 draw call. И еще одно замечание — на некоторых устройствах возможны проблемы с отображение динамических теней, так что замена их на статические тени позволит избежать проблем.
Дальнейшая оптимизация трехмерных моделей
К сожалению, результат проведенной оптимизации оказался неудовлетворительным — игра показывала 20–25 FPS на мощном устройстве. Я решился на написание еще одного скрипта для запекания (bake) моделей. Суть в том, чтобы сгенерировать новый mesh и наложить на него текстуру для раскраски зданий. Текстура представляет собой простую палитру цветов размером 8×8, которая умеет автоматически заполняться при появлении новых материалов (цветов). При этом необходимо сформировать корректную UV-развертку (UV-mapping). На Asset Store есть плагины для запекания объектов, но они не умели генерировать текстуру по цветам материалов. Не буду подробно останавливаться на деталях реализации, т.к. понимаю, что моя ситуация довольно специфичная. Хотя с другой стороны, этот метод может найти применение в популярной ныне воксельной (voxel) графике. Ведь делать прототипы моделей в Unity гораздо удобнее, чем изучать моделирование. Если конечно, у вас в команде, нет моделлера, как у меня. У меня вообще никого не было).
Конечный результат превзошел все ожидания — скорость игры увеличилась в разы. Игра показывает 50–60 FPS на огромном городе. Дальнейшая оптимизация не требуется.
Прошу прощения за то, что статья оказалась весьма большой. Ведь хотелось и про игру рассказать, и опытом поделиться. И напоследок мой совет — делайте классные прототипы и не тратьте силы на оптимизацию игры до появления рабочей версии.
Буду рад услышать ваши советы по оптимизации в комментариях, а также готов ответить на вопросы!
Ссылка на игру в Google Play