Вагон-Вагон. Клубок технологий внутри простой текстовой игры

История создания интерактивной новеллы для мобильных телефонов. Игра проста на вид, однако под капотом у неё нетипичное сочетание технологий, а разработка в режиме “по вечерам в свободное время” заняла почти два года.





“Два года? Правда? А кажется, тут дел на пару месяцев”, — примерно так реагировали друзья на рассказ о пути к релизу нашего дебютного проекта.

И действительно, что может быть сложного в игре с почти статичной графикой и с механикой “читаем текст, выбираем один из вариантов действия, получаем новую порцию текста”? Оказалось, много чего. Причём не только в творческой части, но и в технологической.

На технологии я и хочу сфокусироваться в этой статье. Для затравки приведу список: Ink, PyDoit, Phaser, Transcrypt, Jasmine, SikuliX, Language Tool, Apache Cordova, PhoneGap, ImageMagick. Все эти инструменты мы использовали в ходе разработки, причём большинство из них пришлось изучать с нуля.

Зачем они нужны, как и почему мы их выбирали, с какими трудностями при этом столкнулись, читайте далее. Но прежде, чтобы создать (или напомнить) контекст — краткое описание игры.

Описание игры
“Вагон-вагон” — это интерактивная дорожная новелла.

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

Наша игра
Наша игра.

Первые прототипы. Выбор Ink
Нас было двое, сценарист-программист — это я, и художник-музыкант, оба на полставки геймдизайнеры. Начинали с голой идеи.

Подумали однажды: а не сделать ли нам игру? Написали несколько концептов, долго их обсуждали. Сочинили общую канву – День Сурка встречает Grow Cube – зацикленный, меняющийся в зависимости от обстоятельств разговор двух героев. Захотели собрать простой текстовый прототип и проверить, насколько эта механика будет интересна людям. Опыты собирались ставить на друзьях.

Для создания протипа я решил использовать готовую платформу/движок для интерактивной литературы. Цель — одновременно писать художественный текст и выстраивать логику взаимодействия с игроком: сцены, переходы между сценами, деревья диалогов, динамические описания с параметрами.

Для себя я определил следующие критерии выбора платформы:
  1. Простота синтаксиса. Чтобы сделать прототип как можно быстрее.
  2. Простота сборки web-версии игры. Чтобы можно было выложить куда-нибудь страничку и показывать через браузер.
  3. Потенциал для сборки мобильной версии игры под iOS и Android. Это были основные платформы, на которые мы нацелились.
  4. Минимум зависимостей. В идеале я представлял простую библиотеку, которую можно подключить к популярному игровому движку. Например, к Godot или Unity.
  5. Лицензия должна позволять делать коммерческие продукты. Это очень важный пункт. К примеру, для меня было большой неожиданностью, что популярный Twine использует лицензию GPL, и при публикации игры на Twine вы обязаны опубликовать и исходный код.

Первым запустил Ink, давно чесались руки попробовать – на нём написаны одни из самых любимых моих игр последних лет: 80 Days и Sorcery. Язык родился как внутренний инструмент студии-разработчика, а потом перекочевал в Open-Source. При этом студия продолжает его поддерживать и развивать.

Начало игры с одним описанным выбором в редакторе Inky. Слева - текст с разметкой, справа - получившаяся web-страничка.
Начало игры с одним описанным выбором в редакторе Inky. Слева — текст с разметкой, справа — получившаяся web-страничка.

По всем критериям Ink подошёл идеально: синтаксис интуитивно понятен, web-страница без оформления собирается из коробки, есть плагин для популярного движка Unity, а лицензия максимально свободная – MIT.

Другие варианты я перебирать не стал. C точки зрения «взрослой» разработки это непрофессиональное решение. С альтернативами надо познакомиться хотя бы поверхностно, но тут случилась любовь с первой строчки текста, и я решил, что это судьба.

Самое сложное место - главный цикл игры.
Самое сложное место — главный цикл игры. Каждый “+” — это вариант выбора, который появляется у игрока в зависимости от разных условий. “->” — переход к сцене с блоком текста.

На тот момент мне казалось, что лучший вариант разработки под мобильные платформы – это интеграция с Unity, а легкая html5-версия – это сиюминутное удобство. Однако с течением времени именно это решение стало основным.

На практике убедился в верности слов Роберт Мартина из книги “Чистая архитектура”: “Встретившись с фреймворком, не торопитесь вступать с ним в союз. Посмотрите, есть ли возможность отложить решение. Если это возможно, удерживайте фреймворк за архитектурными границами. Может быть, вам удастся найти способ получить молоко, не покупая корову”.

Более сложные прототипы. Система автоматической сборки PyDoit
Первые прототипы друзьям понравились. Но и проблем было много. Начиная с опечаток, заканчивая тем, что многие не понимали, какая у игры цель, и что вообще происходит.

Мы наматывали на ус, постоянно делали правки в тексте и логике, выкатывали новые версии, тестировали на новых людях.

“Выкатывание” состояло из двух операций: сборка web-страницы и заливка на бесплатный хостинг через FTP. Повторять это вручную нам быстро надоело. И ошибиться было легко: залить старую страницу вместо новой.

Чтобы избавиться от рутины сделал скрипт, запускающий этот процесс из командной строки в одно действие. В основе скрипта – система автоматизации PyDoit.

Пример скрипта PyDoit из трёх шагов, с которого я начинал.
Пример скрипта из трёх шагов, с которого я начинал.

При выборе PyDoit сомнений не было. Использую эту систему уже лет восемь на самых разных проектах. Она умеет всё, что должна уметь хорошая система автоматизации: настраивать зависимость между шагами сборки, учитывать изменение или неизменность тех или иных исходных ресурсов (в моём случае это исходные файлы Ink), гибко модифицировать сборку по входным параметрам. Но больше всего мне нравится то, что под капотом обычный код на Python. Таким образом для заливки через FTP можно использовать стандартную питоновcкую библиотеку ftplib.

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

Полагаю, что любая другая система сборки тоже бы подошла. Главное, что она у нас просто была. И была с самого начала. Новые шаги сборки, вроде запуска юнит-тестов или проверки орфографии, оказалось добавлять в неё легко и естественно. Это сэкономило нам много времени и нервов.

Визуальный движок. Выбор Phaser
После многих экспериментов и отзывов от друзей мы убедились, что игра получается интересной. Тогда мы решили сделать для механики достойную обёртку, добавить графику и поучаствовать в КРИЛ-2017. Чтобы игру оценили игроки, которые понимают и любят текстовые игры.

Во многих платформах для создания интерактивной литературы поддержка графики встроена, но возможности её ограничены: делай либо по готовым шаблонам, либо — если хочешь что-то необычное — залезай в исходники платформы.

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

Теперь настало время подключить её к удобному визуальному движку.

Я стал экспериментировать с Unity, собрал тестовую болванку с подключенным плагином Ink. Однако web-версия даже для такой болванки много весила (~15 mb) и долго загружалась (~20 cекунд без учета загрузки ресурсов с сервера). А в конкурсе мы хотели участвовать именно с web-версией.

Так выглядел наш прототип на Unity
Так выглядел наш прототип на Unity (c графикой из тестового проекта для Ink)

При этом та же болванка с простейшим визуальным движком на JavaScript работала существенно быстрее и меньше весила. Но как быть с мобильной версией? Переписывать графику позже на Unity мне не хотелось. После изучения вопроса узнал, что html5-страничку можно превратить в мобильное или десктопное приложение с помощью фреймворка Apache Cordova.

Ещё как вариант движка под мобильные платформу рассматривал PyKivy на питоне. Рассчитывал, что портироваться с JavaScript на питон будет проще, чем на Unity со своей сложной инфраструктурой. Ну и писать игры на питоне – это давняя мечта, которую мне хотелось осуществить в собственном проекте.

В итоге для конкурсной версии решил остановиться на JavaScript-движке. Из большого списка возможных вариантов чуть ли не методом тыка выбрал Phaser. Методом тыка, потому что мне казалось это решение временным. Однако, как известно, нет ничего более постоянного, чем временное…

Простейший пример использования Phaser. Создаём картинку и выводим на экран, сколько раз игрок нажал на эту картинку.
Простейший пример использования Phaser. Создаём картинку и выводим на экран, сколько раз игрок нажал на эту картинку.

JavaScript мне пришлось изучать с нуля, но это оказалось не так сложно. Для моих целей хватило официальной документации Phaser и книги «JavaScript для детей».

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

Наш первый макет главного экрана. Отсюда брал первые болванки для экспериментов с Phaser.
Наш первый макет главного экрана. Отсюда брал первые болванки для экспериментов с Phaser.

В качестве итога этой части скажу: учиться на ходу – это нормально. Даже если ты ошибаешься, это расширяет кругозор. И учиться новому приятно.

Связь визуального и текстового движка. Transcrypt
Итак, у меня был текстовый прототип и визуальный движок. Теперь я хотел передать текст из Ink в Phaser и научиться настраивать внешний вид сцены, исходя из текста. Например, если я в исходном ink-файле пишу «[curtain_up]», то этот текст не должен показываться игроку, а вместо этого должен открываться занавес.

Пример исходного ink-файла с особыми командами и маркерами
Пример исходного ink-файла с особыми командами и маркерами.

В примере весь текст в двойных квадратных скобках — это команды, которые нужно особым образом обработать. Выражение “[[repeat_count_for_player]]” должно замениться на число попыток изменить судьбу в игре; “[[player_status]]” — это особый статус, описывающий прогресс игрока; “[[skippable]]” означает, что для этого текста доступен режим быстрой перемотки; “[[curtain_up]]”, “[[play_music]]”, “[[blink_light]]” — различные визуальные и аудиоэффекты.

Вот, как всё это выглядит в динамике, в игре
Вот, как всё это выглядит в динамике, в игре.

Для связи текстового и визуального движков я решил сделать некий управляющий слой, который назвал GameLogic. На этом слое исходный текст обрабатывается, превращается в типизированные команды и отправляется в Phaser.

На этом же слое живёт вся логика высокого уровня, связанная с основным циклом игры. Например, системы сохранения и загрузки, системы статистики и т.д.

Естественным выбором языка программирования для слоя игровой логики был JavaScript – Phaser написан на JavaScript, Ink экспортируется в JavaScript и легко подключается через библиотеку ink.js. Однако я внезапно решил писать на Python.

Почему?
  1. Хотелось максимально отделить слой игровой логики от остальных частей игры. Это правильно с точки зрения архитектуры приложения. Так, в теории, у меня появляется возможность переключиться с Phaser на PyKivy, заменив только функции для отрисовки и не переписывая логику игры.
  2. Выделенная игровая логика означает, что на неё проще писать тесты. В свою очередь тесты помогают сохранять игровую логику независимой и могут отслеживать лишние обращения к визуальному движку.
  3. На Python я пишу намного быстрее чем на JavaScript. Есть какой-никакой профессиональный опыт.
  4. Просто я очень люблю Python и, повторюсь, всегда мечтал делать на нём игры.

Вопрос: возможно ли это технически? Ведь в итоге в браузере всё равно должна оказаться HTML-страница с JavaScript-кодом. Python браузеры не поддерживают.

Ответ: да. Есть несколько вариантов для автоматического преобразования Python-кода в JavaScript. Самым надежным решением показался Transcrypt, так как не берёт на себя слишком много. Фактически конвертируется только синтаксис без лишних хитрых преобразований и библиотек.

Вот тот же самый код из примера использования Phaser, переписанный на Transcrypt
Вот тот же самый код из примера использования Phaser, переписанный на Transcrypt.

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

И оно всё заработало. Пусть и с некоторым скрипом.

Например, я на практике узнал, почему в JavaScript не рекомендуют использовать цикл вида for...in для итерирования по списку объектов. Потому что на самом деле итерирование проходит не по элементам, а по определённым свойствам объекта. И так случайно совпадает, что у стандартного списка нет других свойств, кроме самих элементов. А вот при трансляции питоновского списка через Transcrypt на выходе получается объект с большим количеством самых разнообразных свойств (__len__, __repr__) и т.д. При попытке пройтись по такому списку через for...in эффект получается очень неожиданным. Осложнялась ситуация тем, что цикл такого вида был описан во внешней библиотеке, которую я пытался подключить к игре. Потратил на эту загадку много времени.

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


Итоговая архитектура игры в самом общем виде. Стрелочки обозначают зависимости между компонентами.


Тесты игровой логики
Как я уже писал, выделение игровой логики в отдельный независимый слой позволяет писать на эту логику юнит-тесты. Приведу пример подобного теста.

Тесты на менеджер локализации.
Тесты на менеджер локализации.

Возникает вопрос: а зачем нужен такой тест? Понятно, что внутри LocalizationManager живёт простой словарь, и мы в зависимости от текущего языка либо не изменяем текст, пришедший из Ink, либо заменяем его на перевод. Казалось бы, где тут можно ошибиться?

Отвечаю:
  1. Такие тесты замечательно ловят опечатки, которые часто случаются в интерпретируемых языках программирования. При этом тест прогоняется и отлаживается очень быстро, без запуска браузера.
  2. Но важнее то, что тест пишется ещё до реализации класса. Он позволяет намного лучше понять, как именно класс будет использоваться в программе, помогает в проектировании класса.
  3. Там где уже есть маленькие тесты, проще добавить более сложные тесты, например, для того, чтобы отловить нетривиальный баг.


Тест посложнее, проверяющий взаимодействие между игровой логикой, историей и отображением выбора для пользователя.
Тест посложнее, проверяющий взаимодействие между игровой логикой, историей и отображением выбора для пользователя. Написан по мотивам реальной ошибки.

Я считаю юнит-тесты первыми и самыми надёжными союзниками программиста. Поначалу оценить их прелесть сложно, и кажется, что тратишь время зря, но чем ближе к концу разработки, тем больше от них выгоды. Они позволяют править код двухлетней давности без боязни, что вся игра необратимо сломается. По опыту “Вагонов” могу сказать, что юнит-тесты действительно свели на нет мелкие ошибки от невнимательности, c которыми я постоянно сталкивался в прошлых проектах.

Вот ссылка на замечательную статью инди-разработчика Ноэля Ллописа о применении юнит-тестов в геймдеве. Ого, сколько ей лет уже… После этой статьи я в своё время заболел TDD.

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

О них я расскажу в следующей части статьи. И ещё расскажу, как мы переводили игру на английский; как обфусцировали код; как превращали веб-страничку в полноценное мобильное приложение; как заливали игру в AppStore без собственного Мака (спойлер: это было сложно). Так что, надеюсь, мы с вами не прощаемся.

Опробовать “Вагон-Вагон” можно через AppStore и Google Play. Следить за нами можно в VK, Facebook, Twitter, Instagram.

Спасибо за внимание!

Нет комментариев