Overview
Создадим оператор перевода для вашего мода.
Введение
Этот вопрос мог возникнуть у многих, кто зашёл сюда просто увидев новое руководство по мододельству.
Те, кто ищет именно возможность устроить свой “переводчик” или хочет разобраться, как работает такой же в “Зелёном Свете”, могут смело пролистать введение.
В модах довольно часто встречаются фразы на других языках. Это могут быть как отсылки, устойчивые выражения, так и диалоги. И чтобы Вас поняли правильно или просто для того, чтобы блестнуть знаниями, хотелось бы выдать игроку перевод этой фразы.
Мододелы придумали массу способов сделать это! Начиная от весьма простого, но не позволяющего видеть сразу оба варианта (с переводом / без перевода) способа в “Саманте”, заканчивая просто написанием перевода рядом с оригинальным текстом, как в “7 Дней с Мику”, или не использованием английского текста вообще, как в “Чистовике”.
Все варианты, в целом, рабочие, но они не позволяют реализовать изначальную задумку, которая, я уверен, у многих заключалась именно в возможности взять и перевести текст, например, нажав на него прямо в окне диалога. Именно о таком способе я вам и расскажу.
Часть 1. Creator-Defined Statements?
Это словосочетание редко встречается в русскоязычных материалах на тему Ren’Py ( упоминания есть на сайтах anivisual[anivisual.net] и русского перевода документации Ren’Py[ru.renpypedia.shoutwiki.com]).
Переводится как определённые разработчиком операторы.
Все разработчики, садящиеся за создание мода для Бесконечного Лета (да и вообще проекта на движке Ren’Py), используют операторы. Например:
В последнем случае используется именно оператор “play music”, а не “play”. Объяснять разницу сейчас не буду.
И благодаря CDS мы можем создать своё кодовое слово или кодовое словосочетание, которое будет восприниматься движком Ren’Py, обрабатываться по тем правилам, которые мы ему зададим и исполняться по заданному нами алгоритму.
Конечно, всё это можно реализовать с помощью различного рода функций (или если Вы прям могёте, то и в ООП можно залезть), но хочу показать Вам пример того, насколько всё лаконично выглядит в коде при использовании CDS:
“gl_translate” здесь – это определённый с помощью CDS оператор из мода “Зелёный Свет”, для которого я его и написал и который постараюсь разобрать.
Часть 2. Начинаем погружение
Для нетерпеливых:
- Вот здесь можно изучить CDS самостоятельно: ссылка на Ren’Py Doc по CDS[www.renpy.org]
Весь код оператора должен находиться внутри блока python early!!!
Итак, каким образом создаётся собственный оператор?
Его создание происходит в четыре этапа:
- Создание парсера
- Создание валидатора
- Создание исполнителя
- Добавление оператора в список воспринимаемых движком
Парсер – это функция, которая выполняет “чтение” кода оператора. В ней мы зададим синтаксис нашего оператора. Она проходит по коду оператора, находит там ключевые слова и возвращает значения, которые пользователь передавал.
Валидатор – функция поиска ошибок в операторе. Если пользователь передал неинициализированное имя или что-либо в переданных оператору данных нам не нравится, то мы не хотим, чтобы это прошло дальше в исполнитель. Здесь и подключается валидатор, заканчивающий выполнение оператора в не понравившиеся нам моменты.
Исполнитель – функция, которая на основе тех данных, что передал ей парсер через валидатор, исполняет особый алгоритм.
В движке Ren’Py есть класс Lexer (в переводе что-то вроде “лексиколог” от слова “лексика”), который предоставляет инструменты для парсинга кода. Инструментов много (вот их полный список[www.renpy.org]), но мы будем использовать лишь несколько, а именно:
-
Lexer.eol()
Этот метод возвращает True, если мы находимся в конце строки и False, если иначе.
-
Lexer.simple_expression()
Этот метод возвращает выражение на языке Python в виде строки (всё будет объяснено на примерах, когда мы дойдём до кода).
-
Lexer.match(re)
Этот метод возвращает строку “re”, если далее в коде следует она, или возвращает None, если далее в коде стоит что-либо другое.
-
Lexer.expect_eol()
Этот метод вызовет ошибку, если мы не находимся в данный момент в конце строки.
-
Lexer.expect_block(statement)
Этот метод вызовет ошибку, если у оператора не найден блок.
-
Lexer.expect_noblock(stmt)
Этот метод вызовет ошибку, если у оператора найден блок.
-
Lexer.subblock_lexer()
Этот метод возвратит новый объект Lexer, который будет использоваться для парсинга строк в блоке.
-
Lexer.advance()
Этот метод используется для парсинга блока. Он переходит на следующую строку блока, возвращая True, а если текущая строка последняя, то он возвратит False.
-
Lexer.catch_error()
Этот метод является неким декоратором. О его предназначении расскажу позже.
-
Lexer.string()
Этот метод возвращает строку, если она следует в коде, и вызывает ошибку, если следует не строка.
Также в Ren’Py присутствуют инструменты для валидатора (вот они[www.renpy.org]). Нам нужны эти:
-
renpy.error(msg)
Выводит ошибку с текстом “msg”.
-
renpy.check_text_tags(s)
Проверяет правильность написания текстовых тегов в строке “s”. Возвращает текст ошибки, если ошибка есть или None, если ошибки не обнаружено.
Часть 3. Парсер
Начинать создание оператора мы будем с парсера, чтобы сразу обозначить тот синтаксис, который мы хотим видеть и использовать.
Код парсера, к которому мы придём:
Итак, начнём с того, что создадим функцию (название может быть любым, но принято называть её “parse_” + название вашего оператора, в моём случае: “parse_gl_translate”), принимающую один аргумент – объект класса Lexer.
Для своего оператора я выбрал такой синтаксис:
Синтаксис:
- Сразу после названия оператора следует персонаж, который произносит реплику.
-
- Либо далее идёт символ “:” и две строки, каждая из которых на новой строчке
- Либо обе строки идут подряд в одну линию с оператором.
Объект Lexer – это куча необработанных символов, следующих после оператора. Символы самого оператора (“gl_translate” в моём случае) в парсер не передаются, поэтому парсинг начинается со слова “sl” в примере выше.
Персонажи в Ren’Py – это объекты класса ADVCharacter, поэтому парсер воспринимает их, абсолютно логично, как выражения на языке программирования Python. Для парсинга Питоновских выражений используем simple_expression():
simple_expression() возвращает Питоновское выражение в виде строки, поэтому позже нам придётся использовать функцию eval().
Пусть наш парсер будет возвращать словарь, а переменная, в которую мы запишем говорящего персонажа, пусть называется “who”.
Далее добавим переменную, которая будет содержать в себе строки с текстом и переводом. Я назвал её “items”:
Логично, что и её мы тоже должны добавить в возвращаемый словарь.
Далее нам нужно понять, какой именно способ сейчас использует кодер. Пишет ли он всё в одну строчку или друг под другом? В выбранном мною синтаксисе (как и в синтаксисе Python) символ “:” обозначает начало блока, поэтому попросим парсер его найти, и если он его найдёт, мы поймём, что далее следует блок, а в ином случае всё записано в одну строчку. Для этого используем метод match(re), где “re” будет “:”:
Здесь стоит отметить, что методы класса Lexer при парсинге удаляют из списка необработанных символов то, что было ими найдено (пробелы и комментарии они игнорируют). Например, после применения simple_expression() к оператору, описанному в примере со “sl” без блока, “sl” будет удалена. Простыми словами: сначала в парсер передали строку ‘sl “Bye!” “Пока!”‘, а после применения simple_expression() там осталось ‘”Bye!” “Пока!”‘. То же самое происходит, когда мы ищем двоеточие: если мы его найдём, оно удалится, если мы его не найдём, строка, которую мы парсим, не изменится.
Давайте напишем алгоритм для парсинга той версии оператора, когда всё пишется в одну строчку.
Вспомним метод eol(), который возвращает False, пока мы не доберёмся до конца строки. Значит, мы можем использовать его, чтобы прекратить выполнение парсинга, как только доберёмся до конца строки:
Ну а так как мы сейчас ищем строку с фразой персонажа и строку с её переводом, то будем использовать описанный во второй части руководства метод string():
Таким образом, метод string() будет добавлять в список “items” строку, которая стоит на пути парсера (вспомним, что теперь наша обрабатываемая строка выглядит как ‘”Bye!” “Пока!”‘ – после первого использования string() в “items” добавится “Bye!”, а обрабатываемая строка сократится до ‘”Пока!”‘), пока не доберётся до конца строки, и нас выкинет из цикла, и парсинг закончится.
В целом, это уже рабочая версия парсера, но давайте всё-таки напишем алгоритм обработки блока.
Так как после двоеточия блок начинается со следующей строчки, то мы обязаны быть в конце строки. Просим парсер это проверить, а также пусть проверит наличие самого блока (используются методы expect_eol() и expect_block(statement), назначение которых описано в предыдущей части):
Далее нам нужно перейти от парсинга одной строки к парсингу блока. Используем для этого subblock_lexer() и запишем новый объект Lexer в переменную “l”:
А дальше, пользуясь advance(), идём по всем строчкам блока и парсим каждую.
Здесь стоит рассказать о предназначении декоратора catch_error(). Он нужен для того, чтобы, вызвав ошибку на одной строке блока, парсер продолжил своё дело и начал парсить последующие строки. То есть, если ошибка встретится в первой строчке блока, то catch_error() выведет оповещение о том, что ошибка была встречена на первой строке блока, а затем успокоится и позволит парсеру работать дальше.
expect_noblock(stmt) используется для того, чтобы предотвратить написание блока в блоке. Другими словами, в данном контексте он выдаст ошибку, если мы напишем
Часть 4. Валидатор
Наш парсер готов. Теперь нам нужно проверить правильность введённых данных.
Парсер лишь проверяет, правильно ли всё оформлено: за словами “gl_translate” должен следовать персонаж, после – либо двоеточие, либо строка. И хотя парсер не позволит вместо строки написать число, проверить на правильность написания саму строку он не может. Для этого будем использовать валидатор.
Код валидатора, к которому мы придём:
Главная наша проблема – количество строк. Обратите внимание на то, что мы в парсере всегда двигаемся “до конца”. Когда мы обрабатываем блок, мы двигаемся до самой последней строчки (см. описание метода Lexer.advance() во второй части), а когда обрабатываем однострочный оператор – до конца строки. И дело в том, что в нашем парсере не установлено ни одной проверки на количество строк. Их должно быть две: фраза и её перевод. А может быть хоть сто. Поэтому нужно проверить длину списка “items”, чтобы понять, сколько строк нашёл парсер.
Создадим функцию валидатора (опять же, принято называть “lint_” + название оператора, но это не принципиально) с одним аргументом:
В валидатор, или линтер, передаётся значение, которое возвращается парсером. То есть тот словарь, который возвращает парсер после своей работы передаётся в валидатор в аргумент “o”.
Поэтому давайте получим значения, которые парсер так активно искал:
Теперь давайте проверим, а вообще существует персонаж, который записан в “who”? Вспомним, что метод simple_expression() возвращает строку, поэтому нам нужно её оценить с помощью Питоновской функции eval(). Если такого персонажа не существует, вызовем ошибку с текстом “Персонаж не определён ” + имя персонажа:
Теперь проверим, сколько всё-таки строк в списке “items”. Если их не две (больше, меньше – не важно: нам нужно именно две строки), выведем ошибку с текстом “Оператор ‘gl_translate’ ожидает две строки, а получает ” + количество строк:
Теперь нам нужно проверить сами строки. Что в них проверять? Ну, например, текстовые теги. Для этого воспользуемся функцией renpy.check_text_tags(s), описанной во второй части.
И, в целом, всё. Больше проверять ничего не нужно. Валидатор готов.
Часть 5. Подготовка к исполнению
Все значения готовы к передаче в исполнитель, но вот визуальная часть пока ещё не работает. Нам нужно создать стиль, который будет изменять цвет при наведении мышкой на него и который будет выполнять действие при клике.
Готовый код для стиля ссылки в тексте:
Итак, чтобы при наведении мышкой текст последний менял цвет, особо сложных манипуляций не нужно. Создадим новый стиль и возьмём за основу стиль “normal_day” – оригинальный стиль для реплик персонажей Бесконечного Лета днём:
Нас интересуют поля hover_color и selected_hover_color, а остальные, в целом, можно и не трогать (я в них записал оригинальный цвет текста просто, чтобы было). В интересующие нас поля записываем цвет, в который хотим чтобы окрашивался текст при наведении на него мыши.
Теперь нам нужно сделать так, чтобы на этот текст можно было кликнуть.
Для этого используем поле hyperlink_functions, котрое принимает кортеж из трех функций: первая – стайлер (функция, которая возвращает объект-стиль, который применится к тексту), вторая – функция-исполнитель (вызывается, когда на текст кликнули), третья – функция фокуса (вызывается, когда на текст навелись мышкой). Третья и первые функции нам ни к чему, поэтому на их место в кортеже можно поставить None.
Все три функции получают один аргумент – аргумент текстового тега {a}. Пример:
Предположим, что к этому тексту применили стиль с готовой функцией-исполнителем. Тогда в аргумент исполнителя (и других функций при их наличии) передастся строка “аргумент”.
Поле hyperlink_functions создано исключительно для работы с текстовым тегом ссылки {a}!
Итак, каким образом мы реализуем перевод?
Я выбрал такой способ:
При нажатии на текст Ren’Py вызовет в новом контексте лэйбл, в котором тот же самый персонаж скажет свою реплику второй раз, только это будет уже переведённая фраза. Для реализации этого нам нужно:
- Знать, какой персонаж говорит
- Знать перевод фразы
Саму фразу мы можем не знать. Она нам не пригодится.
Таким образом, в функцию-исполнитель мы должны передать в виде аргумента персонажа и перевод фразы. Учитывая, что мы получаем лишь один аргумент, будем использовать разделение по определённому символу. Пример:
В функцию-исполнитель передастся строка “mi:Привет!”. Нам нужно её обработать.
Отлично! Теперь мы готовы показать игроку перевод. Для этого создадим лэйбл, который будем вызывать:
У этого лэйбла имеется два аргумента: персонаж, который произносит реплику и текст этой реплики. Благодаря тому, что мы будем вызывать этот лэйбл в новом контексте, оператор “return” вернёт нас в тот момент, когда была показана реплика без перевода.
Усовершенствуем функцию-исполнитель:
Теперь всё готово. Переходим к исполнению оператора.
Часть 6. Исполнитель
Мы получили значения с помощью парсера и проверили их с помощью валидатора. Теперь нашему оператору осталось только исполнить своё предназначение.
Код исполнителя, к которому мы придём:
Создаём функцию (принято “execute_” + название оператора, вы можете как хотите), принимающую один аргумент – тот же словарь, что принимает валидатор:
Получим значения из словаря:
Теперь нам нужно создать такую строку, чтобы она передавала в функцию-исполнитель из прошлой части персонажа и перевод:
Отлично! А теперь пусть персонаж “who” скажет реплику “what”, причём к тексту мы применим стиль для ссылок внутри текста, который мы написали в прошлой части (с помощью аргумента “what_style”):
Часть 7. Завершение
Вот и всё. Наш оператор готов. Осталось только добавить его в список операторов, которые воспринимает Ren’Py.
Это можно провернуть с помощью функции
renpy.register_statement(name, parse=None, lint=None, execute=None, predict=None, next=None, scry=None, block=False, init=False, translatable=False, execute_init=None, init_priority=0, label=None, warp=None, translation_strings=None, force_begin_rollback=False, post_execute=None, post_label=None, predict_all=True, predict_next=None)
Как видите, у неё очень много аргументов. Обо всех них модно почитать здесь[www.renpy.org], а нам понадобятся только эти:
- name. В него передаём название нашего оператора
- parse. В него передаём парсер
- execute. В него передаём исполнитель
- lint. В него передаём валидатор
- block. Здесь нам нужно передать строку “possible”, так как наш оператор може иметь блок, а может и не иметь его
С вами был alex6712, кодер мода “Зелёный Свет”.