Давайте сделаем рогалик (Подземелье)

map.png

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

содержание | назад | вперед