Зацикливаю уровни, и вас научу

Сегодня вторник. В этот день я обычно освещаю недельный прогресс по своим проектам. Уверен, сегодня вам будет интереснее не ЧТО я сделал, а КАК. Поэтому в своём рассказе я поделюсь парой приёмов как сделать в игре бесшовный цикл по уровню. Без привязки к коду или движку. От простого к сложному - всё, чем я занимался последнее время.

Зачем мне это

Напоминаю, что я работаю над проектом Zzzz-Zzzz-Zzzz. Это игра про исследование мира снов. Сновидения ставят игрока в конфуз своей запутанностью и неоднозначностью. Для этого я использую циклические уровни как один из инструментов.

На прошедшей неделе у меня было немного времени на проект, и я успел сделать только Карту Снов. Это центральная локация всей игры. Хаб, из которого вы можете попасть в любой открытый сон.

Зацикливаю уровни, и вас научу

Эта локация имеет вертикальный и горизонтальный циклы. Так я показываю неограниченность пространства. Подчёркиваю атмосферу мира сновидений.

Бесшовный цикл выглядит просто и эффектно. Но реализуется сложно. Чем более свободно двигается камера - тем сложнее организовать бесшовность.

Если камера не двигается - всё гораздо проще. С этого и начнём.

Статичная камера

Неделю назад я рассказывал как сделал уровни в этой же игре. Среди них было два, которые имеют цикл: Пустота и Лестница.

Уровень Пустота маленького размера. Имеет фиксированную камеру и горизонтальный цикл. Для его организации мне нужно было всего лишь переносить персонажа при пересечении левой или правой границы. В этом случае просто двигаю персонажа на ширину цикла.

Ширина цикла совпадает с шириной камеры.
Ширина цикла совпадает с шириной камеры.

Звучит просто, но нужно учесть два момента:

  • Ширина камеры - величина непостоянная. Зависит от разрешения экрана или размера окна.
  • При переносе персонажа на экране всё ещё видна его часть - "задняя". Она резко исчезает, и на другом конце экрана появляется "передняя" часть персонажа.

Размер камеры следует закладывать в рассчёты. Граница переноса персонажа в этом случае будет плавающая. Стоит так же обратить внимание на другие объекты, которые находятся на границе. В моём случае есть коллайдер пола, который должен быть больше любой ширины экрана. Подойдёт и адаптивный рассчёт ширины этого коллайдера.

Чтобы перенос персонажа не выглядел как баг - просто рисуйте его копии справа и слева. Расстояние между копиями - ширина цикла. Как только персонаж начнёт выходить за границы экрана - его отрисованная копия начнёт появляться на другой стороне.

Когда персонаж внутри цикла - его копии не попадают в камеру, можно не отрисовывать.
Когда персонаж внутри цикла - его копии не попадают в камеру, можно не отрисовывать.

Такой метод подходит только для небольших уровней:

  • Уровень должен помещаться на экране.
  • Размер цикла совпадает с размером экрана.

Что делать если размер цикла меньше экрана или больше? Двигаемся дальше.

Локальные циклы

Если размер цикла меньше габаритов камеры - вместо копирования персонажа нужно будет копировать окружение. В Zzzz-Zzzz-Zzzz есть уровень Лестница. Я реализовал в нём вертикальный цикл высотой 64 пикселя. Это гораздо меньше высоты камеры в 192 пикселя.

Почти тот же трюк с перемещением персонажа на границах цикла, но с тремя главными отличиями:

  • Камера двигается и переносится вместе с персонажем.
  • Для бесшовности нужно клонировать часть окружения, которое попадёт в видимость камеры.
  • Копии персонажа при этом рисовать уже не нужно

В первом пункте обращу внимание, что камера должна мгновенно перепрыгнуть на новые координаты. Не используйте для этого скрипты плавного следования за объектом, иначе будет виден "шов".

Ось цикла - это направление движения персонажа, в котором будет повторяться циклический контент.

Здесь ось цикла вертикальная
Здесь ось цикла вертикальная

Клонировать окружение можно разными способами. Я знаю три:

  • Для каждого ассета в циклическом контенте рисуйте его копию. Вдоль каждой оси цикла впереди и позади. Как я делал в статичной камере с персонажем. Большое количество склонированных ассетов может повлиять на производительность.
  • Заранее разместите копии ассетов на уровне. Этот способ использую я.
  • Динамически копируйте или переносите контент цикла при движении персонажа. При этом контролируйте значение координат персонажа, чтобы они не переполнили стек. Это может случиться при длительном движении персонажа в одном направлении.

Чем меньше зацикленный кусочек - тем больше раз его нужно скопировать. Количество копий в одном направлении оси цикла можно рассчитать так:

То есть целое количество контента в камере +3. Или подвигайте камеру в пределах границ цикла и клонируйте контент столько раз, сколько нужно будет.
То есть целое количество контента в камере +3. Или подвигайте камеру в пределах границ цикла и клонируйте контент столько раз, сколько нужно будет.

В 3Д задача сильно усложняется - нужно учитывать эффекты, такие как освещение и туман. На собственном опыте знаю, что добиться бесшовности гораздо сложнее. Чем больше осей цикла - тем сложнее.

Пример цикла в другой моей игре в одном направлении.

В 2Д мире вы скрываете швы при помощи области видимости камеры. В 3Д мире вам придётся использовать другие приёмы: туман, большие объекты.

Глобальные циклы

Когда размер цикла больше области видимости камеры, методы с копированием контента работают плохо. Вам нужно скопировать часть локации размером с камеру по каждой оси цикла. Посмотрите как это должно было выглядеть на Карте Снов:

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

Я регулярно вношу изменения в эту локацию. Значит, должен вносить и в скопированной контент. Зачем мне этот геморрой? Чтобы этого не делать, я использую виртуальные камеры. Они не выводят своё изображение на экран напрямую, а пишут его на виртуальный холст. Этот холст я потом рисую на границах цикла.

Зацикливаю уровни, и вас научу

Для одной оси цикла я создаю одну виртуальную камеру. Она выбирает границу в зависимости от положения персонажа. Поясню на примере горизонтального цикла. Камера смотрит на левую границу цикла, если персонаж приближается к правой. И наоборот - смотрит на правую границу цикла, если персонаж приближается к левой.

Поведение игры на границе цикла - бесшовно.

Для Карты Снов я создал две камеры - для вертикального и горизонтального циклов. Но остаётся ещё диагональный кусочек, если персонаж приближается к пересечению границ цикла. Для него я создаю ещё одну виртуальную камеру.

Зацикливаю уровни, и вас научу

Если это слишком сложно, вы можете сделать как я в старой версии игры. Отодвиньте границы закцикленной зоны от контента на ширину и высоту камеры.

Синяя зона - кольцо пустоты. Оно гарантирует, что игрок не увидит скачок на витке цикла.
Синяя зона - кольцо пустоты. Оно гарантирует, что игрок не увидит скачок на витке цикла.

Этот способ имеет недостаток: персонаж будет находиться в пустоте на границах цикла. При вертикальном движении пару секунд он проведёт в пустоте:

Хуже всего это поведение проявляется в платформерах на горизонтальном цикле. На границе появляется чувство дизориентации.

Ещё немного занудства

Задача цикла решена только с точки зрения игрового персонажа. Начнёте делать игровые механики - вылезет ещё ряд проблем:

  • Скорее всего вы будете дублировать коллайдеры некоторых объектов для адекватного поведения на границе цикла. Например, если вдоль этой границы расположена непроходимая стена.
  • Любите параллаксы и динамичные фоны? Получите целый пласт работ.
  • Вместо виртуальных камер выбрали клонирование или перенос/создание? Будете поддерживать клоны динамических объектов - противников и их выстрелы.
  • Любая рандомизация на границе цикла будет вести себя очень плохо. Проблема быстро всплывёт на спецэффектах с частицами.

Для чего использовать циклы?

Я использую в своих проектах для троллинга игрока. Обычно мотивирую его долго двигаться в сторону цикла, пока он не попробует искать решение в другом месте.

Ещё я использую циклы для катсцен с бесконечным движением. В моей игре Mainframent персонаж в конце игры уезжает на лифте. В это время игра показывает послесловие и благодарит игрока за игру. Лифт движется по циклической шахте - чтобы движение не прекращалось.

Можно использовать отдельные зацикленные зоны в скроллерах. Для засад и боссов. Игрок находится в цикле всё сражение, а потом игра его выпускает дальше. И не только в скроллерах! Игрок прыгает в шахту - начинается сражение с боссом. Как обеспечить нужную глубину шахты, чтобы хватило на всё сражение? Использовать циклы.

В 2Д играх циклы помогают собирать локации с геометрией кольца. Или ломать евклидову геометрию, удлиняя пространство.

Заключение

Я показал вам три вида циклов: статичные, локальные и глобальные. Рассказал, как их организовать. Привёл примеры использования циклов в играх. Делайте свои циклы!

А я продолжу делать свой проект Zzzz-Zzzz-Zzzz. Следующую неделю я обогащу карту снов и добавлю ещё уровней. Если интересно:

  • Смотрите как я в своей группе ВК веду девлог. Рассказываю о проблемах в разработке и показываю их решение.
  • Залетайте на мой дискорд-сервер. Участвуйте в обсуждениях моих и чужих проектов. Спрашивайте совета по своим. Или просто общайтесь на тему инди-игр =)

Спасибо за внимание! Надеюсь, через неделю я смогу ещё вас чем-нибудь порадовать.

6060
3 комментария

Похоже, что это игра про жизнь дэтээферов!

1
Ответить

я так понимаю, что все клонированное нужно собирать в одном «массиве», чтобы иметь возможность разом все удалить, для прерывания цикла (если например решил головоломку или пошел обратно)

Ответить

Это зависит от выбранного способа "клонирования". А так же от конкретной реализации, и от движка. Я всегда использую способ расстановки дублирующихся ассетов заранее в редакторе сцены и просто персонажа с камерой переношу с одной границы цикла на другую. Мне ничего удалять не нужно =)

Чтобы игрок смог пойти обратно я делаю перенос только при движении в одном направлении цикла. Технически это выглядит как будто я всё время отталкиваю игрока назад.

Если мне нужно дать игроку выйти из цикла после решения головоломки - я отключаю скрипт переноса игрока с границы цикла и всё. Он идёт дальше.

Ответить