Давайте сделаем рогалик (Подземелье)
Поскольку мы разобрались с основной идеей создания уровня подземелья, то пришло время запустить редактор и добавить код в нашу игру. На изображении выше, можно увидеть результат наших трудов по отображению карты подземелья. Вы уже видели некоторые куски кода, но уровнем подземелья у нас будет объект, поэтому нам нужно будет добавить кое какие изменения. Весь код, связанный с картой будет находится в файле map.bi, а начнем мы, пожалуй, с рассмотрения определений и значений констант.
'Размер карты. #DEFINE mapw 100 #DEFINE maph 100 'Максимальный и минимальный размер комнаты #DEFINE roommax 8 #DEFINE roommin 4 #DEFINE nroommin 20 #DEFINE nroommax 50 'Флаг пустой ячейки. #DEFINE emptycell 0 'Ширина и высота экрана обзора. #DEFINE vw 40 #DEFINE vh 55 'Размер ячейки сетки (высота и ширина) #DEFINE csizeh 10 #DEFINE csizew 10 'Количество ячеек в сетке (по ширине и высоте). Const gw = mapw \ csizew Const gh = maph \ csizeh
Как вы видите, мы используем многие элементы кода из предыдущей главы,
однако, есть кое что новое. vw и vh, это ширина и высота области экрана для
отображения карты. Высота и ширина ячейки сетки теперь задана 2-мя значениями,
это позволит нам менять размерность сетки, если нам это понадобиться. Все
остальное как и раньше.
'Тип местности на карте. Enum terrainids tfloor = 0 'Пол (можно передвигаться). twall 'Стена (нельзя передвигаться). tdooropen 'Открытая дверь. tdoorclosed 'Закрытая дверь. tstairup 'Лестница вверх. tstairdn 'Лестница вниз. End Enum 'Размер комнаты. Type rmdim rwidth As Integer rheight As Integer rcoord As mcoord End Type 'информация о комнате Type roomtype roomdim As rmdim 'Ширина и высота комнаты. tl As mcoord 'Прямоугольник комнаты br As mcoord secret As Integer End Type 'Структура ячейки сетки. Type celltype cellcoord As mcoord 'Позиция ячейки. Room As Integer 'Индекс комнаты в массиве комнат. End Type 'Информация о ячейке карты Type mapinfotype terrid As terrainids 'Тип местности. hasmonster As Integer 'Монстр в текущей ячейке. monidx As Integer 'Индекс монстра в массиве монстров. hasitem As Integer 'Предмет в ткущей ячейке. visible As Integer 'Персонаж видит ячейку. seen As Integer 'Персонаж уже видел ячейку. End Type 'Информация об уровне подземелья. Type levelinfo numlevel As Integer 'Current level number. lmap(1 To mapw, 1 To maph) As mapinfotype 'Map array. End Type
Большую часть из предыдущего кода вы уже видели, но мы добавили новое
перечисление terrainids, которое содержит типы местности, а также новое
определение типа mapinfotype, которое содержит информацию по текущему тайлу в
массиве уровня.
Каждый тайл содержит несколько элементов данных, связанных с ним. Тип местности содержится в переменной terrid. Если в данной ячейке карты находится монстр, то переменная hasmonster устанавливается в TRUE, и поле monidx будет содержать индекс монстра в массиве монстров. Поле hasitem устанавлен в TRUE, если на тайле находится какой либо предмет. Когда мы доберемся до реализации предметов и инвентаря, то сможем просмотреть массив предметов, и определить, что за предмет находится в этой ячейке. Два последних поля, visible и seen показывают, соответственно, видит ли персонаж данный тайл в текущий момент, и видел ли он его раньше. Другое новое определение — levelinfo, содержит идентификатор текущего уровня и массив карты, который состоит из элементов mapinfotype, чтобы мы могли отслеживать все сведения, связанные с каждым тайлом.
Все эти типы данных используются в объекте уровня нашего подземелья
map.bi
'Объект уровня подземелья. Type levelobj Private: _level As levelinfo 'Структура карты уровня. _numrooms As Integer 'Номер комнат на уровне. _rooms(1 To nroommax) As roomtype 'Информация о комнатах. _grid(1 To gw, 1 To gh) As celltype 'Информация о ячейках сетки. _blockingtiles As Integer Ptr 'Список типов тайлов блокирующих обзор. _blocktilecnt As Integer 'Кол-во типов тайлов блокирующих обзор. Declare Function _BlockingTile(tx As Integer, ty As Integer) As Integer 'Returns true if blocking tile. Declare Function _LineOfSight(x1 As Integer, y1 As Integer, x2 As Integer, y2 As Integer) As Integer 'Returns true if line of sight to tile. Declare Function _CanSee(tx As Integer, ty As Integer) As Integer 'Может ли персонаж видеть тайл. Declare Sub _CalcLOS () 'Рассчитывает прямую видимость с последующей пост обработкой для удаления артефактов Declare Function _GetMapSymbol(tile As terrainids) As String 'Возвращает ascii символ для заданного ID местности. Declare Function _GetMapSymbolColor(tile As terrainids) As Uinteger Declare Sub _InitGrid() 'Инициализировать сетку. Declare Sub _ConnectRooms( r1 As Integer, r2 As Integer) 'Соединить комнаты. Declare Sub _AddDoorsToRoom(i As Integer) 'Добавить двери в комнату. Declare Sub _AddDoors() 'Добавить двери во все комнаты. Declare Sub _DrawMapToArray() 'Добавить данные из сетки в массив карты. Public: Declare Constructor () Declare Destructor () Declare Property LevelID(lvl As Integer) 'Устанавливает текущий номер уровня. Declare Property LevelID() As Integer 'Возвращает текущий номер уровня. Declare Sub DrawMap () 'Вывести карту на экран. Declare Sub GenerateDungeonLevel() 'Создать новый уровень подземелья. End Type
Как вы можете видеть, большая часть кода находится в приватном блоке, так
как она используется только для генерации уровня и отображения карты. Большую
часть кода вы уже видели. Давайте взглянем на элементы данных.
map.bi
... _level As levelinfo 'Структура карты уровня. _numrooms As Integer 'Номер комнат на уровне. _rooms(1 To nroommax) As roomtype 'Информация о комнатах. _grid(1 To gw, 1 To gh) As celltype 'Информация о ячейках сетки. _blockingtiles As Integer Ptr 'Список типов тайлов блокирующих обзор. _blocktilecnt As Integer 'Кол-во типов тайлов блокирующих обзор. ...
Данные в переменных _level, _numrooms, _rooms и _grid такие же как и в
предыдущей главе. В списке blockingtiles приведен список типов местности,
которые блокируют линию прямой видимости. blocktilecnt это количество элементов
в списке. Этот список используется в расчете прямой видимости персонажа, чтобы
определить тайлы карты, которые персонаж видит в данный момент. Остальные
функции и процедуры используются, чтобы создать подземелье, так что мы
рассмотрим каждую из них.
map.bi
'Создать новый уровень подземелья. Sub levelobj.GenerateDungeonLevel() Dim As Integer x, y 'Очистим уровень For x = 1 To mapw For y = 1 To maph 'Установим тайл стены _level.lmap(x, y).terrid = twall _level.lmap(x, y).visible = FALSE _level.lmap(x, y).seen = FALSE _level.lmap(x, y).hasmonster = FALSE _level.lmap(x, y).hasitem = FALSE Next Next _InitGrid _DrawMapToArray End Sub
GenerateDungeonLevel вызывается из основной программы, чтобы создать
новый уровень подземелья. Первое что мы должны сделать, очистить текущую
информацию о уровне, после этого можем создавать новый. Затем мы вызываем
InitGrid, который вы уже видели в предыдущей главе, а затем DrawMapToArray,
который передает данные из сетки в массив карты. Давайте посмотрим на
DrawMapToArray, так как мы добавили некоторые новые функции которых раньше не
было.
map.bi
'Добавить данные из сетки в массив карты. Sub levelobj._DrawMapToArray() Dim As Integer i, x, y, pr, rr, rl, ru, kr 'Запишем первую комнату в массив карты For x = _rooms(1).tl.x + 1 To _rooms(1).br.x - 1 For y = _rooms(1).tl.y + 1 To _rooms(1).br.y - 1 _level.lmap(x, y).terrid = tfloor Next Next 'Запишем остальные комнаты в массив карты и соединим их. For i = 2 To _numrooms For x = _rooms(i).tl.x + 1 To _rooms(i).br.x - 1 For y = _rooms(i).tl.y + 1 To _rooms(i).br.y - 1 _level.lmap(x, y).terrid = tfloor Next Next _ConnectRooms i, i - 1 Next 'Добавим двери во все комнаты. _AddDoors 'Установим позицию персонажа на карте. x = _rooms(1).roomdim.rcoord.x + (_rooms(1).roomdim.rwidth \ 2) y = _rooms(1).roomdim.rcoord.y + (_rooms(1).roomdim.rheight \ 2) pchar.Locx = x - 1 pchar.Locy = y - 1 'Установим лестницу вверх. _level.lmap(pchar.Locx, pchar.Locy).terrid = tstairup 'Установим лестницу вниз в последней комнате. x = _rooms(_numrooms).roomdim.rcoord.x + (_rooms(_numrooms).roomdim.rwidth \ 2) y = _rooms(_numrooms).roomdim.rcoord.y + (_rooms(_numrooms).roomdim.rheight \ 2) _level.lmap(x - 1, y - 1).terrid = tstairdn End Sub
Большую часть кода мы уже видели, но мы добавили вызов подпрограммы
AddDoors, которая, очевидно, добавляет двери в комнатах, мы добавили код для
установки расположения нашего персонажа вместе с типом местности лестниц.
Лестница до предыдущего уровня находится в той же комнате, где и персонаж,
которая является первой в списке комнат.
Лестница вниз расположена в последней комнате из списка. Так как комнаты расположены на карте случайным образом, то лестницы будут в случайных местах, но никогда — в одной и той же комнате. Используя список комнат, сделать это очень легко. Конечно, две комнаты могут быть рядом друг с другом, но высока вероятность того, что есть некоторое расстояние между ними.
map.bi
'Добавить двери во все комнаты. Sub levelobj._AddDoors() For i As Integer = 1 To _numrooms _AddDoorsToRoom i Next End Sub
Подпрограмма AddDoors просто перебирает все комнаты из списка, и для
каждой вызывает AddDoorsToRoom.
map.bi
'Добавляет двери в комнату. Sub levelobj._AddDoorsToRoom(i As Integer) Dim As Integer row, col, dd1, dd2 'Проверка верхней стены комнаты. For col = _rooms(i).tl.x To _rooms(i).br.x dd1 = _rooms(i).tl.y dd2 = _rooms(i).br.y 'Если нашли пол вместо стены. If _level.lmap(col, dd1).terrid = tfloor Then 'Add door. _level.lmap(col, dd1).terrid = tdoorclosed Endif 'Проверка нижней части комнаты. If _level.lmap(col, dd2).terrid = tfloor Then _level.lmap(col, dd2).terrid = tdoorclosed End If Next 'Проверим левую стену. For row = _rooms(i).tl.y To _rooms(i).br.y dd1 = _rooms(i).tl.x dd2 = _rooms(i).br.x If _level.lmap(dd1, row).terrid = tfloor Then _level.lmap(dd1, row).terrid = tdoorclosed End If 'Проверим правую стену. If _level.lmap(dd2, row).terrid = tfloor Then _level.lmap(dd2, row).terrid = tdoorclosed Endif Next End Sub
Этот код просто проверяет все стены в комнате, и если обнаруживает тип
местности «пол», то заменяет его на тип местности «закрытая дверь». За один цикл
проверяются 2 стены: верхняя — нижняя. и левая — правая. Мы используем массив
комнат, чтобы получить информацию о комнате, так что мы не должны обследовать
всю карту в поисках дверных проемов. Это делает процесс очень эффективным.
После того, как уровень создан, нам необходимо отобразить карту на экране, но для этого нужно определить тайлы карты, которые персонаж может видеть. Есть два условия, которые описывают видимые тайлы на карте: FOV (field of view) - поле зрения, и LOS (line of sight) - линия прямой видимости. Иногда эти 2 термина используются как синонимы, но они относятся к разным аспектам видимости карты и рассчитываются по разному.
Поле зрение представляет собой площадь, которую актер может видеть, и измеряется в градусах. Если окулист проверял ваше периферийное зрение, то что он проверял, это и есть поле зрения, т. е. то, какую область вы можете видеть за один раз. В большинстве шутерах от первого лица используется FOV 90 градусов по горизонтали, а по вертикали рассчитывается в соответствии с соотношением сторон монитора. Это дает хороший обзор и снижает эффект искажения пространства. Для расчета FOV нам нужно знать направление взгляда и угол обзора. Затем с помощью некоторых тригонометрических функций вычисляется площадь, которую актер может видеть. Большинство рогаликов имеют угол обзора 360 градусов, что бы мы могли эффективно игнорировать все расчеты поля зрения. Однако, если вам нужно будет написать рогалик основанный на стелс режиме, то вам нужно придется рассчитывать угол обзора, прежде чем вычислить, что актер может видеть на карте. Таким образом персонаж сможет подкрасться к монстру или NPC (не игровому персонажу) так, чтобы те его не заметили.
FOV дает вам набор тайлов, которые персонаж потенциально может увидеть, а видит ли он их реально или нет, зависит от прямой видимости, или расчета LOS. Если дверь находится в поле видимости персонажа, а монстр стоит перед дверью, то монстр блокирует прямую видимость. И персонаж не увидит дверь. Мы должны показать это на карте не рисуя дверь.
Существуют различные методы расчета прямой видимости, от очень сложных, комплексных алгоритмов, до простейшей трассировки лучей. Мы будем использовать трассировку лучей, т. к. это быстро и дает хорошие результаты (с шагом пост обработки), и он довольно прост. Для трассировки лучей вы просто выбираете тайл на карте и проводите линию от выбранного тайла до персонажа. Если вы достигли персонажа прежде, чем попали на тайл, блокирующий линию прямой видимости, то персонаж может видеть выбранный тайл. Давайте посмотрим на код.
map.bi
'Рассчитывает прямую видимость с последующей пост обработкой для удаления артефактов 'Caclulate los with post processing. Sub levelobj._CalcLOS () Dim As Integer i, j, x, y, v = vw / 2, h = vh / 2 Dim As Integer x1, x2, y1, y2 'Очичтить карту видимости For i = 1 To mapw For j = 1 To maph _level.lmap(i, j).visible = FALSE Next Next 'Проверяем только то что, что попало в область отображения x1 = pchar.Locx - v If x1 < 1 Then x1 = 1 y1 = pchar.Locy - h If y1 < 1 Then y1 = 1 x2 = pchar.Locx + v If x2 > mapw - 1 Then x2 = mapw - 1 y2 = pchar.Locy + h If y2 > maph - 1 Then y2 = maph - 1 'Перебор области видимости. For i = x1 To x2 For j = y1 To y2 'Не расчитыва то, что уже видим If _level.lmap(i, j).visible = FALSE Then If _CanSee(i, j) = TRUE Then _level.lmap(i, j).visible = TRUE _level.lmap(i, j).seen = TRUE End If End If Next Next ...
Этот код рассчитывает прямую видимость, остальные подпрограммы — шаг пост
обработки, которые мы рассмотрим чуть позже. Первое, что нужно сделать, это
очистить карту видимости. Мы отмечаем все тайлы на карте как невидимые. Теперь
нам нужно только проверить те тайлы, которые находятся в пределах области
отображения карты (так как остальная часть карты не будет видна в любом случае),
поэтому мы задаем окно просмотра, которая соответствует отображаемой области с
персонажем в центре и проверяем только ее.
Однако, если персонаж находится на краю карты, то после расчета окна просмотра, у нас могут получиться отрицательные координаты, или координаты, которые больше чем размерность массива карты. Поэтому мы добавляем дополнительные проверки на выход за размерность массива и усекаем окно просмотра, если необходимо. После этого мы проверяем на видимость все тайлы из области просмотра при помощи функции CanSee.
map.bi
'Проверяет, может ли игрок видеть объект. Function levelobj._CanSee(tx As Integer, ty As Integer) As Integer Dim As Integer ret = FALSE, px = pchar.Locx, py = pchar.Locy Dim As Integer dist dist = CalcDist(pchar.Locx, tx, pchar.Locy, ty) If dist <= vh Then ret = _LineOfSight(tx, ty, px, py) End If Return ret End Function
Первое, что мы делаем, это вычисляем расстояние до проверяемого тайла, и
если оно больше, чем вертикальное расстояние до края экрана, то мы ее не видим.
Нам необязательно делать эту проверку, т. к. у нас задано окно просмотра, но
если нам понадобиться добавить в игру расы персонажа у которых различается
дальность обзора (например эльф может видеть дальше чем гном). То мы просто
заменим vh на характеристику персонажа «дальность видимости». Расстояние
рассчитывается при помощи быстрой версии стандартной формулы расчета расстояния
и почти не влияет на производительность. CalcDist содержится в utils.bi.
Если тайл находится в пределах диапазона видимости персонажа, то мы рассчитываем прямую видимость, строя луч к координате позиции персонажа.
map.bi
'Алгоритм Брезенхе?ма для линий Function levelobj._LineOfSight(x1 As Integer, y1 As Integer, x2 As Integer, y2 As Integer) As Integer Dim As Integer i, deltax, deltay, numtiles Dim As Integer d, dinc1, dinc2 Dim As Integer x, xinc1, xinc2 Dim As Integer y, yinc1, yinc2 Dim isseen As Integer = TRUE deltax = Abs(x2 - x1) deltay = Abs(y2 - y1) If deltax >= deltay Then numtiles = deltax + 1 d = (2 * deltay) - deltax dinc1 = deltay Shl 1 dinc2 = (deltay - deltax) Shl 1 xinc1 = 1 xinc2 = 1 yinc1 = 0 yinc2 = 1 Else numtiles = deltay + 1 d = (2 * deltax) - deltay dinc1 = deltax Shl 1 dinc2 = (deltax - deltay) Shl 1 xinc1 = 0 xinc2 = 1 yinc1 = 1 yinc2 = 1 End If If x1 > x2 Then xinc1 = - xinc1 xinc2 = - xinc2 End If If y1 > y2 Then yinc1 = - yinc1 yinc2 = - yinc2 End If x = x1 y = y1 For i = 2 To numtiles If _BlockingTile(x, y) Then isseen = FALSE Exit For End If If d < 0 Then d = d + dinc1 x = x + xinc1 y = y + yinc1 Else d = d + dinc2 x = x + xinc2 y = y + yinc2 End If Next Return isseen End Function
Как видите, мы просто используем алгоритм Брезенхе?ма для построения
линий. Есть множество описаний этого алгоритма в интернете, так что наберите в
google «алгоритм брезенхема», если вы хотите получить подробное описание
процесса. Мы проводим линию от тайла к персонажу, а не наоборот, чтобы получить
симметричную область прямой видимости. Это поможет избавиться от проблемы
заглядывания за углы, которая, иногда, возникает.
Для того, чтобы определить, видимый ли проверяемый тайл, мы проверяем тайлы, через которые проходит луч, на блокировку видимости.
map.bi
'Returns True if tile is blocking tile. Function levelobj._BlockingTile(tx As Integer, ty As Integer) As Integer Dim ret As Integer = FALSE Dim tid As terrainids = _level.lmap(tx, ty).terrid 'Если на тайле стоит монстр, то обзор блокируется. If _level.lmap(tx, ty).hasmonster = TRUE Then ret = TRUE Else 'Убедимся что указатель инициализирован. If _blockingtiles <> NULL Then 'Ищем текущий тайл в списке. For i As Integer = 0 To _blocktilecnt - 1 'Найдено, значит обзор блокируется. If _blockingtiles[i] = tid Then ret = TRUE Exit For Endif Next End If Endif Return ret End Function
Вначале мы проверяем, находится ли на данном тайле монстр, и если это
так, то обзор блокируется. Мы могли бы пропустить этот шаг, но это несколько
усложнит нашу игру и добавит в нее немного стратегии, т. к. игрок не будет знать
что находится за приближающимся к нему монстром. Возможно гуськом к игроку
приближается целая толпа монстров! Возможно, это глупо, если монстр крыса, но мы
можем добавить для монстров параметр: блокирует он обзор или нет, и проверять
блокировку прямой видимости в соответствии со значением этого параметра. Мы
подумаем об этом, когда доберемся до добавления в игру монстров.
Если тайл свободен от монстров, то мы проверяем тип тайла на карте с типами тайлов из списка блокирующих прямую видимость. Стены или закрытая дверь будут блокировать прямую видимость. По мере продвижения разработки, мы всегда можем дополнить этот список, поэтому мы и используем для него указатель, т. к. на данном этапе мы не знаем, сколько типов тайлов у нас в нем будет. В результате, указатель выступает в роли массива переменной длины, что облегчает внесения изменений в код.
Главным преимуществом использования алгоритма линии Брезенхема является то, что он работает очень быстро, но у него есть проблема появления артефактов на экране. Он использует целочисленную математику и, иногда, не может определить тайл стены, которая, по логике, должна быть видна. Это происходит потому, что алгоритм никогда не достигнет данного тайла из-за целочисленных вычислений. Мы можем исправить это, выполнив шаг пост обработки
map.bi
'Пост обработка карты для удаления артефактов. For i = x1 To x2 For j = y1 To y2 If (_BlockingTile(i, j) = TRUE) And (_level.lmap(i, j).visible = FALSE) Then x = i y = j - 1 If (x > 0) And (x < mapw + 1) Then If (y > 0) And (y < maph + 1) Then If (_level.lmap(x, y).terrid = tfloor) And (_level.lmap(x, y).visible = TRUE) Then _level.lmap(i, j).visible = TRUE _level.lmap(i, j).seen = TRUE Endif Endif Endif
Это только часть кода пост обработки, остальные части такие же, только
проверяют другие случаи. Выглядит достаточно запутанно, но большинство кода, это
просто проверка на то, что рассматриваемый тайл находится в пределах зоны
карты.
Мы перебираем тайлы из области просмотра, как и раньше. Для ткущего тайла мы проверяем, если он блокирует линию прямой видимости и персонаж ее не видит, если _BlockingTile(i, j) = TRUE и _level.lmap(i, j).visible = FALSE, то мы проверяем тайл, находящийся прямо под ним: y=j-1, если это тайл пола и он видим персонажем, то блокирующий обзор тайл мы делаем видимым: _level.lmap(i, j).visible = TRUE.
Это частный случай ситуации, когда стена расположено горизонтально и тайлы пола возле стены отмечены как видимые, но настенные тайлы отмечены как невидимые, поскольку алгоритм проверки прямой видимости не смог до них добраться. Это может произойти в горизонтальном коридоре, если персонаж стоит в его центре. Данная пост обработка сделает всю стену видимой, как и следовало ожидать. Остальная часть кода пост обработки работает также, только проверяет другие возможные ситуации, когда расчет прямой видимости мог пропустить необходимые тайлы.
После того, как прямая видимость рассчитана, мы можем нарисовать нашу карту.
map.bi
'Выведем карту на экран. Sub levelobj.DrawMap () Dim As Integer i, j, w = vw, h = vh, x, y, px, py, pct Dim As Uinteger tilecolor, bcolor Dim As String mtile Dim As terrainids tile _CalcLOS 'Получим координаты области обзора i = pchar.Locx - (w / 2) j = pchar.Locy - (h / 2) If i < 1 Then i = 1 If j < 1 Then j = 1 If i + w > mapw Then i = mapw - w If j + h > mapw Then j = mapw - h 'Нарисуем видимую часть карты. For x = 1 To w For y = 1 To h 'Очистим текущее знакоместо (нарисуем черный квадрат). tilecolor = fbBlack PutText acBlock, y, x, tilecolor 'Напечатаем тайл. If _level.lmap(i + x, j + y).visible = True Then 'Получим ID тайла tile = _level.lmap(i + x, j + y).terrid 'Получим ASCII символ тайла mtile = _GetMapSymbol(tile) 'Получим цвет тайла tilecolor = _GetMapSymbolColor(tile) 'Нарисуем маркер предмета. If _level.lmap(i + x, j + y).hasitem = True Then 'Тут обрабатываем предмет. Endif PutText mtile, y, x, tilecolor 'Если на тайле монстр, то нарисуем его If _level.lmap(i + x, j + y).hasmonster = TRUE Then 'Тут обрабатываем монстра. Endif Else 'Не на прямой видимости. If _level.lmap(i + x, j + y).seen = TRUE Then If _level.lmap(i + x, j + y).hasitem = True Then PutText "?", y, x, fbSlateGrayDark Else PutText mtile, y, x, fbSlateGrayDark End If End If End If Next Next 'Нарисуем персонажа px = pchar.Locx - i py = pchar.Locy - j pct = Int((pchar.CurrHP / pchar.MaxHP) * 100) If pct > 74 Then PutText acBlock, py, px, fbBlack PutText "@", py, px, fbGreen Elseif (pct > 24) AndAlso (pct < 75) Then PutText acBlock, py, px, fbBlack PutText "@", py, px, fbYellow Else PutText acBlock, py, px, fbBlack PutText "@", py, px, fbRed Endif End Sub
Вначале мы рассчитываем прямую видимость, затем создаем область
просмотра. Перебирая все тайлы в области просмотра, мы проверяем, видим тайл или
нет, если видим, то получаем символ тайла, его цвет и выводим на экран. Здесь мы
используем новую функцию PutText, которая отображает тайл на экране.
utils.bi
'Выводит текст в указанных строке и столбце. Sub PutText(txt As String, row As Integer, col As Integer, fcolor As Uinteger = fbWhite) Dim As Integer x, y x = (col - 1) * charw y = (row - 1) * charh Draw String (x, y), txt, fcolor End Sub
Эта подпрограмма преобразует текстовые координаты строк и столбцов в
пиксельные координаты и рисует в них строку. Координаты окна просмотра заданы в
текстовых координатах, т. е. строках и столбцах, поэтому нам нужно преобразовать
их в координаты пикселей экрана, которые использует функция Draw String. Это
равносильно использованию команды Locate для текстового режима.
Вы заметили, что подпрограмма DrawMap содержит заглушки для монстров и предметов, позже, мы добавим туда код отображения монстров и предметов. Пока же, пока у нас нет ни монстров ни предметов, мы просто рисуем пустое подземелье.
map.bi
... 'Не на прямой видимости. If _level.lmap(i + x, j + y).seen = TRUE Then If _level.lmap(i + x, j + y).hasitem = True Then PutText "?", y, x, fbSlateGrayDark Else PutText mtile, y, x, fbSlateGrayDark End If End If ...
Эта часть кода реализует «память» персонажа. Если тайл не находится на
прямой видимости, но персонаж видел его раньше, то мы отображаем его другим
цветом, так же мы рисуем символ «?» в том месте, где персонаж видел лежащий
предмет. Признак того, что персонаж уже видел тайл, устанавливается в
подпрограмме расчета прямой видимости CalcLOS, там же считается и дальность
видимости персонажа, т. е. изменяя дальность видимости персонажа мы можем
задавать дальность свечения факела или лампы.
Последняя часть кода просто рисует символ персонажа @.
map.bi
'Нарисуем персонажа px = pchar.Locx - i py = pchar.Locy - j pct = Int((pchar.CurrHP / pchar.MaxHP) * 100) If pct > 74 Then PutText acBlock, py, px, fbBlack PutText "@", py, px, fbGreen Elseif (pct > 24) AndAlso (pct < 75) Then PutText acBlock, py, px, fbBlack PutText "@", py, px, fbYellow Else PutText acBlock, py, px, fbBlack PutText "@", py, px, fbRed Endif
Цвет значка персонажа завит от уровня его здоровья. Мы вычисляем процент
здоровья персонажа из значений текущего здоровья и максимального, а затем
выбираем цвет основываясь на полученном проценте. При использования процентного
соотношения здоровья, мы уходим от абсолютных значений. Если персонаж в добром
здравии — значок будет зеленым, при смерти — красным. Это дает постоянную
визуальную подсказку о здоровье персонажа и держит игрока в напряжении, когда он
видит как цвет персонажа меняется с зеленого на желтый а потом и красный.
Последнее, что нам осталось рассмотреть в нашем объекте уровня подземелья, это конструктор и деструктор объекта.
map.bi
'Инициализируем объект. Constructor levelobj () 'Установим количество блокирующих обзор типов местности. _blocktilecnt = 3 'Выделим место для списка. _blockingtiles = Callocate(_blocktilecnt * Sizeof(Integer)) 'Заполним список типами местности. _blockingtiles[0] = twall _blockingtiles[1] = tdoorclosed _blockingtiles[2] = tstairup End Constructor 'Очистим объект. Destructor levelobj () If _blockingtiles <> NULL Then Deallocate _blockingtiles _blockingtiles = NULL Endif End Destructor
Конструктор объекта выполняется при его создании. В нашем случае он
выделяет память для динамического массива со списком типов местности которые
блокируют обзор и заполняет этот список. Мы используем здесь динамическое
выделение памяти для того, чтобы в любой момент мы могли изменить список типов
объектов, например добавить тип местности «статуя». Конечно, мы могли бы
использовать и массив фиксированного размера, но указатель на массив является
хорошим примером создания динамических массивов в описании типов переменных.
Деструктор вызывается когда объект выходит из области видимости программы и разрушается. Здесь мы просто проверяем, является ли указатель на наш список действительным и если это так, то очищаем выделенную для него в конструкторе память. После устанавливаем значение указателя равное NULL. Это всегда хорошая практика — присваивать указателю значение NULL после очистки занимаемой области памяти для того, чтобы мы всегда могли убедится, является ли указатель действительным.
Мы также добавили несколько новых свойств в объект персонажа, которые необходимы для процесса построения и вывода на экран карты уровня.
character.bi
'Объект персонажа. Type character Private: _cinfo As characterinfo Public: Declare Property CharName() As String 'Имя персонажа. Declare Property Locx(xx As Integer) 'Установить X координату персонажа. Declare Property Locx() As Integer 'Возвращает X координату персонажа. Declare Property Locy(xx As Integer) 'Установить Y координату персонажа. Declare Property Locy() As Integer 'Возвращает Y координату персонажа. Declare Property CurrHP(hp As Integer) 'Установить здоровье. Declare Property CurrHP() As Integer 'Возвращает значение здоровья. Declare Property MaxHP() As Integer 'Возвращает максимальное знамение здоровья. Declare Sub PrintStats () Declare Function GenerateCharacter() As Integer End Type
Свойства Locx и Locy возвращают текущие x и y координаты расположения
персонажа. Нам нужно будет иметь возможность задать эти координаты, когда мы
перейдем к движению персонажа, а также получить значения этих переменных для
расчета области просмотра отцентрированной по позиции нашего персонажа. Свойство
CurrHP позволяет как получить значение текущего здоровья персонажа, так и
установить его, поскольку это значение будет меняться в результате боевых
действий или применения лечебных трав. MaxHP является расчетным значением,
поэтому данное свойство позволяет только получить значение максимально
возможного здоровья персонажа.
Это первый раз, когда мы коснулись кода генерации и отображения подземелья. Мы будем возвращаться к нему снова и снова, чтобы добавлять новые возможности в нашу игру, но, вначале, мы должны добавить возможность исследовать созданное подземелье, чтобы просто убедиться что все работает так, как мы и задумывали. Это означает, что мы должны реализовать какой то код для возможности передвижения нашего персонажа, чем мы и займемся в следующей главе.
Перевод на русский: Fantik
содержание | назад | вперед