Ren'Py: функции, экшены и curried-функции
Лирическое отступление: этот гайд будет сосредоточен на функциях и их применении в контексте Ren'Py и визновелл. Общие принципы программирования будут затронуты в минимально необходимых рамках.

Дисклеймер: все конкретные факты и утверждения о работе Ren'Py актуальны на момент публикации и для версии 7.0. В дальнейшем все может устареть и отпасть за ненадобностью. Печаль.

По традиции: эта же статья, только в гуглодоках.

Зачем мне функции?

Представьте, что вы, в составе молодой и подающей надежды команды, пишете самую обыкновенную рпг-новеллу с блэкджеком и статами. И у главного героя, как и положено герою рпг, есть сила, ловкость и интеллект. И харизма еще, и каждого на старте по десять очков. Заведем для их хранения словарь.
Код
default stats = {
  "str": 10,
  "dex": 10,
  "int": 10,
  "cha": 10
}

Допустим, мы решили не делать полноценную боевку, а заменить ее текстовыми энкаунтерами (перепрыгнуть овраг, оттащить поваленное дерево с дороги, поругаться с торговцем), которые будут происходить по определенной схеме. В каждый энкаунтер мы кидаем кубик, выбрасываем на нем случайное значение и сравниваем его с соответствующей характеристикой героя. Если у героя больше, он победил и получает небольшую прибавку к этому стату:
Код
"Герой попал в магическую ловушку..."
$ stat_id = "int"
$ random_roll = random.randint(stats[stat_id] - 5, stats[stat_id] + 5)

if stats[stat_id] > random_roll:
  $ random_bonus = random.randint(1, 3)
  $ stats[stat_id] += random_bonus
  "...и благодаря мощи своего ума смог освободиться! И получил +[random_bonus] к интеллекту."

else:
  "...застрял и просидел в ней, пока бродячий маг не вызволил его."

От стычки к стычке (в зависимости от того, какой показатель проходит проверку) меняется stat_id, остальной код, за вычетом фраз-реакций, абсолютно одинаковый. Пять строчек кода, всего ничего. Допустим, таких стычек в игре будет сто. Сто блоков однотипного/повторяющегося кода, не проблема.

Окей, говорит гейм-дизайнер, а что если добавить штрафы? Ну, если провалил проверку — пусть ловит случайный дебафф к соответствующей характеристике!

Окей, говорите вы. Это всего-то пару строк исправить.
В ста вхождениях кода.
Код
if stats[stat_id] > random_roll:
  $ random_bonus = random.randint(5, 10)
  $ stats[stat_id] += random_bonus
  "...и благодаря мощи своего ума смог освободиться! И получил +[random_bonus] к интеллекту."

else:
  $ random_bonus = random.randint(1, 3)
  $ stats[stat_id] -= random_bonus
  "...застрял и просидел в ней, пока бродячий маг не вызволил его, и получил -[random_bonus] к интеллекту."

На девяносто пятом блоке к вам приходит гейм-дизайнер и, сияя, сообщает, что дописал еще пятьдесят стычек. Ах да, прибавки к статам какие-то конские, сделай их поменьше на пару единиц.

Вы с тоской смотрите на сто старых блоков, требующих исправлений, и пятьдесят новых, на гейм-дизайнера... А можно как-нибудь эти действия собрать в одном месте, чтобы не исправлять каждый раз триста вхождений?

Ага. Вот для этого (в том числе) и нужны функции.

tldr: функции объединяют набор команд в один контейнер, упрощая правки, работу, вызов и сокращая объемы кода.

Лирическое отступление: Вообще, стремление укоротить код и избавиться от повторяющихся фрагментов — один из признаков того, что вы уже хапнули печального опыта в кодерстве. Искренне советую весь повторяющийся и параметризуемый код убирать в соответствующие контейнеры: в стили, экраны, ATL-конструкции и функции.

Почему не лейблы, а функции? Закономерный вопрос, который может возникнуть: зачем мне писать функцию, если я могу запихнуть повторяющийся код в лейбл, а потом делать call label в нужных местах? Все просто: большие повторяющиеся массивы текстовых реплик или операторов идут в лейблы. Питоновский код — в функции. Все по местам, не смешивать.

Что такое функция? Как выглядит стандартная функция?

Функция — это обособленный кусок кода, который можно вызвать из (условно) любого места программы. Сваливаешь огромную кучу многократно повторяющихся команд в одно специально огороженное место, и каждый раз, когда нужно их выполнить, не пишешь все команды заново, а говоришь "выполни вон ту кучу команд".

В Ren'Py (а на самом деле, в Python, бацьке Ren'Py) функции могут быть анонимными (безымянными), но нас пока интересуют именные.

Чтобы пользоваться функцией, в первую очередь нужно объявить и определить ее. Для именных функций это делается с помощью команды def. Поскольку это все питоновский код, его нужно уложить в блок init python.


init python: — оператор, говорящий, что дальше идет питоновский код (python), и что выполнять этот блок нужно при старте игры (init). Обязателен для объявления функций. В одном init python блоке может быть сколько угодно функций.

def <имя функции>(<параметры функции через запятую>): — обязательная конструкция-заголовок, которая должна быть у любой именной функции. Функция может не иметь параметров, в этом случае в скобках не нужно ничего писать.

Тело функции — должно быть не пустым, т.е. содержать хотя бы одну команду (например, return-конструкцию).

return <значение> — эта строка не обязательна. Если ее нет, компилятор по умолчанию считает, что функция возвращает None. Строка, состоящая только из оператора return, без значения, эквивалентна return None. Вместо <значения> можно подставить любое значение — переменную, число, строку, выражение, сокращенную форму условного оператора. Подробнее о возвращаемых значениях — чуть ниже.


Объявленную функцию можно вызывать из разных участков кода. Когда мы вызываем функцию, это заставляет отрабатывать весь код внутри нее.
Код
"Герой попал в магическую ловушку..."
$ Encounter()

Замечательно! Теперь мы сможем написать свою функцию, описывающую результат стычки!
Код
init python:
  def Encounter():
  random_roll = random.randint(stats[stat_id] - 5, stats[stat_id] + 5)
  ...

...Так, нет, стоп. Откуда мы возьмем stat_id? Оно же каждый раз должно быть разным! Как нам сказать функции, какую именно характеристику мы собираемся тестировать во время каждого конкретного вызова?

В этом нам помогут...

Параметры (аргументы) функции.

Аргументы функции — это данные, которые она принимает из внешнего кода, для того, чтобы использовать их внутри себя.

Параметры и аргументы с точки зрения юзера — одна и та же сущность. С точки зрения функции, параметры — это то, что описывает принимаемые извне значения, а аргументы — это эти значения и есть. Если совсем упрощать, то в объявлении функции в скобках — параметры, а в вызове — аргументы.


Аргументы при вызове разделяются запятыми и бывают позиционные и именованные. Позиционные — это те, для которых важна их очередность в скобках при вызове. Это единственный способ понять, в какой параметр вы передаете этот аргумент. Именованые — это те, у которых для идентификации есть имя, и стоять в своей группе они могут в каком угодно порядке (как в списках и словарях: одним важен порядок, другие хранятся вперемешку).


Параметры тоже могут быть обозначены, как строго-позиционные или строго-именованные. По умолчанию в питоновской функции все параметры — позиционно-именованные. Это значит, что при вызове можно как соблюдать очередность, передавая аргументы, так и указывать имя параметра, в который передаешь аргумент.

Не по-умолчанию:

Если вы видите в объявлении функции параметр, предваренный звездочкой, например *args, то значит, перед вами переменно-позиционный параметр.

Если вы видите в объявлении функции параметр, предваренный двумя звездочками, например **kwargs, то значит, перед вами переменно-именованный параметр.

Переменные параметры используются тогда, когда точное число аргументов в вызове функции неизвестно и/или слишком велико, чтобы не описывать каждый возможный параметр, объявляя функцию.

Параметры могут быть обязательными или не обязательными. Если при объявлении функции вы указали для параметра значение по умолчанию, он становится необязательным. Иначе — он обязателен. Переменно-именованные и переменно-позиционные параметры являются необязательными, это контейнеры для "остатков", которых может быть или не быть неопределенное количество.

Несоответствие количества аргументов количеству параметров.

Функция всегда ждет от вас некоторое число аргументов (от нуля до бесконечности). Всем обязательным параметрам нужно передать какое-то значение (необязательные можно не указывать).

Аргументов в вызове функции не должно быть меньше, чем обязательных параметров, иначе получим ошибку вида:

<имя_функции> takes at least X argument (Y given)

Аргументов в вызове не должно быть больше, чем всех параметров, иначе получим ошибку вида:

<имя_функции> takes at most X argument (Y given)

Исключение: если, см. выше, при объявлении функции были указаны переменные параметры, т.е. есть куда собирать лишние аргументы.

Порядок передачи аргументов при вызове.

Порядок позиционных аргументов при вызове строгий, и задается в объявлении функции. Кроме этого, группы параметров разных типов должны сохранять определенный порядок друг относительно друга (как в вызове, так и в объявлении).

Порядок групп параметров в объявлении функции:


- группа обычных (позиционно-именованных) параметров, для которых не указаны значения по умолчанию (если такие есть)
- группа обычных параметров, для которых заданы значения по умолчанию (если такие есть)
- группа переменно-позиционных параметров (если такие есть)
- группа переменно-именованных параметров (если такие есть)

Порядок параметров внутри каждой группы не имеет значения.

Порядок групп аргументов в вызове функции:

tl;dr: сначала безымянные в строгом порядке, потом — именованные, как угодно.

- группа позиционных аргументов (без имен) (если такие есть). Порядок аргументов в этой группе должен соответствовать порядку соответствующих параметров в объявлении функции
- группа именованных аргументов (если такие есть), порядок внутри группы не важен
- аргументы в виде *кортеж или список значений без имен через запятую (если такие есть), порядок внутри группы не важен
- аргументы в виде **словарь или список пар вида имя = значение через запятую (если такие есть), порядок внутри группы не важен

При этом, важно! Любые именованные аргументы должны стоять строго после всех позиционных.



(Кроме того, существуют частные случаи, когда в вызове функции можно переставлять группы местами и смешивать аргументы разных групп. Например, переменно-именованные с именованными, если в вызове в принципе нет позиционных. Но правило "сначала безымянные, потом с именами" остается.)

Окей, гугл, мы вызвали функцию и передали ей внутрь какие-то значения из внешнего кода. Что с ними делать дальше? Как функция их видит?

Работа с переданными значениями

Полученные значения функция распределяет по локальным переменным в соответствии с именами параметров и позициями. Если для какого-то параметра было задано значение по умолчанию, то переданное значение для конкретно этого вызова его перепишет.

Переменно-позиционные параметры *args принимают данные в виде кортежа. Эти данные помещаются в локальную переменную с тем же именем, без звездочки. Т.е. данные, переданные в параметр *something будут помещены в переменную something.

Аналогично — с переменно-именованными. Данные, переданные в параметр **anything будут помещены в виде словаря в локальную переменную anything.


Готовыми локальными переменными внутри тела функции можно пользоваться так же, как и обычно. Почему я все время добавляю "локальные"? Потому что...

Область видимости

...еще одно понятие, которого стоит коснуться, говоря о функциях — область видимости переменных (scope).

В контексте обсуждаемого вопроса области видимости (и переменные) делятся на локальные и глобальные. Локальных областей видимости может быть несколько, глобальная — одна. Объявленные в ней данные доступны для чтения из любого места программы. Т.е. если вы объявили переменную в глобальной области видимости, эта переменная будет глобальной, и обращаться к ее содержимому можно из любой точки кода.

Переменные, объявленные в локальной области видимости, будут доступны только внутри этой конкретной локальной области, а за ее пределами не будут существовать, или будут иметь совершенно другие значения. Мы не можем ни считывать, ни записывать в них информацию извне области их видимости.

В Ren'Py все переменные внутри проекта по умолчанию являются глобальными. Так, если в одном файле проекта вы объявили внутри скрипта переменную my_variable, то она будет доступна по этому имени во всех других файлах.

Исключения, когда переменная будет локальной:

1. Если имя переменной начинается с двойного нижнего подчеркивания. В данном случае переменная станет локальной для конкретного файла в котором была объявлена, и будет доступна только в нем и в файлах с тем же именем (но расположенных в разных каталогах).

2. Если имя переменной объявлено в блоке python hide.
Код
python hide:
  my_variable = 0

В этом случае областью действия переменной станет область действия блока python, в котором она объявлена, и будет доступна только в нем.

3. Переменные, объявленные внутри функций, классов и других локальных объектов всегда являются локальными.

Последний пункт и функции интересует нас больше всего. Внутри функции мы можем попытаться обратиться к некоторой переменной, которую мы внутри этой функции не объявляли. Функция выглянет наружу и проверит, есть ли в глобальной области переменная с таким именем. Если она есть, мы сможем считать ее текущее значение (а если ее нет, выпадет ошибка). Но даже если она есть, когда мы попытаемся что-то в нее записать, то (по умолчанию) первая же операция присвоения внутри функции создаст локальную переменную с таким же именем. И все операции с этой переменной останутся в локальной области действия функции, и не сохранятся "снаружи".


С одной стороны это полезно, потому что в десятке функций мы можем спокойно работать с переменными с одинаковыми именами, не боясь, что одна перетрет данные в другой. Но что, если нам нужно изменить значение глобальной переменной изнутри функции? Для этого нужно явно объявить, что мы собираемся работать с глобальной переменной. Синтаксически есть несколько способов сделать это, но самый удобный и простой — воспользоваться ключевым словом global.
Код
def YetAnotherFunction():
  global global_one, global_two, global_three

  # какой-то код

После ключевого слова через запятую перечисляются имена переменных, находящихся в глобальной области видимости. Теперь все изменения, проделанные с ними внутри функции, сохранятся и вне ее.


Другой способ обращаться к глобальной переменной внутри функции — через словарь globals(), в котором хранятся по ключам все существующие глобальные переменные. Вот так:
Код
def YetAnotherFunction():
  # какой-то код
  globals()["some_variable"] = "new value"

Да, запись такая и должна быть, прямо с круглыми скобками после globals.

Этот и предыдущий способы характерны для Python. У Ren'Py есть еще один, свой. Работает он за счет того, что все глобальные переменные по умолчанию Ren'Py кидает в место под названием store. Это не словарь, а объект, и к переменным внутри store обращаться надо чуть иначе:
Код
def YetAnotherFunction():
  # какой-то код
  store.some_variable = "new value"

Возвращаемое значение

Функция умеет получать данные извне и что-то с ними делать. Кроме этого, функция умеет возвращать какие-то данные обратно во внешний код с помощью оператора return. Если return внутри функции отсутствует, то она все равно неявно возвращает None.

В общем случае, если просто вызвать функцию, то ее возвращаемое значение провалится в никуда, потому что не будет контейнера, который бы его принял. Чтобы положить возвращаемое значение в какую-нибудь переменную, достаточно вызвать функцию из операции присвоения.


Код
init python:
def CustomFunc1():
  return 20

def CustomFunc2():
pass

hero "Я тут что-то говорю."
$ val1 = CustomFunc1()
$ val2 = CustomFunc2()
hero "Возвращенные значения: [val1] и [val2]"


В результате val1 будет равен 20, val2None.

В нашем конкретном примере с героем и проверкой статов мы можем возвращать результат проверки (True для успешной и False для проваленной), а также размер прибавки/штрафа. Применим всю полученную информацию на практике и наконец-то построим код целиком.

Внутренности функции в общем виде выглядят одинаково: роллим "препятствие", сравниваем его с показателем героя, делаем прибавку/штраф в зависимости от результата сравнения, возвращаем результат во внешний код. В качестве единственного аргумента нам необходимо передать идентификатор характеристики (ключ в словаре stats). Не забываем указать, что словарь stats — глобальная переменная (функция должна будет изменять значения внутри нее).
Код
init python:
  def Encounter(stat_id):
  global stats

  random_roll = random.randint(stats[stat_id] - 5, stats[stat_id] + 5)

  if stats[stat_id] > random_roll:
  random_bonus = random.randint(2, 4)
  stats[stat_id] += random_bonus

  return (True, random_bonus)

  else:
  random_debuff = random.randint(1, 3)
  stats[stat_id] -= random_debuff

  return (False, random_debuff)

После того, как внешний код получит результат стычки, мы уже с помощью операторов Ren'Py и этого результата разрулим ситуацию с фразами-реакциями:
Код
"Герой попал в магическую ловушку..."
$ victory, stat_change = Encounter("int") # старая-добрая распаковка кортежа, который вернуло return'ом

if victory:
  "...и благодаря мощи своего ума смог освободиться! И получил +[stat_change] к интеллекту."

else:
  "...застрял и просидел в ней, пока бродячий маг не вызволил его, и получил -[stat_change] к интеллекту."

Вызов функций не в любом месте кода.

В Ren'Py у нас есть последовательный, пошаговый код скрипта, исполнением которого мы можем осознанно управлять, и есть неуправляемая машина в виде экранов. Код экранов может быть выполнен в совсем неожиданные для вас моменты (есть всякие predict'ы, есть перерисовка экранов в момент взаимодействия...). Если вы зачем-то поместили в код экрана питоновский код (в том числе и вызов функции), то будьте готовы, что этот код будет выполняться не только по очевидной команде show screen.

Есть, однако, моменты, когда нам нужно привязать выполнение функций и манипуляцию с данными именно к экранам.

Экшены в Ren'Py или "Хочу, чтобы по нажатию кнопки выполнялась функция!"

Экшен в Ren'Py — это действие, которое выполняется при нажатии, наведении курсора на кнопку и отведении курсора с кнопки (и еще в некоторых случаях, вроде истечения времени в таймере). В Ren'Py из коробки доступны самые разные экшены для самых разных целей, с параметрами и без.

Лирическое отступление: переведенный список всех описанных в документации экшенов находится здесь.

Вызов экрана, прыжок на метку, присвоение переменной некоторого значения — все это охвачено стандартным функционалом, и перед тем, как писать кастомную функцию. проверьте, может быть уже есть готовый экшен для этих целей. А если нет одного, то, возможно, можно использовать список из нескольких:
Код
# по нажатию на кнопку: прибавляем к значению внутри my_var единицу, прячем экран my_screen, прыгаем на метку my_label
textbutton "Кнопка" action [ SetVariable("my_var", my_var + 1), Hide("my_screen"), Jump("my_label") ]


Кроме этого, если вам нужно ограничить выполнение экшенов каким-то условием, на помощь придет специальный экшен If(). Скажем, вы хотите в какой-то момент запретить пользователю нажимать на кнопку, но не прятать ее совсем, а сделать неактивной:
Код
default number_of_times_to_press = 5

screen conditional_btn():
  textbutton "Кнопка" action If(
  number_of_times_to_press > 0,
  true = SetVariable("number_of_times_to_press", number_of_times_to_press - 1)
  )

В этом примере переменная number_of_times_to_press управляет активностью кнопки. Каждое нажатие отнимает от ее значения единицу, и когда это значение станет равным нулю, кнопку больше нельзя будет нажать.

Общий вид экшена If() выглядит так:

If (<условие>, true = <экшены, которые выполнятся, если условие истинно>, false = <экшены, которые выполнятся, если условие ложно>)

If здесь пишется с большой буквы, а true и false — наоборот, с маленькой, потому что в данном случае If — это экшен, а true и false — его параметры.

Не-функции.

На вид экшены из коробки подозрительно напоминают функции: тот же вид, те же параметры. На самом деле это разные сущности. Экшен — это НЕ то же самое, что и функция.

Громко, хором: ЭКШЕН — НЕ ФУНКЦИЯ, ФУНКЦИЯ — НЕ ЭКШЕН.

Функция может выступать в роли экшена при одном условии: если ее можно вызвать без аргументов. То есть:
- если у вашей функции в принципе нет параметров
- если у вашей функции есть параметры, но ни один из них не обязателен

Очень важный момент: если мы употребляем функцию в роли экшена, то пишем ее без скобок.
Код
init python:
def CustomFunc():
  # какой-то код

...

textbutton "Кнопка" action CustomFunc

Со скобками (даже пустыми) — это вызов функции. Без скобок — это имя. Мы не вызываем функцию в экшене, мы говорим "в качестве экшена используй функцию по имени X"

Экшен не знает, какие аргументы нужно передать функции во время его (экшена) исполнения. Если бы мы попытались в экшене вызвать функцию с аргументом, например:
Код
textbutton "Кнопка" action CustomFunc(0)

- то произошло бы следующее: во время выполнения кода экрана, в котором находится кнопка (а код экрана может выполняться в самые неожиданные для юзера моменты, напоминаю):
- вызывается и выполняется функция CustomFunc(0);
- CustomFunc(0) возвращает некоторое значение (в нашем случае None);
- это значение экран пытается трактовать как экшен, и если это не экшен, выпадает ошибка компиляции.

Но что делать, если по нажатию кнопки должна отработать функция, вызванная с определенными аргументами?

Для этого есть два способа: экшен Function() и curried-функции. Первый легче в обращении и понимании, второй имеет более привычный вид чисто внешне.

Экшен Function()

Тут все довольно просто: в Ren'Py из коробки есть специальный экшен, который принимает в качестве своих аргументов имя функции, которую нужно вызвать по экшену, и набор аргументов, который нужно передать этой функции в момент вызова.

Function(callable, *args, **kwargs)

Он может принимать три параметра:

- callable: обязательный параметр. В него мы кладем имя (только имя, без скобок и всего остального!) функции, которую нужно вызвать.
- args: необязательный параметр, переменно-позиционный, мы уже такие видели. Сюда сбрасываются все позиционные аргументы, которые должны быть переданы функции при вызове.
- kwargs: необязательный параметр, переменно-именованный, аналогично. Сюда сбрасываются все именованные аргументы, которые должны быть переданы функции при вызове.

Коротенький проект для примера того, как работает этот экшен:
Код
default gid = 0
default gname = ""

init python:
  def someFunction(id, name = "Имя"):
  global gid, gname

  gid = id
  gname = name

  renpy.restart_interaction()

screen test():
  vbox:
  text "id: [gid]"
  text "name: [gname]"

  textbutton "Заполнить раз!" action Function(someFunction, "123", random_name)
  textbutton "Заполнить два!" action Function(someFunction, id = "456", name = "Синдзи")

label start:
  scene black
  show screen test

  $ random_name = "Случайное имя"

  pause

renpy.curry()

В Ren'Py есть специальная функция renpy.curry(). Она:
- принимает в качестве параметра callable-объект (функцию)
- возвращает нечто, которое:
- будучи вызванным, возвращает кое-что, которое:
- будучи вызванным, вызывает функцию (из п.1).

Другими словами, объект, который возвращает renpy.curry() от некоторой функции, если его вызвать дважды, делает то же самое, что единожды вызванная функция.

То же самое, в виде картинки:


То же самое, в виде псевдокода:
Код
a = renpy.curry(MyFunction)
b = a(5)
c = b()

Эквивалентно:
Код
c = MyFunction(5)

В строке c = b() вызывается функция MyFunction. При этом передача аргументов состоялась строкой выше, в b = a(5). Значит, в c = b() аргументы не нужны. Значит, мы можем вызывать b без аргументов, а значит, использовать b в роли экшена. А b = a(5) по своей сути, так что если мы поставим a(5) в экшен кнопки, и если при этом a будет являться curried-формой функции MyFunction, то при выполнении экшена будет вызвана функция MyFunction с нужными нам аргументами.

(Если вам и сейчас ничерта не понятно, не беспокойтесь, это нормально.)
Код
init python:
  def MyFunction(num):
  renpy.notify("MyFunction вызвали с аргументом %d!" % num)

  MyCurriedFunction = renpy.curry(MyFunction)

screen MyScreen:
  textbutton "Кнопка" action MyCurriedFunction(5)

В примере выше MyCurriedFunction(5) будет выполнена во время вывода экрана MyScreen, но вместо того, чтобы запустить функцию MyFunction, она вернет новую функцию (в явном виде ее нет, назовем ее X). Которая, когда ее вызовут, выполнится так, как выполнилась бы MyFunction(5). Функция X становится экшеном, и это решает нашу проблему.

Такие дела.

Напоследок скажу, что функции в Ren'Py могут быть использованы не только для выполнения всяких там вычислений и установки флагов. В наших проектах они, например, отвечают еще и за вывод экранов, переключение фонов и сцен и другую мелкую визуальную работу. Это очень удобно.
Код
# функция winLight используется для генерации цветного огонька по заданному шаблону

init python:
  def winLight(color):
  return Fixed(
  AlphaMask(Solid(color), "animations/aroom/aroom-winlight.webp"),
  Transform("animations/aroom/aroom-winlight.webp", zoom = .45, offset = (8, 8)),
  xysize = (27, 27)
  )

# цветные огоньки с лого винды

image winlr = winLight("#d84035")
image winlg = winLight("#adc23d")
image winly = winLight("#d0ac26")
image winlb = winLight("#4474cf")




Еще раз оставляю ссылку на список всех экшенов Ren'Py на русском (перевод документации): ссылка


Автор материала: Ikuku
Материал от пользователя сайта.

Ren'Py 06 Марта 2019 2147 Ikuku 4.7/14

Комментарии (5):
Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]
3
1 moonfork   (17 Марта 2019 14:22)
5668
Отличная статья.

3
2 Shiigeru   (18 Марта 2019 12:40)
66183
Полезно!

1
3 stop_control   (23 Марта 2019 20:40)
53339
Все собрано в одном месте - подготовлено для комфортного чтения иллюстрациями )))
Супер.
Очень нравится подача материала.

p.s. Надо будет все попробовать, может вопросы возникнут.

0
4 pumpkinofhell   (31 Марта 2019 23:22)
39598
Где, блин, 1001 ночь! angry angry angry Уже джва года ждём. Спасибо за статью) love

0
5 Misiakova   (24 Апреля 2019 20:20)
87871
Спасибо, полезно. :>