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

Создание модели структуры данных для рогалика, или любой другой игры, которая должна манипулировать с различными предметами, представляет интересную задачу для программиста. Структура данных должна быть в состоянии хранить предметы обладающие различными свойствами и характеристиками, сохранять эти элементы в коллекции и давать возможность легко всем этим управлять на протяжении всего игрового процесса. Также нам необходима эффективная и самодостаточная структура данных, которая могла бы содержать в себе и манипулировать совершенно разными структурами данных — для организации списков предметов разных типов. Задача кажется почти невозможной, но на самом деле все достаточно просто. Мы создадим составной тип данных, в котором будем хранить информацию о различных параметрах различных предметов используя определения типов Type Defs и объединения Union.

Вот главный тип предмета инвентаря.

inv.bi

'Тип инвентаря.
Type invtype
    classid As classids     'Идентификатор класса данных в объединении.
    desc As String * 30     'Текстовое описание.
    icon As String * 1      'Иконка предмета.
    iconclr As Uinteger     'Цвет иконки предмета.
    Union                   'Объединение из различных типов предметов.
    gold As goldtype      'Золото.
    supply As supplytype  'Расходные материалы/еда.
    End Union
End Type


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

Объединение, это сегмент памяти, который может содержать различные данные, в зависимости от того, что мы туда поместили. Размер выделяемой оперативной памяти под объединение равен размеру памяти выделяемой под наибольший из объединяемых типов данных. Например, если у нас в объединении переменная типа Integer (4 байта) и какой либо другой тип данных состоящий из 4-х элементов типа Byte (4 байта), то объединение все также будет занимать 4 байта в оперативной памяти, т. к. они будут размещены в одном адресном пространстве, а обращаться к данным, мы можем как к переменной типа Integer так и к каждому байту по отдельности. Следующий код (которого нет в DOD) иллюстрирует, как это работает.

Пример объединения Union:

'Тип пикселя.
Type Pixel_Color
    B As Ubyte
    G As Ubyte
    R As Ubyte
    A As Ubyte
End Type
'Тип цвета.
Union Pixel
    Channel As Pixel_Color
    Value   As Uinteger
End Union


Здесь у нас есть тип Pixel_Color содержащий 4 переменных типа Ubyte. Он используется в объединении Pixel (переменная Channel) совместно с переменной Value типа Uinteger. В результате переменная Channel и Value находятся в одном адресном пространстве оперативной памяти. Это означает, что если в переменную Value мы запишем RGB код цвета, то в переменной Channel мы сможем получить значение цвета конкретного канала из внутренних переменных типа B, G, R и A. Если бы нам понадобилось изменить только голубую составляющую цвета, то мы бы просто записали новое значение в переменную B, а в переменной Value получили бы код вновь полученного цвета. Меняя значение в одной из переменных, мы меняем и другую переменную, так как данные находятся в одном адресном пространстве.

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

Давайте более детально рассмотрим наш составной тип данных.

inv.bi

'Тип инвенторя.
Type invtype
    classid As classids     'Идентификатор класса данных в объединении.
    desc As String * 30     'Текстовое описание.
    icon As String * 1      'Иконка предмета.
    iconclr As Uinteger     'Цвет иконки предмета.
    Union                   'Объединение из различных типов предметов.
        gold As goldtype      'Золото.
        supply As supplytype  'Расходные материалы/еда.
    End Union
End Type


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

inv.bi

'ID классов предметов.
Enum classids
    clNone
    clGold
    clSupplies
    clPotion
    clWand
    clWeapon
    clArmor
    clLight
    clAmmo
    clShield
    clScrolls
    clSpellBook
End Enum


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

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

inv.bi

'ID предмета «золото».
Enum goldids
    gldGoldNone   'Нет золота.
    gldGold       'Золотые монеты.
    gldBagGold    'Мешок золота.
End Enum

'пердмет «золото».
Type goldtype
    id As goldids    'тип золотого предмета.
    amt As Integer   'количество монет.
End Type


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

С расходными материалами несколько сложнее, но не намного.

inv.bi

'ID эффекта.
Enum effectsid
    effEffectNone  'Без эффекта.
    effMaxHealing  'Лечит здоровье до максимума.
    effStrongMeat  'Добавляет бонус силы на определенное время.
    effBreadLife   'Лечит отравление.
    effSeeAll      'Делает всю карту видимой.
End Enum

'ID использования предмета.
Enum itemuse
    useNone      'Не используется
    useDrinkEat  'Еда или питье.
    useWieldWear 'Можно одеть или вооружиться.
End Enum

'ID еды.
Enum supplyids
    supSupplyNone  'Не еда.
    supHealingHerb 'Лечебная трава, лечит 50% от максимального здоровья.
    supHunkMeat    '25% от максимального здоровья.
    supBread       '10% от максимального здоровья.
End Enum

'Определение типа «расходные материалы».
Type supplytype
    id As supplyids      'Указывает на тип предмета.
    evaldr As Integer    'Рейтинг сложности, используется для определения предмета, если магический: 0 = не магический.
    eval As Integer      'Истина, если был определен.
    effect As effectsid  'Тип магического эффекта.
    sdesc As String * 30 'Тайное имя/описание для магического эффекта. Выявляется при определении.
    noise As Integer     'Количество шума, генерируемого предметом при использовании или в рюкзаке, при перемещении персонажа.
    use As itemuse       'Как используется.
End Type


Расходные материалы, это все предметы, которые может потреблять персонаж. Т. е. этот тип данных описывает все предметы, которые можно есть или пить. Их употребление приносит пользу для здоровья персонажа. Так как эти предметы могут содержать магию, то в описании типа добавлено несколько полей для указания магических эффектов этих предметов. Каждый магический эффект соответствует конкретному предмету. effMaxHealing, это магический эффект от лечебной травы, effStrongMeat является эффектом от куска мяса, и effBreadLife — эффект от волшебного хлеба.

Поле use указывает на то, что можно сделать с этим предметом, это useDrinkEat либо useWieldWear и будет использоваться в командной части инвентаря, для фильтрации и вывода предметов определенного типа, если пользователь выбирает Еда/Питье или Одежда/Оружие.

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

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

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

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

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

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