Статьи: Ren'Py для «чайников»
24.09.2017


Примечание

Данная статья была написана мной более года назад в качестве отчёта о практике для универа. В кои-то веки я решил её опубликовать. Надеюсь, ещё не совсем потеряла актуальности :/

В связи со спецификой появления, думаю, во избежание лишних вопросов мне стоит пояснить, что САФУ/NArFU — это название университета, а ИСМАРТ — конкретный институт в его составе, где я учусь. Стиль изложения также может несколько отличаться от моих обычных публикаций, так как подразумевалось, что читать текст будут люди совсем далёкие от игр и современной сетевой культуры.

Также для основной версии сайта я прикрутил подсветку синтаксиса (на lite- и pda-версиях не работает), но Ren'Py — довольно специфичный язык, поэтому используется подсветка от Python'а. В итоге подсвечены будут, по сути, только строки и некоторые совпадающие ключевые слова.

Если при чтении возникнут какие-то проблемы или вопросы по коду, то можно скачать архив с примерами. Он упакован в формат 7z, потому что, в отличие от zip'а, тот умеет упаковывать идентичные файлы так, чтобы они занимали место, как один.


Гайд по созданию визуальной новеллы

Здравствуй, дорогой читатель! Возможно, ты уже слышал о такой широко известной в узких кругах видеоигре от отечественных разработчиков, как «Бесконечное лето» (Everlasting Summer)? Впрочем, если нет, то ничего страшного: я расскажу о ней вкратце.

Как гласит официальный сайт, «„Бесконечное лето“ — это визуальная новелла от российских разработчиков, дарящая самые искренние и светлые переживания об ушедших днях и надеждах, которым ещё предстоит сбыться». Поясняю:

  • Визуальный роман (но чаще встречается вариант с англицизмом: визуальная новелла) — это жанр компьютерных игр, в котором упор делается на текстовый контент, сдобренный порциями аудиовизуального сопровождения. По сути, это скорее книги, чем игры, но обильно снабжённые иллюстрациями, анимацией, музыкой и звуками окружающего мира. Также, в отличие от книг, представители данного жанра обычно имеют нелинейный сюжет с несколькими концовками, и исход зависит от выборов игрока.
  • «Бесконечное лето» повествует историю об одном асоциальном человеке, волею судеб оказавшемся в загадочном пионерлагере. Многим игра нравится именно атмосферой, позволяющей почувствовать себя пионером либо поностальгировать о временах былой молодости.

Однако мы здесь собрались вовсе не для того, чтобы слушать рекламу и хвалебные оды, не так ли? Так что перейдём ближе к сути.

«Бесконечное лето» написано на бесплатном движке Ren'Py. Это специальный движок, предназначенный для создания визуальных романов сценаристами, далёкими от программирования. Он имеет предельно простой синтаксис и множество встроенных средств для реализации практически всех функций, которые могут понадобиться в подобных играх. Тем не менее, сам движок свободен (open source) и расширяем, предоставляя возможность пользоваться всей мощью языка Python, на котором он написан.

В этом небольшом гайде мы рассмотрим базовые возможности Ren'Py, а также слегка коснёмся темы создания модификаций к игре «Бесконечное лето».


SDK

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

Окно Ren'Py Launcher

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

Создай новый проект, укажи папку (желательно, без кириллицы, но это не обязательно), имя проекта (вот тут только латиница!) и выбери любую тему (это непринципиально).


Основы основ

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

  • В options.rpy находится большинство настроек: размеры окна, стили, переходы по умолчанию и т. д.
  • В screens.rpy описываются меню, диалоговое окно и прочие «экраны».
  • script.rpy предназначен, собственно, для самого скрипта-сценария романа.

Вообще, это всё условно. Все файлы проекта находятся в папке game и Ren'Py читает там все файлы с расширением *.rpy, так что можно разбивать скрипты на любое количество файлов.

Я бы мог рассказать ещё про основы языка Python, как сделано в документации, но всё, что нам понадобится для базового знакомства, интуитивно-понятно или будет пояснено в месте применения. Так что предлагаю просто нажать по ссылке на scripts.rpy в «лончере» (launcher), выбрать редактор (любой, я вообще пользуюсь Sublime Text 3 с плагином) и приступим к созданию твоего первого творения на Ren'Py!


Текст, картинки, переходы

Сотри всё, что уже есть в файле: мы начнём с нуля.

Для упорядочивания кода Ren'Py пользуется системой меток (привет, ассемблер и GOTO!). При выборе кнопки «Начать игру» в главном меню игры, которое отображается сразу после запуска, управление передаётся на метку start:

label start:
    "Здесь начинается наш рассказ. Со слов рассказчика."
    "Аня" "Кто здесь?"
    "Ответом была лишь тишина."
    "Аня" "Эй! Я точно знаю, что ты там!"

Что мы видим? После объявления метки start начинается принадлежащий ей блок кода, строки которого обладают отступом в 4 пробела. Внутри блока расположены речевые конструкции (say statements), с помощью которых выводится текст.

В первой и третьей строчках (блока) располагаются слова автора или рассказчика. Фраза просто заключается в двойные кавычки (если внутри фразы нужно вывести кавычки, то они предваряются обратным слешем: \").

Во второй и четвёртой строках находится фраза персонажа под именем Аня. Видно, что имя персонажа, так же заключённое в двойные кавычки, и его фраза отделяются пробелом.

Возникает первая проблема: постоянно приходится повторять имя персонажа (а в больших сценариях с более длинными именами это реально представляет проблему). Ну и сразу обращаю внимание на вторую: все персонажи отображаются абсолютно одинаково, без какой-либо толики индивидуальности. Решим обе эти проблемы:

define a = Character('Аня', color='#ffaaaa')   # могут быть и двойные кавычки
# Ах да, после знака решётки идут комментарии, которые просто игнорируются.

label start:
    "Здесь начинается наш рассказ. Со слов рассказчика."
    a "Кто здесь?"
    "Ответом была лишь тишина."
    a "Эй! Я точно знаю, что ты там!"

В самое начало я добавил новую строку, которая определяет (define) нового персонажа (character) и задаёт ему имя и красноватый цвет имени (color). Новый персонаж присваивается короткой переменной a, которая теперь используется в речевых конструкциях вместо имени. Обратите внимание, что определения (что персонажей, что изображений, как мы увидим позже) пишутся вне любых меток, так как выполняются во время инициализации (немного про это будет сказано позже).

Что же у нас получилось?

Первый запуск нашего проекта

Хм, надо бы избавиться от этих шашечек и добавить собственный фон. Я использую, например, фотографию крыльца своего института, найденную в Интернете. Закинь любое изображение достаточного размера в папку images внутри папки game. Внутри этой папки Ren'Py сканирует все изображения и по определённым правилам создаёт для них имена, но для наглядности мы определим фон вручную. Самый простой способ:

image bg ismart = 'images/ismart.jpg'

Просто путь к файлу! Но если мы сделаем так, то получим странную картину:

Фон слишком большой

Виной всему слишком большой размер изображения. Поэтому придётся немного усложнить определение. Вот весь код:

image bg ismart = im.Scale('images/ismart.jpg', config.screen_width, config.screen_height)
define a = Character('Аня', color='#ffaaaa')

label start:
    scene bg ismart

    "Здесь начинается наш рассказ. Со слов рассказчика."
    a "Кто здесь?"
    "Ответом была лишь тишина."
    a "Эй! Я точно знаю, что ты там!"

Во! Теперь всё в порядке:

Теперь фон вписывается идеально

Теперь объясню, что делает добавленный код и как вообще происходит отображение. Всего появились две новые строчки: конструкция с image и конструкция с scene:

  • image, собственно, определяет изображение. Между ключевым словом и знаком равенства помещается имя отображаемого объекта. С ним не всё так просто. Как мы увидим дальше, в играх такого жанра, часто используются так называемые спрайты, то есть изображения персонажей с разными эмоциями и одеждой. Соответственно, частой операцией является смена спрайтов одного персонажа. По этой причине название изображения для Ren'Py состоит из, собственно, имени и атрибутов, разделённых пробелами. Всё это станет понятнее, когда будем рассматривать спрайты. А сейчас достаточно понять, что bg — название изображения, ismart — атрибут.
  • scene, как понятно из названия, предназначен для смены сцены. Выражение с этим словом очищает экран от любых других изображений и выводит указанное (обычно фон, хотя может и не выводить ничего).
  • im.Scale(d, width, height) — функция масштабирования, которая принимает отображаемый объект (displayable), в частности изображения, и изменяет его размеры к указанной ширине (width) и высоте (height). Сразу замечу, что существует аналогичная функция без префикса im.* (вне объекта im), которая отличается тем, что производит расчёты при каждом отображении и не сохраняет результат вычислений при определении.
  • config.screen_width и config.screen_height — две переменные в объекте конфигурации, хранящие заданные в настройках ширину и высоту экрана игры (их легко можно найти в options.rpy).

Надеюсь, я объяснил достаточно понятно. Давай теперь поиграем со стилями? Открой screens.rpy, найди в самом начале описание экрана диалогов (screen say) и удалим всё «лишнее», добавив немного своего (выделено полужирным):

screen say:

    default side_image = None
    default two_window = True

    vbox:
        style "say_two_window_vbox"

        if who:
            window:
                style "say_who_window"
                background "#555522"

                text who:
                    id "who"

        window:
            id "window"
            background "#555599"

            has vbox:
                style "say_vbox"

            text what id "what" color "#000000" italic True

Рассмотрение стилей, к сожалению, выходит за рамки данного гайда, так что я не стану ничего пояснять. Будем считать, что разобраться в работе стилей будет твоим домашним заданием ;)

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

Зря мы трогали стили

«Бесконечное лето»

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

Прежде всего скачай саму игру: либо через сервис цифровой дистрибуции Steam, либо через торрент-трекер Rutracker. Актуальные ссылки можно всегда найти на официальном сайте. Далее зайди в папку с игрой (для версии из Steam это <папка библиотеки>\SteamApps\common\Everlasting Summer). Там обычная структура файлов игры на движке Ren'Py. Заходим в game, как и прежде, а оттуда в папку mods. Скопируй туда scripts.rpy нашего проекта. Можешь даже его как-нибудь переименовать — не суть.

Теоретически можно создать наш файл со скриптом в любом месте внутри папки game, но давай будем придерживаться соглашений. К тому же, нам надо перекинуть и картинку. Создай папку narfu_images рядом с файлом скрипта и скопируй туда наш фон.

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

Определение изображений и персонажей, как было сказано в примечании ранее, автоматически размещается в блоке инициализации, который выполняется до загрузки игры. Соответственно, это то самое место, где наш скрипт будет заявлять о себе. Добавь этот код в самое начало скрипта:

init python:
    mods["narfu_practice"] = "Учебный мод"

Блок инициализации объявляется ключевым словом init. Если дальше идут выражения на языке Python, а не на Ren'Py, то добавляется слово python. Вообще, в любом месте блоки кода на Python объявляются с помощью этого слова. Другой путь написать одиночную команду на Python — использовать знак доллара ($) в начале строки.

init 0:
    $ mods["narfu_practice"] = "Учебный мод"

В примере выше я вместо слова python явно задал приоритет блока. Блоки выполняются от меньшего приоритета к большему. По умолчанию подразумевается блок с нулевым приоритетом. Порядок выполнения блоков с одинаковым приоритетом не гарантирован и может быть случайным.

Теперь объясняю саму команду. Переменная mods — это специальный список «ключ-значение», где перечисляются все модификации. Ключ должен быть уникальным значением на латинице без пробелов и в кавычках. Значение — название мода для отображения в загрузчике модификаций. Ключ играет роль названия метки при запуске мода, так что исправь label start на label narfu_practice. Также давай уберём определение персонажа a. Он нам больше не понадобится.

Теперь сделаем магию! Вставляй в редактор оставшийся код и запускай игру через меню модификаций в настройках!

label narfu_practice:
    scene bg ismart with fade

    window show
    "Здесь начинается наш рассказ. Со слов рассказчика." 

    show dv scared pioneer with dissolve
    dv "Кто здесь?"
    "Ответом была лишь тишина."

    show dv rage pioneer
    dv "Эй! Я точно знаю, что ты там!" with dissolve

    window auto
Наш мод в загрузчике модификаций

С чего бы начать? «Бесконечное лето» предоставляет интерфейс, персонажей, спрайты к ним, фоны и музыку. Здесь мы пока использовали только первые три пункта.

dv — это персонаж и тег изображений для Алисы Двачевской. Соответственно, scared, raged и pioneer — это атрибуты. Вообще, названия всех спрайтов в игре строятся по следующему плану: короткое_имя_персонажа эмоция одежда. Полный список спрайтов можно найти в знаменитых уроках LolBot'а (один из программистов игры, создатель системы модификаций). Официальной ссылки, насколько мне известно, не существует, а прочие быстро станут не актуальны, так что Google в помощь!

При показе второго изображения с таким же тегом, первое исчезает. Это нужно как раз для удобной смены эмоций, как сделано в нашем примере. Чтобы отобразить два одинаковых изображения, можно воспользоваться конструкцией show..as <новая метка> (см. следующий параграф).

Для отображения спрайтов используется ключевое слово show, похожее на scene, но не очищающее экран. Для обеих конструкций могут быть указаны переходы с помощью конструкции with. Строго говоря, изображение вообще отображается только после этой конструкции. Таким образом можно планировать показ нескольких изображений, для которых потом задавать один переход. Если with не встречается до следующего диалога, то Ren'Py использует with None, просто выводящий изображения мгновенно. А ещё его полезно использовать, когда нужно одно изображение отобразить мгновенно, а лишь второе — с переходом.

Ren'Py имеет ряд встроенных переходов (мы использовали dissolve и fade), а также позволяет создавать свои с помощью специальных функций (например, with Dissolve(0.5) для «растворения» одного изображения в другом за полсекунды). Всё это очень интересно, но выходит за рамки данного гайда. Смотри документацию, если хочешь знать больше!

Обрати внимание, как отображаются диалоги с изображениями в обоих случаях речи Алисы: в первом сначала рисуется персонаж, а затем печатается текст, а во втором — и текст, и персонаж переключатся с «растворением» одновременно.

Для того, чтобы диалоговое окно не мерцало при показе изображений, я использовал window show, которое держит окно всегда на экране. По умолчанию же оно показывается при отображении диалогов и скрывается при показе изображений, что я обратно и включаю в конце скрипта (надо помнить, что мод заканчивается, но игра продолжает выполняться, и мы обязаны всё вернуть в исходное состояние!).

Разумеется, существуют конструкции hide для скрытия изображении и window hide для скрытия диалогового окна.

А теперь давайте добавим второго героя, музыки и украшательств!

label narfu_practice:
    # Специальная функция и переменная «Бесконечного лета», которые задают
    # правильное освещение спрайтов и вид интерфейса для меню и диалогового окна.
    $ day_time()
    $ persistent.sprite_time = "day"

    # Да будет музыка!
    play music music_list["so_good_to_be_careless"]

    scene bg ismart with fade

    window show
    "Здесь начинается наш рассказ. Со слов рассказчика." 

    show dv scared pioneer with dissolve
    dv "Кто здесь?"
    "Ответом была лишь тишина."

    show dv rage pioneer
    dv "Эй! Я точно знаю, что ты там!" with dissolve

    # Отображаем спрайт за пределами экрана,
    # чтобы потом его переместить оттуда
    show sl normal pioneer:
        xanchor 0.0 xpos 1.0
    with None

    # Двигаем спрайты
    show dv rage pioneer:
        xanchor 0.5 xpos 0.25
    show sl normal pioneer:
        xanchor 0.5 xpos 0.75
    with move

    sl "О, привет, {i}Алиса{/i}!"
    sl "Как дела?"

    window auto

Прочитал комментарии? Я не буду пояснять про особенности игры, так как статья не об этом. Здесь мы обратим внимание позиционирование изображений, стилизацию текста и музыку.

Для проигрывания музыки и звуков существуют play music и play sound соответственно. Они принимают либо путь к файлу, либо список путей к файлам (в данном случае все пути к музыке из игры расположены в специальном списке, из которого мы берём конкретный). Для остановки используются stop music и stop sound, соответственно.

Музыка и звуки воспроизводятся на разных каналах, громкость которых игрок может регулировать в меню по отдельности. Ну а ещё звуки выполняются один раз, а музыка зацикливается. Разработчик же может регулировать не только громкость, но и скорость воспроизведения, баланс в колонках и многое другое. В том числе, можно создавать свои собственные каналы и играть несколько звуков одновременно.

Расположение спрайтов задаётся при показе либо с помощью at, либо в виде отдельного блока. В первом случае указывается предопределённое расположение (left или right, например), а во втором используется специальный язык ATL. Подробнее о нём можно прочитать в документации, а я лишь опишу базовое позиционирование, о котором также можно посмотреть наглядно в туториале, прилагаемом к Ren'Py SDK.

Положение изображений по обеим координатам задаётся с помощью двух точек: одной на изображении, называемой якорем (anchor), и второй на экране (pos). Задаётся либо с помощью целого числа, обозначающего координаты относительно верхнего левого угла экрана, либо с помощью дробного числа от нуля до единицы, обозначающего долю экрана, как сделано в примере.

Стилизация текста. Текст может содержать специальные теги, показывающие, как он должен отображаться. Для знакомых с языком HTML тут будет совсем просто: чаще всего используются точно такие же теги: {b}, {i}, {u}, {a} (для ссылок). Хотя есть и различия: {size=+5}. Просто. Также в тексте в квадратных скобках могут использоваться подстановки из переменных, определённых в коде. Если же есть необходимость использовать в тексте сами символы фигурных и квадратных скобках, но их нужно продублировать: {{, }}, [[, ]].

Внизу на картинке приведён результат. Неплохо получилось, не так ли?

Вот что у нас получилось

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

Удачи в творческих начинаниях!