Теперь у нас есть структура данных для инвентаря и мы можем создать некоторое количество предметов и разместить их на карте подземелья, чтобы игрок смог их найти. Для того, чтобы иметь предметы на карте, нам нужно добавить к описанию уровня подземелья нашу структуру данных предмета инвентаря.
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Данная функция возвращает базовое описание предмета, если он не был распознан, или секретное, если иначе. Это может показаться окольным путем размещать данную процедуру не в объекте карты, но мы будем использовать данную функцию не только для вывода описаний при перемещении персонажа по локации. Она нам понадобиться, также. Для получения описаний предметов в инвентаре персонажа, поэтому, лучше, иметь только одну функцию для получения описаний, а не размещать несколько функций с аналогичными задачами в разных местах программы. У нас будет только одна функция которую нужно будет обновлять при добавлении новых предметов в игру, использующаяся для отображения карты и отображения инвентаря персонажа, поэтому не нужно будет заниматься синхронизацией нескольких идентичных функций.
Теперь на нашей карте есть предметы, следующим шагом, будет, дать возможность персонажу их подобрать. Этим мы и займемся в следующей главе.
Комментариев нет:
Отправить комментарий