Истории

Статья: Пишем текстовую игру для браузера [Константин "Hogart" Китманов]

Пишем текстовую игру для браузера

Константин "Hogart" Китманов решил поддержать КОНТИГР 2019 и написать статью, которая является одновременно кратким обзором существующих платформ и движков, позволяющих делать текстовые игры, запускающиеся в браузере -- и познавательным пошаговым обучением работе в одной из них.

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

Статистика показывает, что игроки куда охотнее запускают игры, доступные через браузер. К счастью, в 2019 году для этого имеется богатый выбор инструментов. Плееры для классических форматов типа urqw и instead-em.js мы сейчас рассматривать не будем, а обратимся к более современным образчикам IF-остроительной мысли. Я вкратце освещу наиболее заметных и интересных претендентов, а потом я расскажу об одном из них подробнее.
 

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

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


AXMA Story Maker
Экспорт: только в платной версии.
Визуальный редактор: да.
Подсветка синтаксиса: да.
Кастомизация: обширная. Есть встроенные темы, которые можно в определенных пределах подтюнить под свои нужды. Богатое форматирование текста.
Особенности: Картинки и музыку можно загружать прямо в онлайн-редакторе. Движок не дает игроку копировать текст игры (без дополнительных ухищрений). Есть онлайн- и офлайн-версия (для Win, Linux и Mac)


Ink (только английский)
Экспорт: да (онлайновой версии нет).
Визуальный редактор: нет.
Подсветка синтаксиса: да.
Кастомизация: из коробки работают только HTML-тэги, но теоретически неограниченная.
Особенности: есть только офлайн-редактор, зато он обеспечивает обновление игры "на лету" по мере редактирования. Выразительный мощный язык. Позволяет разделять историю на несколько файлов. Экспорт возможен не только в html, но и в Unity3D. Де-факто индустриальный стандарт для инди-разработки с упором на текст и нарратив.


InStory
Экспорт: только в качестве резервной копии.
Визуальный редактор: да.
Подсветка синтаксиса: нет.
Кастомизация: фоновые изображения, фоновая музыка, базовое форматирование.
Особенности: приятный визуальный редактор, напоминающий Blueprint в Unreal Engine. Переменные только трёх типов (строки, числа, булевые). Выбор игрока может осуществляться как гиперссылками, так и вводом текста.


Митрил (КвестБук/Сторигейм)
Экспорт: только в PDF (интерактивный и пригодный для печати).
Визуальный редактор: да (граф связей между параграфами).
Подсветка синтаксиса: нет.
Кастомизация: иллюстрации к параграфу, фоновая музыка (нужно создать плейлист на Яндекс.Музыке), базовое форматирование (жирный, курсив, подчеркивание и надстрочный индекс — подстрочного при этом нет).
Особенности: совместное редактирование истории. Игроки могут отправлять баг-репорты, которые автор видит прямо в редакторе. Бизнес-логика строится на булевых ("ключевые слова") и целочисленных ("ресурсы") переменных. Встроенный механизм для чекпойнтов/автосохранений (но не более 5 на игру) и достижений (не более 3).


Kvester
Экспорт: нет.
Визуальный редактор: да (граф связей между параграфами).
Подсветка синтаксиса: нет.
Кастомизация: цвет фона и текста, фоновая картинка.
Особенности: для встраивания музыки используется почивший в бозе prostopleer.com. Встроенный в движок инвентарь, а так же счетчик и таймеры.


Texture (только английский)
Экспорт: да.
Визуальный редактор: да (граф связей между параграфами).
Подсветка синтаксиса: нет.
Кастомизация: ограниченная. Поддерживается вставка картинок, очень базовое форматирование, выбор темы оформления из списка.
Особенности: уникальная механика перетаскивания действий на слова в основном тексте. Бизнес-логика возможна только самая примитивная — переменные только булевого типа.


Twine
Экспорт: да.
Визуальный редактор: да.
Подсветка синтаксиса: зависит от выбранного формата.
Кастомизация: практически неограниченная.
Особенности: есть онлайн- и офлайн-версия (для Win, Linux и Mac). Изображения и музыка поддерживаются, но их нужно либо размещать в интернете, либо публиковать вместе с игрой. Богатая экосистема. Открытые исходники со свободной лицензией.


Twine

Удачно, что он оказался последним в списке — именно о нём я бы хотел поговорить.
Первое, что стоит отметить:Twine — это экосистема, состоящая из как минимум 2 редакторов, 2 компиляторов и 3 форматов. Компиляторы — это отдельная тема для продвинутых пользователей, а из двух редакторов один давно устарел. Что касается форматов, то именно формат отвечает за синтаксис (хотя у всех трех есть общие элементы), доступные возможности и за то, как игра будет выглядеть "из коробки".


Формат по умолчанию — Harlowe, и только для него работает подсветка синтаксиса в редакторе. Он считается самым для дружелюбным для, политкорреткорректно выражаясь, правополушарных авторов. Зато он и самый негибкий, расширить его новыми возможностями нельзя.
Второй формат — Snowman — нарочито минималистичен и из коробки умеет только работать с ссылками. Всё остальное придется добавлять самому.
Третий формат это SugarCube — он одновременно самый богатый "из коробки" и самый расширяемый. Не стоит его бояться, он не сложнее Harlowe. Говорить я буду именно о нём.


Современный редактор Twine существует в виде web-приложения и точно такого же приложения, завернутого в инсталлятор для вашей ОС. По возможностям они совершенно одинаковые, за тем исключением, что web-версия хранит ваши данные в хранилище браузера, а десктопная — на жестком диске. Обе версии вынуждают делать ручные бэкапы в облачные хранилища, если надо перенести работу на другой компьютер. Зато в десктопной версии отладка менее удобна.


Итак, чем же хорош Twine сам по себе?

  • Визуальный редактор позволяет автору наглядно представлять поток игры. Сразу видно географию, тупиковые ветви и т.п.
  • Файл с игрой одновременно является исходником. Если послать кому-то игру, он может безо всяких проблем открыть ее в редакторе и внести изменения.
  • Построен на основе веб-технологий — не нужны никакие сакральные знания потаенных уголков эзотерических языков — html/css/js актуальны уже давно, останутся актуальными еще долго и имеют массу учебников и справочников.
  • Официальная книга рецептов.
  • Живое сообщество.
Чем хорош SugarCube?
  • Мощный движок с поддержкой ручных сохранений, чекпойнтов, и undo/redo истории.
  • Большое количество встроенных макросов: контроль потока (if..else, циклы, перенаправление на другой параграф и т.д.), управление звуком, низкоуровневая манипуляция DOM-деревом, таймеры, пользовательский ввод.
  • Относительная легкость создания своих макросов.
  • Простой синтаксис, понятный всякому, кто редактировал википедию или писал Markdown.
  • Удобный отладочный режим.
Попробуем сделать несложную игру
 
Традиционно в ифне как Hello, world! используется Cloak of Darkness, но я уже делал про неё видео и повторяться не хочется. Поэтому мы будем делать продолжение "Хайди" — про её брата Фредди (игру придумал Виталий Блинов).
По структуре это получается классическая книга-игра с ссылками после текста, поэтому, не мудрствуя лукаво, так и сделаем.



  1. Открываем Twine: http://twinery.org/2/#!/stories. Это список наших игр.
  2. В правой колонке нажимаем кнопку "Форматы". В появившемся окне выбираем "SugarCube 2.28.2" (на момент написания это последняя версия; она может поменяться, но соль в том, что нам нужна версия 2.x)
  3. Жмем большую зеленую кнопку "+ История". Twine создаст новую историю с одним пустым параграфом и откроет её.
  4. Переименуем этот параграф в "1". Вообще имеет смысл давать параграфам читабельные названия, т.к. они отображаются в автосохранениях, но у нас их не будет, да и игра небольшая.
  5. Скопируем текст вступления в этот параграф. (Пока что пропустим переменные, вернемся к ним чуть-чуть позже.) Хмм, вообще-то этот текст игрок должен увидеть только один раз. В текущей версии игры мы никогда не возвращаемся на этот параграф, но что, если в будущем это поменяется? Чтобы показать текст только при выполнении какого-то условия, нам нужно заключить его в макрос <<if><</if>>. В качестве условия используем счетчик посещений этого параграфа: функцию visited(). Эта функция, если вызвать её без аргументов, возвращает количество посещений текущего параграфа. Должно получиться вот так:
  6. Скопируем туда же текст первого параграфа. Ссылки на другие параграфы в Twine делаются просто (и одинаково во всех форматах): [[текст ссылки|название_пассажа]] (или так: [[текст ссылки->название_пассажа]], или даже так: [[название_пассажа<-текст ссылки]] — "стрелочка" показывает в ту сторону, где написано название целевого параграфа). Однако первая же ссылка у нас не только переводит игрока на другой параграф, но и меняет переменную. Специально для этого предусмотрен особенный синтаксис: [[текст ссылки|название_пассажа][$myVar = 'value']]. К сожалению, SugarCube не позволяет именовать переменные кириллицей, поэтому назовем её $onTheTree.
  7. Закроем параграф. Можно заметить, что Twine автоматически создал два новых параграфа — 2 и 3; мы вернемся к ним позже. Внизу справа находится кнопка "Запустить" — она открывает нашу игру в новой вкладке. Давайте посмотрим, что у нас получилось.
    Тут можно заметить, что SugarCube заменил наш двойной минус на красивое длинное тире.
  8. Вернемся к переменным. Теоретически, объявлять/инициализировать их не обязательно, но если этого не делать, то рано или поздно вы наткнетесь на какой-то баг и будете долго его искать — поверьте моему опыту. Для инициализации переменных есть специальное место — параграф со служебным именем StoryInit. Он не показывается игроку, но движок выполняет его перед запуском игры. Для присвоения переменной значения используется специальный макрос <<set $myVar = 'value'>> (обратите внимание, у него нет закрывающей пары). Кстати, вместо знака = можно использовать служебное слово to. В одном макросе можно присвоить несколько переменных через запятую. В правом нижнем углу находится большая зеленая кнопка "+ Параграф", нажмем ее и переименуем новый параграф в StoryInit. Впишем туда макрос <<set>> и внутри него — наши переменные. Вот так:
  9. Итак, редактор услужливо создал нам пустые параграфы по именам, взятым из наших ссылок и нарисовал между ними стрелочки. Скопируем в них текст. Как выводить текст по условию, мы уже знаем, но теперь нам нужно ветвление типа если … иначе .... Внутри макроса <<if>> можно использовать <<else>> и <<elseif>>. Главное, не забыть закрывающий <</if>> в конце. Вторая новинка заключается в том, что в ссылке можно присвоить значение нескольким переменным — как в <<set>>, через запятую.
  10. Продолжаем переносить игру из текста в Twine, на этом этапе никаких затруднений у вас возникнуть не должно. Не забывайте время от времени запускать игру и проверять, как она работает — если вы забудете закрыть какой-то макрос, то SugarCube покажет место ошибки. Если игра работает неправильно, то запустите её в режиме отладки с помощью кнопки "Тестировать". В этом режиме SugarCube позволяет прыгать между ходами (1), отслеживать значения переменных (2) и показывать скрытые макросы и их параметры (3, по наведению мыши).
  11. Вы можете скачать готовую игру по ссылке (файл "Фредди.html"), и, импортировав её из списка историй, сравнить со своей версией.
Наводим красоту и добавляем свистелки
 
Пишем виджет
Мне не очень нравится, как заканчивается наша игра, простым словом "конец". Не впечатляет.


Давайте увеличим в этом слове размер шрифта и выровняем по центру. Откроем пассаж 7 и заключим слово "КОНЕЦ" в html-тэг h1, и еще добавим немного стилей. Вот так:
<h1 style="text-align: center; margin-top: 2em">КОНЕЦ</h1>
Правильнее было бы использовать css-классы, и если вы знаете, о чем речь, воспользуйтесь этим.


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

В терминологии SugarCube виджет — это простенький кастомный макрос (так же можно делать свои полноценные макросы, но это требует как минимум начальных познаний в JS). Создавать их следует в отдельном параграфе, который помечен тегом widget. Итак, создадим новый параграф — назвать его можно как угодно, но я предпочитаю давать имена вида widget-имя_виджета, и добавим ему требуемый тэг. В самом же параграфе напишем такой код:
<<widget "theEnd">>
    <h1 style="text-align: center; margin-top: 2em;">КОНЕЦ</h1>
<</widget>>

Использовать мы его будем вот так: <<theEnd>>.
 

Чтобы разобраться, плохая это концовка или хорошая, мы будем передавать в наш виджет один аргумент — слово good или bad. А внутри кода виджета это значение будет доступно нам в переменной $args[0] (специальная переменная $args — это массив, и содержит все аргументы, которые мы передаем в виджет).
На этом этапе у вас уже должно хватать знаний, чтобы вывести разный текст с помощью макроса <<if>>, но я буду менять не текст, а его цвет.
<<set _color = $args[0] === 'bad' ? 'red' : 'green'>>
<<set _theEndStyle = 'text-align: center; margin-top: 2em; color: ' + _color>>

 

До сих пор мы видели только переменные, начинающиеся с $ — они были глобальными. С _ начинаются локальные переменные — они доступны только внутри того параграфа, где объявлены. Инициализировать в StoryInit их не надо.
Содержимое макроса <<set>> интерпретируется как JS, чем я беззастенчиво и пользуюсь. В первой строке я использую тернарный оператор, а во второй просто складываю две строки, чтобы получить желаемый стиль. Остается только вставить этот стиль в наш h1. Для этого есть специальный синтаксис (см. раздел Attribute Directive):
<h1 @style="_theEndStyle">КОНЕЦ</h1>
Все вместе выглядит вот так:

 
Теперь надо заглянуть в параграфы 2, 5 и 7 и поменять слово КОНЕЦ на <<theEnd good>> или <<theEnd bad>>.

Чтобы сразу проверить, как это выглядит, кликнем на кнопку "Тестовая история начинается здесь" из выпадающего меню параграфа 7. Если вас смущает слишком большой вертикальный отступ перед словом "КОНЕЦ", то убрать его можно, добавив обратный слэш в конце строчек с <<set>>.


Для вящей драматичности добавим небольшой таймер:
<<timed 1s t8n>><h1 @style="_theEndStyle">КОНЕЦ</h1><<timed>>
t8n — сокращение от transition, с этим параметром текст появится плавно.
Домашнее задание: выводить разный текст в зависимости от концовки. Задание «со звездочкой»: переписать со встроенных стилей на CSS-классы и этим упростить код виджета.


Локализация

Сильно бросается в глаза, что игра на русском, а интерфейс на английском. Это легко исправить: качаем архив с русской локализацией с официального сайта (из раздела "Localization"), распаковываем, открываем единственный файл текстовым редактором и копируем его содержимое. В Twine кликаем на выпадающее меню в левом нижнем углу (где написано название нашей игры) и кликаем на "Редактировать JavaScript". В появившемся окне вставляем скопированный код. Как вариант, можно скопировать код в человекочитаемом виде из моего репозитория.
 

Светлая тема
Темная тема — это сейчас модно, но не всегда и не всем удобно. Пощадим глаза наших игроков и предоставим возможность переключаться между ночным и дневным режимом по желанию. Для этого нам понадобится скопировать три файла: menuButton.js, daymode.js и daymode.css. JS копируем туда же, куда копировали локализацию, а редактирование CSS находится в том же меню, в пункте "Редактировать таблицу стилей". Теперь при запуске игры в левом меню должна появиться кнопка "Day mode". Чтобы надпись была по-русски, надо в скрипты вписать следующую строчку:
l10nStrings.uiBarNightMode = 'Дневной режим';
Её надо вписать после того места, где мы вставили локализационный код, но перед тем, где мы вставили содержимое daymode.js.


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

При выборе шрифта справа внизу появится окошечко. В первую очередь надо зайти во вкладку Customize и поставить галочку на Cyrillic.
Там же, во вкладке Embed —> @IMPORT нужно скопировать код между тэгами <style></style>. У меня вышло так:
@import url('https://fonts.googleapis.com/css?family=Merriweather&subset=cyrillic');
Полученный код нужно вставить в стили нашей истории.
 

Возвращаемся на Google Fonts и оттуда же копируем следующий кусок кода — тот, который начинается с font-family.
Теперь надо указать, где именно мы используем этот шрифт. Если мы хотим поменять шрифт везде, включая интерфейс, то в стили надо вписать следующее:
body {
    font-family: 'Merriweather', serif;
}

А если только в тексте самой игры, то вот так:
#story {
    font-family: 'Merriweather', serif;
}

Можно дать пользователю возможность менять размер шрифта (при простом масштабировании верстка страницы разъезжается). Для этого в скрипты нужно добавить файл fontSize.js (а перед ним — menuButton.js, если вы пропустили предыдущий шаг), а потом вставить строчку:
scUtils.createFontSizeBtn();
Домашнее задание: подобрать читабельный шрифт.

Можете скачать готовую игру (файл Фредди 2.0.html) и сравнить с тем, что получилось у вас.


FAQ

 
«Но… я хочу красивую игру. Как все-таки вставить картинки?»
Есть несколько вариантов.

  1. Картинка должна быть доступна в интернете по абсолютному URL. Удобно тем, что работать будет везде, где есть интернет.
  2. Использовать SVG. Это векторный формат изображений, что идеально подходит для иконок, схем и т.п., к тому же векторные картинки отлично масштабируются под любой размер. SVG предназначен для веба и встраивается в html. Множество иконок в едином стиле можно найти на game-icons.net. Полученное изображение нужно открыть любым текстовым редактором (Блокнот, Notepad++, VS Code, и т.д.) и скопировать его код в Twine (я советую создать отдельный пассаж для каждого изображения и <<include>>-ить его в нужном месте).
  3. По инструкции перекодировать картинку в base64 и результат вставить в пассаж, помеченный тэгом Twine.image. Таким же образом можно встроить и аудио. У этого метода есть недостаток — медиа-файлы в base64 весят на 33% больше.
  4. Для некоторых игр могут подойти эмодзи.
  5. Редактировать игру без картинок, и проверять, как всё выглядит, запуская её локально.
«Где можно выложить игру?»
Если у вас один файл без внешних ресурсов, то самый простой и быстрый вариант — http://philome.la/. Если ваши амбиции простираются дальше, то в туториале по ink есть раздел про публикацию на itch.io. Этот вариант хорош наличием простенькой аналитики (сколько человек запустили вашу игру), плюс там можно вести девлог.
Если же вы хотите опубликоваться в Steam и/или мобильных сторах — вам поможет HTMLE.
 

«Слышал страшные истории. Как не потерять все наработки?»
В списке историй, в правой колонке есть кнопка "Архив". По ее нажатию вам предложат скачать html-файл, содержащий все ваши игры разом (они не будут запускаться, это просто архив). Этот файл потом можно загрузить в Twine и получить все игры обратно. Делайте бэкапы регулярно. Я каждую игру после редактирования сохраняю в облачное хранилище или коммичу в git-репозиторий.


«Я перфекционист, а Twine очень криво расставляет параграфы. Бесит! И хоткеев не хватает...»
Мне тоже, поэтому я сделал расширение для Google Chrome под названием Twine Enhancer, где постарался исправить этот и некоторые другие недочеты. С ним можно выровнять все параграфы точно по сетке и хоткеями запустить игру, открыть редактор JS и CSS и кое-что еще.


«Как мне сделать %название_фичи%?»
Посмотрите в официальную книгу рецептов, там описаны многие часто требуемые фичи. Не исключено даже, что нужное встроено прямо в SugarCube.


8 комментариев:

  1. Мне интересно, что в понимании Автора значит подсветка синтаксиса, если в Аперо ее нет. Или вопрос какого года статья?

    ОтветитьУдалить
    Ответы
    1. Спасибо за замечание. Связался с автором статьи, ошибка исправлена!

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

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Вы можете поставить кнопку внутрь тэга условного оператора if, и проверять там значение переменной, которое меняется при переходе на параграф по кнопке.
      Заходите в discord (https://discord.gg/X86kkzM), там есть отдельный канал по twine, где вам точно помогут.

      Удалить
  3. Добрый вечер. Может быть вы сможете мне подсказать, а то я сам никак не дойду.
    Чтобы не делать четыре локации и соответственно, четыре перехода, пытаюсь изобразить следующее:

    Надо вооружиться. Вы подошли к оружейной стойке, чтобы [[взять меч|выбор оружия]][(set: $sword to 1)] или [[взять копье|выбор оружия]][(set: $lance to 1)]

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

    ОтветитьУдалить
    Ответы
    1. Добрый вечер!
      Я спросил знатоков Twine, говорят, можно попробовать так:
      Надо вооружиться. Вы подошли к оружейной стойке, чтобы [взять меч]<sword| или [взять копье]<lance|.

      []<choice|
      (click: ?lance)[ (set: $lance to 1)(goto: "выбор оружия") ]
      (click: ?sword)[ (set: $sword to 1)(goto: "выбор оружия") ]

      Или просто использовать формат SugarCube.

      Вообще, заходите на discord-сервер интерактивной литературы на русском языке и спрашивайте: там есть целый канал по Twine, и именно там мне подсказали, что Вам ответить :)

      https://discord.gg/KT3m4nmvca

      Удалить
  4. Добрый день, можно ли сделать так, чтобы за определенный выбор начислялась единица предмета (например карма) и они все складывались? И в определенный момент случается выбор с проверкой переменной, если у тебя больше 5 единиц кармы, то будет дополнительный вариант ответа, а если нет, то не будет? Та же проверка if/else только более сложная

    ОтветитьУдалить