Делаем карту как в Метроидваниях
Думаю, многие знают о метроидваниях не по наслышке. Одной из характерных черт (но необязательных) является игровой мир, поделённый на комнаты. Вот пара классических примеров:
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
19 комментариев