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

Материал из Клуб любителей рогаликов
Версия от 20:44, 30 декабря 2016; Apromix (обсуждение | вклад)
(разн.) ← Предыдущая | Текущая версия (разн.) | Следующая → (разн.)
Перейти к: навигация, поиск
Richard D Clark-Let's Build a Roguelike-Genchar.png

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

character.bi

'Определение типа данных характеристик персонажа. 
Type characterinfo 
  cname As String * 40 'Имя персонажа. 
  stratt(2) As Integer 'Сила (0), бонус силы (1) 
  staatt(2) As Integer 'Выносливость (0), бонус выносливости (1) 
  dexatt(2) As Integer 'Ловкость (0), бонус ловкости (1) 
  aglatt(2) As Integer 'Проворство (0), бонус проворства (1) 
  intatt(2) As Integer 'Интеллект (0), бонус интеллекта (1) 
  currhp As Integer    'Текущее здоровье
  maxhp As Integer     'Максимальное здоровье
  ucfsk(2) As Integer  'Рукопашный бой (0), бонус рукопашного боя (1) 
  acfsk(2) As Integer  'Оружие ближнего боя 0), бонус оружия бл.боя (1) 
  pcfsk(2) As Integer  'Дистанционное оружие (0), бонус дист. оружия (1) 
  mcfsk(2) As Integer  'Магическая атака (0), бонус маг. атаки (1) 
  cdfsk(2) As Integer  'Защита (0), бонус защиты (1) 
  mdfsk(2) As Integer  'Магическая защита (0), бонус маг. защиты (1) 
  currxp As Integer    'Текущий, расходуемый опыт. 
  totxp As Integer     'Общая сумма опыта, за время жизни персонажа.
  currgold As Integer  'Текущее количество золота. 
  totgold As Integer   'Общая сумма золота за время жизни персонажа. 
  locx As Integer      'Текущая x координата персонажа на карте. 
  locy As Integer      'Текущая y координата персонажа на карте. 
End Type

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

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

Тип characterinfo является частью определения объекта персонажа (character), который мы будем использовать в игре.

character.bi

'Описание объекта персонаж.
Type character
  Private:
    _cinfo As characterinfo
  Public:
    Declare Sub PrintStats ()
    Declare Function GenerateCharacter() As Integer
End Type

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

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

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

Когда игрок выберет пункт «Новая игра» из главного меню, мы будем вызывать функцию GenerateCharacter.

dod.bas

'Получим выбранный пункт меню 
Dim mm As mmenu.mmenuret 
'Повторить, пока пользователь не выбирает New, Load или Quit.
Do 
  'Нарисуем главное меню. 
   mm = mmenu.MainMenu 
  'Обработаем выбранный пункт меню. 
  If mm = mmenu.mNew Then 
    'Сгенерируем персонажа. 
    Var ret = pchar.GenerateCharacter 
    'Не выходим из меню, если пользователь нажмет ESC. 
    If ret = FALSE Then 
      'Установим переменную, чтобы не выйти из цикла. 
      mm = mmenu.mInstruction 
    EndIf 
  ElseIf mm = mmenu.mLoad Then 
    'Загрузка cохраненной игры. 
  ElseIf mm = mmenu.mInstruction Then 
    'Показать инструкцию. 
  EndIf 
Loop Until mm <> mmenu.mInstruction

Функция GenerateCharacter вернет FALSE, если игрок нажимает клавишу Escape и TRUE, если игрок нажмет Eenter. Если игрок нажмет Escape в функции генерации персонажа, то мы устанавливаем переменную mm в значение mmenu.mInstruction, чтобы не выйти из цикла и снова отобразилось главное меню. Если была нажата клавиша Enter то программа выйдет из цикла меню и войдет в основной цикл игры.

character.bi

'Генерация нового персонажа.
Function character.GenerateCharacter() As Integer
  Dim As String chname, prompt, skey
  Dim As Integer done = FALSE, ret = TRUE, tx, ty
  
  'Подсказка для пользователя.
  prompt = "Press <r> to roll again, <enter> to accept, <esc> to exit to menu."
  tx = (CenterX(prompt)) * charw
  ty = (txrows - 6) * charh   
  'Получим имя персонажа.
  Do
    Cls
    'Используем простейший ввод строки.
    Input "Enter your character's name (40 chars max):",chname
    'Проверим введенное имя. 
    If Len(chname) > 0 And Len(chname) < 40 Then
      done = TRUE
    Else
      'Сообщим игроку, что он сделал не так.
      Cls
      If Len(chname) = 0 Then
        Print "Name is required. <Press any key.>"
        Sleep
        ClearKeys
      EndIf
      If Len(chname) > 40 Then
        Print "Name is too long. 40 chars max. <Press any key.>"
        Sleep
        ClearKeys
      EndIf
    EndIf
    Sleep 10
  Loop Until done = TRUE
  done = FALSE
  'Генерируем параметры персонажа.
  Do
    With _cinfo
      .cname = chname
      .stratt(0) = RandomRange (1, 20)
      .staatt(0) = RandomRange (1, 20)
      .dexatt(0) = RandomRange (1, 20)
      .aglatt(0) = RandomRange (1, 20)
      .intatt(0) = RandomRange (1, 20)
      .currhp = .stratt(0) + .staatt(0) 
      .maxhp = .currhp
      .ucfsk(0) = .stratt(0) + .aglatt(0) 
      .acfsk(0) = .stratt(0) + .dexatt(0) 
      .pcfsk(0) = .dexatt(0) + .intatt(0)
      .mcfsk(0) = .intatt(0) + .staatt(0)
      .cdfsk(0) = .stratt(0) + .aglatt(0)
      .mdfsk(0) = .aglatt(0) + .intatt(0)
      .currxp = RandomRange (100, 200)
      .totxp = .currxp
      .currgold = RandomRange (50, 100)
      .totgold = .currgold
      .locx = 0
      .locy = 0
    End With
    'Выведем на экран текущие параметры персонажа.
    PrintStats
    DrawStringShadow tx, ty, prompt
    'Получим команду от пользователя.
    Do
      'Получим нажатую клавишу.
      skey = Inkey
      'Преобразуем символ в нижний регистр.
      skey = LCase(skey)
      'Если Escape то выйдем в меню.
      If skey = key_esc Then
        done = TRUE
        ret = FALSE
      EndIf
      'Если нажат enter, выйдем, и продолжим с этим персонажем в игре.
      If skey = key_enter Then
        done = TRUE
      EndIf
      Sleep 10
    Loop Until (skey = "r") Or (skey = key_esc) Or (skey = key_enter)
  Loop Until done = TRUE 
  Return ret
End Function

Метод, который мы используем, для создания персонажа не слишком сложный. Первая часть кода настраивает переменные для вывода подсказки пользователю со списком команд: генерировать персонажа заново, принять текущий вариант или вернуться в главное меню.

Следующим шагом нам нужно получить имя персонажа. Так как переменная для хранения имени персонажа в типе characterinfo у нас 40 символов, то нам нужно убедиться, что игрок ввел имя длинной не менее одного символа и не более 40. Эта проверка не особенно необходима, т. к. компилятор все равно усечет длинную переменную до 40 символов, поскольку мы используем фиксированную длину строки, но проверка делает программу более профессиональной (в freebasic и вправду не критично, но если бы вы писали программу на других языках, например C, то ни к чему хорошему отсутствие проверки не привело бы (примечание переводчика)). Пользователь увидит, что мы потратили некоторое время на размышления о том, что мы делаем и то, что мы хотим, чтобы игрок получил удовольствие от игры, раз мы заботимся даже о таких незначительных мелочах.

character.bi

...
  'Получим имя персонажа.
  Do
    Cls
    'Используем простейший ввод строки.
    Input "Enter your character's name (40 chars max):",chname
    'Проверим введенное имя. 
    If Len(chname) > 0 And Len(chname) < 40 Then
      done = TRUE
    Else
      'Сообщим игроку, что он сделал не так.
      Cls
      If Len(chname) = 0 Then
        Print "Name is required. <Press any key.>"
        Sleep
        ClearKeys
      EndIf
      If Len(chname) > 40 Then
        Print "Name is too long. 40 chars max. <Press any key.>"
        Sleep
        ClearKeys
      EndIf
    EndIf
    Sleep 10
  Loop Until done = TRUE
...

Мы используем команду Input чтобы получить имя игрока, а затем проверяем длину введенного имени. Если оно больше 0 и меньше либо равно 40, то мы выходим из цикла, иначе мы выводим сообщение об ошибке и снова вызываем команду Input, чтобы пользователь ввел другое имя персонажа. После получения имени мы переходим к генерации параметров.

character.bi

  'Генерируем параметры персонажа.
  Do
    With _cinfo
      .cname = chname
      .stratt(0) = RandomRange (1, 20)
      .staatt(0) = RandomRange (1, 20)
      .dexatt(0) = RandomRange (1, 20)
      .aglatt(0) = RandomRange (1, 20)
      .intatt(0) = RandomRange (1, 20)
      .currhp = .stratt(0) + .staatt(0) 
      .maxhp = .currhp
      .ucfsk(0) = .stratt(0) + .aglatt(0) 
      .acfsk(0) = .stratt(0) + .dexatt(0) 
      .pcfsk(0) = .dexatt(0) + .intatt(0)
      .mcfsk(0) = .intatt(0) + .staatt(0)
      .cdfsk(0) = .stratt(0) + .aglatt(0)
      .mdfsk(0) = .aglatt(0) + .intatt(0)
      .currxp = RandomRange (100, 200)
      .totxp = .currxp
      .currgold = RandomRange (50, 100)
      .totgold = .currgold
      .locx = 0
      .locy = 0
    End With

Параметры создаются заданием случайных чисел от 1 до 20. Как только мы получили характеристики персонажа, мы рассчитываем его навыки в соответствии с формулами, которые мы определили когда создавали ролевую систему. Для хранения значений каждого параметра мы используем целочисленные массивы из 2-х элементов. По индексу 0 мы храним текущее значение параметров и навыков, которое мы будем использовать во всех расчетах. Золото и опыт представляют собой жизнь персонажа «до игры». Очки опыта позволят игроку улучшить некоторые параметры персонажа, а золото — приобрести какое нибудь снаряжение от «Блуждающего Торговца». Блуждающие Торговцы будут рассмотрены нами позже.

Выше мы использовали новую функцию, которая называется RandomRange. Ее мы определили в utils.bi

utils.bi

'Возвращает случайное число в пределах заданного диапазона. 
Function RandomRange(lowerbound As Integer, upperbound As Integer) As Integer 
  Return Int((upperbound - lowerbound + 1) * Rnd + lowerbound) 
End Function

Функция возвращает целое случайное число, лежащее в пределах минимального и максимального значения включительно. В этой функции хорошо то, что мы можем использовать ее для получения не только положительных чисел, но и отрицательных, а также чисел, лежащих между отрицательным и положительным значениями диапазона, например, для создания бонуса = RandomRange(-10,10). Мы будем использовать эту технику, когда добавим монстров в игру и будем задавать им различные характеристики.

Как только параметры и навыки получены, мы вызываем подпрограмму PrintStats, чтобы вывести результаты на экран.

character.bi

'Выведем на экран текущие параметры персонажа. 
Sub character.PrintStats () 
  Dim As Integer tx, ty, row = 8 
  Dim As String sinfo 
  
  ScreenLock 
  'Рисум задний фон. 
  DrawBackground charback() 
  'Выведем заголовок. 
  sinfo = Trim(_cinfo.cname) & " Attributes and Skills" 
  ty = row * charh 
  tx = (CenterX(sinfo)) * charw 
  DrawStringShadow tx, ty, sinfo, fbYellow 
  'А теперь параметры. 
  row += 4 
  ty = row * charh 
  tx = 70 
  sinfo = "Strength:     " & _cinfo.stratt(0) 
  DrawStringShadow tx, ty, sinfo 
  row += 2 
  ty = row * charh 
  sinfo = "Stamina:      " & _cinfo.staatt(0) 
  DrawStringShadow tx, ty, sinfo 
  row += 2 
  ty = row * charh 
  sinfo = "Dexterity:    " & _cinfo.dexatt(0) 
  DrawStringShadow tx, ty, sinfo 
  row += 2 
  ty = row * charh 
  sinfo = "Agility:      " & _cinfo.aglatt(0) 
  DrawStringShadow tx, ty, sinfo 
  row += 2 
  ty = row * charh 
  sinfo = "Intelligence: " & _cinfo.intatt(0) 
  DrawStringShadow tx, ty, sinfo 
  row += 2 
  ty = row * charh 
  sinfo = "Hit Points:   " & _cinfo.currhp 
  DrawStringShadow tx, ty, sinfo 
  row += 3 
  ty = row * charh 
  sinfo = "Unarmed Combat:    " & _cinfo.ucfsk(0) 
  DrawStringShadow tx, ty, sinfo 
  row += 2 
  ty = row * charh 
  sinfo = "Armed Combat:      " & _cinfo.acfsk(0) 
  DrawStringShadow tx, ty, sinfo 
  row += 2 
  ty = row * charh 
  sinfo = "Projectile Combat: " & _cinfo.pcfsk(0) 
  DrawStringShadow tx, ty, sinfo 
  row += 2 
  ty = row * charh 
  sinfo = "Magic Combat:      " & _cinfo.mcfsk(0) 
  DrawStringShadow tx, ty, sinfo 
  row += 2 
  ty = row * charh 
  sinfo = "Combat Defense:    " & _cinfo.cdfsk(0) 
  DrawStringShadow tx, ty, sinfo 
  row += 2 
  ty = row * charh 
  sinfo = "Magic Defense:     " & _cinfo.mdfsk(0) 
  DrawStringShadow tx, ty, sinfo 
  row += 3 
  ty = row * charh 
  sinfo = "Experience: " & _cinfo.currxp 
  DrawStringShadow tx, ty, sinfo 
  row += 2 
  ty = row * charh 
  sinfo = "Gold:       " & _cinfo.currgold 
  DrawStringShadow tx, ty, sinfo 

  ScreenUnLock 
End Sub

Здесь просто выводятся значения всех параметров и навыков на экран. Вначале, как и на предыдущих экранах, мы выводим фоновое изображение при помощи ASCII графики, а затем. Используя новую подпрограмму DrawStringShadow, которую мы добавили в utils.bi, распечатываем информацию.

utils.bi

'Высести строку с обтрасываемой тенью. 
Sub DrawStringShadow(x As Integer, y As Integer, txt As String, fcolor As UInteger = fbWhite) 
  Draw String (x + 1, y + 1), txt, fbBlack
  Draw String (x, y), txt, fcolor
End Sub

Данная функция выводит текстовую строку с тенью, при помощи техники, которую мы использовали ранее. т. к. мы собираемся пользоваться данным методом неоднократно, то имеет смысл вынести код в отдельную функцию, чтобы мы могли вызывать ее из любого места программы. Подпрограмма выводит «тень» строки, печатая эту строку со смещением в 1 пиксель по вертикали и горизонтали используя черный цвет, а затем выводит саму строку (без смещения) цветом fcolor, или белым, если переменная fcolor не задана. Параметры x и y заданы в пикселях, а не в текстовых знакоместах, так что, перед вызовом данной функции нам предварительно нужно рассчитать эти координаты.

Как только информация о персонаже напечатана, мы выводим внизу экрана строку с подсказкой для пользователя и входим в цикл ждущий ввод данных с клавиатуры. Этот цикл находится внутри основного цикла в котором генерируется персонаж и выводятся на экран данные. Нужно это для того, чтобы пользователь мог пересоздать своего персонажа. Как только игрок доволен персонажем (нажатие Enter) программа выходит из цикла генерации персонажа, цикла главного меню и переходит в главный цикл игры.

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


[ < Предыдущая глава ][ Оглавление ][ Следующая глава > ]