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

Месяц назад я выпустил игру «Охота на Вампуса» для голосового ассистента Алисы. Игра получила много положительных отзывов и побывала в топ-10 развлекательных навыков. В этой статье я поэтапно расскажу о процессе её создания: от переосмысления идей первоисточника — до технической реализации.

Почему я решил поделиться своим опытом?

Рассказывая о своих проектах, я увидел определённый интерес со стороны геймеров, молодых родителей и любителей настолок. Люди начали делиться со мной концептами голосовых игр, многие из которых были ну просто «Оу, май!». Кроме того, меня регулярно спрашивают о планах создать полноценную D&D для Алисы или адаптацию игро-книг Браславского.

Как геймер, я хочу видеть больше комплексных голосовых игр вместо очередных викторин и «Угадай %name%», а как VUI-дизайнер — способствовать развитию этого направления, рассказывая о своём опыте и вдохновляя людей на реализацию их идей.

Часть 1. Общий обзор

Первоисточник

В основе игры лежит классическая текстовая аркада Hunt The Wumpus, написанная в 1972 году Грегори Йобом. От оригинала были позаимствованы название и базовые механики. Впоследствии игра обросла собственными идеями и сильно изменилась по настроению.

Действие игры происходит в пещере из 20 комнат, связанных между собой коридорами. Цель игры — выследить и подстрелить монстра Вампуса. Если игрок окажется с ним в одной комнате, то игра закончится, а убить монстра можно только выстрелом из соседней комнаты. 

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

Новые идеи

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

  • Тематические локации. Вместо безликих комнат игрок теперь исследует уникальные места: ледяной туннель, руины подземного города, гейзеры и многие другие. 

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

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

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

  • Катаклизмы. Особые события, повышающие сложность игры и преображающие комнаты. Подробнее расскажу ниже.

  • Таблица очков. В конце похода игра подсчитывает заработанные очки и сравнивает их с результатами других игроков. Так абстрактные цифры превратились в показатель крутости игрока.

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

Часть 2. Рассказчик

Персона

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

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

  • Нейтральный: Здравствуйте. В этой викторине мы проверим, насколько хорошо вы знаете популярных блогеров. Готовы начать?

  • Инициативный: Привет. Прошерстила тренды и приготовила новые вопросы о ваших любимых блогерах. Готовы проверить свои знания?

  • Неформальный: Йоп! Бот-блогеровед здесь. Готовы блеснуть знаниями в моей викторине?

Есть игры без персонажей, но нет игр без персоны.

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

  • Описать Tone of Voice по методике «Четырёх измерений».

  • Взять за основу одного-двух реально существующих персонажей.

В «Вампусе» персона завязана на Рассказчике — харизматичном компаньоне, который сопровождает игроков во время их спуска в пещеру. При его создании я вдохновлялся Бобом — владельцем таверны на полях сражений Hearthstone. Выбрал его за умение подбодрить и заполнить неловкую паузу.

Персонаж

Если вводите в игру персонажа, продумайте его биографию и запоминающиеся черты:

  • Как персонаж связан с миром игры? Как попал в него?

  • С какими ситуациями уже сталкивался персонаж? О каких событиях знает и как реагирует на них?

  • Что нравится и не нравится персонажу? Чего он боится? Чем занимается в свободное время?

  • Есть ли что-то особенное в том, как персонаж говорит или мыслит? 

  • Есть ли истории, которыми персонаж мог бы поделиться с игроком?

  • Понимает ли персонаж, что он — часть игры?

Если не уделять внимание деталям, то на выходе получится очередной весёлый болванчик или покорный «раб-компаньон». Это простительно для игр с низким уровнем персонификации, но не подходит для тех проектов, где общение с персонажем является важной частью игрового процесса или инструментом создания атмосферы.

Уберите детали и персонаж превратится в болванчика.

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

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

Голос

Рассказчик говорит голосом Эрмила из Yandex.SpeechKit. Его завораживающий тембр позволил составлять реплики, звучащие не то из программы «В мире животных», не то с экрана загрузки третьего «Ведьмака». Стандартный же голос Алисы был слишком весёлым и не вписывался в атмосферу игры.

При тестировании оказалось, что Эрмил слишком торопится и не понимает вопросительных интонаций. Вот что я делал, чтобы справиться с этим:

  • разбивал фразы, которые нельзя прочитать на одном дыхании; 

  • переформулировал неестественно звучащие фразы;

  • вручную расставлял паузы между словами и предложениями;

  • использовал вопросительные слова и частицу «ли», чтобы фраза воспринималась как вопрос даже без нужной интонации.

Допиливайте голос вручную.

Реплики Рассказчика часто выходят за рамки озвучивания текста с экрана. Он как ассистент, который зачитывает инструкцию и комментирует происходящее: «Здесь можно повернуть налево или направо. О, смотрите какой интересный гриб за тем камнем!». С одной стороны это сокращает количество текста на экране. С другой стороны — добавляет живости и делает реакцию Рассказчика неожиданной для игрока.

Часть 3. Поиск идей

Новые идеи обычно рождаются из наблюдений и создания связей между ранее несвязанными вещами. Этот процесс наглядно показан в книге Остина Клеона «Кради как художник». Но когда речь заходит о создании чего-то из ряда вон выходящего, нужно что-то помощнее. Здесь на сцену выходит латеральное мышление.

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

Для примера возьмем базовую механику «Вампуса» и сгенерируем на её основе несколько новых идей для сиквела:

  • Базовое состояние — игрок выслеживает монстра с помощью звуков в пещере.

  • Дополнение — игрок выслеживает монстра и разгадывает загадки.

  • Инверсия — монстр выслеживает игрока / пещера следит за игроком и монстром.

  • Исключение — игрок никого не выслеживает, просто изучает локации.

  • Гиперболизация — игрок выслеживает нескольких монстров.

  • Изменение порядка — игра начинается с того, что игрок уже нашел монстра / монстр съел игрока.

Не сковывайте себя рамками адекватности или технических трудностей. Фантазия не терпит ограничений!

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

Часть 4. Игровой процесс

Чтобы быстро разобраться в механике и получить полное впечатление о возможностях игры — посмотрите вот это видео:

Пещера и базовые механики

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

Когда игрок находится рядом с Вампусом и промахивается, монстр начинает паниковать и перемещается в одну из трех соседних комнат. C вероятностью 33% он оказывается в одной комнате с игроком. В этом случае игрок либо умирает, либо перемещается в случайную комнату с помощью летучей мыши. Иногда это приводит к курьёзным ситуациям и мыши переносят его в яму.

Связи между комнатами и пути отступления Вампуса пришлось прописывать вручную. Это был самый нудный и кропотливый этап проектирования.

События

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

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

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

Ловушки и трёп рассказчика нужны для поддержания атмосферы и разбавления геймплея.

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

  • Вампус и летучие мыши перемещаются в другие комнаты.

  • В пещере появляется ещё одна яма.

  • Меняется описание 2/3 комнат. В зависимости от типа катаклизма они становятся либо разрушенными, либо затопленными, либо сгоревшими. Некоторые комнаты получают особые названия вроде Обсидианового туннеля, который возникает после воздействия лавы на Ледяной туннель.

Иллюзия общения

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

Чтобы определить темы для диалогов, я выписал себе все сущности, которые встречаются в игре. Это комнаты, сокровища, опасности и диковинки. Получилось порядка 200 сущностей, из которых Рассказчик может отреагировать примерно на 50–70. Для остальных пока не нашлось остроумного ответа. 

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

Выходите за рамки основного сценария.

Разнообразие

У большинства событий и реплик в игре есть от 4 до 15 различных вариантов. Это делает каждое прохождение непохожим на предыдущее. Нет ничего хуже, чем бот, который всё время повторяет одно и то же.

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

Стартовые реплики при втором, десятом и двадцатом запуске игры
Стартовые реплики при втором, десятом и двадцатом запуске игры

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

Сделайте реплики вариативными.

Часть 5. Звуки

Каждый звук в «Вампусе» выполняет определённую функцию — предупреждает об опасности или задаёт атмосферу. Например, звук натяжения тетивы создаёт небольшую напряженную паузу между репликой игрока и оглашением результата выстрела.

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

Звуки создают атмосферу и заполняют неловкую паузу.

Напоследок — несколько выводов по работе со звуком:

  • Оптимальная продолжительность звукового эффекта — 3–5 секунд. Длинные звуки отделяют пользователя от основного геймплея, и он начинает скучать.

  • В одной реплике лучше не использовать больше двух звуков подряд. Иначе можно сбить пользователя с толку.

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

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

Часть 6. Удобство

Подсказки

В любой момент игры пользователь может задать вопрос в свободной форме:

  • Как победить Вампуса?

  • Для чего нужны сокровища?

  • Сколько у меня осталось стрел?

  • Почему дует сквозняк?

  • Как узнать, где находится яма?

  • Куда я могу пойти?

  • Напомни номера комнат.

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

Обработка ошибок

Рано или поздно игрок скажет что-то, что не было предусмотрено системой. Это называется ошибкой No Match. В этом случае игрока нужно нежно вернуть на правильный путь и не показаться слишком навязчивым.

В «Вампусе» используется два уровня обработки No Match:

  • 1 уровень. Пользователь столкнулся с ошибкой. В этом случае Рассказчик просит его перефразировать свой запрос.

  • 2 уровень. Пользователь дважды столкнулся с ошибкой первого уровня. На третий раз Рассказчик предлагает ему ознакомиться со списком основных команд.

Обучение и тренировка

Получая первые логи, я заметил серьёзную проблему. Четверть игроков переставала играть после озвучивания правил (это два экрана текста). Другие игроки начинали играть, не зная правил, после чего закономерно говорили о том, что игра слишком непонятная.

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

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

Часть 7. Техническая реализация

Выбор инструмента

Проектировать голосовые игры можно разными способами: на Python, JavaScript или с помощью конструкторов диалогов. Последний вариант будет особенно полезен тем, кто мало знаком с языками программирования.

Изначально я делал «Вампуса» в конструкторе Aimylogic. Но вскоре стало понятно, что мой проект слишком большой и сложный для этого. Поэтому я перешел в JAICP.

JAICP — это платформа для разработки голосовых интерфейсов. Работает на собственном языке DSL, поддерживает Kotlin и JavaScript. На базовом уровне осваивается за пару вечеров.

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

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

Основы DSL

Подробно об использовании языка DSL можно узнать из документации Just AI. Я лишь кратко опишу базовые вещи, необходимые для понимания этой статьи.

В основе работы голосовых навыков лежат стейты (state). Стейт — это текущее состояние системы: что сказал пользователь и как на это отреагировал бот.

      state: Main
        intent: / Привет
        script: $session.hello = 1
        a: Здравствуйте! Готовы начать новую игру?
        buttons:
            "Да" -> ./NewGame
            "Нет" -> ./Exit

        state: NewGame
            intent: / Согласие
            intent!: / Новая игра
            a: Рад это слышать. Приступим!
            go!: ./Engage

Что может входить в стейт:

  • intent — намерение пользователя (фразы, на которые должен отреагировать бот);

  • intent! — тоже намерение, но может срабатывать в любом месте диалога;

  • script — различные скрипты (например, назначение переменных);

  • a — ответ бота;

  • buttons — кнопки (саджесты);

  • go! — переход в другой стейт.

Глобальные и локальные переменные

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

Переменные бывают локальными и глобальными:

  • $session — локальные, обнуляются при выходе из игры.

  • $client — глобальные, сохраняются для текущего пользователя и не обнуляются при выходе.

В «Вампусе» глобальные переменные считают количество завершенных игр и убитых Вампусов. Чем больше игр сыграно, тем короче становится приветственное сообщение. Чем больше монстров убито, тем выше ранг пользователя.

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

       script:
        if: $client.session_number > 0
            script: $client.session_number += 1.
        else: 
            script: 
                $client.session_number = 0.
                $client.session_number += 1.

Локальные переменные определяют всё остальное: ачивки, экипировку, количество стрел и так далее. Стартовые значения для них задаются и обнуляются перед запуском новой партии. Например:

    #объекты
            $session.wampus = $jsapi.random(20) + 1;
            $session.bat_1 = $jsapi.random(20) + 1;
            $session.bat_2 = $jsapi.random(20) + 1;
            $session.pit_1 = $jsapi.random(20) + 1;
            $session.pit_2 = 0;
            
    #экипировка
            $session.player_ammo = 9;
            $session.player_shot = 0;
            $session.hook = 0;
            $session.compas = 0;
            $session.compas_left = 0;
            $session.map = 0;
                
    #события
            $session.score_total = 0; 
            $session.wampus_dead = 0;
            $session.items = 0;
            $session.moves = 0;
            $session.rooms = 0;

Рандомизация

В «Вампусе» рандом встречается на каждом шагу: от наименования комнат — до путей бегства монстра.

Чтобы события происходили случайно, я использую системную переменную $jsapi.random(х) +1, где x — количество возможных значений. Затем прописываю поведение системы для каждого значения переменной (с помощью условного оператора).

    state: Disaster
        intent: / Предсказание
        script: $session.disaster = $jsapi.random(3) + 1;
        if: $session.disaster == 1
            a: Будет землетрясение.
        elseif: $session.disaster == 2
            a: Будет потоп.
        else:
            a: Будет выброс лавы.

Обратите внимание, что оператор = записывает значение в переменную, а оператор == сравнивает с этим значением.

Рандом в фразах делается с помощью YAML-справочника. В нём хранятся варианты фраз для каждого случая:

pit:
  summary: Игрок угодил в яму
  answers:
    - Ещё один путник сгинул в бездонной яме. Охота окончена
    - Вы искали славы, но нашли лишь бездонную яму. Охота окончена
    - Теперь мы знаем, что даже у бездонной ямы есть дно. Охота окончена
    - Вы упали в яму. Как жаль, что с её дна не видно звёзд на небе. Охота окончена

sorry:
  summary: Диалог восстановился после ошибки
  answers:
    - Давайте вести себя так, будто всё заработало с первого раза.
    - Прошу прощения за это недоразумение. Попробуем ещё раз.
    - Ума не приложу, как это могло произойти. Давайте ещё раз.

Чтобы фраза выбиралась случайно, в стейт добавляется скрипт с методом $reactions.random:

       script: $temp.index = $reactions.random(phrases.pit.answers.length);
       a: ????️ Яма! \n {{phrases.pit.answers[$temp.index]}}.
       go!: /score/count

Здесь phrases — это название справочника, а pit — вариативная фраза.

Библиотека звуков

Чтобы быстро подставлять и менять звуки, их удобно хранить в одном месте. Для этого также используется YAML-справочник. 

stone:  
  summary: Валун
  answers: sil <[200]> <speaker audio="dialogs-upload/03d361af-8c50-449a-9ab9-b61df7c67660/2b62773e-5a75-408e-81ed-9a22681a74f3.opus"> sil <[200]>

snake:  
  summary: Змея
  answers: sil <[200]> <speaker audio="dialogs-upload/03d361af-8c50-449a-9ab9-b61df7c67660/4c8548fc-2dff-4093-aae7-3e09872db862.opus"> sil <[200]>

sil <[200]> указывает на то, что перед воспроизведением звука должна быть пауза в 200 миллисекунд. Коды звуков я беру из Платформы диалогов Яндекса, куда загружаю исходные аудиофайлы.

Чтобы сослаться на звук из справочника, достаточно вставить в текст озвучки  конструкцию вида {{sounds.torch.answers}}, где sounds — название файла, а torch — название звука. 

    state: Torch
        a: Давайте зажжем новый факел.
            || tts = "Давайте зажжём новый факел. {{sounds.torch.answers}}", ttsEnabled = true

Параметр tts (text-to-speech) указывает на то, что произносимая ботом реплика будет отличаться от той, что выводится на экран.

Подсчёт очков

В конце игры Рассказчик озвучивает количество полученных пользователем очков и сравнивает это значение с результатами других очков. Также выводится список полученных достижений и случайная напутственная фраза. 

Итоговый счёт складывается из значений других переменных, которые заполняются по ходу игры. Например, каждая пройденная комната увеличивает значение переменной $session.moves. Затем к каждой переменной применяется свой множитель очков.

    state: count
        script: $session.score_total = ($session.moves * 25) + ($session.items * 100) + ($session.money * 250)  + ($session.wampus_dead * 5000)

Далее считаются ачивки. Для этого тоже используются переменные, расставленные в комнатах. Например, достижение «Неосмотрительный» (ach_quickpit) выдаётся в том случае, если пользователь упал в яму в первые три хода:

        if: $session.pit_dead == 1 && $session.moves < 4
            script:
                $session.ach_quickpit = 1;
                $session.score_total += 100

Итоговая сумма очков заносится в Google Таблицу. Для этого используется встроенная интеграция:

   state: Google
        GoogleSheets:
            operationType = writeDataToLine
            integrationId = 04202467-3727-421d-897c-da6583afb0fc
            spreadsheetId = 1O_snInw9vWlrVUHDoRXsQyNwZwFadAs31FEwRPkinmA
            sheetName = score
            body = {"values":["{{$session.score_total}}"]}
            okState = /score/Total
            errorState = /score/Total

Настроить проверку значений таблицы в реальном времени мне не удалось, поэтому я взял выборку из 10 000 последних значений и отсортировал их. Затем выделил ключевые интервалы, чтобы сравнивать с ними результат пользователя.

    state: Total        
        a: Ваш счет: {{$session.score_total}} очков \n 
        
        if: $session.score_total > 375 && $session.score_total < 1050
            random:
                a: Лучше, чем у 25% игроков.\n ||tts = "Справились лучше, чем 25% игроков.", ttsEnabled = true
                a: Лучше, чем у 25% игроков.\n ||tts = "Оставили позади себя 25% игроков.", ttsEnabled = true

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

    # Считаем очки:
        a:  ???? Комнаты: {{$session.rooms}} * 25 \n
            ???? Диковинки: {{$session.items}} * 100 \n
            ???? Сокровища: {{$session.money}} * 250 \n
            ???? Вампус: {{$session.wampus_dead}} * 5000 \n\n
                
    # Выдаём ачивки:
        if: $session.ach_quickpit == 1 || $session.ach_shapeshifter == 1
        
            a: Достижения: \n||tts = "Получили достижения: sil <[400]>", ttsEnabled = true
 
            if: $session.ach_quickpit == 1
                a: ???? Неосмотрительный: +100 \n||tts = "Неосмотрительный sil <[300]> — за падение в яму в первые три хода sil <[600]>", ttsEnabled = true
            if: $session.ach_shapeshifter == 1
                a: ???? Перевертыш: +100 \n||tts = "Перевертыш sil <[300]> — за использование секретной комбинации sil <[600]>", ttsEnabled = true

Комнаты

Когда игрок попадает в комнату, игра записывает номер комнаты, чтобы пользователь в любой момент мог спросить, где он находится. Далее идёт проверка на наличие в ней особых объектов. Если таких объектов нет — переходим в стейт Event.

    state: Main
        script: $session.player_position = 1;

        if: $session.wampus == 1 
            go!: /wampus/Main
        elseif: $session.pit_1 == 1 || $session.pit_2 == 1
            go!: /pit/Main
        elseif: $session.bat_1 == 1 || $session.bat_2 == 1
            go!: /bat/Main
        else:
            go!: /room_1/Events

В стейте Events мы увеличиваем счётчик совершенных пользователем ходов и смотрим на получившееся число. При выпадении определённого значения переходим в соответствующее событие:

  • SmallLoot и BigLoot — сундуки;

  • Trap — ловушка;

  • Talk — трёп Рассказчика;

  • Earthquake, Flud и Lava — варианты катаклизмов. Какой именно произойдет — зависит от значения переменной $session.disaster (генерируется случайно).

   state: Events
        script: $session.moves += 1;
        if: $session.moves == 2 
            go!: /room_1/Events/SmallLoot 
        elseif: $session.moves == 4 || $session.moves == 16 || $session.moves == 22 || $session.moves == 28 || $session.moves == 34 || $session.moves == 40
            go!: /room_1/Events/Trap
        elseif: $session.moves == 6 || $session.moves == 12 || $session.moves == 18|| $session.moves == 24 || $session.moves == 30 || $session.moves == 36
            go!: /room_1/Events/Talk
        elseif: $session.moves == 8 || $session.moves == 14 || $session.moves == 20 || $session.moves == 26 || $session.moves == 32 || $session.moves == 38 
            go!: /room_1/Events/BigLoot  
        elseif: $session.disaster == 1 && $session.moves == 10
            go!: /room_1/Events/Earthquake 
        elseif: $session.disaster == 2 && $session.moves == 10
            go!: /room_1/Events/Flud
        elseif: $session.disaster == 3 && $session.moves == 10
            go!: /room_1/Events/Lava
        else:         
            go!: /room_1/Inspection  

В стейте Inspection происходит осмотр комнаты. Становится запутаннее, держитесь!

Каждая комната в игре — перевёртыш, то есть может называться по-разному в зависимости от значения переменной $session.room_X_name. За счёт этого в пещеру из 20 комнат удалось вместить 40 наименований.

Код
state: Inspection         
 if: $session.room_1_name == 1 
            if: ($session.earthquake == 0 && $session.flud == 0 && $session.lava == 0) || $session.room_1_status == 1
                if: $session.room_1_sound == 0
                    script: $session.room_1_sound = 1;
                    a: 1️⃣ Ледяной туннель \n
                        ???? Коридоры: 2, 5 и 8 \n
                        || tts = "Комната №1. sil <[100]> Ледяной туннель. Коридоры ведут в комнаты 2, sil <[100]> 5, sil <[100]> и 8. sil <[600]> Как же здесь всё-таки холодно, аж пальцы деревенеют. sil <[300]>", ttsEnabled = true
            else:
                    a: 1️⃣ Ледяной туннель \n
                        ???? Коридоры: 2, 5 и 8 \n
                        || tts = "Комната №1. sil <[100]> Ледяной туннель. Коридоры ведут в комнаты 2, sil <[100]> 5, sil <[100]> и 8. sil <[300]>", ttsEnabled = true

            if: $session.room_1_status == 2 || $session.room_1_status == 3 
                if: $session.earthquake == 1
                    a: 1️⃣ Ледяные глыбы \n
                        ???? Коридоры: 2, 5 и 8 \n
                        || tts = "Комната №1. sil <[100]> Расколотые ледяные глыбы. Коридоры ведут в комнаты 2, sil <[100]> 5, sil <[100]> и 8. sil <[300]>", ttsEnabled = true
                if: $session.flud == 1
                   a: 1️⃣ Плавающие айсберги \n
                        ???? Коридоры: 2, 5 и 8 \n
                        || tts = "Комната №1. sil <[100]> Плавающие айсберги. Коридоры ведут в комнаты 2, sil <[100]> 5, sil <[100]> и 8. sil <[300]>", ttsEnabled = true
                if: $session.lava == 1
                    if: $session.room_1_sound == 0
                        script: $session.room_1_sound = 1;
                        a: 1️⃣ Обсидиановый тоннель \n
                            ???? Коридоры: 2, 5 и 8 \n
                            || tts = "Комната №1. sil <[100]> Обсиди+ановый тоннель. sil <[100]> Редкое явление, связанное с быстрым охлаждением лавы. Коридоры ведут в комнаты 2, sil <[100]> 5, sil <[100]> и 8. sil <[300]>", ttsEnabled = true
                    else:   
                       a: 1️⃣ Обсидиановый тоннель \n
                          ???? Коридоры: 2, 5 и 8 \n
                            || tts = "Комната №1. sil <[100]> Обсиди+ановый тоннель. Коридоры ведут в комнаты 2, sil <[100]> 5, sil <[100]> и 8. sil <[300]>", ttsEnabled = true 

В некоторых комнатах проигрываются собственные звуки или уникальные реплики Рассказчика. Чтобы они не воспроизводились при повторном прохождении через комнату, используется переменная $session.room_X_sound.

После наступления катаклизма часть комнат меняет своё название. Произойдет ли это или нет — зависит от переменной $session.room_X_status: 1 — не меняется, 2 или 3 — меняется (сделал так для того, чтобы менялось большинство комнат, а не строго половина).

Далее идёт учёт диковинок, если они есть в комнате
            if: ($session.item_1 == 1 || $session.item_2 == 1 || $session.item_3 == 1 || $session.item_4 == 1 || $session.item_5 == 1 || $session.item_6 == 1 || $session.item_7 == 1) && $session.item_R1_collected == 0 
                script:
                    $session.item_R1_collected = 1;
                    $session.items += 1;           
                random:
                    a: ???? Замороженный желудь \n || tts = "Нашли диковинку – замороженный желудь. +100 очков. sil <[300]> Пожалуй это самая ленивая отсылка во всей игре", ttsEnabled = true
                    a: ???? Замороженная белка \n || tts = "Нашли диковинку – замороженную белку. +100 очков. sil <[300]> Интересно, нашла ли она свой желудь?", ttsEnabled = true   

Как только диковинка собрана, переменная $session.item_RX_collected принимает значение 1. Это значит, что при повторном посещении комнаты диковинки уже не будет.

Ну и последний шаг в этом стейте — проверка наличия опасностей в соседних комнатах и воспроизведение соответствующего звука:

        if: $session.wampus == 2 || $session.wampus == 5 || $session.wampus == 8
            a: ???? Вампус \n||tts = "{{sounds.wampus_roar.answers}}", ttsEnabled = true
        if: $session.pit_1 == 2 || $session.pit_1 == 5 || $session.pit_1 == 8 
            a: ???? Яма \n||tts = "{{sounds.wind.answers}}", ttsEnabled = true
        if: $session.pit_2 == 2 || $session.pit_2 == 5 || $session.pit_2 == 8
            a: ???? Яма \n||tts = "{{sounds.wind.answers}}", ttsEnabled = true
        if: $session.bat_1 == 2 || $session.bat_1 == 5 || $session.bat_1 == 8 
            a: ???? Летучие мыши \n||tts = "{{sounds.bats.answers}}", ttsEnabled = true
        if: $session.bat_2 == 2 || $session.bat_2 == 5 || $session.bat_2 == 8
            a: ???? Летучие мыши \n||tts = "{{sounds.bats.answers}}", ttsEnabled = true
        go!: /room_1/Navigation

Заметьте, что проверка условий заканчивается переходом в Navigation — это вспомогательный стейт с кнопками, который «пристёгивается» к основным:

   state: Navigation
        buttons: 
            "???? в 2" -> /room_2/Main
            "???? в 5" -> /room_5/Main
            "???? в 8" -> /room_8/Main
            "???? Огонь!" -> /room_1/Shoot
    
    state: To2
        intent: /Комната 2
        go!: /room_2/Main
            
    state: To5
        intent: /Комната 5
        go!: /room_5/Main
        
    state: To8
        intent: /Комната 8
        go!: /room_8/Main

Вот так выглядит полностью «собранный» стейт в самой игре:

Дальнейшее развитие событий зависит от пользователя. Если он решает выстрелить в комнату, то выполняется проверка соседних помещений, вычитаются стрелы и объявляются результаты выстрела.

Часть 8. Тестирование

Логи

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

Первое время я просматривал все логи вручную. Но когда их стало приходить по 500–600 штук в день — начал фильтровать по ключевым словам и наличию ошибок.

Друзья и коллеги

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

Обратная связь в игре

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

Тестовая комната

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

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


Часть 9. Что дальше

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

Кстати, если вам интересен мир разговорных технологий — ищите актуальные новости, исследования и экспертные заметки в телеграм-канале Hey Voice, который ведёт наша команда TORTU.

Комментарии (2)


  1. dbf
    25.12.2021 10:45

    Серьёзная работа проделана, было интересно почитать, подметил для своих навыков несколько моментов. А каких показателей посещаемости удалось достичь? По моим впечатлением, в топы залезают как раз угадайки и подобные игры с короткими сессиями и простыми механиками, а среди игроков большой процент детей. Интересно было бы узнать, много ли запускают игру, сколько доходит до конца или хотя бы проходит несколько комнат.


    1. Rainvention
      25.12.2021 11:11
      +2

      Когда игра была в топ-10 навыков, было около 500-700 новых пользователей ежедневно. Сейчас показатели спали да 400-500. Но это при том, что я не вкладывался в таргетированную рекламу. Возвращаются в игру по 150-170 человек в день (одни и те же это люди, или нет - не знаю).

      Некоторые пользователи запускают игру несколько дней подряд и завершили более 20 успешных сессий (победили Вампуса). Продолжительность сессий сильно варьируется в зависимости от рандома и тактики игрока (проверяет ли он комнаты стрелами или же предпочитает рисковать). Это может быть как 1-2 комнаты, так и 15-20.

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

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