[Перевод] Реализация движения по лестницам в 2D-игре
Движение по лестницам уже давно было головной болью для разработчиков. Свой код я написал для нашего старого прототипа 2017 года и до недавнего времени он оставался почти неизменным. Он едва покрывал потребности нашего прототипа и не должен был дожить до релиза.
Однако именно потому, что он создавал так много багов и выявил так много ловушек, я теперь могу сказать, на что вам стоит обратить внимание при создании дизайна собственной системы. Я употребил термин «дизайн», потому что в основном имею в виду гейм-дизайн, а не само программирование. Поэтому в статье не будет примеров кода, иначе бы она оказалась слишком объёмной.
Во-первых, я хочу вкратце описать ключевые особенности нашей системы движения. Если у вашей системы совершенно иные требования, то предложенные мной решения вам не подойдут.
1. Игра представляет собой двухмерный сайд-скроллер, в котором игрок может ходить и бегать. В нём нет прыжков и рывков, то есть на лестницы можно попасть с самого верха или низа.
2. Движение по лестницам имеет собственные анимации, а не те же, которые используются при горизонтальной ходьбе (и беге).
3. Есть два типа лестниц: фронтальные (вертикальные) и боковые (диагональные). Последние могут опускаться вниз (с левого верха в правый низ) или подниматься вверх (с левого низа в правый верх).
4. Все ступеньки имеют одинаковый размер для соответствия анимациям игрока. Лестницы могут иметь любую длину.
5. Коллайдер модели игрока (соответствующий движению по лестницам) находится в районе бёдер. Однако для работы всей системы это необязательное требование.
6. В нашей игре нет боёв и других факторов, которые могут прикладывать к игроку силы, когда он движется по лестнице.
7. Наша цель заключается в создании интуитивно понятного, не подверженного багам и красивого движения по лестницам.
Разобравшись с требованиями, давайте приступим к решению задач по мере их появления! Начнём мы с боковых (т.е. диагональных) лестниц: как вы увидите в дальнейшем, большинство установленных для них правил будет применимо и к фронтальным (т.е. вертикальным) лестницам.
Первое, чего мы хотим добиться — заставить игрока попасть на лестницу. Давайте создадим очень простую первую итерацию наших лестниц: когда игрок проходит мимо лестниц, он накладывается на коллайдер (мы назовём это «проверкой прохождения»), который переключает его на управление в режиме движения по лестнице. Поместим с другой стороны лестницы ещё одну проверку прохождения, переключающую игрока на обычный режим управления.
Коллайдер игрока показан зелёным, проверка прохождения — синим.
Первая проблема, которую мы будем решать, поначалу кажется экзотической, но вы обязательно с ней столкнётесь, поэтому давайте разберёмся с ней сразу же: игрок может не столкнуться с коллайдером в нужный момент, особенно при низкой частоте кадров, если скорость движения игрока зависит от FPS (а скорее всего, так и будет). При меньшем количестве кадров в секунду увеличивается проходимое между кадрами расстояние, и чем больше оно становится, тем дальше игрок будет проскакивать через лестницу.
При высоком FPS игрок сталкивается с коллайдером сразу, как попадает в область его действия:
Однако при низком FPS он может коснуться проверки прохождения только задней частью своего коллайдера:
В таком случае анимация ходьбы не будет соответствовать спрайту лестницы. Именно поэтому во второй итерации мы просто телепортируем игрока в нужное положение при начале («входная точка») или завершении («выходная точка») движения по лестнице. Так как расстояние телепортации становится больше только при низком FPS, то телепортирование игрока при и так уже дёрганой игре будет незаметным.
Стрелкой показано движение из одного кадра в другой.
Но что если игроку нужно пройти мимо лестницы? Пока игрок всегда заходит на неё, как только касается одного из её коллайдеров.
Чтобы решить эту проблему в третьей итерации, давайте начнём с задания другого коллайдера (назовём его «интервалом входа»), расположенного рядом с началом лестницы. Игрок будет начинать двигаться по лестнице только при нажатии «вверх» (если он внизу) или «вниз» (если он наверху). Если он двинется влево или вправо, то покинет входные точки и продолжит своё обычное движение по горизонтали.
Теперь проблема заключается в том, что когда игрок начинает нажимать «вверх» или «вниз» внутри интервала входа, из-за большой ширины коллайдера телепортация к точке начала выглядит слишком заметной:
Чтобы решить эту проблему в четвёртой итерации, мы заставим игрока подходить к настоящей входной точке, пока находясь в интервале входа игрок удерживает «вверх» или «вниз». Например, если игрока находится справа от входной точки внизу лестницы, то при нажатии «вверх» персонаж сначала подойдёт к входной точек, а затем начнёт подниматься по лестнице. Чтобы это сработало, нам нужно добавить к обычному управлению движением дополнительное условное управление, применяемое только в пределах интервала входа.
Стрелками в квадратах показана кнопка, которую нажимает игрок.
Теперь мы избавились от телепортации и в результате этого снова потеряли контроль над тем, где именно игрок входит на лестницу, поэтому анимация выглядит смещённой. Именно поэтому в пятой итерации мы скомбинируем интервал входа с проверкой прохождения! Однако последнюю нужно дополнить, чтобы игрок мог подходить к лестнице с обеих сторон. Этот коллайдер должен автоматически переключаться на противоположную от входной точки лестницы сторону, чтобы игрок при столкновении с ним находился в нужной позиции.
Эта комбинация в конечном итоге работает так: если игрок находится в пределах интервала входа и удерживает «вверх»/«вниз», то он двигается в сторону входной точки, а затем, когда сталкивается с проверкой прохождения, начинает двигаться по лестнице.
Обратите внимание: коллайдер проверки прохождения всегда переключается на противоположную от игрока сторону.
Теперь игрок может проходить через входную точку всех лестниц. Но что если лестница иногда является единственным способом перемещения влево или вправо? Что если иногда они должны действовать как в первой итерации, заставляя игрока заходить на неё, если он удерживает рядом с лестницей «влево» или «вправо»?
В шестой итерации мы добавим отличие между двумя типами входных точек лестниц (они будут «проходимыми» и «непроходимыми»). Для этого давайте добавим булеву переменную bypassable
, которую можно будет задавать по отдельности для каждого объекта-лестницы. Если входная точка не является «проходимой» (bypassable), то коллайдер проверки прохождения не будет динамически менять стороны, потому что ко входной точке можно подойти только с одной стороны. В этом случае коллайдер всегда будет оставаться на противоположной, недостижимыой стороне от приближающегося игрока.
Также нам нужно учитывать все возможные кнопки или комбинации кнопок, которые игрок может использовать, чтобы выразить своё намерение зайти на разные типы входных точек лестниц. «Вверх» (внизу лестницы) и «вниз» (вверху лестницы) уже работают для всех лестниц. Однако при приближении к «непроходимой» лестнице слева нажимающий «вправо» игрок тоже выражает намерение зайти на лестницу (и наоборот), потому что движение мимо неё недопустимо. Теперь при столкновении с коллайдером проверки прохождения игрока будет заходить на лестницу, если нажимает одну из кнопок, соответствующих текущему типу.
Если все лестницы в этом примере «проходимы», то чтобы забраться на них, игрок должен удерживать «вверх», а чтобы пройти мимо — нажимать «влево»/«вправо».
То же самое относится к верхней части «проходимых» лестниц.
Если они «непроходимы», то нажатие «вправо» или «вверх» будет интерпретировано так, что игрок намеревается зайти на лестницу.
То же самое относится к верхней части «непроходимых» лестниц.
Если реализовать управление определённым образом, то можно создать целое множество новых проблем — ведь мы только что дали игроку возможность нажимать разные кнопки рядом с лестницами, чтобы пройти по ним.
Чтобы устранить их в седьмой итерации, нам нужно определить, какие кнопки взаимно обнуляют друг друга на входных точках разных типов лестниц, а какие не должны приводить к суммированию скоростей. Также нужно соответствующим образом настроить анимации, например, если нажатие двух клавиш обнуляет их действие (например, если игрок держит «влево» и «вправо»), то модель игрока не должна идти на месте. Чтобы учесть все возможные проблемы, важно разделять поднимающиеся лестницы (слева снизу вправо вверх) и опускающиеся лестницы (сверху слева вправо вниз). Например, если игрок стоит в интервале входа опускающейся лестницы, то он может вызвать анимацию «ходьбы на месте», удерживая «влево» (эта кнопка заставляет персонажа удаляться от лестницы) и «вниз» (эта кнопка в данном случае обозначает приближение к лестнице). Или же игрок может удерживать «вниз» (движение к лестнице) и вправо (тоже движение к лестнице); при этом его скорость удвоится. Нам нужно учитывать все случаи сочетаний различных кнопок на всех типах лестниц, чтобы это не вызывало никаких проблем.
Удерживание «влево», удерживание «вниз» и удерживание «влево» + «вниз» должны давать в данном случае одинаковые результаты.
И «вниз» + «вправо», и «влево»+ «вправо» противоречат друг другу. При удерживании этих комбинаций игрок должен останавливаться.
Рекомендую не лениться на этом этапе и учесть все возможные случаи, чтобы избавиться от багов и сделать движение как можно более интуитивно понятным.
Отлично. Движение по боковым лестницам теперь работает. Есть ли какие-то особые правила для фронтальных лестниц?
Для добавления в восьмой итерации фронтальных лестниц нам достаточно внести всего лишь небольшие изменения, потому что базовая логика останется той же. Мы сделали так, что может использоваться только заданная линия фронтальной лестницы, а не вся поверхность. В результате этого у нас получилось две чётко заданные входные точки, как и у боковых лестниц. Кроме того, это означает, что игрок всегда может проходить мимо фронтальных лестниц, то есть их верх и низ «проходимы», ведь ширина спрайта лестницы всегда шире интервала входа. Разумеется, в некоторых случаях игрок может пройти мимо фронтальной лестницы и сразу же столкнутся со стеной, которая всё равно его остановит. Остальное (то, что касается движения по лестницам) работает точно так же. Более того, нам даже не нужно учитывать, что игрок будет пытаться зайти на фронтальную лестницу, нажимая «влево» или «вправо».
Если удерживать «вверх» в интервале входа, то игрок подойдёт ко входной точке, а затем начнёт двигаться вверх по лестнице.
Если удерживать «вниз» в интервале входа, то игрок подойдёт ко входной точке, а затем начнёт двигаться вниз по лестнице.
Если вы добрались досюда, то достойны похвалы. Обещаю, что вход на лестницу останется самой сложной частью системы. Теперь давайте разберёмся с движением по разным типам лестниц.
В некоторых случаях (например, если в игре используется настоящая физика и гравитация) движение вверх по диагонали или вертикали может вызывать определённые проблемы, например, войдя на лестницу сверху, игрок может сам скатиться вниз. Именно поэтому в девятой итерации мы будем перемещать игрока вдоль лестниц по «рельсам». Он будет не физически стоять на коллайдере, а двигаться вдоль линии под углом без влияния физики. При входе на лестницу мы будем отменять все силы, действующие на объект игрока и игнорировать все силы, когда он находится на лестнице. В случае нашей игры нам, к счастью, не нужно учитывать бои и другие факторы, способные прикладывать к игроку силы, пока он движется по лестницам.
Но как гарантировать, что игрок всегда будет оказываться наверху или внизу, двигаясь вверх и вниз по этому углу? Если он промахнётся мимо выходной точки, то навечно останется в ловушке лестницы, бесконечно двигаясь вверх или вниз. Мы можем вычислить угол между верхом и низом, но поскольку ступеньки всегда имеют одинаковый размер, соответствующий анимации, угол всех боковых ступеней вне зависимости от длины автоматически будет оставаться одинаковым, и мы можем просто задать его численно.
Угол показан синим цветом. Он не является коллайдером и существует только в коде.
Отлично, так как же будет действовать на лестницах управление движением? Давайте создадим его в десятой итерации. Игрок ожидает, что нажатие «вверх», «вправо» и «вверх» + «вправо» приведёт к подъёму по лестнице (и наоборот для опускающейся лестницы). Не забудьте, что скорости не должны складываться.
Также нам нужно учитывать обнуляющие друг друга сочетания кнопок. Например, на поднимающихся лестницах «влево» и «вниз» одинаковы, поэтому должны обнулять «вверх» и «вправо».
А как насчёт управления на фронтальных лестницах? Настало время для одиннадцатой итерации!
Можно подумать, что здесь достаточно учесть нажатия «вверх» и «вниз». Однако необходимость удерживать «вверх» или «вниз» до самого конца лестницы, пока они не смогут двинуться влево или вправо, кажется неинтуитивной и несовершенной. Может также случиться, что игрок остаётся на 1 пиксель выше низа или ниже верха лестницы, не понимая, почему он не может пойти вправо или влево. Именно поэтому мы должны задать часть лестницы в пределах 1 метра с каждого конца как «интервал выхода» верха или низа. Когда игрок находится в интервале выхода, нажатие «влево» или «вправо» должно перемещать его к выходной точке лестницы. Тогда, как только он достигнет выходной точки, то сойдёт с лестницы и продолжит ходьбу в удерживаемом направлении. Возможно, вы уже заметили, что принцип здесь такой же, как и у интервала входа, который позволяет игрокам использовать дополнительные кнопки для движения к входной точке (её мы добавили в третьей и четвёртой итерациях). Повторюсь, мы должны учитывать комбинации клавиш, которые обнуляют друг друга или удваивают скорость.
Интервал выхода в верху и низу лестницы показан синим. Для него можно использовать и коллайдер, но мы вычисляем его из расстояния между игроком и ближайшей выходной точкой.
Хотя добавление интервала выхода сильно совершенствует нашу систему, оно привносит и новый набор проблем, которые придётся решать в двенадцатой итерации. Например, что если фронтальная лестница имеет высоту всего 2 метра или даже меньше, из-за чего игрок одновременно попадает в интервалы выхода сверху и снизу? Давайте сделаем высоту интервала выхода равной, допустим, 20% от общей длины лестницы, а не ровно 1 метру.
Кроме того, если лестница меньше определённого предела, то у неё вообще не будет интервала выхода. Во время тестирования мы выяснили, что это не особо большая проблема, потому что на очень коротких лестницах игрок обычно удерживает «вверх» или «вниз» дольше необходимого, благодаря чему в 99% случаев он пробегает лестницу до конца.
Это решает проблему коротких лестниц, но вызывает новую проблему с длинными, для которых величина 20% слишком большая для интервала выхода. Поэтому вдобавок к правилам, созданным в двенадцатой итерации, в тринадцатой итерации мы зададим ещё и максимальный интервал выхода, равный для каждого интервала выхода 1 метру.
Мы почти закончили! Игрок умеет заходить на лестницы и двигаться по ним, так что научить его сходить с них будет довольно просто.
Пока игрок просто проходит после верха и низа в бесконечность:
В четырнадцатой итерации мы создадим коллайдеры, которые позволят игроку сходить с лестниц (т.е. возвращаться к обычному управлению ходьбой с физикой). На самом деле, мы снова используем для этого коллайдеры интервалов верха и низа:
Единственная проблема здесь в том, что из-за высоты коллайдера игрока коллайдеры срабатывают слишком рано, особенно наверху. Давайте разместим коллайдеры верха и низа выше и ниже, чтобы игрок входил в них в самом конце движения вверх или вниз по лестнице.
Отлично, но игрок всё равно находится внутри этого коллайдера при входе на лестницу, поэтому он может повернуться и пройти прямо через него без повторного срабатывания коллайдера, что позволит ему пройти через выходную точку. В пятнадцатой итерации мы решим эту проблему так же, как в случае со входом на лестницу: зададим все кнопки, выражающие намерение игрока сойти с лестницы для каждого случая (поднимающаяся/опускающаяся/фронтальная лестница), как и в шестой итерации. Когда игрок нажимает одну из этих кнопок, находясь в интервале выхода, то снова сходит с лестницы.
В шестнадцатой итерации мы снова должны учесть низкий FPS, из-за которого игрок может проскочить выходную точку. Мы телепортируем игрока ровно к выходной точке, аналогично тому, как телепортировали его к входной точке:
Хоть это и защитит нас от большинства багов (например, от прохода через выходную точку или проваливание сквозь пол), у такого решения есть две проблемы: 1. Оно заметно при низких FPS, потому что игрока можно видеть под полом по крайней мере в течение одного (долгого) кадра. 2. Решение не рассчитано на очень низкий FPS, потому что игрок всё равно может полностью проскочить коллайдер выхода, если расстояние перемещения между кадрами окажется достаточно большой.
Поэтому в окончательной семнадцатой итерации мы будем изменять высоту коллайдера выхода в соответствии с текущим FPS: чем ниже FPS, тем выше он становится. Благодаря этому он будет срабатывать раньше и игрок никогда его не пропустит, что решит обе проблемы шестнадцатой итерации.
И на этом всё! Всего за семнадцать простых итераций мы достигли своей цели — создали интуитивно понятное и красивое движение по лестницам с защитой от багов.
После записи этого видео мы ещё немного усовершенствовали систему. Мы сгладили вход и выход, настроив точки телепортации, а также добавили специальные анимации ожидания для персонажа, неподвижно стоящего на фронтальной лестнице. Анимации зависят от предыдущего направления движения.
Разумеется, мы понимаем, что подобная система движений не настолько сложна или изощрённа, как некоторые трёхмерные системы перемещения по различным рельефам с управлением ИИ, которые сегодня создают гении программирования. Однако для нашей игры она вполне подходит и её легко будет реализовать для похожей 2D-игры с сайд-скроллингом.