Лирическое отступление: на практике статья будет полезна скорее тем, кто кодит дополнительный функционал на Ren'Py, чем создателям простых кинетических новелл.
Что вообще такое операторы? Операторы (statements) в Ren'Py – короткий эквивалент вызову питоновской функции (или целому набору вызовов). Их видел и использовал каждый, кто кодил новеллы на Ren'Py. Для примера:
Код
show bg street # show – оператор
jump ending # jump – оператор
char "Hello, world!" # даже тут в неявном виде лежит оператор say
Все они являются эквивалентами не то чтобы совсем громоздких вызовов, но все-таки, разве не проще написать
show bg street вместо
$ renpy.show("bg street")? Как минимум, привычнее. Да и потом, если можно не пользоваться голыми питоновскими функциями, зачем мы будем ими пользоваться?
Упомянутые операторы – часть стандартного функционала Ren'Py.
Зачем мне нужны кастомные операторы? Теперь представьте ситуацию: вы решили добавить в вашу новеллу инвентарь. Или экран смс-сообщений, или что угодно, что не предусмотрено стандартным функционалом, и требует вызова кастомных функций. Допустим, вы эти функции уже написали, и они работают, и теперь ваш script.rpy (или chapter001.rpy, или где они у вас там лежат) выглядит примерно так:
Код
label start:
# много всякого нужного текста
hero "Вот этот подарок отлично подойдет девушке, которую я собираюсь закадрить!"
$ AddInventory("gift") # вызов функции, добавляющей предмет в инвентарь
# много другого текста
hero "Привет, девушка, которую я собираюсь закадрить! Держи подарок!"
$ RemoveInventory("gift") # вызов функции, удаляющей предмет из инвентаря
girl "Спасибо, герой! Мое отношение к тебе поднялось на пять пунктов!"
И
AddInventory(), и
RemoveInventory() используют питоновский вызов (со значком доллара и всем таким). Когда таких вызовов немного, то можно оставить и так, читабельность файла не сильно пострадает – напротив, сразу будет видно, где у вас происходят определенные события. Но что если с помощью кастомных функций у вас реализовано общение героев через смски или чат? Тогда код может превратиться в:
Код
label start:
# много всякого нужного текста
hero "Напишу-ка я ей сообщение!"
$ msgAdd(hero, "Привет!")
$ msgAdd(hero, "Как дела?")
$ msgAdd(hero, "Чем занимаешься?")
hero "Почему она так долго не отвечает?"
$ msgAdd(hero, "Я вот пишу сценарий для визуальной новеллы!")
hero "Главное, чтобы она не подумала, что я извращенец."
$ msgAdd(girl, "Привет, герой!")
$ msgAdd(girl, "Прости, что долго не отвечала.")
$ msgAdd(girl, "Кормила ручного медведя и настраивала балалайку!")
Удельный вес питоновских вызовов тут больше, чем say-операторов, и они гораздо более громоздкие. Чувствуете, как вам уже хочется заменить все эти
$ msgAdd() на привычные глазу
msg hero "Привет!"? Вот тут вам и помогут CDS – кастомные операторы.
tl;dr: Зачем мне нужно создавать свои операторы? – Чтобы привести код к единому читабельному виду. Миленько же!
Лирическое отступление: вполне реально сделать смс-общение через встроенный say-оператор, в сети даже есть пример:
ссылка. Но в своих проектах я предпочитаю не смешивать функционал, поэтому пользовалась для той же цели CDS. Диалог отдельно, чат-лог отдельно.
Как создавать и задавать CDS. На словах все выглядит сложно, на практике – в разы проще.
tl;dr: инструкция на английском из официальной документации:
https://www.renpy.org/doc/html/cds.html Первое правило CDS: никому не говорить- oh wait. Первое правило CDS состоит из двух частей:
- инициализация оператора должна проходить в early-блоке (это как
"init -1 python:", только
"python early:")
- все кастомные операторы должны быть созданы и описаны не только не в том файле, где они будут использованы, но и
раньше его. Ren'Py подключает файлы в алфавитном порядке (если еще точнее, в порядке следования символов в таблице Юникода). Поэтому, скажем, если у вас файл script.rpy набит кастомными операторами, объявить вы их должны были в каком-нибудь 00_my_cds.rpy: файлы, которые начинаются на 00, гарантированно будут подключены раньше, чем те, что начинаются с букв.
Второе правило CDS: создание кастомного оператора проходит два этапа: создание функций-обработчиков и регистрация оператора.
На примере пирога, которым вы хотите угостить симпатичную соседку: вы печете пирог (функции-обработчики) и приходите колотить в дверь к соседке (регистрация оператора): эй, ты, я пирог испек, выходи! Одно без другого не работает: прийти к соседке с приглашением, но без пирога, так же глупо, как испечь пирог и сидеть дома, надеясь. что голодная соседка сама найдет вас по запаху.
Регистрирует оператор функция
renpy.register_statement(). Полный список параметров, которые она принимает, можно посмотреть по ссылке на мануал, нам для создания простых операторов понадобятся всего несколько.
renpy.register_statement(name, parse=None, lint=None, execute=None, <остальные параметры опущены>) name: этот параметр принимает строковое значение – имя нашего оператора (как
show,
jump или
say). Если передать в него набор слов, разделенных пробелами, они все будут эквивалентами одного и того же оператора. Если передать пустую строку, то мы зададим новый оператор по умолчанию, и он заменит встроенный say-оператор. Т.е. команда
char "Hello, world!" больше не будет выводить диалог от лица персонажа
char, а будет делать то, что мы скажем.
Код
renpy.register_statement("msg", …) # объявили оператор msg
renpy.register_statement("msg sms chat", …) # строки, которые начинаются операторами msg, sms и chat, будут выполнять одно и то же действие
renpy.register_statement("", …) # переопределили встроенный оператор say
parse: это функция-парсер. Она принимает в качестве параметра Lexer-объект. Ее задача – разобрать его на фрагменты и вернуть набор (тоже объект, это важно) этих фрагментов, передавая его следующим функциям-обработчикам.
На примере пирога: ваша соседка из предыдущего примера – инопланетянка, которая ни разу не была на Земле. Вы пришли к ней, держа в руках блюдо с пирогом, вилкой, ножом и салфеткой. Соседка (дальнейшие функции-обработчики) видит у вас в руках нечто, состоящее из фрагментов (Lexer-объект). Она не знает, что с ними делать, но ей на помощь приходите вы (parse-обработчик)! "Вот это – салфетка, ее кладут на стол, вот это – пирог, его кладут в рот, вот это – нож, им режут пирог, вот это – блюдо, оно нам больше не понадобится, оно так просто."
(Parse-функция пишется вашими руками. Вы отвечаете за то, каким образом будут интерпретированы полученные данные. Можете делать с доверчивыми обработчиками что угодно, даже убедить их сожрать нож и надеть пирог на голову. Но пользы от этого будет мало.) lint: это функция-валидатор. Она принимает объект из рук парсера, и внутри этой функции вы можете задать проверку на ошибки в наборе данных. Все найденные ошибки должны быть переданы функции
renpy.error() для вывода на экран ошибок.
На примере пирога: у вашей соседки-инопланетянки строгая квартирная хозяйка (lint-функция). После того, как вы распарсили поднос с пирогом, она хватает пирог, придирчиво оглядывает его и говорит: да он же из теста! Ты что, не знаешь, что у венерианок аллергия на тесто?! И выставляет вас из квартиры с позором, крича вслед "I'm sorry, but errors were detected in your cake!", или что-то вроде того.
Лирическое отступление: в принципе, если вы в себе уверены и считаете, что ошибок в данных не сделаете, на эту функцию можно наплевать. А вот на парсер и исполнитель плевать не стоит.
execute: это функция-исполнитель. Она принимает объект из рук парсера и делает с ним всякие неприличные вещи, которые вы велели ей делать.
На примере пирога: ваша-соседка-инопланетянка наконец-то ест пирог, который вы ей принесли. Надеюсь, что вы сделали все правильно, и она ест именно пирог, а не нож или салфетку, и у нее не будет аллергии.
Теперь, когда мы немного разобрались, зачем нужны обработчики, посмотрим еще раз на функцию-парсер. В качестве параметра она получает Lexer-объект. Что такое Lexer-объект? В нашем случае это строка символов, глобально – это сканер-интерпретатор. Каждый lexer-объект снабжен набором функций, которые помогают понять, что в наборе букв является "словом", "символом", "числом", и так далее.
Полный список таких функций можно посмотреть в мануале. Вам, скорее всего, понадобятся всего несколько:
word(): считывает слово из переданного набора символов. Слово = кусок строки, заканчивающийся пробелом. Возвращает считанный кусок, не включая пробел.
integer(): считывает целое число из переданного набора символов, возвращает его в виде строки.
simple_expression(): считывает простое питоновское выражение, возвращает его в виде строки.
rest(): откидывает первый пробел и возвращает остаток строки (оставшийся после прочих преобразований).
Вернемся к парсеру и рассмотрим весь этот зоопарк на примере. Допустим, вы уже написали и зарегистрировали свой кастомный оператор и вызвали его внутри script.rpy командой:
Код
msg hero 1 "Привет, мир!"
Как Ren'Py интерпретирует эту строку?
msg было откинуто на этапе узнавания оператора. Все остальное – то есть
hero 1 "Привет, мир!" было объявлено lexer-объектом и, не разбираясь, кинуто парсеру, который должен с помощью функций lexer'а определить, что есть что.
Мы знаем, что первый набор символов в цепочке – это объявленный где-то персонаж. Персонаж с точки зрения Ren'Py – это simple expression. Поэтому командуем парсеру: сожри из строки первое простое выражение и положи его в переменную
who. Второй набор символов – это целое число. Командуем парсеру: сожри целое число из оставшегося куска и положи его в переменную
value. Остаток – это слова героя, выбирать оттуда ничего не нужно, поэтому просто говорим "кинь остаток в переменную
message". И все, что получилось, сложи в объект и передай дальше.
С точки зрения кода вышеописанное будет выглядеть так:
Код
def parse_myparser(lex):
who = lex.simple_expression()
value = lex.integer()
message = lex.rest()
return (who, value, message)
Теперь у нас есть все, чтобы
нарисовать остаток долбаной совы написать простой оператор, который будет выводить сообщения персонажей на дополнительный экран (имитация окна чат-лога). Ниже – код создания CDS с комментариями. Полный код тестового проекта (нескомпилированный, только нужные rpy-файлы) – в архиве по ссылке
https://yadi.sk/d/pTAoH--T3MRh4i.
Код
python early:
def msg_parse(lex):
who = lex.simple_expression()
what = lex.rest()
return (who, what)
def msg_lint(o):
# раскидываем объект по отдельным переменным
who, what = o
# валидация переменной who: проверяем, был ли объявлен соответствующий персонаж
try:
eval(who)
except:
renpy.error("Character not defined: %s" % who)
# валидация переменной what: проверяем, правильно ли расставлены тэги
tte = renpy.check_text_tags(what)
if tte:
renpy.error(tte)
def msg_execute(o):
# объявляем, что будем работать с переменной chat_log, объявленной в глобальном пространстве переменных, а не с ее локальной копией
global chat_log
# раскидываем объект по отдельным переменным
who, what = o
# превращаем строку с названием переменной персонажа в саму переменную персонажа с помощью функции eval
char = eval(who)
# готовим одноразовую переменную-словарь под сообщение, которое добавим в лог
msg = {}
msg['color'] = char.who_args['color']
msg['who'] = char.name
# поскольку lexer получил сообщение ("остаток строки") с кавычками, убираем кавычки спереди и сзади (берем подстроку с первого символа и без последнего)
msg['what'] = what[1:-1]
# добавляем получившееся сообщение в массив chat_log
chat_log.append(msg)
# это нужно, чтобы сообщения не пролистывались сами, а требовали ввода пользователя, как при стандартном выводе диалога
renpy.pause()
renpy.register_statement("msg", parse = msg_parse, execute = msg_execute, lint = msg_lint)
На этом все. С вами был кодер ТГ "Красавица Икуку", которому захотелось поделиться прикольными штуками с народом. Кормите соседок пирогами и создавайте кастомные операторы!
Комментарии к записи: 0