суббота, 3 марта 2012 г.

Давайте сделаем рогалик. Глава 08: Подземелье

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

Это первый раз, когда мы коснулись кода генерации и отображения подземелья. Мы будем возвращаться к нему снова и снова, чтобы добавлять новые возможности в нашу игру, но, вначале, мы должны добавить возможность исследовать созданное подземелье, чтобы просто убедиться что все работает так, как мы и задумывали. Это означает, что мы должны реализовать какой то код для возможности передвижения нашего персонажа, чем мы и займемся в следующей главе.

Комментариев нет:

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