воскресенье, 22 апреля 2012 г.

Давайте сделаем рогалик. Глава 25: Обобщаем атрибуты персонажа

В следующей главе мы будем добавлять зелья в нашу игру. Зелья будут влиять на атрибуты персонажа, но в данный момент доступ к атрибутам не очевиден. Если вы помните, для каждого атрибута мы создали массив размерностью в 3 элемента. Значение с индексом 0 содержит базовое значение характеристики, 1 — бонус и 2 — время действия бонуса заданное в ходах. И хотя это работает, возможно спустя некоторое время увидев в коде программы число 0 мы не будем знать, что это за значение и что оно означает. Вместо использования 0-ля мы можем использовать мнемоническое значение, например idxAttr, которое будет указывать на базовое значение атрибута. Это гораздо яснее и позволяет изменять программу очень легко, просто изменив значение idxAttr.

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

character.bi
'Индексы для массивов атрибутов и факторов.
 Enum attrindex
   idxAttr = 0 'Атрибут или фактор
   idxAttrBon  'Бонус
   idxAttrCnt  'Счетчик
   idxArrMax   'Максимальный индекс массива. Добавляйте индексы перед этим.
 End Enum

Мы могли бы использовать #Define для этих значений, но перечисление даст нам некоторые преимущества. Предположим что нам понадобиться добавить новый элемент в массив хранящий значения атрибутов и бонусов. Все что нам нужно будет сделать, это вставить в перечисление названия индекса нового элемента перед idxArrmax, что увеличит размерность массивов атрибутов автоматически, так как цифровое значение idxArrmax автоматически увеличиться на единицу. Нам не придется вручную вносить изменения в массивы описанные в объекте персонажа. Все что нам нужно будет сделать, это добавить новые функции, необходимые для обработки нового элемента массива. Это сделает работу по обновлению объекта персонажа на одном дыхании.

Хотя у нас довольно много мест в коде где мы обращались к атрибутам персонажа по индексу обновление кода будет не очень затратным. Мы просто используем функцию поиска и замены нашего редактора. Будем заменять (0) на (idxAttr). Это требует некоторой осторожности, но при помощи «поиска и замены» а не глобального «заменить все» мы сможем контролировать процесс и изменять только необходимые строки кода.

Давайте рассмотрим несколько примеров того, как это пойдет нам на пользу.

character.bi
'Определение атрибутов персонажа.
 Type characterinfo
   cname As String * 35 'Имя.
   stratt(idxArrMax) As Integer 'Сила (0), Бонус силы (1), Продолжительность действия бонуса в ходах (2)
   staatt(idxArrMax) As Integer 'Выносливость 
   dexatt(idxArrMax) As Integer 'Скорость 
   aglatt(idxArrMax) As Integer 'Подвижность 
   intatt(idxArrMax) As Integer 'Интелект 
   currhp As Integer    'Текущее здоровье
   maxhp As Integer     'Максимальное здоровье
   currmana As Integer  'Текушая мана
   maxmana As Integer   'Максимальная мана
   ucfsk(idxArrMax) As Integer  'Безоружный бой
   acfsk(idxArrMax) As Integer  'Контактный бой
   pcfsk(idxArrMax) As Integer  'Бистанционный бой
   mcfsk(idxArrMax) As Integer  'Магическая атака 
   cdfsk(idxArrMax) As Integer  'Защита
   mdfsk(idxArrMax) As Integer  'Магическая защита 
 ...

Здесь мы создаем массивы атрибутов и факторов персонажа. Обратите внимание, что вместо загадочных 3, теперь используется idxArrMax. Если мы изменим количество значений в перечислении, то размерность массивов поменяется автоматически и нам не нужно будет ничего менять в данном коде.

character.bi:GenerateCharacter
...
 'Отображение характеристик персонажа.
 If ret = TRUE Then
   'Генерируем характеристики персонажа.
   Do
      With _cinfo
         .cname = chname
         .stratt(idxAttr) = RandomRange (10, 20)   'Значение характеристик.
         .staatt(idxAttr) = RandomRange (10, 20)
         .dexatt(idxAttr) = RandomRange (10, 20)
         .aglatt(idxAttr) = RandomRange (10, 20)
         .intatt(idxAttr) = RandomRange (10, 20)
         .stratt(idxAttrBon) = 0                   'Очистим все бонусы.
         .staatt(idxAttrBon) = 0
         .dexatt(idxAttrBon) = 0
         .aglatt(idxAttrBon) = 0
         .intatt(idxAttrBon) = 0
         .stratt(idxAttrCnt) = 0                   'Очистим счетчики бонусов.
         .staatt(idxAttrCnt) = 0
         .dexatt(idxAttrCnt) = 0
         .aglatt(idxAttrCnt) = 0
         .intatt(idxAttrCnt) = 0
         .currhp = .stratt(idxAttr) + .staatt(idxAttr) 
         .maxhp = .currhp
         .currmana = .intatt(idxAttr) + .staatt(idxAttr) 
         .maxmana = .currmana
         .ucfsk(idxAttr) = .stratt(idxAttr) + .aglatt(idxAttr) 
         .acfsk(idxAttr) = .stratt(idxAttr) + .dexatt(idxAttr) 
         .pcfsk(idxAttr) = .dexatt(idxAttr) + .intatt(idxAttr)
         .mcfsk(idxAttr) = .intatt(idxAttr) + .staatt(idxAttr)
         .cdfsk(idxAttr) = .stratt(idxAttr) + .aglatt(idxAttr)
         .mdfsk(idxAttr) = .aglatt(idxAttr) + .intatt(idxAttr)
         .ucfsk(idxAttrBon) = 0 'Очистим бонусы. 
         .acfsk(idxAttrBon) = 0 
         .pcfsk(idxAttrBon) = 0
         .mcfsk(idxAttrBon) = 0
         .cdfsk(idxAttrBon) = 0
         .mdfsk(idxAttrBon) = 0
         .ucfsk(idxAttrCnt) = 0 'Очистим счетчики.
         .acfsk(idxAttrCnt) = 0 
         .pcfsk(idxAttrCnt) = 0
         .mcfsk(idxAttrCnt) = 0
         .cdfsk(idxAttrCnt) = 0
         .mdfsk(idxAttrCnt) = 0
 ...

Здесь показан фрагмент кода генерации персонажа. Вместо непонятных 0, 1 и 2 теперь указаны понятные людям теги, которые дают нам гораздо больше информации. Мы можем сразу сказать что это атрибут или фактор, бонус или его продолжительность. И если нам нужно будет поменять порядок их следования, то нам не придется менять этот код, за нас это сделает компилятор.

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

character.bi
'Возвращает лучшую броню для текущей силы персонажа.
 Function character._GetBestArmor() As armorids
   Dim As armorids ret = armArmorNone
 
   If _cinfo.stratt(idxAttr) >= strPlate Then
      ret = armPlate
   ElseIf _cinfo.stratt(idxAttr) >= strScale Then
      ret = armScale
   ElseIf _cinfo.stratt(idxAttr) >= strChain Then
      ret = armChain   
   ElseIf _cinfo.stratt(idxAttr) >= strBrigantine Then
      ret = armBrigantine   
   ElseIf _cinfo.stratt(idxAttr) >= strRing Then
      ret = armRing
   ElseIf _cinfo.stratt(idxAttr) >= strCuirboli Then
      ret = armCuirboli
   ElseIf _cinfo.stratt(idxAttr) >= strLeather Then
      ret = armLeather   
   ElseIf _cinfo.stratt(idxAttr) >= strCloth Then
      ret = armCloth
   EndIf
 
   Return ret
 End Function

Подчеркивание в имени функции означает что она приватная и может быть вызвана только изнутри объекта. Мы начинаем без брони и поочередно сравниваем силу персонажа с требованиями силы для различных доспехов. Здесь используются конструкции If — Else для того, чтобы сразу остановить проверку, как только мы нашли броню с необходимыми требованиями, а не перебирать все варианты.

Требования значения силы для каждого типа брони содержится в inv.bi.

inv.bi
'Требования значения силы для брони.
 Const strCloth = 10
 Const strLeather = 50
 Const strCuirboli = 100
 Const strRing = 150
 Const strBrigantine = 200
 Const strChain = 250
 Const strScale = 300
 Const strPlate = 350

Мы используем определение констант для указаний требуемого значения силы. Определения #Define сработали бы также хорошо, возможно, даже слишком. Я всегда использую Const если есть вероятность что данные константы необходимо указать внутри объекта или пространства имен. Область видимости Const — блок в котором он объявлен, в отличии от #Define, который глобально влияет на всю программу в целом.

Теперь, когда мы определили константы для требований силы, мы можем использовать их и при создании брони.

inv.bi:GenerateArmor
'Установим значения для конкретной брони.
   Select Case item
      Case armCloth
         inv.desc = "Cloth Armor" 'Cloth armor: 1% damage reduction.
         inv.armor.noise = 1
         inv.armor.dampct = .01
         inv.armor.struse = strCloth
      Case armLeather 'Leather armor: 5% damage reduction
         inv.desc = "Leather Armor"
         inv.armor.noise = 5
         inv.armor.dampct = .05
         inv.armor.struse = strLeather
 ...

Также, как мы делали с атрибутами, здесь мы использовали символические значения переменных. Чтобы сделать обновления значительно проще. Для внесения изменений нам необходимо будет просто поменять значения констант, которые также используется и в методе GetBestArmor.

Теперь нам необходимо обновить подпрограмму генерации персонажа, чтобы выдать ему броню.

character.bi:GenerateCharacter
...
   'Добавим лучшую бронб.
   arm = _GetBestArmor
   GenerateArmor inv, 1, arm
   SetInvEval inv, TRUE
   AddInvItem wArmor, inv
 ...

Переменная arm будет содержать идентификатор брони, полученный из GetBestArmor. Создаем броню, передавая этот идентификатор в GenerateArmor и помещаем ее в слот брони персонажа. Теперь персонаж экипирован наилучшей броней, доступной ему при текущем показателе силы.

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

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

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