Сайт Anivisual.net закрыт

Он продолжит функционировать в виде архива. Новые материалы добавлять нельзя.

Creator-Defined Statements: ваши ручные операторы (на примере пирога)

Creator-Defined Statements: ваши ручные операторы (на примере пирога)

Лирическое отступление: на практике статья будет полезна скорее тем, кто кодит дополнительный функционал на 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)

На этом все. С вами был кодер ТГ "Красавица Икуку", которому захотелось поделиться прикольными штуками с народом. Кормите соседок пирогами и создавайте кастомные операторы!
18
Декабрь
0
3.9
7185
Добавлять комментарии могут только зарегистрированные пользователи.

Комментарии к записи: 0