Делаем карту как в Метроидваниях

Думаю, многие знают о метроидваниях не по наслышке. Одной из характерных черт (но необязательных) является игровой мир, поделённый на комнаты. Вот пара классических примеров:

Super Metroid

Image result for super metroid map

Metroid Fusion

Image result for metroid fusion map screen

Metroid Zero Mission

Image result for metroid zero mission map screen

AM2R

Related image

Samus Returns

Related image

И немного оффтопом карта из Metroid Prime:

Image result for metroid prime map screen

Castlevania Circle of the moon

Related image

Castlevania Harmony Of Dissonance

Image result for castlevania harmony of dissonance map screen

Castlevania Aria Of Sorrow

Image result for Castlevania: Aria of Sorrow map screen

Castlevania Symphony of the night

Image result for Castlevania symphony of the night map screen

Hollow Knight

Image result for Hollow Knight map screen

Ori and the Blind Forest

Image result for Ori map screen

Environmental Station Alpha

Image result for Environmental station alpha map screen

Valdis Story

Image result for Valdis Story map screen

Sundered

Image result for Sundered map screen

Song of Deep

Related image

Insanely Twisted Shadow Planet

Axiom Verge

Image result for Axiom Verge map screen

Hero Core

Image result for hero core map screen

Как видно из примеров, делают по-разному. Есть карты совсем упрощённые — геометрически простые комнаты. Есть карты, показывающие рельеф. Геометрические комнаты могут быть упрощены до комбинации квадратов или же иметь косые углы. Я думаю, что это детали и что если каждой комнате сопоставить картинку то можно рисовать вместо прямоугольников эту картинку.

И комнаты могут иметь чисто прямоугольные формы. Т.к. я ещё новичок в такого рода делах, то меня это абсолютно устраивает, хотя я почти уверен, что мой метод можно легко расширить на непрямоугольные комнаты.


Что такое комната? В Game Maker Studio это ассет, сцена, в которой разработчик расставляет объекты. Она имеет определённые размеры и ещё ряд настроек несущественных в данном посте. Можно добавлять объекты в комнату в рантайме и даже собирать уровни или загружать если они были созданы заранее в другом редакторе. Но это парсинг данных и всё такое. Поэтому вариант догрузки нужной комнаты встык — это не мой вариант. Можно пофантазировать как это могло бы быть и выяснилось бы что там свои геморрои — например нужно хранить связи входов и выходов, их нужно помечать прям внутри комнат, что влечёт ещё один менеджмент и обнаруживает очередной камень преткновения в реализации.

Так что в данном посте я рассматриваю набор заранее созданных комнат и переходов между ними.


Когда-то я делал что-то подобное в своей игре BrainStrom:TowerBombarde

ky4eUHd

Я вдохновлялся Hero Core, поэтому там комнаты были одного размера. Я их хранил в матрице и переходы считал по факту выхода игрока за пределы этой комнаты. Это элементарная фигня:
1) Игрок вышел за пределы комнаты
2) Посмотрели какая клетка следующая
3) Считаем сдвиг по X (если игрок переходит вертикально) или Y (если игрок переходит горизонтально)
4) Если X или Y меньше нуля — значит в новой комнате эта координата будет равна ширине/высоте комнаты, а другая — сдвигу который считали на шаге 3), аналогично если X или Y больше ширины/высоты комнаты

Когда комнаты разного размера, и даже разной формы — задача меняется.

И тогда и возникает проблема: «Как позиционировать игрока в новой комнате?
Как понять в какую дверь игрок вышел и в какую вошёл?»

Сперва поймём как можно хранить связи между комнатами. Один из способов — ассоциация дверей/триггеров одной комнаты с дверьми/триггерами в другой. Довольно гибкий способ решающий проблему выше, но он требует поддержки данных внутри каждой комнаты, плюс непонятно как выводить миникарту. Обход какой-то мутить что ли? А если я захочу комнаты махнуть местами? Много вопросов.


Поэтому я храню карту комнат в виде матрицы. Все мои комнаты имеют размеры кратные самой маленькой комнате, а самая маленькая комната помещается тютелька-в-тютельку на экран — и это одна ячейка матрицы. Если комната большая — она занимает сразу несколько ячеек.

Из вот такого

srvzud9

Получаем вот такое

VGdHId3

По сути это раздробление локации на экраны, и каждая локация занимает определённое количество экранов. Эта задача ПРИМЕРНО сводится к той, что я решал ранее, но координаты придётся пересчитать.

Итак, первое, что нужно понять — это положение игрока в этой карте. Если он находится в первой комнате, то в какой из её ячеек?

vpIHSUN

Это считается просто, если знаем размер экрана в пикселях. Например, ширина — это 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);

Это я посчитал координаты игрока относительно комнаты. А нужно посчитать внутри карты. Для этого нужно найти координату верхнего левого угла комнаты в карте.

5eqHAQG

Просто обходим массив обычным двойным циклом и как только встречаем комнату с нашим 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;

Теперь нужно посчитать смещение игрока внутри изначального экрана, который по сути ячейка.

Q3x0BL2

Это нужно чтобы в следующей комнате выставить игрока с нужным смещением.

p_dx = x-px_room*screen_w;
p_dy = y-py_room*screen_h;

Далее, я знаю направление перехода игрока. Если x<0 то игрок идёт влево, если x>room_width, то вправо, аналогично и по вертикали. Значит теперь я могу взять ячейку, в которую игрок переходит, т. е. ID следующей комнаты.

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

1favGmD

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) Моя карта комнат — это уже практически готовая миникарта. Для полноты картины только список рёбер сделать. Это можно автоматизировать если сделать редактор этой миникарты, а его точно нужно будет делать.

Но есть и специфичные недостатки у этого метода.
Например то что переходы только по двум осям и что комнаты должны быть кратного размера. Можно придумать игру, в которой это будет неудобно.Стыковать комнаты по диагоналям, делать комнаты неправильной формы и т. д.

Но я расматривал комнаты «как в метроидах и каслваниях» — и в данных условиях этот метод полностью рабочий. Можно его слегка модифицировать, чтобы он считал переходы между непрямоугольными комнатами, составленными из экранов. Тогда нужно будет вводить триггеры внутри комнаты и для них дописать немного логики, но пресчёт координат по матрице будет работать точно так же.


Звучит довольно просто, но до этого меня эта задача немного пугала. Мерещились очень сложные перерасчёты и самые разные вариации.