Примерно год назад мы с товарищем задумали сделать небольшую текстовую игру приблизительно в духе Sunless Sea и 80 days: про мореплавание, торговлю, исследование странных поселений и общение со странными личностями. Там должна была фигурировать религия, а лучше несколько, главного героя хотелось видеть не спасителем, героем страны и прославленным мореходом, а умеренно неудачливым предпринимателем/авантюристом, до которого и дела никому нет, а модный выбор между меньшим и большим злом заменить на выбор между добром и добром: никакого набившего оскомину гримдарка ради гримдарка. Довольно быстро придумались основные фракции и персонажи, крупные порты, политическая обстановка и куча симпатичных мелочей вроде подводной охоты на осьминогов (изображена на КДПВ) и гениальной идеи дать почти всем персонажам венгерские имена, которые звучат экзотичней привычных европейских и вызывают некоторую неявную симпатию. В общем, деревянных домиков понабигало немало.
В команде у нас на тот момент был один писатель и один программист (то есть я). Требования в предыдущем абзаце относятся скорее к сетингу и духу игры, так что исполнять их должен был мой товарищ, а передо мной встали вопросы геймдизайна и функциональности движка. Во-первых, большую часть времени игрок будет тратить, читая текст и выбирая действия главного героя. Для этого нужна только сносная типографика и возможность писать сценарий с меню, опциями и переменными. Вскоре подключилась художница, так что надо было думать ещё и об иллюстрациях. Во-вторых, игра про исследования и торговлю, поэтому нужно где-то в доступном игроку виде хранить информацию о собранных слухах и купленных товарах (а также всячески её обрабатывать). И, наконец, в игре про мореходство нужна карта и возможность по ней перемещаться; просто команда “поплыть к тартарам и послушать сказки морских лошадей” явно не соответствует духу проекта. Значит, движок должен ещё и поддерживать хотя бы несложные мини-игры, а не ограничиваться только показом текста и обсчётом игровых переменных.
Почему Ren'Py
Сразу скажу, что писать движок с нуля мы даже не пытались: велосипедостроение увлекательно само по себе, но малоэффективно, если стоит цель всё-таки выпустить игру до выхода на пенсию. Также мы не рассматривали парсерную Interactive Fiction: у неё и на английском языке очень небольшая аудитория, а на русском наш проект, будь он парсерным, мог бы заинтересовать в лучшем случае несколько сот человек. А хочется если не заработать денег, то хотя бы пройти гринлайт и набрать какую-никакую репутацию. К счастью, большинство нынешних англоязычных разработчиков текстовых игр перешло от некоммерческих хобби-проектов к профессиональному геймдеву буквально несколько лет назад. Поэтому основные движки либо опенсорсны, либо, во всяком случае, бесплатны. Давайте посмотрим, что нам предлагают.
Первый вариант, пришедший мне в голову – Storynexus от Failbetter games, разработчиков Fallen London и Sunless Sea. Проекты на нём редактируются через браузер, хостятся Failbetter и через браузер же доступны игрокам. Возможности для монетизации с прошлого года удалили. Главный минус, однако, не в этом, а в том, что в Fallen London большая часть событий представлена картами, выпадающими из колоды, и сделать на Storynexus игру, не использующую эту метафору – задача нетривиальная. Да и вообще намертво привязывать свой проект к стороннему серверу с закрытым кодом, который теоретически может вообще прекратить работу в любой момент, довольно рискованно.
Есть ещё два хороших проприетарных движка для Choose Your Own Adventure, то есть игр примерно нашего типа: ChoiceScript и Inklewriter. Оба обещают прекрасную типографику, простоту разработки (браузерный редактор у Inklewriter, скриптовый язык у ChoiceScript) и возможность коммерческой публикации. К сожалению, оба позволяют делать только чистое CYOA: нет никакой возможности добавлять в игру что-то помимо собственно текста, меню и иллюстрациий. Внимательный читатель воскликнет: “Но как же так? В 80 days ведь был довольно сложный инвентарь и интерфейс путешествий, верно? А в Sorcery! я точно видел боёвку!” Увы, эти системы разрабатывались Inkle Studios под конкретные игры и в редакторе нет ни их, ни хоть какой-нибудь возможности сделать себе такие же. По той же причине (а также потому что он, эм, своеобразный) мы отказались от Twine.
Единственным устраивающим нас вариантом оказался Ren'Py. Это бесплатный опенсорсный движок для визуальных новелл (например, именно на нём сделаны “Бесконечное лето” и “Katawa shoujo”), который довольно легко настраивается для наших задач. Игры получаются кроссплатформенные: сборка дистрибутива под Win/Mac/Linux – вопрос нажатия одной кнопки, причём даже не надо иметь под рукой целевую ОС. Android и iOS также заявлены и Ren'Py-релизы под мобильные оси существуют, но мы сами пока на мобильный рынок не целимся и о разработке для него рассказать не можем. К тому же у Ren'Py очень дружелюбное и живое сообщество на русском и английском.
Простейший сценарий на Ren'Py
Ren'Py написан на Python 2.7 + Pygame и имеет собственный DSL. На этом языке, во-первых, за счёт команд типа “Показать bg_city_night_53.png в качестве фона без анимации” или “Произнести реплику «Cем… СЕМПАЙ!!!» от имени персонажа nyasha1” в императивном стиле пишется собственно сценарий. Во-вторых, подмножеством этого языка является Screen Language, на котором можно в декларативном стиле собирать из ограниченного набора Displayables (то есть виджетов: кнопок, изображений, текстовых полей и тому подобного) экраны и настраивать их функциональность. Если встроенных возможностей недостаточно, то с помощью Python можно добавлять собственные. Этим мы займёмся в следующей статье, а пока разберёмся со сценарием.
Сценарий в Ren'Py состоит из последовательности реплик, действий с экранами и ввода игрока. Про экраны и ввод чуть ниже, а для начала мы разберёмся с персонажами. В визуальной новелле они создаются так (код из официального туториала, с незначительными правками):
define m = Character('Me', color="#c8c8ff")
define s = Character('Sylvie', color="#c8ffc8")
image sylvie smile = "sylvie_smile.png"
label start
m "Um... will you..."
m "Will you be my artist for a visual novel?"
show sylvie smile
s "Sure, but what is a \"visual novel?\""
Создано два персонажа: протагонист и Сильви, оба пишут бледно-синим цветом в стандартное окошко внизу экрана. У Сильви к тому же есть портрет, который появится на экране перед тем, как она начнёт говорить. Выглядит это вот так:
Если бы мы создавали визуальную новеллу, то продолжали бы в том же духе, но мы-то не собираемся показывать портреты персонажей, да и иллюстраций пара десятков на всю игру. Большая часть текста вдобавок не является прямой речью персонажей, так что нелогично было бы привязывать её к кому-то из них. Лучше создадим виртуального персонажа-рассказчика:
define narrator = Character(None, kind = nvl, what_color="#000000", size = 12)
Его зовут narrator; это специальное имя, которое отдаёт ему весь текст, явно не аттрибутированный другим персонажам (строго говоря, его зовут None, а narrator, как и m и s в предыдущем примере – переменная, в которую помещается объект персонажа и из которой вызываются его методы, например, say) Аргумент kind принимает два значения: adv и nvl. Первое – это дефолтное поведение, описанное выше, а второе включает nvl-режим, в котором портреты не показываются, а текстовое поле занимает большую часть экрана. Как раз то, что нам было нужно. Этот режим описывается экраном nvl_screen в файле screens.rpy и группой стилей styles.nvl* (файлы screens.rpy и options.rpy соответственно), в которых мы зададим шрифт, фон текстового поля, цвет меню и всё остальное.
label start:
image bg monet_palace_image = Image('images/1129_monet_palace.jpg', align=(0 .5, 0.5))
nvl clear
hide screen nvl
scene bg monet_palace_image
$ Ren'Py.pause(None)
" — Я всегда говорил: твои песенки — дерьмо, Люсьен, и я не понимаю, где ты только находишь музыкантов, согласных это исполнять!"
Разберём построчно: сперва объявляется ярлык start, с которого начнётся игра. Это название зарезервировано и движок всегда будет переходить на него после нажатия кнопки “Новая игра”, где бы в сценарии он ни находился. Всё, что следует за ярлыком, логически находится “внутри” этого ярлыка, поэтому выделяется индентацией: она в Ren'Py работает так же, как и в чистом питоне. Инициализация картинки достаточно очевидна, а вот следующая строчка делает важную вещь: убирает весь текст с экрана nvl_screen. Автоматически это не делается, поэтому, если не расставлять nvl clear в конце каждой страницы, текст спокойно уползёт за пределы экрана и будет выводиться туда, пока экран не будет наконец очищен. Вроде бы мелочь, но на отладку пропущенных nvl clear я потратил намного больше времени, чем готов признать. Свежевымытый экран мы временно уберём, чтобы позволить игроку полюбоваться фоном, покажем фон, включим бесконечную паузу (то есть дождёмся клика) и начнём историю. Как только на nvl_screen начнёт выводиться текст, экран сам вернётся на место.
Строка с паузой, кстати, уже на питоне: для включения единичной строки её достаточно начать с '$', а более длинные куски кода нужно писать внутри блока 'python:'. Любой код, исполняемый игрой, видит модули самого Ren'Py и явно импортировать их уже не нужно.
Добавляем ветвление и переменные
К этому моменту игра представляет собой читалку, которая показывает текст, меняя при необходимости фоны. Сохранение, перемотка, главное меню и настройки уже работают из коробки. Однако если бы мы хотели написать иллюстрированную повесть, то мы бы её и написали, верно? Добавим перед текстом небольшое меню:
label start:
menu:
"Зайти в меню разнообразного дебага":
$ debug_mode = True
jump debug_menu
"Пропустить вступление":
jump the_very_start_lazlo_nooptions
"Начать вступление":
label the_very_start:
#show screen nvl
nvl clear
hide screen nvl
scene bg monet_palace_image
$ Ren'Py.pause(None)
" — Я всегда говорил: твои песенки — дерьмо, Люсьен, и я не понимаю, где ты только находишь музыкантов, согласных это исполнять!"
Теперь после включения игры пользователь (или, скорее, разработчик) сможет при желании войти в режим дебага или пропустить уже готовый кусок вступления и начать тестировать сразу кусок из последнего коммита. Строка show screen nvl закомменчена за ненадобностью – как я уже упоминал выше, экран покажется сам собой, когда на нём обновится текст. Комменты, как видите, работают абсолютно очевидным образом.
Ярлыки, меню и другие индентированные блоки могут быть вложены до произвольной глубины, но на практике мы стараемся дробить текст на эпизоды в десяток страниц. Каждый такой эпизод описан внутри отдельного ярлыка с нулевой индентацией (он уже не обязан быть внутри ярлыка start или даже в одном с ним файле), а переходы из одного эпизода в другой осуществляются прыжками. Так мы не только боремся с десятками уровней индентации, но и обеспечиваем модульность кода: каждый эпизод может тестироваться отдельно и довольно несложно проверить, какие переменные он читает, в какие пишет и куда позволяет перейти.
Внутриигровые меню и переменные устроены абсолютно так же. Поскольку и переменных, и ярлыков даже в небольшом эпизоде на десять минут игры разводится невероятное количество, мы приняли несложный вариант венгерской нотации: имя ярлыка 'the_very_start_lazlo_nooptions' состоит из трёх частей: названия локации the_very_start (то есть период от начала игры до первого выхода в море), названия эпизода lazlo (то есть пьянка у Лазло, на которой можно нанять молодых бездельников в матросы) и имени собственно ярлыка. При таком подходе имена получаются достаточно громоздкими, но лучше так, чем обнаружить при тестировании, что три месяца назад кто-то уже создал переменную ship_listing, выставил True бог весть где и теперь крен из одного случайного события влияет на исход другого случайного события на другом конце моря.
Вместо заключения
К этому моменту мы уже воспроизвели на Ren'Py функционал упоминавшихся выше Choicescript и inklewriter. Вроде бы наш кораблик готов к отплытию. В следующей статье я покажу, как можно создавать более сложный интерфейс с использованием экранного языка RenPy и ещё более сложный — на чистом питоне.
Комментарии (31)
napa3um
16.06.2016 20:06Ссылка на GitHub игрушки не открывается, исправьте, пожалуйста.
Karamax
17.06.2016 02:26Извините, но где вы видели ссылку? Пять раз пересмотрел-не смог найти. А хочется. Кажется, Весчь.
synedra
17.06.2016 04:34Ссылки нет, потому что самопиар запрещён правилами песочницы и я бы с ней, скорее всего, не прошёл модерацию. Ссылка на мой гитхаб есть в аккаунте, но игра ещё не готова даже к бета-релизу.
HedgeSky
17.06.2016 13:47Я тоже искал ссылку на гитхаб, но не смог найти. В вашем профиле, насколько я могу судить, её нет.
synedra
17.06.2016 15:28Странно, вроде ставил. Репозиторий игры тут, но серьёзно, там ещё и десятой части необходимого нет и вместо большей части веток тупые шуточки. Для запуска вам понадобится установить ренпай.
Amareis
16.06.2016 20:20Странно что ренпай не предоставляет пространства имён для всех этих переменных. А вообще, выглядит очень даже неплохо, недаром этот движок столь популярен в соответствующих кругах.
Alexey2005
16.06.2016 20:52+2Вот чем мне не нравится большинство движков для текстовых игр — при разработке пальцы уже через пару часов намертво прирастают к Alt+Shift. Только и делаешь, что переключаешь раскладку туда-сюда, т.к. английский код перемешан с русским текстом и спецсимволами, которые вводятся только в английской раскладке. Задалбывает неимоверно.
Zawullon
16.06.2016 21:07Renpy позволяет очень легко делать локализацию. Вполне можно писать на английском, потом в отдельных файлах делать перевод. Только этим, почему-то, никто не пользуется.
DeXPeriX
16.06.2016 23:07+1А мне здесь скорее мешала Windows, которая по умолчанию не позволяет поставить переключение раскладки на Capslock…
Sirion
16.06.2016 22:25+5Как сделать кроссплатформенную текстовую игру на русском с иллюстрациями, звуком, работающими сохранениями, без проблем с кириллицей, и с каким-никаким геймплеем?
На js, очевидно же.
Заголовок спойлераИли на чистом html, если обрезать последний пункт. Для простой машины состояний набора страниц без скриптов вполне достаточно.Sirion
16.06.2016 23:09+3Я не хочу устраивать холивар, я хочу указать, что описываемые фичи какие-то… смешные. В браузере они поддерживаются из коробки либо реализуются с минимальными усилиями.
synedra
17.06.2016 04:27А я и не спорю, это же первая статья из серии. В следующей части будет немного геймплея, ради которого мы ренпай и выбрали. Люди на нём, например, аж целую пошаговую стратегию делали.
MonkAlex
17.06.2016 08:47Всмысле, серьезно, вы предлагаете писать игру на js?
Забить на существующие готовые решения, в которых авторы уже набили за вас шишки и сообщество имеет опыт работы с типовыми кейсами?napa3um
17.06.2016 09:34+1Не стоит забивать на готовые и отлаженные решения, конечно: https://html5gameengine.com/
MonkAlex
17.06.2016 09:45Ни одну из указанных игр не видел, а вот на ренпай — полно. Может я не в то играю, конечно…
Oreolek
17.06.2016 10:07Есть специальные движки для текстовых игр. Для тех, кто хотя бы немного знает веб-разработку, есть Undum, Raconteur и Salet. Для тех, кто не хочет заморачиваться — Twine и русская AXMA Story Maker.
Если сесть и разобраться, то Twine рулит. Особенно как консольный компилятор, потому что визуальный редактор будет тормозить на масштабных разработках (консольная и визуальная версии очень плохо совместимы). Вы можете выбрать любой из кучи форматов (на вики перечислены только три, на самом деле форматов больше) и быстро набросать всё что захочется. Но документация сильно размыта, а вики в основном рассказывает про Twine 1. Правда, недавно вышел учебник-справочник.k12th
17.06.2016 11:09It's more a game you can hack into your game. (Like Undum, yeah.)
Спасибо, нет:)
Sirion
17.06.2016 11:44Бывают случаи, когда написание велосипеда имеет смысл. В частности, когда велосипед небольшой, а контроля над его возможностями хочется полного. Вопрос в том, насколько объёмным был бы движок текстовой игры на js. Мне кажется, что совсем не объёмным. Но я могу ошибаться.
k12th
16.06.2016 23:01Оффтопик: Twine правда очень своебразный, хотя хорошие идеи там есть.Оунер тоже очень своебразный. Я пытался туда контрибутить, но это было похоже на плаванье против сильного течения. Баг-репорты и фиче-реквесты по большей части пропускаются мимо ушей. Предложение сделать серверное и/или облачное хранение с подробным обоснованием, зачем это нужно, было просто проигнорировано. В итоге я сдался и сейчас делаю свой редактор, совместимый (для начала) по форматам.
Tallefer
17.06.2016 06:43+2Как сделать кроссплатформенную текстовую игру на русском с иллюстрациями, звуком, работающими сохранениями, без проблем с кириллицей, и с каким-никаким геймплеем?
Ребят, но… но как же… INSTEAD, а? -_-
Побуду мимокрокодил-евангелистом немногоКроссплатформенность, говорите? Вон там выше помянули js, так вот, Инстед основан на SDL и Lua и потому легко может работать даже на КПК с 2003 карманной виндой и телефонах на симбиане, где уже давно никаким современным браузерным фичам не хватит ресурсов. :) Но он также может работать и в браузере через js, если вдруг понадобится, для этого пилится отдельный проект.
Геймплей? Посредством того же Lua на движке без проблем сделаны довольно быстрые аркады, к примеру — клон Boulder Dash.
Для коммерческих релизов встроена простенькая обфускация кода, но можно и самостоятельно откомпилить вообще весь движок с ресурсами, если приспичит, благо опенсорс. Уже несколько игрушек успешно прошли так стим-гринлайт.
Комьюнити хоть и маленькое, даже камерное, но доброе. Имеется репозиторий с играми на этом движке. На сайте есть все нужные контакты, а для непосредственного общения заходите в джаббер-конференцию instead@conference.jabber.ru
И спасибо, статья хорошая, чужой опыт всегда интересен и полезен, продолжайте писать! :)
synedra
17.06.2016 06:52Правда ваша, INSTEAD мы незаслуженно упустили из виду. А гринлайт часом не Вы сами проходили? Я бы с удовольствием почитал про подводные камни, тем более что для текстовой игры они должны быть довольно специфичными.
Tallefer
17.06.2016 14:38Нет, не я, к сожалению. Автор одной игры — Антоколос, другую забыл. Я не уверен, можно ли давать ссылки на стим, поэтому снова напомню про конфу, он там иногда бывает. :)
Alexey2005
17.06.2016 11:55Интересно, а среди текстовых движков есть такие, которые позволяют учитывать разрешение экрана и DPI? Чтоб если игра запущена на десктопе, подгрузить один UI, если на мобильном 3.5" экранчике — другой, а также учитывать разворот экрана, сразу перекомпоновывая UI.
К сожалению, большинство текстовых движков предполагает, что их запускают на большом экране, и потому их создатели даже не задумывались о динамическом интерфейсе или особенностях компоновки. В итоге после портирования на мобильные устройства такие игрушки выглядят откровенно хреново (например, микроскопический шрифт или всё за пределы экрана вылезает).k12th
17.06.2016 12:19С этой стороны web-based движки хороши тем, что сделать адаптивный интерфейс относительно просто, все для этого есть из коробки и не требуется эзотерических познаний особенностей данного движка (достаточно вменяемого верстальщика).
Tallefer
17.06.2016 14:46В Инстеде последние полгода-год велась работа над этим.
Есть несколько режимов, которые автор игры может прописать сам и получить либо менее совместимую с разными девайсами игру, но более заточенную под конкретный экран, либо использовать дефолтный универсальный и почти резиновый режим масштабирования, который страдает в пограничных случаях (ультра-широкие экраны).
tmnhy
Вы так хорошо начали с «функциональностью» и все-таки в «Вместо заключения» проскочил неправильный «функционал»…
Где поиграть?