Данное руководство посвящено особенностям реализации и дополнительным возможностям интерпретатора FireURQ по сравнению с классическим URQL. Здесь и далее под "классической" будет пониматься реализация URQL в интерпретаторе URQ_DOS (Досурке) от Виктора Корянова (официального стандарта данного языка не существует).
Наиболее полное описание классического URQL содержится в замечательной документации от Евгения Туголукова a.k.a. Korwin, которое можно скачать, например, здесь. Если вы незнакомы с URQL, изучите ее перед прочтением настоящего документа.
Материал построен следующим образом: в каждом уроке ставится более-менее прикладная задача, для которой затем приводится решение с использованием новых средств FireURQ. Еще раз подчеркну - руководство не рассчитано на новичков.
Удачи!
По сравнению с классическим вариантом языка в FireURQ введены некоторые альтернативные формы записи при обработке строковых переменных. Рассмотрим их на примере.
Допустим, мы хотим реализовать в игре кодовый замок. Выглядеть это будет так: перед игроком будет закрытая дверь, рядом с которой расположена клавиатура. Создадим кнопку, по которой квест будет запрашивать у игрока код. Если игрок введет с клавиатуры правильный код, дверь откроется.
Код будем генерировать случайным образом из первых шести букв латинского алфавита. В целом программа на FireURQ могла бы выглядеть так.
; Исходные данные для генерации кода ДлинаКода=6 ; В коде - шесть символов ; Задаем символы для кода Символ1="A" Символ2="B" Символ3="C" Символ4="D" Символ5="E" Символ6="F" ; Начало генерации кода доступа - инициация цикла КодДоступа="" :ГенерацияКода ; Получаем очередной символ - случайным образом СимволКода=rnd6 КодДоступа=КодДоступа+Символ#СимволКода$ ; (1 - оператор сложения строк есть только в FireURQ) ; Проверка условия цикла if (ДлинаКода-КодДоступа)>0 then goto ГенерацияКода ; (2 - используется числовое представление строки) ; Генерация завершена, начинается собственно локация :СтартоваяЛокация p Ты стоишь в маленькой каморке с металлическими стенами и уныло смотришь на дверь, находящуюся прямо перед тобой. Сбоку от двери видна клавиатура. pln Она состоит из шести кнопок с буквами A, B, C, D, E и F. pln btn НаборКода, Набрать код на клавиатуре end ; Локация, "отвечающая" за набор кода на клавиатуре :НаборКода ; Инициируем строковую переменную instr ВведенныйКод= p Введите #ДлинаКода$-значный код:#$ input ВведенныйКод ; Проверяем введенный код на (не)соответствие заданному if ВведенныйКод<>КодДоступа then pln На табличке над клавиатурой на короткое время загорается надпись "В доступе отказано". & goto СтартоваяЛокация ; (3 - используется оператор "не равно") ; Если мы здесь, значит, код подобран правильно. Выдаем соответствующее сообщение pln Раздается мелодичный сигнал, и дверь медленно отъезжает в сторону. Путь к дальнейшим приключениям открыт! end
Жирным выделены строки, работающие в FireURQ, но непонятные URQ_DOS. Чтобы данный пример функционировал и в Досурке, их необходимо переписать. Первая из них примет следующий вид:
КодДоступа="#%КодДоступа$#%Символ#СимволКода$$"
Я никому не хочу навязывать свое мнение, но, на мой взгляд, использование оператора сложения для "слияния" строк делает запись намного проще для восприятия и значительно сокращает вероятность ошибки. По крайней мере, при тестировании данного примера вариант для FireURQ я сразу ввела без опечаток (хотя длинные ногти и мешали), а вот досурковский вариант пришлось пару раз исправлять прежде, чем он заработал.
В проверке условия во второй "неклассической" строке кода используется тот факт, что в FireURQ существует числовое представление строки, равное ее длине. В нашем примере с каждым новым присоединенным символом эта длина увеличивается, и когда она достигает заданной максимальной длины, условие выполняется и цикл завершается. Нельзя отрицать, что при небрежном программировании это может стать дополнительным источником ошибок, однако при умелом применении такой подход позволяет оптимизировать код - как, например, в данном случае. В URQ_DOS одной строчкой уже не обойтись - потребуется как минимум две:
Счетчик=Счетчик+1 if (ДлинаКода-Счетчик)>0 then goto ГенерацияКода
Наконец, последнюю строку - с оператором "не равно" (<>), который в классическом URQL отсутствует, необходимо переписать следующим образом:
if not (ВведенныйКод==КодДоступа) then ...
Не сказать, что данный оператор является революционным прорывом в деле создания квестов, но ведь когда есть возможность реализовать одно и то же действие разными способами, это всегда хорошо, правда?
Слегка изменим условия загадки из предыдущего урока. Пусть теперь наш кодовый замок работает следующим образом: игрок будет нажимать кнопки на панели по очереди. При нажатии правильной кнопки над дверью будет загораться лампочка, а при неправильной - все лампочки, зажегшиеся до этого, будут гаснуть, и ввод кода придется начинать сначала. Когда зажгутся все лампочки, дверь открывается. Кроме того, позволим игроку выбирать уровень сложности, т. е. длину вводимого кода: 2, 4 или 6 символов.
В целях упрощения копирования-вставки код примера будет приведен целиком. При этом зеленым будут выделены строки, добавленные/измененные по сравнению с примером из прошлого урока, а жирным - строки, непонятные URQ_DOS.
; Исходные данные для генерации кода ; Выбор уровня сложности pln Задайте уровень сложности: pln btn ВыборУровня(2), Легкий (длина кода = 2 символа) btn ВыборУровня(4), Средний (длина кода = 4 символа) btn ВыборУровня(6), Сложный (длина кода = 6 символов) end ; Здесь происходит собственно задание длины кода на основании значения, выбранного игроком :ВыборУровня ДлинаКода=ВыборУровня_1 ; Задаем символы для кода Символ1="A" Символ2="B" Символ3="C" Символ4="D" Символ5="E" Символ6="F" ; Начало генерации кода доступа - инициация цикла КодДоступа="" :ГенерацияКода ; Получаем очередной символ - случайным образом СимволКода=rnd6 КодДоступа=КодДоступа+Символ#СимволКода$ ; Проверка условия цикла if (ДлинаКода-КодДоступа)>0 then goto ГенерацияКода ; Генерация завершена, начинается собственно локация ; Но сначала инициируем номер нажатия кнопки НомерНажатия=1 :СтартоваяЛокация p Ты стоишь в маленькой каморке с металлическими стенами и уныло смотришь на дверь, находящуюся прямо перед тобой. Сбоку от двери видна клавиатура. pln Она состоит из шести кнопок с буквами A, B, C, D, E и F. pln ; Дополняем локацию описанием лампочек над дверью pln Над дверью расположен ряд из #ДлинаКода$ лампочек. Из них горит: #(НомерНажатия-1)$. pln ; Выводим кнопки для набора кода Счетчик=0 :ВыводКнопок Счетчик=Счетчик+1 ; Для кнопок учитываем номер нажатия и букву, которой она соответствует btn НажатиеКнопки(Символ#Счетчик$), Нажать кнопку #%Символ#Счетчик$$ if Счетчик<6 then goto ВыводКнопок end ; Нажатие очередной кнопки на клавиатуре :НажатиеКнопки ; Разбиение кода доступа на символы instr tokens_delim=char tokens КодДоступа ; Отработка нажатий происходит в разных процедурах в зависимости от того, угадали ли мы очередной символ кода или нет if (НажатиеКнопки_1==token#НомерНажатия$) then proc ПравильныйСимвол(НажатиеКнопки_1) else proc НеправильныйСимвол(НажатиеКнопки_1) ; По умолчанию возвращаемся в стартовую локацию goto СтартоваяЛокация end ; Отработка нажатия кнопки при угаданном символе :ПравильныйСимвол p Ты нажимаешь кнопку #%ПравильныйСимвол_1$.#$ ; В зависимости от номера символа выводим разные сообщения ; Код угадан полностью - переходим в финальную локацию, предварительно сбросив все вызывавшиеся ранее процедуры if НомерНажатия=ДлинаКода then forget_procs & goto Финал ; Вводится первый символ if НомерНажатия=1 then pln Над дверью загорается крайняя слева лампочка.#/$ ; Вводится символ от 2-го до предпоследнего включительно if НомерНажатия>1 then pln Над дверью загорается очередная лампочка.#/$ ; Увеличиваем счетчик правильно введенных символов НомерНажатия=НомерНажатия+1 end ; Отработка нажатия кнопки, когда символ введен неверно :НеправильныйСимвол p Ты нажимаешь кнопку #%НеправильныйСимвол_1$. Раздается резкий, недовольный гудок ; Сообщаем о том, что лампочки гаснут, если какие-либо из них горели if НомерНажатия>1 then p , и все лампочки над дверью гаснут pln .#/$ ; Сбрасываем номер вводимого символа НомерНажатия=1 end ; Финальная локация :Финал pln Все лампочки над дверью вспыхивают и тут же гаснут. Затем раздается мелодичный сигнал, и дверь медленно отъезжает в сторону. pln Путь к дальнейшим приключениям открыт! end
Во всех выделенных жирным строках происходит переход (по btn или по proc) с передачей параметра. В данном примере передается по одному параметру, однако их количество теоретически не ограничено. Значение переданного параметра сохраняется в переменной с именем ИмяМетки_n, где ИмяМетки - метка, на которую ссылается оператор перехода (помимо упомянутых в примере, это может быть также и goto), n - порядковый номер переданного параметра (счет ведется слева направо и начинается с единицы).
Передача параметров является достаточно мощным средством, которое позволяет делать код более компактным и упрощает отладку. В классическом URQL выбор уровня сложности из нашего примера выглядел бы примерно так:
btn ВыборУровняЛегкий, Легкий (длина кода = 2 символа) btn ВыборУровняСредний, Средний (длина кода = 4 символа) btn ВыборУровняСложный, Сложный (длина кода = 6 символов) end ; Отдельные локации, в которых задается длина кода :ВыборУровняЛегкий ВыборУровня_1=2 goto ВыборУровня :ВыборУровняСредний ВыборУровня_1=4 goto ВыборУровня :ВыборУровняСложный ВыборУровня_1=6 goto ВыборУровня ; Здесь происходит собственно задание длины кода на основании значения, выбранного игроком :ВыборУровня ДлинаКода=ВыборУровня_1
То есть появляются три дополнительные локации, очень схожие между собой, но все же чуть-чуть отличающиеся. Для реализации же нажатия разных кнопок в нашем примере потребовалось бы аж шесть новых локаций. При этом цикл с добавлением кнопок в стартовой локации видоизменился бы следующим образом:
Счетчик=0 :ВыводКнопок Счетчик=Счетчик+1 ; Для кнопок учитываем номер нажатия и букву, которой она соответствует btn НажатиеКнопки#%Символ#Счетчик$$, Нажать кнопку #%Символ#Счетчик$$ if Счетчик<6 then goto ВыводКнопок end
Сами же дополнительные локации выглядели бы так:
:НажатиеКнопкиA НажатиеКнопки_1="A" goto НажатиеКнопки
и т. д.
Если подобных ситуаций в игре (да и в любой программе) набирается много, она может превратиться в кошмар для разработчика - очень уж возрастает риск сделать опечатку, а отыскивать потом место ошибки в джунглях почти одинаковых локаций - то еще занятие.
Отдельно остановимся на строчке с вызовом процедур с параметром:
if (НажатиеКнопки_1==token#НомерНажатия$) then proc ПравильныйСимвол(НажатиеКнопки_1) else proc НеправильныйСимвол(НажатиеКнопки_1)
Конечно, это может показаться избыточным - вполне рабочим вариантом могло бы быть использование в процедурах ПравильныйСимвол и НеправильныйСимвол непосредственно переменной НажатиеКнопки_1. Однако такое решение принято опять-таки для упрощения последующей отладки - с этой точки зрения гораздо лучше, когда переменные "привязаны" по именам к локациям. Кроме того, есть еще одна особенность, вследствие которой использование переменных-параметров (с именами вида ИмяМетки_n) желательно ограничить "материнской" локацией: при переходе на ту же метку, но без параметров (либо с меньшим числом параметров, чем в предыдущий раз) значения всех "лишних" переменных такого вида сбрасывается. Т. е. если бы где-либо в нашем примере выполнялся, скажем, переход типа goto НажатиеКнопки, то после него переменная НажатиеКнопки_1 оказалась бы равна нулю, и все манипуляции с ней потеряли бы смысл.
Пусть в нашей игре у игрока имеется пистолет. Игрок может зарядить его (в этом случае название предмета в инвентаре должно поменяться на "Заряженный пистолет"). Кроме того, в соседней локации находится другой персонаж, которого игрок может застрелить.
Вот как будет выглядеть данный пример на FireURQ (жирным выделены инструкции, отсутствующие в классическом URQL).
; Вносим пистолет в инвентарь (Функциональность пистолета будет добавлена ниже) inv+ 1, Пистолет ; Пистолет изначально не заряжен Пистолет_Заряжен=0 ; Стартовая локация :Спальня p Ты находишься в своей спальне. Как и положено, центральное место в ней занимает кровать. Правда, сейчас она имеет не слишком приглядный вид -#$ pln все-таки не стоило вчера, вернувшись из клуба, заваливаться на нее спать прямо в грязных сапогах... pln В боковой стене находится дверь, ведущая в твой кабинет. Она приоткрыта, и из-за нее доносится молодецкий храп. pln Use_Пистолет_Застрелить лакея_hide=1 ; (1 - прячем действие в отношении персонажа, о котором игрок пока ничего не знает) btn Кабинет, Идти в кабинет end ; Следующая локация - кабинет, в котором спит лакей :Кабинет p Как во всяком порядочном кабинете, здесь есть удобный письменный стол с креслом, шкафы с книгами, а также мягкий кожаный диван.#$ pln Увы - последний в данный момент осквернен дрыхнущим на нем бездельником-лакеем. Вот ведь скотина! pln Use_Пистолет_Застрелить лакея_hide=0 ; (2 - теперь действие "Застрелить лакея" будет доступно игроку) btn Спальня, Идти в спальню end ; Действия для пистолета: мы его можем зарядить и выстрелить из него :use_Пистолет_Зарядить if Пистолет_Заряжен=1 then pln Пистолет уже заряжен! else Пистолет_Заряжен=1 & pln Ты заряжаешь пистолет. & idisp_Пистолет="Заряженный пистолет" (3 - меняем описание предмета на "Заряженный пистолет") end ; Возможность застрелить лакея :use_Пистолет_Застрелить лакея if Пистолет_Заряжен=0 then pln Пистолет не заряжен. else pln Вне себя от гнева, ты поднимаешь трясущимися с похмелья руками пистолет и разряжаешь его в лакея. Кабинет заволакивает дымом... end
Невозможно даже описать, насколько расширяются возможности по написанию квестов в FireURQ благодаря этим инструкциям. Чтобы реализовать смену названия объекта по инструкции idisp_ под URQ_DOS, пришлось бы создавать два практически идентичных объекта с разными названиями и слегка модифицированными действиями и подменять их в инвентаре игрока по мере надобности - примерно вот так:
; Действие "зарядить" для незаряженного пистолета :use_Пистолет_Зарядить ; Выводим нужный текст pln Ты заряжаешь пистолет. ; Заменяем "просто" пистолет на заряженный пистолет inv- 1, Пистолет inv+ 1, Заряженный пистолет end ; Действие "зарядить" для заряженного пистолета :use_Заряженный пистолет_Зарядить ; Выводим текст pln Пистолет уже заряжен! end
Далее, "официально" возможность скрыть действие "Застрелить лакея" в классическом URQL отсутствует. На практике этого эффекта можно добиться, определив второй объект с почти таким же названием, в котором одна или несколько букв кириллицы заменены на аналогичные по написанию буквы латинского алфавита - например, "Пистолет" (зеленым выделена латинская буква "e") и так же осуществлять его подмену по мере надобности. Для полной эмуляции нашего примера средствами классического URQL потребуется четыре объекта: "Пистолет", "Пистолет", "Заряженный пистолет" и "Заряженный пистолет". И не дай Бог при разработке для одного из объектов понадобится поменять/добавить какое-либо действие - отладка превратится в кошмар... А если у нас окажется несколько персонажей, о которых игрок вначале игры ничего не знает, но при этом их можно будет застрелить, то одинаково пишущихся по-русски и по-латински букв в названии предмета может и не хватить. Тогда, возможно, проще будет вообще отказаться от реализации этого действия через инвентарь и просто добавлять соответствующую кнопку в локации. Тоже выход, конечно (кстати, столь же просто реализуемый и в FireURQ) - но, во-первых, при наличии большого количества персонажей в одной комнате экран окажется перегруженным кнопками, а во-вторых, это зачастую может обеднять квест.
На самом деле инвентарь - это один из тех немногих аспектов, для которых преемственность FireURQ по отношению к URQ_DOS соблюдена не в полной мере. Если выразить отличие в обработке инвентаря этими двумя интерпретаторами коротко, то получится следующее: в Досурке инвентарь - это proc, а в Фурке - практически goto. Как это отличие может проявляться при создании реальных игр?
Модифицируем пример из предыдущего урока. Пусть при попытке застрелить лакея главный персонаж перемещается в другую локацию.
:use_Пистолет_Застрелить лакея ; Проверяем, заряжен ли пистолет, если нет, уходим на конец обработки if Пистолет_Заряжен=0 then pln Пистолет не заряжен. & goto КонецОбработки ; Здесь обрабатываем расстрел лакея. pln Вне себя от гнева, ты поднимаешь трясущимися с похмелья руками пистолет и разряжаешь его в лакея. Кабинет заволакивает дымом, pln а тело лакея сползает с дивана. Ты уже собираешься повернуться и уйти, как вдруг твой слуга издает мерзостный рев и поднимается! pln Не переставая реветь, он поворачивается в твою сторону и протягивает к тебе руки. Буквально на твоих глазах его ногти удлиняются, pln превращаясь в жуткие когти, такая же метаморфоза происходит с его зубами. Он медленно шагает к тебе, капая кровью из простреленного бока pln на дорогой ковер. Вне себя от ужаса, ты рвешься к окну и, выбив его, выпрыгиваешь наружу. Мгновением позже лицо твое утыкается pln в сырую, холодную клумбу внизу...#/$#/$#/$ ; Сбрасываем все вызовы proc forget_procs ; Переходим в другую локацию goto Спальня2 :КонецОбработки end :Спальня2 pln ...которая оказывается полом твоей спальни. Кошмар был настолько жуток, что во сне ты свалился с кровати. pln Привычная обстановка твоей любимой комнаты, впрочем, быстро возвращает тебе душевное равновесие. Разве что молодецкий храп, pln доносящийся из-за ведущей в кабинет двери, несколько действует на нервы. btn Кабинет, Идти в кабинет end
В URQ_DOS этот фрагмент будет выполняться совершенно корректно. В то же время в FireURQ произойдет "размножение кнопок" - кнопка "Идти в спальню" той локации, из которой произошло обращение к инвентарю, не будет очищена. Проблему можно решить, добавив сразу после forget_procs команду clsb. Это недокументированный оператор FireURQ, по которому осуществляется очищение кнопок, в данный момент ожидающих вывода на экран. (На работу URQ_DOS данный оператор влияния не окажет - Досурка просто игнорирует все конструкции, которые не может распознать).
Справедливости ради стоит сказать, что если вы любите использовать переходы по goto между локациями, то может оказаться, что поведение FireURQ даже упростит реализацию игры. Ниже приведен пример, иллюстрирующий данный тезис.
inv+ 1, Кирпич Великан=1 :Старт ; Описание входа на поляну - в зависимости от того, на месте ли великан if Великан=1 then pln Ты выходишь на поляну и видишь пещеру, вход в которую охраняет ужасный великан! else pln Ты выходишь на поляну. btn Чаща, Снова углубиться в лесную чащу ; Переход в другую локацию goto Поляна end :Поляна ; Вторая часть описания поляны if Великан=1 then pln Великан смотрит на тебя без особого дружелюбия. else pln Вход в пещеру свободен. btn Поляна, Осмотреться ; Когда великана нет, можно войти в пещеру if Великан=0 then btn Пещера, Войти в пещеру end :Чаща ; Дополнительная локация pln Побродив по лесу, ты не обнаруживаешь ничего интересного. btn Старт, Вернуться на поляну end :Пещера pln В пещере обнаружилась масса сокровищ. btn Поляна, Выйти end :use_Кирпич_Отдать великану pln Ты протягиваешь кирпич великану. "Класс!" - говорит он. - "Пойду достраивать дом." pln С этими словами он скрывается в неизвестном направлении. ; Убираем кирпич из инвентаря inv- 1, Кирпич ; Сбрасываем флаг наличия великана Великан=0 end
В URQ_DOS после применения кирпича игрок не сможет "снова углубиться в лесную чащу", а в FireURQ это действие останется доступным, что в данном конкретном случае более логично.
По сравнению с классическим URQL в FireURQ немного по-другому работает функция anykey, осуществляющая сканирование нажатой на клавиатуры клавиши. Рассмотрим эти отличия на следующем примере.
:КапитанскийМостик ; Основная локация p Ты находишься на капитанском мостике звездолета. Все помещение забито разнообразной аппаратурой, но центральное место занимает#$ pln скромный персональный компьютер. К нему прикреплена какая-то невзрачная табличка. btn Табличка, Читать табличку btn Компьютер_старт, Работать с компьютером end ; Подсказка: чтение таблички :Табличка pln На табличке написано: "Внимание! Режим прокладки маршрута включается функциональными клавишами." btn КапитанскийМостик, Закончить чтение end ; Запуск компьютера :Компьютер_старт ; Приглашение на ввод pln Для начала работы с компьютером нажмите любую клавишу#/$ p >#$ ; Ждем нажатия клавиши anykey if is_syskey=1 then goto Компьютер_Маршрут ; Проверка системной переменной, отражающей нажатие одной из функциональных клавиш ; Нажата любая другая клавиша (не функциональная) p Компьютер открыл обычный рабочий стол, усеянный значками приложений - преимущественно игр. Ты поиграл в одну из них,#$ pln но тебе быстро надоело. btn КапитанскийМостик, Завершить сеанс end ; Игрок активизировал режим прокладки маршрута :Компьютер_Маршрут pln Экран компьютера светлеет, и на нём появляется надпись: "Программа-навигатор приветствует вас!" end
В этом примере компьютер переходит в режим навигатора при нажатии функциональной клавиши. В работе с этими клавишами и заключается отличие реализации anykey в FireURQ от Досурки. В FireURQ эта функция возвращает для клавиш F1..F10 коды 59..68, для F11 и F12 - 87 и 88, при этом системная переменная is_syskey устанавливается равной единице, а одновременное нажатие клавиш Alt, Shift и Ctrl никак не влияет на возвращаемый скан-код. В URQ_DOS же возвращаемые коды совпадают с FireURQ'шными только для клавиш F1..F10; при нажатии Shift+F1..F10 будут возвращены скан-коды 84..93, Ctrl+F1..F10 - 94..103, Alt+F1..F10 - 104..113. Про клавиши F11 и F12 Досурка просто "не знает", и нажатие на них не вызывает никакой реакции. Системной переменной is_syskey в URQ_DOS также нет. Поэтому, чтобы отследить нажатие функциональной клавиши в примере этого урока, для URQ_DOS пришлось бы проверять все возможные диапазоны скан-кодов.