среда, 7 марта 2012 г.

Давайте сделаем рогалик. Глава 17: Монстры

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

Первое что нужно сделать, это определить идентификаторы монстров.

monster.bi
'Идентификаторы монстров.
  Enum monids
    monNone
    monDarkangel
    monGiantbat
    monGiantscorpion
    monDragon
    monElfwarrior
    monWisp
    monGiant
    monHarpy
    monIncubus
    monJadegolem
    monKraken
    monLamia
    monManticore
    monNaga
    monOgre
    monPhantomfungus
    monQuorn
    monRockgolem
    monSkeleton
    monTroll
    monUruk
    monVampire
    monWombat
    monXerth
    monYeek
    monZombie
    monFlameangel
    monWerebear
    monGiantcentipede
    monDemonspawn
    monElemental
    monFlamegolem
    monGolem
    monHobgoblin
    monInterloper
    monRovingjelly
    monKobold
    monLich
    monMage
    monNazgul
    monOrc
    monPulsingeye
    monTwinhead
    monRogue
    monShurik
    monGianttarantula
    monGiantbeetle
    monVarghoul
    monWraith
    monXorn
    monYekki
    monGriffon
  End Enum

Все эти монстры будут у нас в игре. Есть 52 монстра — по одному на каждую букву алфавита верхнего и нижнего регистра. Далее нам нужно определить структуру данных для монстров. Мы будем подходить к монстрам также, как и к предметам инвентаря. Монстры станут частью объекта уровня, так что программный код для монстров будет походить на код для предметов инвентаря.

monster.bi
'Определение типа данных для монстров. 
  Type montype
    id As monids         'Идентификатор монстра.
    mname As String * 15 'Имя монстра.
    micon As String * 1  'Иконка.
    mcolor As UInteger   'Цвет иконки.
    ismagic As Integer   'Может использовать магию.
    spell As Integer     'Атакующее/Зашитное заклинание
    cd As Integer        'Фактор защиты
    cf As Integer        'Фактор атаки.
    md As Integer        'Фактор магической защиты
    mf As Integer        'Фактор магической атаки.
    currhp As Integer    'Текущее здоровье
    xp As Integer        'Получаемый за монстра опыт.
    dropcount As Integer 'Количество предметов в инвентаре.
    dropitem(1 To 4) As invtype 'Предметы, оставшиеся после смерти монстра.
    atkdam As Integer    'Наносимые монстром повреждения.
    armval As Single     'Значение защиты в процентах.
    atkrange As Integer  'Дистанционная атака. 
    psighted As Integer  'Указывает что монстр увидел персонажа.
    plastloc As mcoord   'Последняя позиция персонажа, нде его видел монстр.
    currcoord As mcoord  'Ткущая позиция монстра.
    flee As Integer      'Монстр убегает.
    isdead As Integer    'Монстр мертв.
  End Type

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

Используя данное определение типа мы можем создать монстра.

monster.bi
'Генерируем монстра.
  Sub GenerateMonster(mon As montype)
    Dim As invtype inv
    Dim scaling As Integer   'Коэффициент масштабирования атрибутов монстра.
    Dim stratt As Integer    'Сила.
    Dim staatt As Integer    'Выносливость.
    Dim dexatt As Integer    'Ловкость.
    Dim aglatt As Integer    'Подвижность.
    Dim intatt As Integer    'Интеллект.
    Dim ucfsk As Integer     'Навык безоружного боя.
    Dim acfsk As Integer     'Навык использования оружия ближнего боя.
    Dim pcfsk As Integer     'Навык использования дистанционного оружия.
  
    'Установим характеристи монстра.
    mon.id = RandomRange(monDarkangel, monGriffon)
    scaling = pchar.CurrStr / 2
    stratt = RandomRange(pchar.CurrStr - scaling, pchar.CurrStr + scaling)
    scaling = pchar.CurrSta / 2
    staatt = RandomRange(pchar.CurrSta - scaling, pchar.CurrSta + scaling)
    scaling = pchar.CurrDex / 2
    dexatt = RandomRange(pchar.CurrDex - scaling, pchar.CurrDex + scaling)
    scaling = pchar.CurrAgl / 2
    aglatt = RandomRange(pchar.CurrAgl - scaling, pchar.CurrAgl + scaling)
    scaling = pchar.CurrInt / 2
    intatt = RandomRange(pchar.CurrInt - scaling, pchar.CurrInt + scaling)
    'Рассчитаем боевые факторы.
    acfsk = stratt + dexatt 
    pcfsk = dexatt + intatt
    'Установим боевые факторы.
    mon.cd = stratt + aglatt
    mon.md = aglatt + intatt 
    mon.mf = intatt + staatt
    mon.cf = stratt + aglatt 
  
    'Зададим уровень здоровья.
    mon.currhp = stratt + staatt
  
    'Не убегает.
    mon.flee = FALSE
    'Не видит персонажа.
    mon.psighted = FALSE
    'Не знает о последнем местонахождении персонажа.
    mon.plastloc.x = 0
    mon.plastloc.y = 0  
    'Цвет иконки.
    mon.mcolor = fbRedBright
    'Монст мертв.
    mon.isdead = FALSE
  
    'Очистим массив предметов монстра.
    For i As Integer = LBound(mon.dropitem) To UBound(mon.dropitem)
      ClearInv mon.dropitem(i)
    Next
  ...

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

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

Следующий шаг в процедуре GenerateMonster, это задание значений параметрам, зависящим от идентификатора монстра.

monster.bi
'Установим индивидуальные атрибуты.
    Select Case mon.id
      Case monDarkangel
         mon.mname = "Dark Angel" 'Имя.
         mon.micon = "A"          'Иконка.
         mon.ismagic = TRUE       'Использует ли магию.
         mon.spell = 0            'Заклинание.
         mon.atkrange = 1         'Дальность атаки.
         mon.dropcount = 1        'Количество предметов в инвентаре.
         GenerateWeapon inv, currlevel, RandomRange(wpSmallsword, wpBishopsflail) 'Оружие.
         mon.dropitem(1) = inv    'Добавим предмет в инвентарь.
         mon.atkdam = mon.dropitem(1).weapon.dam 'используем повреждения от оружия.
         mon.armval = .8 'Рейтинг брони.
         mon.cf = acfsk 'Фактор атаки.
      Case monGiantbat
         mon.mname = "Giant Bat"  'Имя.
         mon.micon = "B"          'Иконка.
         mon.ismagic = FALSE      'Использует ли магию .
         mon.spell = 0            'Заклинание .
         mon.atkrange = 1         'Дальность атаки.
         mon.dropcount = 0        'Количество предметов в инвентаре.
         mon.atkdam = stratt / 4  'Сколько повреждений наносит.
         mon.armval = .1          'Рейтинг брони.
      Case monGiantscorpion
         mon.mname = "Giant Scorpion" 'Имя.
         mon.micon = "C"              'Иконка.
         mon.ismagic = FALSE          'Использует ли магию.
         mon.spell = 0                'Заклинание.
         mon.atkrange = 2             'Дальность атаки.
         mon.dropcount = 0            'Кол-во предметов в инвентаре.
         mon.atkdam = stratt / 4      'Сколько повреждений наносит.
         mon.armval = .9              'Рейтинг брони.
  ...

Здесь у нас приведен пример установки характеристик Темному ангелу, Гигантской летучей мыши и Гигантскому скорпиону. У каждого из них есть уникальные черты, которые задают индивидуальность монстра. Обратите внимание, что у Темного ангела есть в инвентаре один предмет, это оружие, генерируемое случайным образом. Урон Темного использует сгенерированное оружие, поэтому наносимый им урон зависит от урона, наносимого этим оружием. Броню он не носит, но у него есть естественная броня, которая поглощает 80% урона и задается параметром armval. Также он может использовать магию, и, когда мы дойдем до реализации магии, мы добавим соответствующие заклинания в параметр spell.

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

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

monster.bi
Case monElfwarrior
         mon.mname = "Elf Warrior" 'Имя
         mon.micon = "E"           'Иконка.
         mon.ismagic = TRUE        'Использует ли магию.
         mon.spell = 0             'Заклинание.
         mon.atkrange = 1          'Дальность атаки.
         mon.dropcount = 3         'Кол-во предметов в инвентаре.
         GenerateWeapon inv, currlevel, RandomRange(wpSmallsword, wpBishopsflail) 'Оружие. 
         mon.dropitem(1) = inv     'Добавим предметы в инвентарь.
         mon.atkdam = mon.dropitem(1).weapon.dam
         ClearInv inv
         GenerateArmor inv, currlevel, RandomRange(armCloth, armPlate)
         mon.dropitem(2) = inv 
         mon.armval = mon.dropitem(2).armor.dampct 'Armor rating.
         ClearInv inv 'Extra item.
         GenerateSupplies inv, currlevel
         mon.dropitem(3) = inv
         mon.cf = acfsk            'Фактор атаки.

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

Поле atkrange задает дальность атаки монстра. Для большинства оно будет равно единице, но некоторые монстры будут способны достать персонажа и на расстоянии.

monster.bi
Case monDragon
         mon.mname = "Dragon" 'Name
         mon.micon = "D"      'Map icon.
         mon.ismagic = FALSE 'Magic flag.
         mon.spell = 0      'If true then spell.
         mon.atkrange = 6 'Attack range.
         mon.dropcount = 0  'Number of items in inventory.
         mon.atkdam = stratt / 4 'How much damage mon does.
         mon.armval = .9 'Armor rating.
         mon.cf = pcfsk 'Combt attack factor.

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

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

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

map.bi
'Ячейка уровня (тайл)
  Type mapinfotype
    terrid As terrainids  'Тип местности.
    monidx As Integer     'Индекс в массиве монстров.
    visible As Integer    'Персонаж может видеть ячейку.
    seen As Integer       'Персонаж уже видел ячейку.
    doorinfo As doortype  'Параметры двери.
  End Type
  
  'Информация о уровне подземелья.
  Type levelinfo
    numlevel As Integer                       'Ткущий уровень подземелья.
    lmap(1 To mapw, 1 To maph) As mapinfotype 'Массив карты.
    linv(1 To mapw, 1 To maph) As invtype     'Инвентарь карты.
    nummon As Integer                         'Кол-во монстров на карте: от 10 до maxmonster.
    moninfo(1 To nroommax) As montype         'Массив монстров.
  End Type

Мы обновили структуру mapinfotype добавив в нее поле содержащее индекс монстра из массива монстров, находящихся на карте. Помните, что mapinfotype — это ячейка двумерного массива карты подземелья, которая содержит в себе параметры типа местности, видимости и т. д. В levelinfo мы добавили два новых поля, это nummon — которое содержит количество всех монстров на уровне подземелья, и moninfo, которое представляет из себя массив значений типа montype и содержит характеристики конкретного монстра. Вопрос в том, какое максимальное количество монстров мы должны добавить на уровень? Что бы облегчить задачу, мы поместили монстров в комнатах, в результате. Вполне естественно, что максимальное количество монстров зависит от количества комнат на текущем уровне. Поэтому мы использовать nroommax как верхнюю границу массива монстров.

Для размещения монстров мы начнем с процедуры GenerateDungeonLevel.

map.bi
'Создает новый уровень подземелья.
  Sub levelobj.GenerateDungeonLevel()
    Dim As Integer x, y, i
  
    'Clear level
    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).monidx = 0               'Нет монстра.
            _level.lmap(x, y).doorinfo.locked = FALSE  'Дверь не заперта.
            _level.lmap(x, y).doorinfo.lockdr = 0      'Не указа сложность.
            _level.lmap(x, y).doorinfo.dstr = 0        'Не указана сила.
            _level.nummon = 0                          'Кол-во монстров 0.
            ClearInv _level.linv(x,y)                  'Очистим слоты предметов.
        Next
    Next
    _InitGrid
    _DrawMapToArray
    _GenerateItems
  End Sub

Мы устанавливаем monindex в 0 для каждой клетки на карте и задаем общее количество монстров на карте (nummon) так же равным нулю. Создавать монстров мы будем в подпрограмме DrawMapToArray.

map.bi
'Поместим данные из сетки в массив карты уровня.
  Sub levelobj._DrawMapToArray()
    Dim As Integer i, x, y, pr, rr, rl, ru, kr, nid, moncnt
    Dim As mcoord ncoord
    
    'Рисуем в массиве первую комнату
    For x = _rooms(1).tl.x + 1 To _rooms(1).br.x - 1
        For y = _rooms(1).tl.y + 1 To _rooms(1).br.y - 1
            _level.lmap(x, y).terrid = tfloor
        Next
    Next
    'Рисуем остальные комнаты и соединяем их.
    For i = 2 To _numrooms
        For x = _rooms(i).tl.x + 1 To _rooms(i).br.x - 1
            For y = _rooms(i).tl.y + 1 To _rooms(i).br.y - 1
                _level.lmap(x, y).terrid = tfloor
            Next
        Next
       'Получим центр комнаты.
       ncoord.x = _rooms(i).roomdim.rcoord.x + 1
       ncoord.y = _rooms(i).roomdim.rcoord.y + 1
       'Добавим в комнату монстра.
      If RandomRange(-5, maxlevel) <= currlevel Then
         moncnt += 1
         If moncnt <= nroommax Then
             _level.nummon = moncnt
             GenerateMonster _level.moninfo(_level.nummon)
             _level.lmap(ncoord.x, ncoord.y).monidx = _level.nummon
             _level.moninfo(_level.nummon).currcoord = ncoord
         End If 
       End If
        _ConnectRooms i, i - 1
    Next
    'Добавим двери в выбранную комнату.
    _AddDoors
    'Зададим местоположение персонажа.
    x = _rooms(1).roomdim.rcoord.x + (_rooms(1).roomdim.rwidth \ 2) 
    y = _rooms(1).roomdim.rcoord.y + (_rooms(1).roomdim.rheight \ 2)
    pchar.Locx = x - 1
    pchar.Locy = y - 1
    'Добавим лестницу вверх.
    _level.lmap(pchar.Locx, pchar.Locy).terrid = tstairup
    'Добавим лестницу вниз в последней комнате.
    x = _rooms(_numrooms).roomdim.rcoord.x + (_rooms(_numrooms).roomdim.rwidth \ 2) 
    y = _rooms(_numrooms).roomdim.rcoord.y + (_rooms(_numrooms).roomdim.rheight \ 2)
    _level.lmap(x - 1, y - 1).terrid = tstairdn
  End Sub

Количество монстров в подземелье основывается на количестве комнат и глубине уровня. Код RandomRange (-5, maxlevel) <= currlevel устанавливает вероятность появления в комнате монстра. На глубине нескольких первых уровней монстров будет немного, но, по мере того как персонаж будет спускаться все глубже и глубже, монстры будут встречаться все чаще. Это даст игроку ощущение опасности на более глубоких уровнях подземелья.

map.bi
'Получим центр комнаты.
      ncoord.x = _rooms(i).roomdim.rcoord.x + 1
      ncoord.y = _rooms(i).roomdim.rcoord.y + 1
      'Добавим в комнату монстра.
      If RandomRange(-5, maxlevel) <= currlevel Then
         moncnt += 1
         If moncnt <= nroommax Then
             _level.nummon = moncnt
             GenerateMonster _level.moninfo(_level.nummon)
             _level.lmap(ncoord.x, ncoord.y).monidx = _level.nummon
             _level.moninfo(_level.nummon).currcoord = ncoord
         End If 
       End If

Как только мы добавили комнату, мы можем добавить монстра. Для этого мы вызываем процедуру GenerateMonster, которая его создаст монстра, записываем его идентификатор в ячейку карты по координатам ncoord, а также добавляем эти координаты в поле currcoor информации о монстре. Это расставит наших монстров на карте.

Последнее, что нам осталось сделать, это отобразить монстров на карте локации, что мы и сделаем в процедуре DrawMap.

map.bi
'Если в текущей ячейке есть монстр — нарисуем его.
  If _level.lmap(i + x, j + y).monidx > 0 Then
    monid = _level.lmap(i + x, j + y).monidx
    mtile = _level.moninfo(monid).micon
    tilecolor = _level.moninfo(monid).mcolor
    PutText acBlock, y + 1, x + 1, fbBlack
    PutText mtile, y + 1, x + 1, tilecolor
  EndIf

Если монстр находится в зоне видимости персонажа, то нам нужно его отобразить. Как вы можете видеть, процесс достаточно прост. Если monidx больше 0 то мы получаем иконку монстра и ее цвет из массива монстров и выводим ее на экран, предварительно очистив ячейку: PutText acBlock, y + 1, x + 1, fbBlack.

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

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

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

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