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

moving.png

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

Исправление ошибок

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

Была найдена небольшая ошибка в процедуре DrawMap. Старый код выглядел следующим образом:

map.bi

'Нарисуем видимую часть карты.
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


Обратите внимание, куда мы поместили функции GetMapSymbol и GetMapSymbolColor — в обработку прямой видимости, в результате, если мы не видим тайл в данный момент, то при его выводе используется переменная mtitle которая содержит не код текущего тайла, а значение, которые было в нее записано на более ранних шагах. Исправить это достаточно просто, нужно вынести эти две функции из оператора If, тогда все будет работать как и ожидалось.

map.bi

'Нарисуем видимую часть карты.
For x = 1 To w
    For y = 1 To h
        'Очистим текущее знакоместо (нарисуем черный квадрат).
        tilecolor = fbBlack
        PutText acBlock, y, x, tilecolor
        'Получим ID тайла
        tile = _level.lmap(i + x, j + y).terrid
        'Получим ASCII символ тайла
        mtile = _GetMapSymbol(tile)
        'Получим цвет тайла
        tilecolor = _GetMapSymbolColor(tile)
        'Выведем тайл.
        If _level.lmap(i + x, j + y).visible = True Then
            'Нарисуем маркер предмета.
            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


Передвижение персонажа

Для того, чтобы игрок перемещал персонажа, нам нужно получить ввод с клавиатуры. В Подземелье Судьбы, для перемещения персонажа, мы будем использовать как клавиши со стрелками, так и клавиши на дополнительной цифровой клавиатуре. Цифровая клавиатура должна быть в режиме NumLock, чтобы мы получали числа, а не клавиши управления. Информацию об этом нужно обязательно добавить в файл справки по игре. Мы будем использовать дополнительную клавиатуру для передвижения на 8 сторон света, а клавиши стрелок — для быстрого перемещения по подземелью. Одним из усовершенствований игры, могла бы быть возможность пользователю задать свой набор клавиш для различных действий в игре, но, для простоты, мы не будем это реализовывать в этой версии игры.

Программная логика довольно проста. После того, как игрок вышел из меню, мы генерируем новый уровень подземелья, отображаем его, а затем входим в цикл сбора нажатых клавиш. Если игрок наживает клавиши направления, то мы попытаемся переместить персонажа. Если игрок наживает клавишу «вниз по лестнице», то мы генерируем новый уровень подземелья. Если клавишу «выход», то мы выходим из главного цикла программы.

dod.bas

'Главний цикл игры.
If mm <> mmenu.mQuit Then
    'Строим первый уровень подземелья
    level.GenerateDungeonLevel
    'Нарисуем главный экран.
    DrawMainScreen
    Do
        ckey = Inkey
        If ckey <> "" Then
            'Получим клавиши направления со стрелок или нумпада.
            'Проверим клавишу вверх и 8
            If (ckey = key_up) OrElse (ckey = "8") Then
                mret = MoveChar(north)
                If mret = TRUE Then DrawMainScreen
            Endif
            'Проверим 9
            If ckey = "9" Then
                mret = MoveChar(neast)
                If mret = TRUE Then DrawMainScreen
            Endif
            'Проверим клавишу вправо и 6.
            If (ckey = key_rt) OrElse (ckey = "6") Then
                mret = MoveChar(east)
                If mret = TRUE Then DrawMainScreen
            Endif
            'Проверим 3
            If ckey = "3" Then
                mret = MoveChar(seast)
                If mret = TRUE Then DrawMainScreen
            Endif
            'Проверим вниз и 2.
            If (ckey = key_dn) OrElse (ckey = "2") Then
                mret = MoveChar(south)
                If mret = TRUE Then DrawMainScreen
            Endif
            'Проверим 1
            If ckey = "1" Then
                mret = MoveChar(swest)
                If mret = TRUE Then DrawMainScreen
            Endif
            'Проверим влево и 4.
            If (ckey = key_lt) OrElse (ckey = "4") Then
                mret = MoveChar(west)
                If mret = TRUE Then DrawMainScreen
            Endif
            'Проверим 7
            If ckey = "7" Then
                mret = MoveChar(nwest)
                If mret = TRUE Then DrawMainScreen
            Endif
            'Спуск на нижний уровень.
            If ckey = ">" Then
                'Проверим, есть ли лестница.
                If level.GetTileID(pchar.Locx, pchar.Locy) = tstairdn Then
                    'Строим новый уровень подземелья.
                    level.GenerateDungeonLevel
                    'Нарисуем главный экран.
                    DrawMainScreen
                End If
            Endif
        End If
        Sleep 1
    Loop Until ckey = key_esc
Endif


Вначале мы проверяем, была ли нажата клавиша. Функция InKey возвращает строку с символом нажатой клавиши или пустую строку, если буфер клавиатуры пуст. Если какая либо клавиша была нажата, то, при помощи нескольких команд If — Then, в зависимости от того, какая клавиша нажата, мы выполняем то или иное действие.

dod.bas

...
'Проверим клавишу вверх и 8
If (ckey = key_up) OrElse (ckey = "8") Then
    mret = MoveChar(north)
    If mret = TRUE Then DrawMainScreen
Endif
...


Этот код проверяет, нажата ли клавиша вверх, или клавиша 8 на цифровой клавиатуре. Все остальные клавиши проверяются аналогичным образом. Если стрелка вверх или клавиша 8 нажата, то мы вызываем функцию MoveChar, для перемещения персонажа. Параметр, передаваемый в MoveChar, это одно из восьми направлений света, в данном случае это северное направление. Функция возвращает «истина» если персонаж переместился, или «ложь» - если он не может двигаться в указанном направлении. Если персонаж не двигался, то нам не нужно перерисовывать дисплей, что сэкономит немного процессорного времени, и, как результат, сделает программу несколько быстрее. Теперь давайте рассмотрим функцию MoveChar.

commands.bi

'Перемещение персонажа по сторонам свчета.
Function MoveChar(comp As compass) As Integer
    Dim As Integer ret = FALSE, block
    Dim As vec vc = vec(pchar.Locx, pchar.Locy) 'Создадим вектор.
    Dim As terrainids tileid

    vc+= comp
    'Проверим на выход за пределы карты.
    If (vc.vx >= 1) And (vc.vx <= mapw) Then
        If (vc.vy >= 1) And (vc.vy <= maph) Then
            'Проверим на блокирующий перемещение тайл.
            block = level.IsBlocking(vc.vx, vc.vy)
            'Перемещаем персонажа.
            If block = FALSE Then
                'Установим новые координаты персонажа.
                pchar.Locx = vc.vx
                pchar.Locy = vc.vy
                ret = TRUE
            Else 'Проверим специфические типы местности.
                'Получим id местности.
                tileid = level.GetTileID(vc.vx, vc.vy)
                Select Case tileid
                    Case tdoorclosed 'Закрытая дверь.
                        ret = OpenDoor(vc.vx, vc.vy)
                        'Если дверь не открылась — выведем сообщение.
                        If ret = FALSE Then
                            'тут сообщение выводим.
                        Else
                            'Установим новые координаты персонажа.
                            pchar.Locx = vc.vx
                            pchar.Locy = vc.vy
                            ret = TRUE
                        Endif
                    Case tstairup 'Возможно перемещение по лестнице вверх.
                        'Установим новые координаты персонажа.
                        pchar.Locx = vc.vx
                        pchar.Locy = vc.vy
                        ret = TRUE
                End Select
            Endif
        Endif
    Endif

    Return ret
End Function


Первое, что бросается в глаза, это то, что здесь мы используем объект вектора, который мы рассмотрим позже, упрощающий нам расчет координат. Оператор += для него был перегружен, для того, чтобы автоматически получить новые координаты персонажа в нужном направлении. Как только мы получаем новые координаты, нам нужно проверить, вначале, что они лежат в рамках нашего массива карты, а затем мы проверяем, находится ли в новых координатах на карте блокирующий передвижение тип местности. Функция IsBlocking это новая функция, которую мы добавили в наш объект карты уровня.

map.bi

'Возвращает истина или лож, если в координатах x, y блокирующий тип местности.
Function levelobj.IsBlocking(x As Integer, y As Integer) As Integer
    Return _BlockingTile(x, y)
End Function


Вы можете видеть, что это просто вызов приватного метода BlockingTile, который возвращает истина, если по указанным координатам на карте находится блокирующий тип местности, или ложь, если нет. Мы используем возвращаемое значение, чтобы определить, может ли персонаж находится на данных координатах карты. Если это так, то мы просто изменяем текущие координаты персонажа и возвращаем «истина», чтобы указать на успешность перемещения.

commands.bi

...
'Перемещаем персонажа.
If block = FALSE Then
'Установим новые координаты персонажа.
pchar.Locx = vc.vx
pchar.Locy = vc.vy
ret = TRUE
...


Пока все хорошо, но в случае, если проход заблокирован, то нам нужно проверить несколько исключений: лестницы и закрытые двери.

commands.bi

...
Else 'Проверим специфические типы местности.
'Получим id местности.
tileid = level.GetTileID(vc.vx, vc.vy)
Select Case tileid
    Case tdoorclosed 'Закрытая дверь.
        ret = OpenDoor(vc.vx, vc.vy)
        'Если дверь не открылась — выведем сообщение.
        If ret = FALSE Then
            'тут сообщение выводим.
        Else
            'Установим новые координаты персонажа.
            pchar.Locx = vc.vx
            pchar.Locy = vc.vy
            ret = TRUE
        Endif
    Case tstairup 'Возможно перемещение по лестнице вверх.
        'Установим новые координаты персонажа.
        pchar.Locx = vc.vx
        pchar.Locy = vc.vy
        ret = TRUE
End Select
Endif
...


Для того, чтобы узнать, является ли блокирующая проход местность одним из наших исключений, мы должны получить идентификатор тайла местности. GetTileID, это еще одна новая функция, которую мы добавили в объект карты уровня.

map.bi

'Возвращает id типа местности по координатам x, y.
Function levelobj.GetTileID(x As Integer, y As Integer) As terrainids
    Return _level.lmap(x, y).terrid
End Function


Эта функция получает доступ к приватному массиву карты уровня и возвращает тип местности, находящийся по координатам x, y. Мы используем полученное значение в функции MoveChar, чтобы проверить наши исключения. Есть два случая которые нас интересуют, это закрытая дверь с идентификатором tdoorclosed и лестница вверх, с идентификатором tstairup. Мы рассмотрим обработку закрытых дверей позже, вначале давайте посмотрим на обработку более простого случая — проверка на обнаружение лестницы вверх.

Хотя лестница вверх у нас находится в списке блокирующих типов местности, для того чтобы блокировать прямую видимость, но мы хотим, чтобы персонаж мог на нее стать, чтобы иметь возможность подняться на предыдущий уровень. Для этого мы просто изменим координаты персонажа и вернем «истина», чтобы указать на то, что персонаж переместился.

Если же мы обнаружили закрытую дверь, то мы хотим, чтобы персонаж ее открыл, если дверь не заперта. Было бы весьма утомительно заставлять игрока, каждый раз, когда он подходит к новой двери, нажимать клавишу «o» для того чтобы ее открыть. Большинство людей просто не будут играть в игру, которая их утомляет. Чтобы облегчить им игру, мы просто откроем дверь автоматически, когда персонаж врезается в нее. Однако, это означает, что мы должны проверить дверь, возможно она заперта или заблокирована и, если дверь не заперта, то только тогда открыть ее и переместить на ее место персонажа. Мы делаем это в функции OpenDoor.

commands.bi

'Открывает закрытую дверь, если не заперта.
Function OpenDoor (x As Integer, y As Integer) As Integer
    Dim As Integer ret = TRUE, doorlocked

    'Проверим, заперта или нет.
    doorlocked = level.IsDoorLocked(x, y)
    If doorlocked = FALSE Then
        'Открываем дверь.
        level.SetTile x, y, tdooropen
    Else
        'Дверь заперта и не может быть открыта.
        ret = FALSE
    End If

    Return ret
End Function


Чтобы проверить, заперта ли дверь, мы используем новую функцию. Которую добавили в объект карты уровня.

map.bi

'Возвращает True если дверь заперта.
Function levelobj.IsDoorLocked(x As Integer,y As Integer) As Integer
    Return _level.lmap(x, y).doorinfo.locked
End Function


Как вы можете видеть. Мы объявили новую структуру в нашем объекте карты, это doorinfo. Следующий код описывает данную структуру.

map.bi

'Описание типа двери.
Type doortype
    locked As Integer   'Истина, если закрыта.
    lockdr As Integer   'Уровень сложности взлома замка.
    dstr As Integer     'Сила двери (для выбивания).
End Type


Данная структура содержит информацию о двери. Если дверь заперта, то поле locked будет установлено в «истина», иначе в «ложь». Поле lockdr указывает на сложность замка, и будет использоваться, когда персонаж будет пытаться его взломать. dstr — прочность двери, которую мы будем проверять, когда персонаж будет пытаться выбить дверь силой. Мы вернемся к этому коду позже, когда будем реализовывать удары персонажа. Информацию о двери мы добавим в нашу структуру mapinfo.

map.bi

'Описание ячейки карты
Type mapinfotype
    terrid As terrainids  'тип сестности.
    hasmonster As Integer 'Монстр в текущей ячейке.
    hasitem As Integer    'Предмет в текущей ячейке.
    monidx As Integer     'Индекс в массиве монстров.
    visible As Integer    'персонаж видит ячейку.
    seen As Integer       'Персонаж видел ячейку.
    doorinfo As doortype  'Информация о двери.
End Type


Поле doorinfo определено как описанный ранее тип doortype. Как вы уже видели, mapinfotype, это часть массива карты уровня, содержащегося в структуре levelinfo.

map.bi

'Информация об уровне подземелья.
Type levelinfo
    numlevel As Integer 'Номер текущего уровня.
    lmap(1 To mapw, 1 To maph) As mapinfotype 'Массив карты уровня.
End Type


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

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

map.bi

'Создает новый уровень подземелья.
Sub levelobj.GenerateDungeonLevel()
    Dim As Integer x, y

    'Очистим уровень
    For x = 1 To mapw
        For y = 1 To maph
            'Set to wall tile
            _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
            _level.lmap(x, y).doorinfo.locked = FALSE
            _level.lmap(x, y).doorinfo.lockdr = 0
            _level.lmap(x, y).doorinfo.dstr = 0
        Next
    Next
    _InitGrid
    _DrawMapToArray
End Sub


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

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
            _level.lmap(col, dd1).doorinfo.locked = FALSE
            If _level.lmap(col, dd1).doorinfo.locked = TRUE Then
                _level.lmap(col, dd1).doorinfo.lockdr = 0
                _level.lmap(col, dd1).doorinfo.dstr = 0
            End If
        Endif
        'Ксли в нижней стене пустое место.
        If _level.lmap(col, dd2).terrid = tfloor Then
            _level.lmap(col, dd2).terrid = tdoorclosed
            _level.lmap(col, dd2).doorinfo.locked = FALSE
            If _level.lmap(col, dd2).doorinfo.locked = TRUE Then
                _level.lmap(col, dd2).doorinfo.lockdr = 0
                _level.lmap(col, dd2).doorinfo.dstr = 0
            End If
        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
            _level.lmap(dd1, row).doorinfo.locked = FALSE
            If _level.lmap(dd1, row).doorinfo.locked = TRUE Then
                _level.lmap(dd1, row).doorinfo.lockdr = 0
                _level.lmap(dd1, row).doorinfo.dstr = 0
            End If
        End If
        'Проверим правую стену.
        If _level.lmap(dd2, row).terrid = tfloor Then
            _level.lmap(dd2, row).terrid = tdoorclosed
            _level.lmap(dd2, row).doorinfo.locked = FALSE
            If _level.lmap(dd2, row).doorinfo.locked = TRUE Then
                _level.lmap(dd2, row).doorinfo.lockdr = 0
                _level.lmap(dd2, row).doorinfo.dstr = 0
            End If
        Endif
    Next

End Sub


Когда мы добавляем закрытую дверь, мы также устанавливаем ее состояние в заблокирована или нет. Сейчас все двери отпираются, но позже, мы добавим код, делающий некоторые двери запертыми. Тогда нам нужно будет устанавливать значения для сложности взлома замка и прочности двери. Все эти данные нужны для возможности открытия двери и заполняются при создании карты уровня, хотя само открытие осуществляется функцией DoorOpen, описанной в файле commands.bi.

commands.bi

'Открывает дверь, если не заперта.
Function OpenDoor (x As Integer, y As Integer) As Integer
    Dim As Integer ret = TRUE, doorlocked

    'проверяем, закрыта дверь или нет.
    doorlocked = level.IsDoorLocked(x, y)
    If doorlocked = FALSE Then
        'Открываем дверь.
        level.SetTile x, y, tdooropen
    Else
        'Дверь заперта и не может быть открыта.
        ret = FALSE
    End If

    Return ret
End Function


Если функция IsDoorLocked возвращает значение «ложно», то значит дверь не заперта и мы можем ее открыть. Для этого мы должны установить в массиве карты уровня тип местности tdooropen, воспользовавшись еще одной новой функцией в объекте нашего уровня — SetTile.

map.bi

'Установить тип местности для координат x, y.
Sub levelobj.SetTile(x As Integer, y As Integer, tileid As terrainids)
    _level.lmap(x, y).terrid = tileid
End Sub


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

Функция OpenDoor возвращает «истина» если дверь открыта или «ложь», если дверь заблокирована и открыть ее не получилось. Возвращаясь к функции MoveChar, теперь мы можем рассмотреть весь процесс открытия двери целиком.

commands.bi

'проверяем, закрыта дверь или нет.
doorlocked = level.IsDoorLocked(x, y)
If doorlocked = FALSE Then
    'Открываем дверь.
    level.SetTile x, y, tdooropen
Else
    'Дверь заперта и не может быть открыта.
    ret = FALSE
End If

Return ret


Рассмотрим теперь вариант, когда на своем пути мы встретили закрытую дверь. Если OpenDoor возвращает «ложь», то мы выводим сообщение, что дверь не открылась и не перемещаем персонажа. Функция MoveChar вернет ложь в программу вызвавшую ее. Если дверь можно открыть, то мы перемещаем персонажа и возвращаем «истина», чтобы сообщить об успешности перемещения.

Как вы можете видеть, добавляя новые возможности в игру, мы постоянно возвращаемся и дополняем/изменяем уже написанный ранее код. Очень важно, чтобы во время этого процесса мы сразу же проверяли внесенные изменения, чтобы не внести ошибку в уже работающий код. Может показаться расточительным — постоянно переписывать уже написанный код, но на самом деле это гарантирует что программа работает правильно и. Если мы обнаружим ошибку, то точно будем знать, что она находится в части кода, который мы только что добавили, и ее гораздо легче будет найти и исправить.

Если бы мы сели и закодировали все функции в один присест, а потом выяснилось, что-то не работает правильно, с чего мы начали искать проблемы? Это может быть в любой из дюжины функций, которые были реализованы, каждый из которых может быть много строк кода. В результате поиск и исправление ошибок окажется долгим и трудоемким процессом. В конечном счете, итеративный процесс кодирования и перекодирования работает быстрее и надежнее, а также, приводит к рабочему коду с намного меньшим количеством расстройств.

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

commands.bi

...
vc+= comp
'Проверим на выход за пределы карты.
If (vc.vx >= 1) And (vc.vx <= mapw) Then
If (vc.vy >= 1) And (vc.vy <= maph) Then
'Проверим на блокирующий перемещение тайл.
block = level.IsBlocking(vc.vx, vc.vy)
'Перемещаем персонажа.
If block = FALSE Then
'Установим новые координаты персонажа.
pchar.Locx = vc.vx
pchar.Locy = vc.vy
ret = TRUE
...


Чтобы получить в векторе новые координаты персонажа, мы просто добавляем к объекту вектора направление, используя перегруженный оператор + =, а затем получаем х и у координаты, обращаясь к свойствам объекта. Чтобы разобраться, как это работает, давайте посмотрим на код объекта вектор.

vec.bi

'Направление на 8 сторон света
Enum compass
   north
   neast
   east
   seast
   south
   swest
   west
   nwest
End Enum

'Описание Типа для 2D вектора.
Type vec
   Private:
   _x As Integer
   _y As Integer
   _dirmatrix(north To nwest) As mcoord = {(0, -1), (1, -1), (1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1)}
   Public:
   Declare Constructor ()
   Declare Constructor (x As Integer, y As Integer)
   Declare Property vx (x As Integer)
   Declare Property vx () As Integer
   Declare Property vy (y As Integer)
   Declare Property vy () As Integer
   Declare Operator += (cd As compass)
   Declare Sub ClearVec()
End Type

'Пустой конструктор, создает нулевой вектор.
Constructor vec ()
   _x = 0
   _y = 0
End Constructor

'Конструктор с инициализацией значений.
Constructor vec (x As Integer, y As Integer)
   _x = x
   _y = y
End Constructor

'Свойства для установки и получения значений x и y координат.
Property vec.vx (x As Integer)
   _x = x
End Property

Property vec.vx () As Integer
   Return _x
End Property

Property vec.vy (y As Integer)
   _y = y
End Property

Property vec.vy () As Integer
   Return _y
End Property

'Обновим значения x и y координат, используя направление из compass.
Operator vec.+= (cd As compass)
  If (cd >= north) And (cd <= nwest) Then
     _x += _dirmatrix(cd).x
     _y += _dirmatrix(cd).y
  End If
End Operator

'Установить вектор в 0.
Sub vec.Clearvec ()
   _x = 0
   _y = 0
End Sub


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

Тип вектор содержит две координаты, x и y в приватной секции, а также массив dirmatrix, в котором описаны смещения этих координат для каждой стороны света, перечисленной в compass. Этот массив используется в перегруженном операторе +=.

vec.bi

...
'Обновим значения x и y координат, используя направление из compass.
Operator vec.+= (cd As compass)
    If (cd >= north) And (cd <= nwest) Then
        _x += _dirmatrix(cd).x
        _y += _dirmatrix(cd).y
    End If
End Operator
...


Параметр cd, это одно из направлений из compass. Мы проверяем, чтобы убедиться, что направление является допустимым, а затем добавляем смещение из dirmatrix к текущем значениям координат х и у вектора. Предполагается, что вектор был инициализирован координатами текущего местоположение персонажа.

Давайте рассмотрим как это работает. В массиве dirmatrix северному направлению соответствует значение (0,-1), где 0, это смешение по координате x, а -1 — смещение по координате y. Каждое значение из матрицы массива добавляется к соответствующей координате вектора. К x мы добавляем 0 и она остается неизменной, к y мы добавляем -1, что уменьшит ее на единицу. В результате мы получим новые координаты в векторе, просто добавив к нему направление по компасу, как мы это видели в функции MoveChar: vc+= comp. Перегрузка оператора += позволяет нам использовать направление как и любую другую переменную или цифру в программе. Это значительно упрощает получение новых координат вектора и показывает всю силу объектов в процессе облегчения программирования.

Конструктор без параметров необходим для задания значение координат вектора по умолчанию. Он устанавливает координаты x и y в нулевое значение. Значения координат вектора можно менять при помощи свойств vx и vy. Нам это понадобиться, если мы будем использовать объект вектора в цикле, и необходимо будет менять его координаты. Мы увидим это в действии, когда реализуем функции поиска.

Подпрограмма ClearVec используется для задания нулевых координат вектора и необходима, если нам понадобится, по какой либо причине, очистить вектор. Для многих объектов инициализация выделяет для них какие либо ресурсы, поэтому всегда полезно иметь метод, который освобождает эти ресурсы и делает объект вновь готовым к инициализации.

Объект вектора достаточно прост, но облегчает написание кода. Поскольку мы будем расширять возможности нашей программы, мы будем его использовать снова и снова.

Возвращаясь к нашему основному коду в dod.bas, следующая команда, которую нам нужно рассмотреть, это перемещение персонажа вниз по лестнице, для спуска на следующий уровень подземелья.

dod.bas

...
'Проверим, есть ли лестница.
If level.GetTileID(pchar.Locx, pchar.Locy) = tstairdn Then
    'Строим новый уровень подземелья.
    level.GenerateDungeonLevel
    'Нарисуем главный экран.
    DrawMainScreen
End If
...


Здесь мы используем функцию GetTileID, чтобы убедиться, что персонаж стоит на лестнице вниз, мы же не хотим, чтобы персонаж мог просачиваться на нижний уровень сквозь пол! Если персонаж стоит на лестнице вниз, то мы генерируем новый уровень и отображаем его, начиная с начальной комнаты. Здесь мы добавили довольно много кода, чтобы заставить персонажа двигаться, но это только начало! В скором времени мы добавим значительно больше.

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

Перевод на русский: Fantik

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