Изометрия, z-индексы в мобильных играх и их оптимизация

в 5:11, , рубрики: Gamedev, iOS, objective-c, spritekit, swift, xcode, z-index, изометрия, разработка мобильных приложений, я пиарюсь

Изометрия, z-индексы в мобильных играх и их оптимизация - 1

Привет! Недавно мы вышли в релиз с нашей игрой, которую долго и упорно готовили и в процессе которой накопилось немалое количество интересных тем, которыми стоит поделиться с сообществом. Тема будет интересна далеко не только iOS и иным мобильным разработчикам, но и всем тем, кому интересно, как всякие графические вещи работают под капотом, а также всем фанатам 2D-стратегий, коим уже третее десятилетие являюсь я сам.

Сегодня поговорим о нюансах такой важной темы, как z-индексы на изометрической поверхности (да-да, не все тут так просто как кажется некоторым умникам). В мире 3d у нас, как ни странно, есть три координаты — x, y, z — которые полностью определяют положение объекта в пространстве. Задача определения близости к камере объектов там также стоит, но ложится целиком на плечи OpenGL. Разработчик лишь оперирует высокоуровневыми параметрами типа глубины z-буфера, которые влияют на производительность, но в остальном можно довериться OpenGL как черному ящику — у него хватает информации.

Совсем иная ситуация наблюдается в нашем “псевдо-3D” мире — каждый объект имеет только (x, y) — координаты и размер спрайта. Первой же задачей, которая становится перед программистом во время написания движка, является задача определения, какие объекты должны перекрывать друг друга перед нашей виртуальной “камерой”.

Синопсис.

Координаты SpriteKit (где (0;0) — центр “мира”, а Y идет вверх) в данном случае нас совершенно не интересуют, т.к. они ничего не значат в нашем с вами изометрическом “мире”, так что давайте оговоримся — у нас есть ромбовидное поле наподобие Age of Empires.

Изометрия, z-индексы в мобильных играх и их оптимизация - 2

Тайл с координатами (0;0) находится в левом углу ромба, абсцисса X увеличивается “вниз” и “вправо”, т.е. растет ближе к наблюдателю, ордината Y увеличивается “вверх” и “вправо”, т.е. уменьшается по мере приближения к наблюдателю.

Также рельсы должны быть “под” поездом, дым из трубы — “над” поездом. Но не будем сейчас заморачиваться со “слоями бытия” — очевидно, ничего не мешает нам сделать сколько угодно изометрических “слайсов”, работающих по одним и тем же правилам. Примем допущение, что в одном тайле всегда расположен один объект — для наглядности большего и не надо.

Изометрия, z-индексы в мобильных играх и их оптимизация - 3

Рассмотрим два поезда выше. Очевидно, что с точки зрения наблюдателя вагоны должны располагаться “ниже” поезда, т.е. их z-индекс должен быть меньше. В то же время “верхний” поезд должен “перекрываться” ближним, быть “дальше”. Можем ли мы, имея только координаты (x; y) построить карту z-индексов для каждого тайла?

Очевидно, да, используя следующую формулу (псевдокод а-ля свифт):

zIndex = pos.x * field.size.width - pos.y

Таким образом мы гарантируем, что по мере роста ординаты объекты отдаляются (-pos.y), а также с ростом абсциссы объекты приближаются (pos.x) и, что немаловажно, любой объект, имеющий абсциссу, скажем, 44, будет заведомо “ближе”, чем любой объект, имеющий абсциссу 43. Дабы добавить сюда “слоеность” (помните, рельсы под поездом, дым над трубой), достаточно добавить какую-нибудь константу “высоты” слоя:

zIndex = layerZIndex + pos.x * field.size.width - pos.y

Все, статью можно заканчивать, а себя похвалить за усвоенные в 10-м классе основы стереометрии и приступать к логике игры. Нет? Если бы! Стал бы я писать про очевидные вещи! (ну как очевидные, пару дней гробится и на это)

Мы только приступаем к самому интересному, идем дальше.

Борьба за производительность.

Каждый, хоть хоть раз запускал тестовый проект под SpriteKit (или кокос, или любой иной движок), видел магические цифры — fps и nodes.

Изометрия, z-индексы в мобильных играх и их оптимизация - 4

Очевидно, что fps — количество кадров в секунду, nodes — количество нод, в основном спрайтов. Но на практике больше всего садит fps не количество нод, а иной параметр, который по умолчанию не выводится, но который также можно вывести одной строчкой — количество перерисовок draws.

Изометрия, z-индексы в мобильных играх и их оптимизация - 5

В одной и той же сцене, как вы сейчас видите, количество нодов около 6000, и количество отрисовок — около 120. Это на минимальном зуме (камера максимально “близко” к поверхности), 1:1.

А теперь отдалим камеру на максимальное расстояние (у нас в игре это 2.5:1)

Изометрия, z-индексы в мобильных играх и их оптимизация - 6

Мы поменяли масштаб всего в 2.5 раза (это еще в примере далеко не все объекты рисуются), а количество draws возросло в 5-6 раз при неизменном nodes count!

Разумеется, количество отрисовок влияет на fps несоизмеримо больше, чем абстрактное количество нодов. SpriteKit просто не рисует ноды, которые не попадают по вьюпорт (в камеру). Единственным исключением, которое я пока нашел, являются эмиттеры частиц, которые рисуются всегда, независимо от того видны они или нет.

Теперь поговорим о том, что же значит эта “отрисовка” draw. Видеокарта располагает все ноды “слоями”, руководствуясь их z-индексами. И проходит всю картинку раз за разом, начиная от самого нижнего и заканчивая самым верхним. Количество таких циклов отрисовки — и есть draws.

Теперь вы понимаете, что если каждый крошечный объект (а карта у нас большая, примерно 6000 х 3000) рисовать со своим собственным z-индексом, это угробит производительность любого телефона.

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

Вот и приходится идти на хитрости.

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

Все это позволяет сократить количество draws в разы, исправляя fps даже на стареньких iPhone. Пришлось на них сильно ограничить некоторые эффекты, но Apple не выпускает для них апдейтов уже год — грех будет жаловаться!

Высота рельефа.

Ну все, движок готов, можно уже приступать к чему-то интересному? Кому-то и можно, а нам еще рано. Ведь поезд должен красиво выезжать из тоннеля, и тут все не так просто, как может показаться.

Изометрия, z-индексы в мобильных играх и их оптимизация - 7

Поезд должен располагаться “выше”, чем “дальняя” стенка тоннеля, и “ниже”, чем крыша тоннеля и следующие за ним горы. Красиво ведь, когда карта такая многоуровневая, с перепадами высот — опять же, не бездушную ерунду делаем, а то что самим нравится!

Но вернемся к деталям — для этого карта была “разрезана” следующим образом.

Изометрия, z-индексы в мобильных играх и их оптимизация - 8

Внутренняя стенка тоннеля и все остальное левее-ниже и

Изометрия, z-индексы в мобильных играх и их оптимизация - 9

верх тоннеля вместе с горами, в которые он перетекает. Тут уж никакие процедурные генерации z-индексов не помогут, только суровый белорусский хардкод.

Внимательный читатель заметил на скриншоте из игры, что близ тоннелей деревья аккуратно “выкошены”, обнажая девственно пляжный песочек. Эта, казалось бы, недоработка, происходит из принципиальной невозможности реализации таких посадок деревьев в 2D. Поезд, выходя из туннеля, должен быть заведомо “выше” деревьев, которые он перекрывает, закрывая их собой. Но эти же деревья должны перекрывать собою крышу тоннеля, под которую должен заезжать поезд! А крыша должна быть выше поезда, и так по кругу, имеем логическое противоречие…

Примерно по схожей причине, из-за несовершенства графического движка, в старых играх типа Duke Nukem и Doom2 нет больших перепадов высот и этажности зданий.

Вот поэтому близ тоннелей деревья и не растут.

Надеюсь, было интересно, игрушка вживую вот тут (free to play), следующая статья цикла будет про красивую реалистичную 2D-воду, не пропустите!

P.S. Кстати, видео для привлечения внимания можно посмотреть на youtube в нормальном качестве.

Автор: iago

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js