Зацикливаю уровни, и вас научу
Сегодня вторник. В этот день я обычно освещаю недельный прогресс по своим проектам. Уверен, сегодня вам будет интереснее не ЧТО я сделал, а КАК. Поэтому в своём рассказе я поделюсь парой приёмов как сделать в игре бесшовный цикл по уровню. Без привязки к коду или движку. От простого к сложному - всё, чем я занимался последнее время.
Зачем мне это
Напоминаю, что я работаю над проектом Zzzz-Zzzz-Zzzz. Это игра про исследование мира снов. Сновидения ставят игрока в конфуз своей запутанностью и неоднозначностью. Для этого я использую циклические уровни как один из инструментов.
На прошедшей неделе у меня было немного времени на проект, и я успел сделать только Карту Снов. Это центральная локация всей игры. Хаб, из которого вы можете попасть в любой открытый сон.
Эта локация имеет вертикальный и горизонтальный циклы. Так я показываю неограниченность пространства. Подчёркиваю атмосферу мира сновидений.
Бесшовный цикл выглядит просто и эффектно. Но реализуется сложно. Чем более свободно двигается камера - тем сложнее организовать бесшовность.
Если камера не двигается - всё гораздо проще. С этого и начнём.
Статичная камера
Неделю назад я рассказывал как сделал уровни в этой же игре. Среди них было два, которые имеют цикл: Пустота и Лестница.
Уровень Пустота маленького размера. Имеет фиксированную камеру и горизонтальный цикл. Для его организации мне нужно было всего лишь переносить персонажа при пересечении левой или правой границы. В этом случае просто двигаю персонажа на ширину цикла. Ширина цикла совпадает с шириной камеры.
Звучит просто, но нужно учесть два момента:
Ширина камеры - величина непостоянная. Зависит от разрешения экрана или размера окна.
При переносе персонажа на экране всё ещё видна его часть - "задняя". Она резко исчезает, и на другом конце экрана появляется "передняя" часть персонажа.
Размер камеры следует закладывать в рассчёты. Граница переноса персонажа в этом случае будет плавающая. Стоит так же обратить внимание на другие объекты, которые находятся на границе. В моём случае есть коллайдер пола, который должен быть больше любой ширины экрана. Подойдёт и адаптивный рассчёт ширины этого коллайдера.
Чтобы перенос персонажа не выглядел как баг - просто рисуйте его копии справа и слева. Расстояние между копиями - ширина цикла. Как только персонаж начнёт выходить за границы экрана - его отрисованная копия начнёт появляться на другой стороне. Когда персонаж внутри цикла - его копии не попадают в камеру, можно не отрисовывать.
Такой метод подходит только для небольших уровней:
Уровень должен помещаться на экране.
Размер цикла совпадает с размером экрана.
Что делать если размер цикла меньше экрана или больше? Двигаемся дальше.
Локальные циклы
Если размер цикла меньше габаритов камеры - вместо копирования персонажа нужно будет копировать окружение. В Zzzz-Zzzz-Zzzz есть уровень Лестница. Я реализовал в нём вертикальный цикл высотой 64 пикселя. Это гораздо меньше высоты камеры в 192 пикселя.
Почти тот же трюк с перемещением персонажа на границах цикла, но с тремя главными отличиями:
Камера двигается и переносится вместе с персонажем.
Для бесшовности нужно клонировать часть окружения, которое попадёт в видимость камеры.
Копии персонажа при этом рисовать уже не нужно
В первом пункте обращу внимание, что камера должна мгновенно перепрыгнуть на новые координаты. Не используйте для этого скрипты плавного следования за объектом, иначе будет виден "шов".
Ось цикла - это направление движения персонажа, в котором будет повторяться циклический контент.
Клонировать окружение можно разными способами. Я знаю три:
Для каждого ассета в циклическом контенте рисуйте его копию. Вдоль каждой оси цикла впереди и позади. Как я делал в статичной камере с персонажем. Большое количество склонированных ассетов может повлиять на производительность.
Заранее разместите копии ассетов на уровне. Этот способ использую я.
Динамически копируйте или переносите контент цикла при движении персонажа. При этом контролируйте значение координат персонажа, чтобы они не переполнили стек. Это может случиться при длительном движении персонажа в одном направлении.
Чем меньше зацикленный кусочек - тем больше раз его нужно скопировать. Количество копий в одном направлении оси цикла можно рассчитать так:
То есть целое количество контента в камере +3. Или подвигайте камеру в пределах границ цикла и клонируйте контент столько раз, сколько нужно будет.
В 3Д задача сильно усложняется - нужно учитывать эффекты, такие как освещение и туман. На собственном опыте знаю, что добиться бесшовности гораздо сложнее. Чем больше осей цикла - тем сложнее. Посмотрите пример цикла в другой моей игре в одном направлении:
В 2Д мире вы скрываете швы при помощи области видимости камеры. В 3Д мире вам придётся использовать другие приёмы: туман, большие объекты.
Глобальные циклы
Когда размер цикла больше области видимости камеры, методы с копированием контента работают плохо. Вам нужно скопировать часть локации размером с камеру по каждой оси цикла. Посмотрите как это должно было выглядеть на Карте Снов:
Синее - зоны, которые будет видеть камера на границах цикла. Туда нужно скопировать контент с противоположной границы.
Я регулярно вношу изменения в эту локацию. Значит, должен вносить и в скопированной контент. Зачем мне этот геморрой? Чтобы этого не делать, я использую виртуальные камеры. Они не выводят своё изображение на экран напрямую, а пишут его на виртуальный холст. Этот холст я потом рисую на границах цикла.
Для одной оси цикла я создаю одну виртуальную камеру. Она выбирает границу в зависимости от положения персонажа. Поясню на примере горизонтального цикла. Камера смотрит на левую границу цикла, если персонаж приближается к правой. И наоборот - смотрит на правую границу цикла, если персонаж приближается к левой. Получается бесшовное поведение игры на границе:
Для Карты Снов я создал две камеры - для вертикального и горизонтального циклов. Но остаётся ещё диагональный кусочек, если персонаж приближается к пересечению границ цикла. Для него я создаю ещё одну виртуальную камеру.
Если это слишком сложно, вы можете сделать как я в старой версии игры. Отодвиньте границы закцикленной зоны от контента на ширину и высоту камеры.
Синяя зона - кольцо пустоты. Оно гарантирует, что игрок не увидит скачок на витке цикла.
Этот способ имеет недостаток: персонаж будет находиться в пустоте на границах цикла. При вертикальном движении пару секунд он проведёт в пустоте:
Хуже всего это поведение проявляется в платформерах на горизонтальном цикле. На границе появляется чувство дизориентации.
Ещё немного занудства
Задача цикла решена только с точки зрения игрового персонажа. Начнёте делать игровые механики - вылезет ещё ряд проблем:
Скорее всего вы будете дублировать коллайдеры некоторых объектов для адекватного поведения на границе цикла. Например, если вдоль этой границы расположена непроходимая стена.
Любите параллаксы и динамичные фоны? Получите целый пласт работ.
Вместо виртуальных камер выбрали клонирование или перенос/создание? Будете поддерживать клоны динамических объектов - противников и их выстрелы.
Любая рандомизация на границе цикла будет вести себя очень плохо. Проблема быстро всплывёт на спецэффектах с частицами.
Для чего использовать циклы?
Я использую в своих проектах для троллинга игрока. Обычно мотивирую его долго двигаться в сторону цикла, пока он не попробует искать решение в другом месте.
Ещё я использую циклы для катсцен с бесконечным движением. В моей игре Mainframent персонаж в конце игры уезжает на лифте. В это время игра показывает послесловие и благодарит игрока за игру. Лифт движется по циклической шахте - чтобы движение не прекращалось.
Можно использовать отдельные зацикленные зоны в скроллерах. Для засад и боссов. Игрок находится в цикле всё сражение, а потом игра его выпускает дальше. И не только в скроллерах! Игрок прыгает в шахту - начинается сражение с боссом. Как обеспечить нужную глубину шахты, чтобы хватило на всё сражение? Использовать циклы.
В 2Д играх циклы помогают собирать локации с геометрией кольца. Или ломать евклидову геометрию, удлиняя пространство.
Заключение
Я показал вам три вида циклов: статичные, локальные и глобальные. Рассказал, как их организовать. Привёл примеры использования циклов в играх. Делайте свои циклы!
А я продолжу делать свой проект Zzzz-Zzzz-Zzzz. Следующую неделю я обогащу карту снов и добавлю ещё уровней.
Спасибо за внимание! Надеюсь, через неделю я смогу ещё вас чем-нибудь порадовать.