Поскольку мы разобрались с основной идеей создания уровня подземелья, то пришло время запустить редактор и добавить код в нашу игру. На изображении выше, можно увидеть результат наших трудов по отображению карты подземелья. Вы уже видели некоторые куски кода, но уровнем подземелья у нас будет объект, поэтому нам нужно будет добавить кое какие изменения. Весь код, связанный с картой будет находится в файле map.bi, а начнем мы, пожалуй, с рассмотрения определений и значений констант.
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-мя значениями, это позволит нам менять размерность сетки, если нам это понадобиться. Все остальное как и раньше.
map.bi'Тип местности на карте. 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 SubGenerateDungeonLevel вызывается из основной программы, чтобы создать новый уровень подземелья. Первое что мы должны сделать, очистить текущую информацию о уровне, после этого можем создавать новый. Затем мы вызываем 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 является расчетным значением, поэтому данное свойство позволяет только получить значение максимально возможного здоровья персонажа.
Это первый раз, когда мы коснулись кода генерации и отображения подземелья. Мы будем возвращаться к нему снова и снова, чтобы добавлять новые возможности в нашу игру, но, вначале, мы должны добавить возможность исследовать созданное подземелье, чтобы просто убедиться что все работает так, как мы и задумывали. Это означает, что мы должны реализовать какой то код для возможности передвижения нашего персонажа, чем мы и займемся в следующей главе.

Комментариев нет:
Отправить комментарий