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

Давайте сделаем рогалик. Глава 09: Бродим

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

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

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

Была найдена небольшая ошибка в процедуре 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, чтобы убедиться, что персонаж стоит на лестнице вниз, мы же не хотим, чтобы персонаж мог просачиваться на нижний уровень сквозь пол! Если персонаж стоит на лестнице вниз, то мы генерируем новый уровень и отображаем его, начиная с начальной комнаты. Здесь мы добавили довольно много кода, чтобы заставить персонажа двигаться, но это только начало! В скором времени мы добавим значительно больше.

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

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

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