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

Давайте сделаем рогалик. Глава 12: Генерация вещей

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

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

Массив inv состоит из элементов составного типа, который мы создали в предыдущей главе. Массив предметов имеет размерность нашего подземелья, т. е. на каждом тайле нашей карты может находится какой либо предмет, с той лишь оговоркой, что в каждой ячейке карты предмет может быть только один. Некоторые рогалики позволяют укладывать на один тайл много предметов, но для простоты мы этого реализовывать не будем. Реализовать это было бы не трудно, добавив стек предметов в описание нашего подземелья, но, на мой взгляд, это не добавит ничего особенного в игру, но сделает код более сложным и трудно управляемым. Стек предметов обычно реализуется для облегчения возможности сбросить предметы на землю, но даже с одним элементом на тайл карты персонаж имеет доступ к 9 тайлам на карте, если находится в комнате, и 3 тайла — если в узком коридоре. Всего 3 тайла кажутся проблемой, но, на самом деле, это добавит немного стратегии в игру, т. к. игрок должен будет более тщательно выбирать — какие предметы ему стоит выбросить в данный момент, если он находится в коридоре.

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

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

Даже если сложный код работает как ожидалось, то его все равно очень трудно поддерживать, так как он создает много проблем: 1) программисту очень трудно выяснить что и как в программе работает, если он не изучал код несколько месяцев или даже лет, 2) внесение изменений достаточно трудоемко, так как сам факт сложности кода добавляет больше работы к необходимой модификации, чем этого бы требовалось. В мои 20+ лет, что я работаю программистом, я работал во многих отраслях, начиная от банковского дела и заканчивая нефтяной отраслью, я работал как с мелкими, так и с крупными компаниями и обнаружил, что просто код, который выполняет поставленные задачи, более надежен и долговечен, чего нельзя сказать о сложном коде, т. к. он не может быть без проблем отлажен и легко поддерживаемым.

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

Итак, у нас есть массив инвентаря в структуре карты подземелья, теперь пришло время создания нескольких предметов. Мы будем их создавать в нашей процедуре генерации карты подземелья.

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          'Нет предмета
           _level.lmap(x, y).doorinfo.locked = FALSE  'Дверь не закрыта
           _level.lmap(x, y).doorinfo.lockdr = 0      'Не заперта
           _level.lmap(x, y).doorinfo.dstr = 0        'Нет силы двери
           ClearInv _level.linv(x,y)    'Очистим слот для инвентаря.
       Next
   Next
   _InitGrid
   _DrawMapToArray
   _GenerateItems
 End Sub

Из процедуры GenerateDungeonLevel() мы вызываем новую подпрограмму GenerateItems, которая создает предметы на только что созданной карте. Прежде чем мы это сделаем, мы должны очистить все слоты для предметов на карте, вызвав новую подпрограмму ClearInv, которая расположена в новом файле inv.bi.

map.bi
'Создает предметы на карте.
 Sub levelobj._GenerateItems()
   Dim As Integer i, x, y
 
   'Создать немного предметов на уровне. 
   For i = 1 To 10
     Do
       'Получим случайное место на карте.
       x = RandomRange(2, mapw - 1)
       y = RandomRange(2, maph - 1)
       'Убедимся, что это пол и тут еще нет предметов.
     Loop Until (_level.lmap(x, y).terrid = tfloor) And (HasItem(x, y) = FALSE)
     GenerateItem _level.linv(x, y), _level.numlevel
   Next
 
 End Sub

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

map.bi
'Возвразает истина, если по координатам x,y находится какой либо предмет.
 Function levelobj.HasItem(x As Integer, y As Integer) As Integer
   'Смотрим слот предмета. Если не установлен ID типа предмета, то пусто.
   If _level.linv(x, y).classid = clNone Then
     Return FALSE
   Else
     Return TRUE
   EndIf
 End Function

Чтобы определить, расположен ли на ячейке карты какой либо предмет, мы проверяем ID типа предмета в композитной структуре этой ячейки карты. Если classid установлен в clNone, то мы знаем, что ячейка пуста. Так как мы очистили все предметы на карте, то ситуация, когда мы попадем на ячейку на которой уже расположен предмет может произойти только в том случае, если мы дважды попали на одну и туже ячейку при данном добавлении предметов, что маловероятно, но проверить это мы должны в любом случае.

Если ячейка пуста, то мы можем добавить на нее какой либо предмет вызвав подпрограмму GenerateItem.
map.bi
GenerateItem _level.linv(x, y), _level.numlevel

Обратите внимание, что в GenerateItem мы передаем фактический элемент массива предметов на карте вместе с номером уровня. Передавая только один элемент массива предметов в подпрограмму, мы ограничиваем ее доступ к остальным элементам массива, что упростит код и гарантирует, что у нас не будет нежелательных побочных эффектов. Единственное что может изменить процедура генерации предметов, это только один слот массива предметов на карте, и, если возникнут проблемы, мы точно будем знать где искать. Если бы процедура создания предмета имела доступ ко всему массиву предметов, то, при возникновении каких либо проблем, найти ошибку было бы очень трудно, особенно, если бы мы ее обнаружили только спустя какое то время (если бы обнаружили вообще). Лучше ограничивать доступ к данным везде где это только возможно, чтобы поддерживать хороший уровень безопасности целостности структур данных.

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

Имея ячейку карты для создания на ней предмета, нам остается только его создать, что мы делаем в подпрограмме GenerateItem.

inv.bi
'Создает новый предмет и помещает его в ячейку на карте.
 Sub GenerateItem(inv As invtype, currlevel As Integer)
   Dim iclass As classids = RandomRange(clGold, clSupplies)
 
   'Очистим текущий предмет, если не пустой.
   If inv.classid <> clNone Then
     ClearInv inv
   EndIf
   'Установим тип класса предмета.
   inv.classid = iclass
   'Создадим предмет основываясь на ID класса пердмета.
   Select Case iclass
     Case clGold
        GenerateGold inv
     Case clSupplies
        GenerateSupplies inv, currlevel
   End Select
 
 End Sub

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

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

Давайте рассмотрим те процедуры генерации, которые у нас имеются на данный момент.

inv.bi
'Создать новый предмет «золото».
 Sub GenerateGold(inv As invtype)
   Dim As Integer rng = RandomRange(1, 10)
 
   'Установим ID типа предмета (монеты/мешок).
   If rng = 1 Then 
     inv.gold.id = gldBagGold
   Else
     inv.gold.id = gldGold
   EndIf
   Select Case inv.gold.id
     Case gldGold
       inv.desc = "Gold Coins"
       inv.gold.amt = RandomRange(1, 10)
       inv.icon = Chr(147)
       inv.iconclr = fbGold
     Case gldBagGold
       inv.desc = "Bag of Gold"
       inv.gold.amt = RandomRange(10, 100)
       inv.icon = Chr(147)
       inv.iconclr = fbGold
   End Select
 End Sub

Подпрограмма GenerateGold достаточно проста, существует шанс 1 из 10, что выпадет мешок золотом а не золотые монеты. Как вы можете видеть, единственное реальное различие между ними заключается в количестве золота. Даже если они оба используют одну и туже иконку и цвет, два поля заполняются по отдельности, так как мы можем, например, решить использовать для мешка с золотом иконку, отличную от иконки, изображающей горстку монет. Поскольку для иконок мы используем набор символов ASCII, то мы ограничены в их количестве, а так как мы собираемся добавлять еще много различных предметов, то мы пока подождем, чтобы увидеть, какие иконки у нас будут задействованы, и только потом примем окончательное решение. Если же у нас все останется так же, как и сейчас, то мы можем вынести команды установки значка и его цвета из блока Case, это не создает проблем, но я бы хотел минимизировать количество строк кода, и, как результат, уменьшить вероятность допустить ошибку.

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

inv.bi
'Возвращает ИСТИНА если предмет магический.
 Function ItemIsMagic(currlevel As Integer) As Integer
   Dim As Integer num
 
   'Получим случайное число от 1 до 100
   num = RandomRange(1, maxlevel * 2)
   'Если полученное число равно или меньше текущего уровня подземелья, то предмет мегический.
   If num <= currlevel Then
     Return TRUE
   Else
     Return FALSE
   EndIf
 
 End Function

Здесь мы получаем случайное число от 1 до максимального количества уровней подземелья умноженного на 2. Если полученное число меньше либо равно номеру текущего уровня, то предмет магический. Это означает, что чем более глубоко в подземелье спускается персонаж, тем больше шансов у него найти магические предметы, но в тоже время шанс получить магический предмет даже на первом уровне подземелья все еще остается, хоть и не большой. По этой формуле, если персонаж достигнет последнего, 50-го, уровня подземелья, то шанс получить магический предмет будет около 50%. Т. е. половина вещей, найденных персонажем, будут обладать магическими свойствами, но также, примерно столько же предметов будут самыми обычными. Мы не хотим, чтобы магических предметов было очень много, из-за того, что, по своей природе, они должны встречаться достаточно редко, т. к. магические предметы дают преимущество персонажу. Персонаж не нуждается в особом преимуществе на ранних уровнях подземелья, так как монстры на них не будут столь же мощными как на нижних, где магические предметы будут играть важную роль. Это место в программе, возможно, придется переделать в последствии, после тестирования баланса игры.

ItemIsMagic используется в подпрограмме GenerateSupplies после того, как предмет уже создан.

inv.bi
'Создает новый предмет типа «еда».
 Sub GenerateSupplies(inv As invtype, currlevel As Integer)
   Dim item As supplyids = RandomRange(supHealingHerb, supBottleOil) 
   Dim As Integer isMagic = ItemIsMagic(currlevel) 
 
   'Установим идентификатор типа еды.
   inv.supply.id = item
   Select Case item
     Case supHealingHerb 
        inv.desc = "Healing Herb"
        inv.supply.noise = 1
        inv.icon = Chr(157)
        inv.iconclr = fbGreen
        inv.supply.eval = FALSE
        inv.supply.use = useDrinkEat
        'Установим магические свойства.
        If isMagic = TRUE Then
           inv.supply.evaldr = RandomRange(currlevel, currlevel * 2)
           inv.supply.effect = effMaxHealing
           inv.supply.sdesc = "Herb of Max Health"
        Else 
           'Установим секретное описание как главное.
           inv.supply.sdesc = inv.desc
        EndIf
     Case supHunkMeat
        inv.desc = "Hunk of Meat"
        inv.supply.noise = 1
        inv.icon = Chr(224)
        inv.iconclr = fbSalmon
        inv.supply.eval = FALSE
        inv.supply.use = useDrinkEat
        'Установим магические свойства.
        If isMagic = TRUE Then
           inv.supply.evaldr = RandomRange(currlevel, currlevel * 2)
           inv.supply.effect = effStrongMeat
           inv.supply.sdesc = "Hunk of Strong Meat"
        Else 
           'Установим секретное описание как главное.
           inv.supply.sdesc = inv.desc
        EndIf
     Case supBread       
        inv.desc = "Loaf of Bread"
        inv.supply.noise = 1
        inv.icon = Chr(247)
        inv.iconclr = fbHoneydew
        inv.supply.eval = FALSE 
        inv.supply.use = useDrinkEat
        'Установим магические свойства.
        If isMagic = TRUE Then
           inv.supply.evaldr = RandomRange(currlevel, currlevel * 2)
           inv.supply.effect = effBreadLife
           inv.supply.sdesc = "Bread of Cure Poison"
        Else 
           'Установим секретное описание как главное.
           inv.supply.sdesc = inv.desc
        EndIf
   End Select
 End Sub

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

Как определить, является ли предмет магическим? Мы просто смотрим на поле evaldr и если оно равно нулю, то предмет без магических свойств. Игрок, разумеется, этого не знает. Дополнительное поле eval, установленное в ложь, отвечает за то, распознал ли игрок данный предмет. Если оно имеет значение «ложно», то предмет в инвентаре будет помечен как не опознанный, и его дополнительные магические свойства, если таковые имеются, отображаться не будут. Это будет заставлять игрока взаимодействовать с инвентарем для идентификации предметов, что бы узнать, имеет ли предмет магические свойства. В большинстве случаев предмет будет совершенно обычным, но есть вероятность того, что он окажется и магическим. Если бы мы отображали как не опознанные только те предметы, которые содержат магию (не отображая магических свойств), то это бы уменьшило удовольствие игрока от обнаружения магического предмета.

Остальные поля заполняются информацией, которая относится к конкретному объекту. В отличии от золота, данный тип предметов создает шум не только во время движения персонажа, но и во время их использования. Шум создаваемый золотом, зависит от его количества, здесь же, каждый предмет имеет свое, конкретное, значение шума. В данном случае, значение шума, генерируемого каждым предметом равно 1, за исключением бутылки масла, которая создает шум в 4 раза больше. На самом деле, шум, равный 4-м, это не так уж много. Мы будем использовать обычную формулу расчета квадрата расстояния для определения дальности распространения шума. Это означает, что шум, создаваемый бутылкой с маслом, будет распространяться лишь на 1 или 2 клетки карты подземелья, не более. Общий шум, создаваемый персонажем когда он передвигается или в бою, будет состоять из суммы значений шума находящихся у него предметов и оборудования.

Код, который мы рассматривали до сих пор, добавляет предмет на карту, но, т. к. мы хотим, чтобы предмет на самом деле отображался на карте, мы должны внести изменения в процедуры отображения.

map.bi
'Рисует видимую часть карты.
    For x = 1 To w
        For y = 1 To h
            'Очистим черным цветом текущую местность.
            tilecolor = fbBlack 
            PutText acBlock, y + 1, x + 1, tilecolor
            'Получим ID тайла карты
            tile = _level.lmap(i + x, j + y).terrid
            'Получим символ для тайла
           mtile = _GetMapSymbol(tile)
           'Получим цвет
           tilecolor = _GetMapSymbolColor(tile)
           'Нарисуем тайл.
           If _level.lmap(i + x, j + y).visible = True Then
               'Выведем маркер предмета.
               If HasItem(i + x, j + y) = True Then
                   'Получим символ для предмета.
                   mtile = _level.linv(i + x, j + y).icon
                   'Получим цвет.
                   tilecolor = _level.linv(i + x, j + y).iconclr
               EndIf
               PutText mtile, y + 1, x + 1, tilecolor
               'Если в текущей позиции монстр, нарисуем его.
               If _level.lmap(i + x, j + y).hasmonster = TRUE Then
                   'Тут отображаем монстра.
               EndIf
           Else
               'Не в поле зрения. Не отображаем монстров, которых не видим.
               If _level.lmap(i + x, j + y).seen = TRUE Then
                   If HasItem(i + x, j + y) = True Then
                       PutText "?", y + 1, x + 1, fbSlateGrayDark
                   Else
                       PutText mtile, y + 1, x + 1, fbSlateGrayDark
                   End If
                End If
           End If
        Next 
    Next

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

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

dod.bas:DrawMainScreen
'Проверим, стоит ли персонаж на предмете.
  If level.HasItem(pchar.Locx, pchar.Locy) = TRUE Then
     txt = level.GetItemDescription(pchar.Locx, pchar.Locy)
     PrintMessage txt
  Else
     'Проверим, стоит ли персонаж на специальной местности.
     terr = level.GetTileID(pchar.Locx, pchar.Locy)
     If (terr = tstairup) OrElse (terr = tstairdn) Then
        txt = level.GetTerrainDescription(pchar.Locx, pchar.Locy)
        PrintMessage txt
     End If
  EndIf

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

map.bi
'Возвращает описание предмета. Находящегося по координатам x,y.
 Function levelobj.GetItemDescription(x As Integer, y As Integer) As String
   Dim As String ret = "None"
 
   If _level.linv(x, y).classid <> clNone Then
     ret = GetInvItemDesc(_level.linv(x, y))
   EndIf
 
   Return ret
 End Function

Здесь мы проверяем, действительно ли на данной ячейке карты находится предмет, и вызываем функцию работы с инвентарем GetInvItemDesc.

inv.bi
'Возвращает описание для предмета по координатам x, y.
 Function GetInvItemDesc(inv As invtype) As String
   Dim As String ret = "None"
 
   'если classid равен None, то ничего не делаем.
   If inv.classid <> clNone Then
     'Полчим описание для золота.
     If inv.classid = clGold Then
        ret = inv.desc
     EndIf
   EndIf
   'Получим описание для «еды». 
   If inv.classid = clSupplies Then
     'Ксли не распознан, вернем базовое описание предмета.
     If inv.supply.eval = FALSE Then
        ret = inv.desc
     Else
        'Вернем секретное описание.
        ret = inv.supply.sdesc
     EndIf
   EndIf
 
   Return ret
 End Function

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

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

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

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