Давайте сделаем рогалик (Запертые двери)
Раз уж мы внедрили механику сотворения заклинаний, пришло время поработать
над тем, чтобы заставить функционировать остальные пункты главного меню. Два из
них — это Вскрыть замок (Pick Lock) и Выбить дверь (Bash Door). Оба действия
относятся к запертым дверям, поэтому нам необходимо реализовать их в игре, чтобы
персонажу было что вскрывать и выбивать. Запертые двери мы интегрируем в наш уже
существующий код дверей.
Прежде чем перейти непосредственно к коду размещения
дверей, давайте взглянем на структуру (Type Def) нашей двери, чтобы освежить в
памяти необходимые данные.
map.bi
'Тип двери. Type doortype locked As Integer 'True, если заперта. lockdr As Integer 'Сложность взлома замка. dstr As Integer 'Прочность двери (для выбивания). End Type
Флаг locked указывает, заперта дверь или открыта, где значение TRUE означает запертую дверь. Переменная lockdr — это сложность замка, то есть то, насколько трудно его вскрыть. Мы будем использовать это значение при броске кубика на взлом. Переменная dstr — это прочность двери, и это значение пригодится для броска на выбивание. Это наше определение данных двери, а теперь давайте посмотрим, как мы заполняем эти данные.
map.bi
'Добавление дверей в комнату. Sub levelobj._AddDoorsToRoom(i As Integer) Dim As Integer row, col, dd1, dd2, nid, roll 'Добавляем двери. For col = _rooms(i).tl.x To _rooms(i).br.x dd1 = _rooms(i).tl.y dd2 = _rooms(i).br.y 'Если в стене есть свободное пространство пола. If _level.lmap(col, dd1).terrid = tfloor Then 'Добавляем дверь. _level.lmap(col, dd1).terrid = tdoorclosed 'Проверяем, заперта ли дверь. roll = RandomRange(0, maxlevel * 2) _level.lmap(col, dd1).doorinfo.locked = (roll < _level.numlevel) If _level.lmap(col, dd1).doorinfo.locked = TRUE Then _level.lmap(col, dd1).doorinfo.lockdr = _level.numlevel _level.lmap(col, dd1).doorinfo.dstr = _level.numlevel * 10 End If Endif ...
Здесь я показываю код только для одной стены комнаты, поскольку все стены
обрабатываются одинаково. Давайте разберем, что мы тут делаем, прежде чем
углубляться дальше. Как только комната размещена и соединена с остальными, мы
перебираем клетки вдоль её стен в поисках тайла пола (tfloor). Если находим
такой, значит, мы наткнулись на проем в стене, который ведет в коридор. Затем мы
ставим в этот проем закрытую дверь, что обозначается типом ландшафта
tdoorclosed.
После размещения двери мы генерируем случайное число (делаем
бросок) от 0 до максимального количества уровней, умноженного на два. Это дает
нам диапазон значений от 0 до 100, так как на данный момент maxlevel у нас равен
50. Как и в случае с другими объектами в игре, такой диапазон обеспечивает
возрастающую вероятность того, что дверь окажется запертой по мере нашего
продвижения вглубь подземелья. На 50-м уровне вероятность встретить запертую
дверь составит примерно 50%. Получив результат броска, мы сравниваем его с
номером текущего уровня. Если выпавшее число меньше номера текущего уровня,
дверь запирается. Мы заполняем структуру данных двери, и вуаля — у нас есть
запертая дверь.
Теперь, когда у нас появились запертые двери, нам нужно иметь
возможность их открывать. Давайте сначала рассмотрим более простой вариант —
выбивание дверей.
dod.bas
'Попытка выбить дверь. Sub BashDoor Dim As vec dt Dim As Integer ret, proll 'Проверка наличия запертой двери. ret = GetLockedDoor(dt) 'Запертая дверь найдена. If ret = TRUE Then 'Получаем значение броска персонажа. proll = pchar.CurrStr + pchar.BonStr ret = level.OpenLockedDoor(dt.vx, dt.vy, proll) If ret = TRUE Then PrintMessage "You bashed open the door." 'Вы выбили дверь. Else 'Вычитаем 1 единицу здоровья (hp). PrintMessage "The door remains closed." 'Дверь осталась закрытой. pchar.CurrHP = pchar.CurrHP - 1 Endif Else PrintMessage "No locked doors found." 'Запертых дверей не найдено. Endif End Sub
Первое, что нам необходимо сделать — убедиться, что запертая дверь
находится вплотную к персонажу. Мы делаем это с помощью функции GetLockedDoor,
которую рассмотрим чуть позже. Если запертая дверь рядом, мы рассчитываем силу
удара персонажа: текущая сила (pchar.CurrStr) плюс любые бонусы к силе
(pchar.BonStr). Затем мы передаем эти данные вместе с координатами запертой
двери в функцию OpenLockedDoor. Мы создали OpenLockedDoor еще когда
реализовывали заклинание открытия дверей, и здесь мы просто повторно используем
этот код. Если дверь поддалась — всё отлично. Если же она не открывается,
персонаж получает 1 единицу урона. Это симулирует то, что удары по упрямой двери
— далеко не лучший способ её открыть.
Давайте взглянем на функцию
GetLockedDoor, так как мы будем использовать её и в подпрограмме взлома
замков.
dod.bas
'Ищет дверь по каждой из сторон света. Function GetLockedDoor(dt As vec) As Integer Dim As Integer ret = FALSE Dim As terrainids tile 'Проверяем дверь рядом с игроком. dt.vx = pchar.Locx dt.vy = pchar.Locy 'Смотрим на север. dt += north 'Является ли тайл дверью? tile = level.GetTileID(dt.vx, dt.vy) If tile = tdoorclosed Then 'Проверяем, заперта ли она. ret = (level.IsDoorLocked(dt.vx, dt.vy) = TRUE) Endif 'Проверяем, нашли ли мы дверь. If ret = FALSE Then 'Смотрим на следующую клетку. dt.vx = pchar.Locx dt.vy = pchar.Locy 'Смотрим на восток. dt += east 'Является ли тайл дверью? tile = level.GetTileID(dt.vx, dt.vy) If tile = tdoorclosed Then 'Проверяем, заперта ли она. ret = (level.IsDoorLocked(dt.vx, dt.vy) = TRUE) Endif Endif 'Проверяем, нашли ли мы дверь. If ret = FALSE Then 'Смотрим на следующую клетку. dt.vx = pchar.Locx dt.vy = pchar.Locy 'Смотрим на юг. dt += south 'Является ли тайл дверью? tile = level.GetTileID(dt.vx, dt.vy) If tile = tdoorclosed Then 'Проверяем, заперта ли она. ret = (level.IsDoorLocked(dt.vx, dt.vy) = TRUE) Endif Endif 'Проверяем, нашли ли мы дверь. If ret = FALSE Then 'Смотрим на следующую клетку. dt.vx = pchar.Locx dt.vy = pchar.Locy 'Смотрим на запад. dt += west 'Является ли тайл дверью? tile = level.GetTileID(dt.vx, dt.vy) If tile = tdoorclosed Then 'Проверяем, заперта ли она. ret = (level.IsDoorLocked(dt.vx, dt.vy) = TRUE) Endif Endif Return ret End Function
В этой функции мы используем наш объект вектора (vec) и проверяем тайлы по четырем основным направлениям: север, юг, восток и запад. Персонаж не может открыть дверь по диагонали — в этом мало логики. Проверка всех направлений работает одинаково, поэтому давайте разберем только одно из них:
dod.bas:GetLockedDoor
'Проверяем дверь рядом с игроком. dt.vx = pchar.Locx dt.vy = pchar.Locy 'Смотрим на север. dt += north 'Является ли тайл дверью? tile = level.GetTileID(dt.vx, dt.vy) If tile = tdoorclosed Then 'Проверяем, заперта ли она. ret = (level.IsDoorLocked(dt.vx, dt.vy) = TRUE) EndIf
Сначала мы берем текущую позицию персонажа и записываем её в объект
вектора. Затем вызываем перегруженный оператор += объекта вектора, чтобы
получить новые координаты для проверки. Мы перехватываем ID тайла в этой точке и
проверяем, является ли он закрытой дверью (tdoorclosed). Если да, мы проверяем,
заперта ли она, с помощью функции IsDoorLocked.
map.bi
'Возвращает True, если дверь заперта. Function levelobj.IsDoorLocked(x As Integer,y As Integer) As Integer Return _level.lmap(x, y).doorinfo.locked End Function
Как видите, мы просто возвращаем состояние флага locked для переданных
координат. Этот флаг возвращается в функцию GetLockedDoor, которая, в свою
очередь, передает его обратно в подпрограмму выбивания дверей
(BashDoor).
Выбивание — лишь один из способов открыть дверь. Персонаж также
может вскрыть замок, так что давайте рассмотрим этот метод следующим.
Прежде
чем мы перейдем к коду взлома, нам нужно добавить в игру инструменты для
вскрытия замков. Я сделал отмычки расходуемым предметом снабжения (supply item),
чтобы мы могли задействовать уже существующую кодовую базу. Вот новые предметы
для взлома замков:
inv.bi
'Идентификаторы предметов снабжения. Enum supplyids supSupplyNone 'Ничего. supHealingHerb 'Целебная трава — исцеляет 50% от макс. HP. supHunkMeat 'Кусок мяса — исцеляет 25% от макс. HP. supBread 'Хлеб — исцеляет 10% от макс. HP. supManaOrb 'Сфера маны — восстанавливает 10% маны. supLockPick 'Набор отмычек. supSkeletonKey 'Отмычка-открывашка (мастер-ключ). End Enum
Для некоторого разнообразия мы добавили в игру отмычки (lock picks) и
мастер-ключи (skeleton keys). Оба предмета могут отпереть замок, если персонаж
экипировал их, находясь рядом с запертой дверью. Несмотря на то, что эти
предметы относятся к категории снабжения (supply items), персонаж должен держать
их в руках как оружие. Поэтому нам нужно обновить структуру типа снабжения,
добавив позиции для экипировки.
inv.bi
'Определение структуры типа предметов снабжения. Type supplytype id As supplyids 'Указывает, какой именно предмет находится в структуре. desc As String * 30 'Описание эффекта предмета. evaldr As Integer 'Рейтинг сложности распознавания. Используется для оценки магических эффектов: 0 = неМагический. eval As Integer 'True, если предмет был успешно распознан. spell As spelltype 'Тип магического эффекта. noise As Integer 'Количество шума, создаваемого предметом (при использовании и в инвентаре). use As itemuse 'Способ использования предмета. sslot(1 To 2) As wieldpos 'Слоты экипировки для отмычек и мастер-ключей. End Type
Обратите внимание, что мы добавили поле sslot — оно ведет себя точно так
же, как слот для оружия. Это отчетливо видно в обновленной процедуре генерации
предметов снабжения.
inv.bi
'Генерация нового предмета снабжения. Sub GenerateSupplies(inv As invtype, currlevel As Integer) ... Case supLockPick 'Набор отмычек. inv.desc = "Lock Pick" inv.supply.desc = "Opens Lock" inv.supply.noise = 3 inv.icon = Chr(168) inv.iconclr = fbSilver inv.supply.eval = FALSE inv.supply.use = useWieldWear inv.supply.sslot(1) = wPrimary inv.supply.sslot(2) = wSecondary Case supSkeletonKey 'Мастер-ключ. inv.desc = "Skeleton Key" inv.supply.desc = "Opens Lock" inv.supply.noise = 1 inv.icon = Chr(13) inv.iconclr = fbSilver inv.supply.eval = FALSE inv.supply.use = useWieldWear inv.supply.sslot(1) = wPrimary inv.supply.sslot(2) = wSecondary Case Else inv.desc = "Uknown Supply" inv.supply.noise = 0 inv.icon = Chr(63) inv.iconclr = fbYellow inv.supply.eval = FALSE inv.supply.use = useNone End Select End Sub
Перед вами два новых предмета снабжения и данные для их инициализации.
Заметьте, что мы устанавливаем позиции экипировки в wPrimary (основная рука) и
wSecondary (левая рука/щит), а флаг использования — в useWieldWear. Благодаря
этим настройкам персонаж сможет брать данные инструменты в руки точно так же,
как обычное оружие, и наш новый код экипировки отработает для них корректно и
предсказуемо.
Как я уже говорил в прошлой главе, мы стремимся избегать
исключений в логике везде, где это возможно, поскольку исключения по определению
усложняют код. Эти простые дополнения к структуре снабжения гарантируют, что
отмычка и мастер-ключ органично встроятся в готовую систему. Разумеется,
остальным процедурам работы с инвентарем тоже потребуются минимальные обновления
для корректной обработки этих предметов.
Теперь мы готовы разобрать код
взлома замков.
dod.bas
'Попытка взломать замок отмычкой. Sub PickLock Dim As vec dt Dim As Integer ret = FALSE, proll, haslp = FALSE Dim As invtype inv 'Ищем запертую дверь поблизости, если она есть. ret = GetLockedDoor(dt) 'Если дверь найдена, проверяем, взят ли в руки мастер-ключ или отмычка. If ret = TRUE Then 'Проверяем основной слот оружия (правая рука). pchar.GetInventoryItem wPrimary, inv 'Отмычки и мастер-ключи относятся к классу предметов снабжения. If inv.classid = clSupplies Then If (inv.supply.id = supLockPick) Or (inv.supply.id = supSkeletonKey) Then 'Инструмент взлома обнаружен в руке. haslp = TRUE End If End If 'Если в основном слоте ничего нет, проверяем дополнительный слот. If haslp = FALSE Then pchar.GetInventoryItem wSecondary, inv If inv.classid = clSupplies Then If (inv.supply.id = supLockPick) Or (inv.supply.id = supSkeletonKey) Then 'Инструмент взлома обнаружен в левой руке. haslp = TRUE End If Endif Endif 'Если отмычка или мастер-ключ в руках, пытаемся открыть дверь. If haslp = TRUE Then 'Проверяем, опознан (идентифицирован) ли предмет. If IsEval(inv) = TRUE Then 'Если предмет опознан, его сложность распознавания (dr) добавляется как бонус к броску игрока. proll = pchar.CurrDex + pchar.BonDex + inv.supply.evaldr Else 'Предмет не опознан. proll = pchar.CurrDex + pchar.BonDex Endif 'Пытаемся открыть запертую дверь. ret = level.OpenLockedDoor(dt.vx, dt.vy, proll) If ret = TRUE Then PrintMessage "You opened the door." 'Вы открыли дверь. Else PrintMessage "You failed to open the door." 'Вам не удалось открыть дверь. Endif Else PrintMessage "You must equip a lock pick or skeleton key to open a locked door." 'Вы должны экипировать отмычку или мастер-ключ, чтобы открыть запертую дверь. Endif Else PrintMessage "No locked doors found." 'Запертых дверей не найдено. Endif End Sub
Этот код немного сложнее, чем алгоритм выбивания двери, но большая его
часть — это просто проверка наличия в руках отмычки или мастер-ключа. Первым
делом мы ищем запертую дверь с помощью написанной ранее функции GetLockedDoor.
Если дверь рядом, мы проверяем, держит ли персонаж один из инструментов взлома.
Нам приходится сканировать как основной (wPrimary), так и дополнительный
(wSecondary) слоты, ведь инструмент может находиться в любом из них. Если
персонаж не держит нужный предмет, переменная haslp останется равной FALSE, и
игра сообщит пользователю, что инструмент необходимо сначала экипировать. Если
же инструмент в руках, мы проверяем, был ли он распознан
(идентифицирован).
Это очень важный шаг. В качестве базы для броска персонажа
мы берем его характеристику Ловкости (pchar.CurrDex + pchar.BonDex). Если
инструмент был успешно распознан игроком ранее, персонаж получает к броску
приятный бонус, равный рейтингу сложности распознавания этого предмета
(inv.supply.evaldr). Если же инструмент не опознан, бонус не начисляется. Это
придает механике идентификации реальный практический смысл в контексте данных
инструментов. Не будь этого бонуса, процесс распознавания стал бы бесполезной
рутиной, которой игроки бы просто пренебрегали. Любые действия в игре должны
иметь последствия — хорошие или плохие, — поэтому мы обязаны вознаграждать
игрока за усилия, потраченные на опознание вещей. Рассчитав финальное значение
броска, мы передаем его в функцию OpenLockedDoor для вычисления
результата.
Поскольку взлом дверей — это действие, требующее мастерства
персонажа, нам также следует поощрять его за успех. Мы обновили функцию
OpenLockedDoor, чтобы она начисляла игроку очки опыта при успешном открытии
замка.
map.bi
'Попытка открыть запертую дверь. Function levelobj.OpenLockedDoor(x As Integer, y As Integer, dr As Integer) As Integer Dim As Integer ret = TRUE, ddr, rolld, rollp Dim tid As terrainids 'Убеждаемся, что перед нами действительно дверь. tid = GetTileID(x, y) If tid = tDoorClosed Then If IsDoorLocked(x, y) = TRUE Then 'Получаем рейтинг сложности замка на двери. ddr = _level.lmap(x, y).doorinfo.lockdr 'Делаем броски кубиков. rollp = RandomRange(1, dr) 'Бросок игрока rolld = RandomRange(1, ddr) 'Бросок двери If rollp > rolld Then 'Открываем дверь. _level.lmap(x, y).doorinfo.locked = FALSE SetTile x, y, tdooropen 'Начисляем персонажу очки опыта. pchar.CurrXP = pchar.CurrXP + ddr Else 'Дверь не поддалась. ret = FALSE Endif Endif Endif Return ret End Function
Как вы можете видеть, в случае успеха персонаж получает количество очков
опыта, равное рейтингу сложности замка двери (ddr). К слову, эта механика будет
работать и с заклинанием открытия дверей (OpenDoor), что дает игроку отличный
стимул использовать магию при её наличии.
Запертые двери могут показаться
незначительной деталью, но они здорово подогревают интерес к происходящему.
Каждая запертая дверь — это вызов, который персонаж (и, как следствие, сам
игрок) должен преодолеть. По сути, это простейшая головоломка низкого уровня, но
именно она привносит в игровой процесс элемент неожиданности. Время от времени
преграждая путь запертой дверью, мы предлагаем игроку задачу, требующую решения.
Если не перебарщивать с их количеством, подобные микро-квесты делают прохождение
увлекательным и удерживают внимание аудитории.
Перевод на русский: GOOGLE AI ASSISTANT
содержание | назад | вперед
