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