Делаем карту как в Метроидваниях
Думаю, многие знают о метроидваниях не по наслышке. Одной из характерных черт (но необязательных) является игровой мир, поделённый на комнаты. Вот пара классических примеров:
Super Metroid
Metroid Fusion
Metroid Zero Mission
AM2R
Samus Returns
И немного оффтопом карта из Metroid Prime:
Castlevania Circle of the moon
Castlevania Harmony Of Dissonance
Castlevania Aria Of Sorrow
Castlevania Symphony of the night
Hollow Knight
Ori and the Blind Forest
Environmental Station Alpha
Valdis Story
Sundered
Song of Deep
Insanely Twisted Shadow Planet
Axiom Verge
Hero Core
Как видно из примеров, делают по-разному. Есть карты совсем упрощённые — геометрически простые комнаты. Есть карты, показывающие рельеф. Геометрические комнаты могут быть упрощены до комбинации квадратов или же иметь косые углы. Я думаю, что это детали и что если каждой комнате сопоставить картинку то можно рисовать вместо прямоугольников эту картинку.
И комнаты могут иметь чисто прямоугольные формы. Т.к. я ещё новичок в такого рода делах, то меня это абсолютно устраивает, хотя я почти уверен, что мой метод можно легко расширить на непрямоугольные комнаты.
Что такое комната? В Game Maker Studio это ассет, сцена, в которой разработчик расставляет объекты. Она имеет определённые размеры и ещё ряд настроек несущественных в данном посте. Можно добавлять объекты в комнату в рантайме и даже собирать уровни или загружать если они были созданы заранее в другом редакторе. Но это парсинг данных и всё такое. Поэтому вариант догрузки нужной комнаты встык — это не мой вариант. Можно пофантазировать как это могло бы быть и выяснилось бы что там свои геморрои — например нужно хранить связи входов и выходов, их нужно помечать прям внутри комнат, что влечёт ещё один менеджмент и обнаруживает очередной камень преткновения в реализации.
Так что в данном посте я рассматриваю набор заранее созданных комнат и переходов между ними.
Когда-то я делал что-то подобное в своей игре BrainStrom:TowerBombarde
Я вдохновлялся Hero Core, поэтому там комнаты были одного размера. Я их хранил в матрице и переходы считал по факту выхода игрока за пределы этой комнаты. Это элементарная фигня:
1) Игрок вышел за пределы комнаты
2) Посмотрели какая клетка следующая
3) Считаем сдвиг по X (если игрок переходит вертикально) или Y (если игрок переходит горизонтально)
4) Если X или Y меньше нуля — значит в новой комнате эта координата будет равна ширине/высоте комнаты, а другая — сдвигу который считали на шаге 3), аналогично если X или Y больше ширины/высоты комнаты
Когда комнаты разного размера, и даже разной формы — задача меняется.
И тогда и возникает проблема: «Как позиционировать игрока в новой комнате?
Как понять в какую дверь игрок вышел и в какую вошёл?»
Сперва поймём как можно хранить связи между комнатами. Один из способов — ассоциация дверей/триггеров одной комнаты с дверьми/триггерами в другой. Довольно гибкий способ решающий проблему выше, но он требует поддержки данных внутри каждой комнаты, плюс непонятно как выводить миникарту. Обход какой-то мутить что ли? А если я захочу комнаты махнуть местами? Много вопросов.
Поэтому я храню карту комнат в виде матрицы. Все мои комнаты имеют размеры кратные самой маленькой комнате, а самая маленькая комната помещается тютелька-в-тютельку на экран — и это одна ячейка матрицы. Если комната большая — она занимает сразу несколько ячеек.
Из вот такого
Получаем вот такое
По сути это раздробление локации на экраны, и каждая локация занимает определённое количество экранов. Эта задача ПРИМЕРНО сводится к той, что я решал ранее, но координаты придётся пересчитать.
Итак, первое, что нужно понять — это положение игрока в этой карте. Если он находится в первой комнате, то в какой из её ячеек?
Это считается просто, если знаем размер экрана в пикселях. Например, ширина — это screen_w, а высота — screen_h. Тогда считаем просто:
px_room = (player.x div screen_w);
py_room = (player.y div screen_h);
Но на практике не так просто как в сферических конях — нужны шаманства, если объект находится вне комнаты. Тогда координаты могут быть больше ширины/высоты и получится что посчитается координата ячейки вне комнаты,
xx=player.x;
yy=player.y;
if xx<0 then xx=8;
if xx> room_width then xx=room_width-8;
if yy<0 then yy=8;
if yy> room_height then yy=room_height-8;
px_room = (xx div screen_w);
py_room = (yy div screen_h);
Это я посчитал координаты игрока относительно комнаты. А нужно посчитать внутри карты. Для этого нужно найти координату верхнего левого угла комнаты в карте.
Просто обходим массив обычным двойным циклом и как только встречаем комнату с нашим ID — это и есть координаты rx_map и ry_map.
var rx_map, ry_map, br=false;
for (ry_map=0;ry_map<ds_grid_height (MAP);ry_map++) {
for (rx_map =0;rx_map <ds_grid_width (MAP);rx_map ++) {
if MAP[# rx_map, ry_map]==room then { br=true; break };
}
if br then break;
}
Координаты игрока на карте считаются простым суммированием координаты комнаты и координаты игрока внутри комнаты
px_map = rx_map + px_room;
py_map = ry_map + py_room;
Теперь нужно посчитать смещение игрока внутри изначального экрана, который по сути ячейка.
Это нужно чтобы в следующей комнате выставить игрока с нужным смещением.
p_dx = x-px_room*screen_w;
p_dy = y-py_room*screen_h;
Далее, я знаю направление перехода игрока. Если x<0 то игрок идёт влево, если x>room_width, то вправо, аналогично и по вертикали. Значит теперь я могу взять ячейку, в которую игрок переходит,
if x<0 && px_map>0 then px_ma --;
if x>room_width && px_map<ds_grid_width (MAP)-1 then px_map++;
if y<0 && py_map>0 then py_map--;
if y>room_height && py_map<ds_grid_height (MAP)-1 then py_map++;
newroom=MAP[# p_mx, p_my];
Здесь я сразу меняю текущие координаты игрока, т.к. они больше не пригодятся.
Дальше примерно то же самое что было выше, но наоборот. Двойным проходом по карте находим левый верхний угол новой комнаты rx_map и ry_map
br=false;
for (ry_map=0;ry_map<ds_grid_height (MAP);ry_map++) {
for (rx_map =0;rx_map <ds_grid_width (MAP);rx_map++) {
if MAP[# rx_map, ry_map]==newroom then { br=true; break };
}
if br then break;
}
Здесь я сразу меняю текущие координаты комнаты в матрице, т.к. они больше не пригодятся.
Теперь мы знаем позицию игрока в матрице и координаты новой комнаты в матрице. Легко находим экран, которому будет ссответствовать позиция игрока в этой комнате, перезаписываем переменные:
px_room = px_map — rx_map;
py_room = py_map — ry_map;
Зная смещение внутри старой комнаты выставляем координаты игрока в новой комнате. Но не всё так просто, есть разные варианты для горизонтальных и вертикальных переходов:
if x>0 && x<room_width then
MAP_player[0]=px_room *screen_w+p_dx;
else {
if x<0 then MAP_player[0]=(px_room +1)*screen_w-8;
if x>room_width then MAP_player[0]=(px_room)*screen_w+8;
}
if y>0 && y<room_height then
MAP_player[1]=py_room *screen_h+p_dy;
else {
if y<0 then MAP_player[1]=(py_room +1)*screen_h-8;
if y>room_height then MAP_player[1]=(py_room)*screen_h+8;
}
Вроде всё.
Итак, чем же это лучше метода триггеров?
1) Вся информация о том куда переходить берётся из единой матрицы. Если я захочу поменять местами комнаты, я меняю их только в этой матрице и ничего не трогаю в самой комнате (может быть положение выхода поправить в одной из комнат)
2) Моя карта комнат — это уже практически готовая миникарта. Для полноты картины только список рёбер сделать. Это можно автоматизировать если сделать редактор этой миникарты, а его точно нужно будет делать.
Но есть и специфичные недостатки у этого метода.
Например то что переходы только по двум осям и что комнаты должны быть кратного размера. Можно придумать игру, в которой это будет неудобно.Стыковать комнаты по диагоналям, делать комнаты неправильной формы
Но я расматривал комнаты «как в метроидах и каслваниях» — и в данных условиях этот метод полностью рабочий. Можно его слегка модифицировать, чтобы он считал переходы между непрямоугольными комнатами, составленными из экранов. Тогда нужно будет вводить триггеры внутри комнаты и для них дописать немного логики, но пресчёт координат по матрице будет работать точно так же.
Звучит довольно просто, но до этого меня эта задача немного пугала. Мерещились очень сложные перерасчёты и самые разные вариации.
- 18 июля 2019, 04:08
- 010
Да кто тебе сказал что комнаты должны иметь абсолютно правильные геометрические размеры? Игрок что, с лупой будет лазать и мерять линейкой размер комнаты и проверять - совпадает ли он с размером на карте?
Квадратные комнаты делали только потому чтобы более экономно использовать память приставки, разбивая весь мир на фрагменты одинакового размера. Сейчас ты можешь делать уровни любых размеров и формы.
Если у тебя не рандомный мир, то карта уровней вообще может быть одной отдельной картинкой, которую ты сам потом нарисовал. В итоге в игре вся структура уровней будет формироваться только с помощью переходов между комнатами. Останется в самой комнате хранить координаты ее верх. левого угла на миникарте (или номер клетки по X, Y, если у тебя все комнаты кратны размеру клетки). Тогда точку персонажа на миникарте рисуешь исходя из этих координат и координат персонажа в самой комнате.
У меня в DrawColor у каждой комнаты было 4 границы перехода - верхняя, нижняя, левая, правая. При пересечении границы код попадал в обработчик комнаты и он, в зависимости от того в какой точке игрок пересек границу, решал - в какую комнату, в какую ее точку перенести персонажа. Это все прописывалось жесткой логикой, поскольку размеры комнат могли иметь любой размер (но всегда были только прямоугольными, AABB). Если обработчик возвращал "1", то перехода не происходило и персонаж как будто утыкался в стену. Могло вернуться "0", если там пустота и "-1", если персонаж должен сразу погибнуть (например, падение в пропасть).
Если же обработчик вернул список с названием комнаты и координатами, то персонаж переносился в эту комнату, в заданные координаты. Координаты выбирались так, чтобы оказаться на небольшом расстоянии (~10 пикселей) от края экрана.
Список всех комнат хранился глобально. Все противники и предметы при выходе из комнаты - удалялись, при входе - создавались заново (как в Смоле).
До реализации миникарты дело еще не дошло, есть только рисунок в Гимпе. Пока эта игра заморожена.
Внутри комнаты размеры могут быть геометрически неправильными, но экраны должны соответствовать друг-друго глобально. Я видел пару раз где это было не так. Чувствуется это всё, чувствуется даже без лупы.
Я планирую часть уровней генерить процедурно. Это будут локации побочных квестов для фарма ресурсов. В остальном я уже догадался сделать так как ты сказал =)
Это я тоже уже сделал и описал в этом самом посте.
Так как ты написал делается в GameMaker автоматичекски. У меня не так будет, я буду делать как в соус-лайк играх. Противники не возрождаются пока игрок не возьмёт чекпоинт. У меня будет список, в которй каждый поверженный враг будет записывать ID состоящий из номера комнаты и координат стартовой позиции. И при входе в комнату самоуничтожаться если он попал в этот список. На чекпоинте я этот список буду просто обнулять.
Я миникарту уже сделал, причём прям ка в метроидах. Надеюсь, сегодня выложу видос.
Зачем? Не проще ли сохранять целиком состояние каждой локации в памяти, вместе со всеми объектами и врагами и сбрасывать его при сохранении?
Но вообще, на мой взгляд, это плохая практика. Одна из вещей, которая меня раздражает в ДС. Зачем? Зачем заново создавать всех врагов при сохранении? Это должно происходить при загрузке, а не сохранении!
Не проще. Если у меня будет под сотню комнат, например. А так у меня будет не сильно большой список
Но на самом деле я ещё не продумывал интеракции с цифровым миром. Вот там то вообще весело будет, т.к. я хочу сохранять нейронные цепи.
По мне это как раз очень крутая штука сколько раз я её ни видел. Это формирует у игрока сессии. Один проход между чеками - это одна сессия. Это мотивация как можно дольше держать сессию, т.к. тебе не нужно зачищать от врагов комнату каждый раз. Больше экслоринга по той же причине, если ты пытаешься понять как понять в какое-то место и шныряешь между несокльки комнатами. + это позволяет понерфить фарм ресурсов и хилок в конкретных комнатах.
Меня наоборот раздражает когда все враги ресаются при заходе в комнату. В эксплоррейшонах это сильно надоедает и ты уже просто пролетаешь комнаты пропуская противников, лишь бы исследовать локацию до конца.
Так не надо хранить состояние каждой комнаты. Храни содержимое только тех, в которую ты уже зашел после сохранения. А на 100 комнат можно хранить состояние последних пяти, в к-е ты заходил, или сбрасывать состояние посещенных ранее комнат по таймеру.
... когда надо опять туда идти и опять мочить тех же врагов. "Отличный" способ растягивать игру без нового контента, постоянным повторением старого челенджа.
Ты же сам бесишься от корпсрана и постоянного повторения одних и тех же действий?
Если взобновлять врагов каждый раз при заходе в комнату - это ещё хуже. Если при загрузке игры - игрок будет ходить по пустым комнатам. Респавн врагов при взятии чека - отличный компромисс. Можно попробовать респавнить врагов по таймеру. Не очень сложно так то.
Я против тупого и неосмысленного корпсрана, но действия в моей игре и не будут прям одни и те же. Я же писал уже про систему с засадами. Довольно большую роль играет дизайн уровней. В Hollow Knight он очень посредственный и там действительно неинтересно возвращаться за собственным трупом.