Разработка визуальной новеллы в одно лицо. Слабоумие и отвага или история успеха?
Сразу хочу сказать, какие цели преследует эта статья. Во-первых, она должна рассказать о том, как я делал свою игру (с целью какого-никакого пиара, разумеется)). А во-вторых, я хочу спросить совета у пользователей — как можно её попиарить? Сама игра скоро выходит в стиме, а у меня закончились идеи, как это делать(
Вот что получилось в итоге.
Но давайте по порядку. История создания визуальной новеллы в одно лицо.
Песнь первая: с какого фига я вообще за это взялся?
Откровенно говоря, я стал инди-разрабом очень неожиданно. Я всегда хотел быть сценаристом, но как-то не сложилось. Вернее, сложилось, но не очень.
В своё время я сделал несколько модификаций для визуальной новеллы (ВН) «Бесконечное лето». Модификации народу понравились, они быстро взлетели на первую страницу рейтинга «за всё время» и до сих пор висят там.Если интересно, то вот ссылка. Такое признание меня взбудоражило и я решил — а почему бы не сделать собственную визуальную новеллу? Тем более я почитал другую визуальную новеллу — «Девушка в Скорлупе 2» и был очень вдохновлён её сюжетом…
Классная вещь, кстати. Рекомендую.
В общем, с этого и началась история моего падения)
Песнь вторая: сюжет и сценарий.
Сюжет в моей голове рождался медленно и размеренно. Как я уже сказал — я был очень вдохновлён другой визуалкой. Настолько там закрученный сюжет, персонажи. Прям ух! Соответственно, из щенячьего восторга и выросла идея сделать что-то похожее.
Следующим источником вдохновения стал Джон Константин. Яркий и харизматичный трикстер-экзорцист. У него я позаимствовал конструкцию имени. Моего главного героя будут звать Джон Виктор. Потому что Виктор по-латыни — «победитель». И это можно клёво обыграть в истории.
В этом моменте я впервые столкнулся с трудностями. Сфера любителей визуалок оказалась довольно ядовитой и я практически сразу стал получать отзывы в стиле «ты что, дурак? Такое имя-фамилию только имбецил придумает». Один из таких уникумов так вообще организовал фан-клуб Джона Виктора. Однако черт с ними, надо писать сценарий.
Сценарий планировался многовариантный. Т.е. в игре три акта, в каждом из них выборы. Эти выборы между собой переплетаются и выводят на разные ситуации. В принципе, это стандартная ВН-схема. Вот только я раньше такие не писал. Я писал кинетику — т.е. книгу, по сути. А тут — если в первом акте выбрать «нож», а во втором «телефон», то в третьем размышления будут про одно. А если в первом выбрать «нож», а во втором «газету», то размышления уже будут совсем про другое.В первом акте таких выборов 6. Количество выборов во втором зависит от того, что игрок выбрал в первом. В третьем акте просчитываются выборы обоих предыдущих.
Только тогда я начал понимать, во что ввязался. Потому что мой гуманитарный мозг начал чуть-чуть трещат… Однако, спустя полгода с сюжетом я всё-таки разобрался. Можно было и быстрее, но лень, работа, другой проект (доделывал мод) и острое чувство «да кому нахрен это надо» сильно мешали.Но нет, всё-таки доделал. Ура!
Песнь третья: ресурсы.
Какое-то время всё шло достаточно гладко. Потихоньку рисовалась графика, на форуме любителей визуалок один энтузиаст предложил написать музыку.
Так герои выглядели первоначально.
А потом в моём мире наступил экономический кризис и я ушёл с работы. И заказывать графику стало не на что. А раз так — прощай, детектив Виктор, увидимся в следующей жизни. Графики нет, да и у композитора начались какие-то свои проблемы, потому он отказался сотрудничать дальше. (Тем не менее, пару мелодий он написал и они мне понравились — эти треки вошли в финальную версию.)
Но мне повезло. Я наткнулся на японский сайт с готовой графикой. Там продавалось много пресетов по низкой цене и я, не долго думая, затарился сразу по самое «не балуйся». Вопрос с графикой был решён.Правда, оставался вопрос, куда девать предыдущие наработки — всё-таки они делались специально под меня и нравились мне. Увы, частью пришлось пожертвовать. А часть была успешно вставлена в нынешнюю версию.
Так герой стал выглядеть в итоге.
Вопрос с музыкой решился схожим образом. В стиме был куплен пак мелодий, техподдержке был задан вопрос «а можно это использовать там-то и так-то?» и получен ответ «можно». Ура! Проблема с ресурсами была решена.
Песнь четвертая: код.
Тут надо оговориться. Я — человек не командный, стремлюсь держать всё под контролем, а потому мне гораздо легче сделать всё самому, чем поручить кому-то и в дальнейшем ходить проверять, сделал он или нет.Потому кодить я тоже стал сам.
С движком RenPy я уже был немного знаком, когда делал модификаций для «Бесконечного лета». Там всё было достаточно лайтово — никакой возни с интерфейсом, размерами шрифтов, оформлением и проч. Просто вставлял текст и менял картинки.И да, напомню ещё раз — я не работал с выборами и переменными.
Тут моя гуманитарная голова начала трещать во второй раз.Прописать развилки на бумаге — это одно. А как их запихнуть в код так, чтобы оно правильно реагировало — это другое.В третьем акте у меня выходило около 16 вариантов развития событий. И эти 16 вариантов складывались из предыдущих выборов.Огромное количество переменных. Все эти «if» «elif», эти флажки и поинты… ой блин.
Но, методом проб, ошибок и какой-то матери, за пару недель я всё это закодил. И, как ни странно, мне понравилось. Видимо, есть во мне какие-то мазохистские черты.Ты что-то делаешь и тут же видишь, как твоя работа воплощается в программе. Прикольно.Также в процессе кода рождалось много интересных идей. А что если персонаж в одном акте спросит имя человека, а в следующем назовёт его по этому имени? А если не спросит — то имени знать не будет? А таких персонажей 7. Здравствуйте, новые переменные.А что, если в ряде сцен экран будет с помехами? Это как раз логично выглядит с учётом сценария. Здравствуйте, эффекты глича и видеошума. А что, если в главное меню запилить ещё кнопку с выводом концовок и сделать одну из них скрытой, доступной только после первого прохождения? А что, если… И всё это реализовывается, и делает игру всё круче и круче.
Спецдеффекты со старой оригинальной графикой.
Однако, работая над этим ты понимаешь, что предела совершенству нет и править можно бесконечно. Нужно когда-то останавливаться и публиковать. И вот, начались вопросы с публикацией.
Песнь пятая: провал.
Сначала мне предложил издать игру друг. Он знал о том, что я делаю успехе в модификациях и предложил свои услуги. Я обрадовался и сказал «океюшки!».Но потом ему не понравился ряд моментов и мы разошлись.
Боль. Я стал искать другого издателя. И, кажется, нашёл (тут немножко рекламы) — такой же инди-разраб 7DOTS. Его мне прямо хочется расцеловать в обе щеки и повторять много раз «спасибо-спасибо»)) В первую очередь за то, что он сказал «не, такое оформление не катит» и в одно лицо перефигачил весь дизайн о_О. Кроме того, он добавил пару ЦГ и даже запилил в ВНку слоу-мо сцену (я вообще в шоке, что там так можно).
Анимированная сцена.
Во-вторых, он всё-таки выпустил игру на андроид, где она продавалась за 35 рублей.
Но потом этот разраб-издатель отвалился из-за ряда личных обстоятельств.
Отчаяние охватило меня. Денег у меня не было, всё шло наперекосяк, пошёл ты к чёрту, детективчик. Не судьба мне стать сценаристом. Тем более в геймдев студии меня тоже не брали. Проект был послан нахрен. Залита версия на рутрекер, андроид сделан бесплатным.
Песнь шестая: возрождение из пепла?
Но время шло, я случайно наткнулся на студента-переводчика (вернее, я думаю, что он студент, доказательств у меня нет) и тот согласился перевести проект. Качество, судя по всему, вышло более-менее читабельным. По крайней мере текст понятен. У меня появились некоторые сбережения и я смог оплатить взнос в стиме. Саму игру немного дополнил и подшлифовал, так что версия отличается от торрент-эдишна.
И теперь игра на проверке, страница в стиме есть, я спамлю друзьям «добавьте в вишлист»…
Но что делать ещё? Везде советуют разное. Отправлять кураторам или нет? Искать стримеров или надеяться на бусты стима? Я запутался.
Игроки, разработчики, помогите советом, пожалуйста.
Ну и, конечно же, прошу заценить страницу в стиме — и, если понравится, добавить игру в вишлист.
А вот тут, кстати, сделанный на коленке трейлер. Его я тоже делал сам и это отдельная головная боль.
В общем, моя игра получилась очень многострадальной. Но из-за этого ещё сильнее хочется, чтобы хоть где-то она поимела успех)
На этом пока всё. Надеюсь, вам было интересно читать. И надеюсь, что коллективный разум подкинет клёвых идей.
Визуальная новелла, интерлюдия: создание разветвленного сценария силами RenPy
И вновь приветствую всех! Пока продолжение прошлого поста, посвященное основам построения сюжета, еще в процессе написания, я попробую максимально доступно описать методы ветвления сюжета на примере детской игры с загадками (и, возможно, не самой лучшей графикой — не могу же я весь контент показать до релиза?).
Эта статья посвящена техническим моментам реализации новеллы при помощи инструментов движка RenPy, в частности — вариативности сюжетных линий.
В комментариях к прошлой записи несколько раз прозвучал вопрос о том, как реализована параллельность сюжета в новелле Spiritual Cavern, которая уже обзавелась собственной группой на Facebook.
Итак, для примера мы создадим крайне простую новеллу, в которой игрок должен отгадывать загадки кубика чтобы вместе с ним пойти на прогулку.
Спойлер: создание новеллы с графикой и написанием кода заняло 20 минут.
Какая новелла может обойтись без действующих лиц? Подготовим нашего персонажа, который будет загадывать загадки:
События происходят не посреди пустоты, потому нам потребуется фон:
Подготовив материалы, мы можем приступить к составлению сюжета — в нашем примере он будет очень простым:
Суть сюжета очень проста: игрок трижды отвечает на вопросы, в зависимости от его ответа меняется настроение кубика. Кроме того, при каждом правильном ответе мы увеличиваем счетчик «Ответы», от которого в последствии зависит концовка игры: если был дан хоть 1
Итак, приступим. Я не буду описывать полный процесс написания кода в RenPy, так как уроков наэту тему довольно много. Но уделю отдельное внимание тем функциям, которые отвечают за ветвление сюжета.
Не пугайтесь, если показанные ниже примеры кода покажутся Вам сложными, или раньше у Вас не было опыта в работе с подобными редакторами — ниже описана структура любой графической новеллы в терминах языка RenPy, а также назначение каждой команды.
Кроме того, при создании нового проекта RenPy создает не пустой файл, а стандартную сцену, которая может быть основой для Ваших первых работ.
Основными конструкциями в RenPy являются:
- label имя_метки — место в коде, к которому в последствии можно перейти при помощи команды jump имя_метки
- scene — команда загрузки фонового изображения из папки images созданного проекта. Важно знать: в случае смены фона требуется заново ввести команду по выводу изображения персонажа.
- show — отобразить картинку (чаще всего — персонажа) поверх существующего фона.
- say — фраза, произносимая персонажем. Чаще всего используется в следующих форматах:Вариант первый: «автор» «фраза» — здесь мы явно указываем, кто говорит фразу и как эта фраза звучит.Однако, для удобства пользователя есть возможность упростить свою жизнь, заранее указав имя автора.Для этого перед меткой начала игры применяется следующая конструкция:define anna = Character(«Anna:»)В дальнейшем достаточно написать anna «фраза», что будет воспринято системой как «Anna:» «фраза»Важно понимать, что нет прямой связи между показываемой картинкой (show) и автором фразы — кроме той, которая формируется у игрока в процессе игры.
- menu — выбор игрока, в зависимости от которого происходит выполнение соответствующего кода, будь то простая фраза либо переход к конкретной метке.
Итак, что же происходит в первом фрагменте кода?
1) мы выводим на экран сцену «bg 1» из папки images нашего проекта
2) от имени игрока (define char = Character(«Я:») перед label start) выводим фразу «Интересно, где же кубик?»
3) плавно (with dissolve) выводим на экран изображение «cube wow»
4) от имени кубика (строка define e = Character(«Кубик:») перед label start) выводим фразу «А вот он я!»
5) после обмена фразами мы переходим к новой для нас команде — menu, отвечающей за выбор игрока и реакцию игры на этот выбор.
Ничего сложного, согласитесь? Требуется лишь следовать правилам составления сцены и корректно вводить команды.
У тех, кто был внимателен при прочтении нашего «сценария», может возникнуть вопрос: а как нам определить, хорошо или плохо заканчивается игра? Всё верно, при помощи команды menu мы можемпредоставить выбор игроку, а в данной ситуации требуется противоположное — сделать выбор на основе данных самой программы.
С этой целью мы добавляем в игру типичный для программ элемент -переменную под названием «answers» и устанавливаем её значение равным нулю.
В дальнейшем, при выборе игроком положительного (правильного) ответа, мы увеличиваем значение этой переменной следующим образом:
Таким образом, после трех вопросов её значение находится в диапазоне от 0 (если не было дано ни одного правильного ответа) до 3 (если все ответы были правильными).
Будем добрыми, и поставим простое условие: отгадал хоть одну загадку — добро пожаловать на прогулку. Для этого нам потребуется конструкция if-else, «если-иначе». Ниже — пример её применения в нашей новелле:
Движок, скриптовый язык и визуальная новелла — за 45 часов
Приветствую. Так получилось, что уже три года подряд в качестве подарка на Новый год определенным людям я делаю игру. В 2018-ом году это был платформер с элементами головоломки, о котором я писал на хабре. В 2019-ом — сетевая RTS для двух игроков, о которой я ничего не писал. И наконец, в 2020-ом — визуальная новелла, о которой далее и пойдет речь, созданная в условиях сильно ограниченного времени.
- проектирование и реализация движка для визуальных новелл,
- игра с нелинейным сюжетом за 8 часов,
- вынос логики игры в скрипты на собственном языке.
Интересно? Тогда добро пожаловать под кат.
Осторожно: тут много текста и
0. Обоснование разработки движка.
- Выбор платформы.
- Архитектура движка и его реализация:
2.1. Постановка задачи.
2.2. Архитектура и реализация. - Скриптовый язык:
3.1. Язык.
3.2. Интерпретатор. - Разработка игры:
4.1. История и разработка логики игры.
4.2. Графика. - Статистика и итоги.
Примечание: Если вам по каким-то причинам неинтересны технические подробности, можете переходить сразу к п. 4 «Разработка игры», однако Вы пропустите основную часть контента
0. Обоснование разработки движка
Разумеется, существует огромное количество готовых движков для визуальных новелл, которые, без сомнения, по всем пунктам лучше нижеописанного решения. Однако каким бы программистом я был, если бы не написал еще один. Поэтому давайте сделаем вид, что его разработка была обоснована.
1. Выбор платформы
Выбор, собственно, был невелик: либо Java, либо C++. Недолго думая, я решил реализовывать задуманное на Java, т.к. для быстрой разработки она даёт все возможности (а именно: автоматическое управление памятью и большую, по сравнению с C++, простоту, которая скрывает много низкоуровневых деталей и, как следствие, позволяет меньше акцентировать внимание на самом языке и думать только о бизнес-логике), а также обеспечивает поддержку окон, графики и аудио из коробки.
Для реализации графического интерфейса был выбран Swing, так как я использовал Java 13, где JavaFX уже не входит в библиотеку, а добавлять в зависимости десятки мегабайт OpenJFX было лень. Возможно, это было не лучшим решением, но тем не менее.
Вероятно, возникает вопрос: что это за игровой движок, да без аппаратного ускорения? Ответ заключается в отсутствии времени для борьбы с OpenGL, а также абсолютной её бессмысленности: для визуальной новеллы неважен FPS (во всяком случае, с таким количеством анимации и графики, как в данном кейсе).
2. Архитектура движка и его реализация
2.1 Постановка задачи
Для того чтобы решить, как что-либо делать, нужно определиться с тем, зачем это делать. Это я о постановке задачи, т.к. архитектура не универсального, а «домен-специфичного» движка по определению напрямую зависит от задумываемой игры.
Под универсальным движком я понимаю движок, поддерживающий относительно низкоуровневые понятия, типа «Игровой объект», «Сцена», «Компонент». Делать было решено именно не универсальный движок, потому что это значительно сократило бы время разработки.
По задумке, игра должна была состоять из следующих частей:
Т. е. имеется фон для каждой сцены, основной текст, а также текстовое поле для пользовательского ввода (визуальная новелла задумывалась именно с произвольным пользовательским вводом, а не выбором из предложенных вариантов, как это часто бывает. Позже я расскажу, почему это было плохим решением). Также на схеме показано, что в игре может быть несколько сцен и, как следствие, между ними могут осуществляться переходы.
Примечание: Под сценой я понимаю логически выделенную часть игры. Критерием сцены может служить одинаковый фон на протяжении этой самой части.
Также среди требований к движку была возможность воспроизведения аудио и показа сообщений (с опциональной функцией пользовательского ввода).
Пожалуй, самым важным желанием было желание писать логику игры не на Java, а на каком-нибудь простом декларативном ЯП.
Также было желание реализовать возможность процедурного анимирования, а именно элементарного движения изображений, при этом чтобы можно было на уровне Java определять функцию, по которой считается текущая скорость движения (например, чтобы график скорости был прямой, либо синусоидой, либо ещё чем-то).
По задумке, всё взаимодействие с пользователем должно было производиться через систему диалогов. При этом диалогом считался не обязательно диалог с NPC или что-то подобное, а вообще реакция на любой пользовательский ввод, для которого был зарегистрирован соответствующий обработчик. Непонятно? Скоро станет яснее.
2.2. Архитектура и реализация
Учитывая всё вышесказанное, можно поделить движок на три относительно большие части, которые соответствуют одноимённым java-пакетам:
- display — содержит всё, что касается вывода пользователю любой информации (графической, текстовой и звуковой), а также приёма ввода от него. Своего рода вид (View), если говорить о MVC/MVP/etc.
- initializer — содержит классы, в которых производится инициализация и запуск движка.
- sl — содержит инструменты для работы со скриптовым языком (далее — SL).
В данном пункте я рассмотрю первые две части. Начну со второй.
В классе инициализатора есть два основных метода: initialize() и run() . Изначально управление приходит в класс лаунчера, откуда и вызывается initialize() . После вызова инициализатор анализируют переданные программе параметры (путь к директории с квестами и имя квеста для запуска), загружает манифест выбранного квеста (о нём чуть позже), инициализирует дисплей, выполняет проверку на то, является ли требуемая квестом версия языка (SL) поддерживаемой данным интерпретатором, и, наконец, запускает отдельный поток для консоли разработчика.
Сразу же после этого, если всё прошло гладко, лаунчер вызывает метод run() , который и приводит в движение фактическую загрузку квеста. Сначала находятся все скрипты, относящиеся к загружаемому квесту (о файловой структуре квеста — ниже), они скармливаются анализатору, результат работы которого отдается интерпретатору. Затем запускается инициализация всех сцен и инициализатор завершает выполнение своего потока, напоследок повесив на дисплей обработчик нажатия Enter’а. И вот когда пользователь нажимает Enter, загружается первая сцена, но об этом позже.
Файловая структура квеста следующая:
Имеется отдельная папка для квеста, в корне которой лежит манифест, а также три дополнительные папки: audio — для звукового сопровождения, graphics — для визуальной части и scenes — для скрипто, описывающих сцены.
Хотелось бы в двух словах описать манифест. Он содержит следующие поля:
- sl_version_req — версия SL, необходимая для запуска квеста,
- init_scene — имя сцены, с которой начнется квест,
- quest_name — красивое название квеста, которое отображается в заголовке окна,
- resolution — разрешение экрана, для которого предназначен квест (об этом пару слов чуть позже),
- font_size — размер шрифта для всего текста,
- font_name — имя шрифта для всего текста.
Стоит отметить, что во время инициализации дисплея, помимо всего прочего, происходил расчёт разрешения рендеринга: т. е. бралось требуемое разрешение из манифеста и ужималось в доступное для окна пространство так, чтобы:
- соотношение ширины к высоте (aspect ratio) оставалось таким, как в разрешении из манифеста,
- было занято всё доступное пространство либо по ширине, либо по высоте.
Благодаря этому разработчик квеста может быть уверен, что его изображения, например 16:9, будут показаны на любом экране именно в таком соотношении.
Также при инициализации дисплея скрывается курсор, так как в игровом процессе он не участвует.
В двух словах о консоли разработчика. Она была разработана по следующим причинам:
- Для дебага.
- Если вдруг во время игры что-то пойдёт не так, оно может быть исправлено через консоль разработчика.
В ней было реализовано всего несколько команд, а именно: вывод дескрипторов конкретного типа и их состояния, вывод работающих потоков, перезапуск дисплея и самая важная команда — exec , которая позволяла выполнить какой-либо SL-код в текущей сцене.
На этом описание инициализатора и связанных вещей заканчивается, и можно переходить к описанию дисплея.
Его финальная структура имеет следующий вид:
Из постановки задачи можно сделать вывод, что всё, что придется делать, — это рисовать изображения, рисовать текст, воспроизводить аудио.
Каким образом обычно рисуется текст/изображения в движках универсального назначения и не только? Имеется метод типа update() , который вызывается каждый тик/шаг/фрейм/рендер/кадр/etc и в котором присутствует вызов метода типа drawText() / drawImage() — таким образом обеспечивается появление текста/изображения в данном кадре. Однако как только вызов таких методов прекращается, отрисовка соответствующих вещей останавливается.
В моем случае было решено поступить немного по-другому. Так как для визуальных новелл текст и изображения имеют относительно перманентный характер, а также являются почти всем, что видит пользователь (то есть достаточно важны), они были сделаны, как игровые объекты — то есть такие вещи, которые нужно лишь заспаунить и они не исчезнут, пока их не попросишь. К тому же данное решение упрощало реализацию.
Объект (с точки зрения ООП), который описывает текст/изображение, назовём дескриптором. То есть для пользователя API движка существуют лишь дескрипторы, которые можно добавить в состояние дисплея и удалить из него. Таким образом, в финальном варианте дисплея имеются следующие дескрипторы (они соответствуют одноименным классам):
Название дескриптора: | Описание дескриптора: |
---|---|
ImageDescriptor | Дескриптор изображения: содержит изображение ( BufferedImage ), позицию на экране и ширину с высотой. Однако при создании задается лишь ширина — высота вычисляется пропорционально исходной высоте (и нет возможности вручную растянуть/сжать изображение непропорционально, поэтому это было плохим решением). |
TextDescriptor | Дескриптор текста: содержит текст, его позицию и размеры. Причем текст не масштабируется по ширине и высоте, а обрезается при выходе за границы. Текст умеет переноситься по слогам и просто по пробельным символам, а также скроллиться построчно. |
AudioDescriptor | Дескриптор аудио, который умеет воспроизводить аудио (либо один раз, либо зациклено), ставить его на паузу и снимать с неё. |
AnimationDescriptor | Дескриптор анимации, которая так же, как и аудио, может быть зациклена. Содержит объект, реализующий анимацию, и дескриптор изображения, для которого анимация и проигрывается. Главный метод — update(long) , принимающий количество миллисекунд, прошедших с последнего вызова update(long) . Их число используется для расчета текущего состояния анимации. |
InputDescriptor | Дескриптор ввода: является текстовым полем, в котором курсор находится всегда в конце текста. Также стоит отметить, что хранение и рендеринг текста из дескриптора ввода осуществляется через неявно создаваемый дескриптор текста, чтобы не дублировать логику. Забавно то, что я учел возможность нажатия Backspace, но не учел Delete; и когда во время игры Delete всё-таки был нажат, в поле появились ▯▯▯, т. к. соответствующей для Delete обработки сделано не было и символ пытался отображаться как текст. |
KeyAwaitDescriptor | Дескриптор, которому передается необходимая клавиша (из KeyEvent ) и колбэк с какой-то логикой, который будет запущен при нажатии соответствующей клавиши. |
PostWorkDescriptor | Дескриптор, принимающий колбэк, который будет вызываться после обработки каждого тика. |
Дисплей содержит также поля для текущего приемника ввода (дескриптора ввода) и поле, указывающее на то, какой текстовый дескриптор сейчас имеет фокус и чей текст будет скроллиться при соответствующих действиях со стороны пользователя.
Игровой цикл выглядит примерно так:
- Обработка аудио — вызов метода update() на аудио-дескрипторах, который проверяет текущее состояние аудио, освобождает память (при необходимости) и выполняет другую техническую работу.
- Обработка нажатий клавиш — передача введенных символов в дескриптор для приема ввода, обработка нажатий на клавиши скролла (стрелки вверх и вниз) и Backspace.
- Обработка анимаций.
- Очистка фона в буфере для рендеринга (в качестве буфера выступал BufferedImage ).
- Отрисовка изображений.
- Отрисовка текста.
- Отрисовка полей для ввода.
- Вывод буфера на экран.
- Обработка PostWorkDescriptor ‘ов.
- Кое-какая работа по замене состояний дисплея, о которой я расскажу позже (в разделе про интерпретатор SL).
- Остановка потока на динамически вычисляемое время, чтобы FPS было равно заданному (30 по умолчанию).
Примечание: Возможно, возникает вопрос «Зачем рендерить поля для ввода, если для них созданы соответствующие текстовые дескрипторы, которые рендерятся на шаг раньше?» На самом деле рендеринга в пункте 7 не происходит — происходит лишь синхронизация параметров InputDescriptor ‘а с параметрами TextDescriptor ‘а — такие как видимость на экране, позиция, размер и другие. Это было сделано, как указывалось выше, по той причине, что пользователь не управляет напрямую соответствующим дескриптору ввода дескриптором текста и вообще о нём ничего не знает.
Стоит отметить, что задание размеров и позиций элементов на экране происходит не в пикселях, а в относительных размерах — числах от 0 до 1 (схема ниже). То есть вся ширина для рендеринга — это 1, и вся высота — это 1 (причем они не равны, о чем я несколько раз забыл и позже пожалел). Также стоило бы сделать, чтобы (0,0) был центром, и ширина/высота были равны двум, но я почему-то про это забыл/не подумал. Однако даже вариант с шириной/высотой равной 1 упрощал жизнь разработчику квестов.
Пару слов о системе освобождения памяти.
Каждый дескриптор имел метод setDoFree(boolean) , который должен был вызываться пользователем, если он хотел уничтожить данный дексриптор. «Сборка мусора» для дескрипторов какого-то типа происходила сразу после обработки всех дескрипторов этого типа. Также аудио, которое проигрывалось один раз, автоматически удалялось после окончания воспроизведения. Ровно так же, как и не зацикленная анимация.
Таким образом, на данный момент можно рисовать всё, что захочется, однако это совсем не та картинка выше, на которой есть лишь фон, основной текст и поле для ввода. И тут вступает в дело обёртка над дисплеем, которой соответствует класс DefaultDisplayToolkit .
При её инициализации она как раз и добавляет в дисплей дескрипторы для фона, текста и т. д. Также она умеет показывать сообщения с опциональными иконкой, полем ввода и колбэком.
Тут всплыл небольшой баг, полное исправление которого потребовало бы переделывания половины системы рендеринга: если посмотреть на порядок отрисовки в игровом цикле, видно, что сначала рисуются изображения и только потом текст. В то же время, когда тулкит показывает изображение, он располагает его посередине экрана по ширине и по высоте. И если текста в сообщении достаточно много, то он должен частично перекрывать основной текст сцены. Однако так как фон сообщения — это изображение (полностью черное, но тем не менее), а изображения отрисовываются до текста — то один текст накладывается на другой (скриншот ниже). Проблема была частично решена вертикальным центрированием не по экрану, а по области над основным текстом. Полное решение включало бы в себя введение параметра глубины и переделывание рендереров от слова «совсем».
Пожалуй, на этом о дисплее, наконец, всё. Можно переходить к языку, весь API для работы с которым содержится в пакете sl .
3. Скриптовый язык
Примечание: Если уважаемый %USERNAME% дочитал досюда, то он молодец, и я попросил бы его не бросать это делать: сейчас будет значительно интересней, чем было до этого.
3.1. Язык
Изначально хотелось сделать декларативный язык, в котором нужно было бы лишь указать все необходимые параметры для сцены, и всё. Всю логику бы на себя брал движок. Однако в итоге я пришёл к процедурному языку, даже с элементами ООП (едва различимыми), — и это было хорошим решением, так как, по сравнению с декларативным вариантом, давало возможность многократно большей гибкости игровой логики.
Синтаксис языка продумывался так, чтобы быть максимально простым для парсинга, что логично, учитывая количество имевшегося в наличии времени.
Итак, код хранится в текстовых файлах с расширением SSF; каждый файл содержит описание одной либо больше сцен; каждая сцена содержит ноль или больше действий (action); каждое действие содержит ноль или больше операторов.
Немного пояснений по терминам. Действие — это просто процедура без возможности передачи аргументов (никоим образом не помешало в разработке игры). Оператор — внешне не совсем то, что имеется ввиду под этим словом в обычных языках (+, -, /, *), однако форма та же: оператор есть совокупность его имени и всех его аргументов.
Возможно, вы жаждете увидеть наконец исходный код на SL, вот он:
Теперь становится ясно, что такое оператор. Видно также, что каждое действие есть блок высказываний (statement) (при этом высказыванием может быть и блок высказываний), а также то, что поддерживаются однострочные комментарии (вводить многострочные не имело смысла, к тому же я и однострочными не пользовался).
Ради упрощения такое понятие, как «переменная», в язык не вводилось; как следствие, все значения, используемые в коде, — литералы. В зависимости от типа выделяются следующие литералы:
Название литерала: | Примечание: |
---|---|
Строковый литерал | Возможность экранирования кавычек и слешей (\) прилагается, также есть возможность вставить в текст перенос |
Целочисленный литерал | Поддерживает отрицательные числа |
Литерал с плавающей точкой | Поддерживает отрицательные числа |
None-литерал | В коде представлен как none |
Булев литерал | В коде — on / off для истины/лжи соответственно |
Общий литерал | Если литерал не подпадает ни под один из вышеперечисленных типов и состоит из букв английского алфавита, цифр и знака нижнего подчеркивания, он общий литерал. |
Пару слов про парсинг языка. Имеется несколько уровней «загрузки» кода (схема ниже):
- Токенизатор — модульный класс для разбиения исходного кода на токены (минимальные смысловые единицы языка). Каждый вид токенов сопоставлен с числом — его типом. Почему модульная? Потому что те части токенизатора, которые и проверяют, является ли какая-либо часть исходного кода токеном определенного типа, выделены из токенизатора и загружаются извне (из второго пункта).
- Надстройка над токенизатором — класс, который определяет внешний вид каждого типа токенов в SL; на нижнем уровне использует токенизатор. Также именно тут происходит отсеивание пробельных токенов и пропуск однострочных комментариев. На выходе даёт чистый поток токенов, который и используется в.
- … синтаксическом анализаторе (он тоже модульный), который и выдаёт абстрактное синтаксическое дерево на выходе. Модульный — потому что сам по себе умеет парсить только сцены и действия, но не умеет анализировать операторы. Поэтому в него загружаются (на самом деле, он сам их загружает в конструкторе, что не очень хорошо) модули, которые умеют парсить каждый свой оператор.
Теперь вкратце об операторах, чтобы появилось представление о функциональности языка. Изначально имелось 11 операторов, затем в процессе продумывания игры некоторые из них слились в один, некоторые изменились, и добавилось ещё 9 штук. Вот сводная таблица:
Имя оператора: | Примечание: |
---|---|
load_image | Загружает в память изображение |
load_audio | Загружает в память аудио |
set_text | Устанавливает основной текст сцены |
set_background | Устанавливает фон для сцены |
play | Проигрывает аудио (может проиграть один раз или зациклить воспроизведение) |
show | Показывает сообщение с колбэком (в SL) и опциональным изображением (вызывается после закрытия сообщения пользователем) |
tag | Устанавливает тег (метку). Его можно рассматривать как глобальную переменную с указанным именем, которая не хранит никакого значения. Это полезно в разных случаях: например, можно отмечать таким образом, нашел ли игрок ключ от двери, был ли он уже на этой локации и т. д. |
if_tag / if_tag_n | Операторы ветвления, которые позволяют выполнить что-либо в зависимости от того, установлен ли соответствующий тег. else -ветвь поддерживается. if_tag выполняется, если тег установлен, if_tag_n — наоборот |
add_dialog | Позволяет добавить диалог в сцену. О них чуть позже |
goto | Переход к другой сцене |
call | Вызов пользовательского действия |
call_extern | Вызов действия из другой сцены. |
stop_all | Остановить воспроизведение всех звуков/музыки |
show_motion | Показывает и двигает изображение из одной точки в другую за указанное время (duration) (с опциональным колбэком, вызывающимся, когда движение закончится) |
animate | Анимирует изображение, предварительно показанное через show_motion . Из опций: можно указывать тип анимации — v_motion / h_motion (вертикальное/горизонтальное движение по функции ), возможность зацикливания, указание времени (duration), за которое анимация должна воспроизвестись. Есть возможность передавать одно числовое значение (одно, т. к. нет возможности передавать переменное количество аргументов, поэтому это частично костыль) в анимацию (для каждой анимации оно означает разные вещи) и опциональный колбэк (вызывается, когда анимация проиграется). |
Операторы для работы со счётчиками — специфичными для сцены целочисленными переменными.
Имя оператора: | Примечание: |
---|---|
counter_set | Создание счетчика и инициализация его каким-либо значением |
counter_add | Добавление значения к счетчику |
if_counter <modifier> | Умеет сравнивать значения двух счетчиков/чисел/числа и счетчика. <modifier> является общим литералом и имеет форму eq/gr/ls[_n] , где eq — равны, gr — больше чем, ls — меньше чем, _n — отрицание (например, gr_n — не больше). Как видно, всё было максимально упрощено. |
Была также мысль ввести оператор return (на уровне ядра интерпретатора даже была добавлена соответствующая функциональность), однако я забыл, да он и не пригодился.
Во время разработки квеста также возникла необходимость в таймерах с колбэками, но вводить новый оператор было лень, поэтому пришлось использовать следующий костыль: запускался show_motion с движением минимальной по размерам (ширина, например, 0.01) картинки за пределами сцены и временем таймера в duration .
На этом операторы заканчиваются и хочется сказать про такую возможность, как поиск (lookup) сущностей (ресурсов времени исполнения): многие операторы требуют указания названия аудио/изображения/счётчика/диалога, которые задаются в качестве параметра в операторах load_audio / load_image / counter_set / add_dialog соответственно. Фича в том, что можно указывать не только имя сущности, но и сцену, в которой он находится, — т. е. имеется возможность взаимодействовать не только с текущей сценой, но и с любой другой. Например, так: » scene_coast.dialog_1 » — диалог dialog_1 в сцене scene_coast .
В загрузчик скриптов встроено не так много проверок на правильность SL-программы, как хотелось бы. Например, всё, что может выброситься на этапе загрузки скрипта, — это исключение о непредвиденном либо неизвестном токене. Во времени исполнения ситуация лучше: есть проверки на типы аргументов операторов (да-да, операндов), на их количество, на наличие запрашиваемой сущности во время lookup ‘а, возможно, и другие, не помню. Однако нет таких проверок времени загрузки, как вызов только существующих действий или использование только существующих сцен в goto и lookup ‘ах, а также проверки типов аргументов и многих других.
В первую очередь из-за нехватки времени и во вторую — из-за того, что анализаторы всех операторов сделаны через базовый анализатор, который умеет только брать из потока n токенов (представляющих аргументов) и строить объект оператора. При этом никакой проверки на типы аргументов или правильность их содержания не производится, потому что анализатор знает только, что ему нужно забрать из потока n токенов и всё. Проверка происходит позже, в рантайме.
Кратко о диалогах. Приведу синтаксис оператора добавления диалога:
Как я прочитал в википедии, очень важной частью любого движка для визуальных новелл с произвольным пользовательским вводом является анализатор ввода на естественном языке. Разумеется, я решил несколько упростить задачу: игра реагировала не на смысл, заложенный во фразе, а лишь на форму самой фразы, которая задавалась регулярным выражением (схема ниже).
Таким образом, если пользователь вводил текст, который совпадал с одним из регулярных выражений (первый аргумент) одного из включенных (последний аргумент) диалогов данной сцены, то выполнялся колбэк (третий аргумент). Для упрощения регулярных выражений пользовательский ввод подвергался следующей обработке: один и более пробельных символов заменялись на один пробел, боковые пробельные символы удалялись, текст переводился в нижний регистр, «ё» заменялось на «е».
Возможно, стоило сделать ещё удаление знаков препинания (знаков вопроса, в частности)
Пример регулярного выражения для диалога, обозначающее «осмотреться»:
Учитывая всё вышесказанное, можно увидеть аналогию с ООП: сцены — классы, все члены которых статичны и публичны, а действия — методы.
Полагаю, в максимальном упрощении языка был и плюс: порог вхождения в него очень низок
3.2. Интерпретатор
Рассказывать про сам интерпретатор особо нечего: он тоже модульный, модули — это «исполнители» операторов (по одному для каждого). Больший смысл имеет рассказать про специальные действия и смену состояний дисплея.
Для самого SL специальных действий не существует, но для интерпретатора они все-таки есть. Вот они:
- init — вызывается во время загрузки ресурсов и предназначен только для загрузки ресурсов, и ничего более (как уже описывалось, загрузка производится до показа первой сцены, следовательно, выполнение любых других операций небезопасно).
- first_come — вызывается, когда игрок впервые посещает данную сцену. В ней обычно устанавливается фон, основной текст, включается музыка и добавляются диалоги.
- Во время работы над игрой стало понятно, что не хватает следующего действия: come — вызывается всякий раз когда, игрок посещает данную сцену (является опциональным).
Примечание: init и first_come — вместе составляют некое подобие конструктора, что ещё раз намекает на легкий объектно ориентированный оттенок языка.
Теперь пришло время поговорить о хранении сцен в памяти. Тут есть одна ужасающая подробность: когда сцены получены из анализатора скриптов, они передаются в интерпретатор, где для них всех вызывается init -действие. Это значит, что все ресурсы всех сцен (а это совсем немало) хранятся в памяти одновременно.
Таким образом, в памяти наличествует одновременно n сцен, и для первой из них уже выполнено first_come -действие (то есть какие-то сущности уже добавлены в сцену и какие-то дескрипторы уже имеются в дисплее). Теперь нужно сделать систему переходов между сценами. Казалось бы, что здесь сложного: требуется лишь поддерживать список уже загруженных сцен, и если сцена еще ни разу не посещалась, вызывать first_come и come , иначе только come (если имеется). Но было важное требование: если в сцену переходят не в первый раз, то ее состояние должно полностью соответствовать состоянию во время последнего выхода из нее, вплоть до продолжения воспроизведение аудио с той же секунды, на которой оно было остановлено при выходе.
Это состояние состоит из состояния дисплея и состояния тулкита (обертки, которая добавляет понятие «фон», «основной текст», «поле для ввода» и т. д.). Стало понятно, что нельзя просто выбрасывать эти состояния, а их где-то надо хранить и как-то заменять. Проблема была в том, что все дескрипторы дисплея и тулкита хранились прямо в классах их представляющих (в полях), а сохранять вручную с десяток объектов никоим образом не хотелось.
На помощь пришла композиция и выделение состояния дисплея и тулкита в отдельный класс (что, вероятно, нужно было сделать сразу). Оставалась одна проблема: каким образом заменять состояния с текущего на старое? Ведь нельзя же во время игрового цикла, а именно во время итерации по спискам дескрипторов, просто взять и заменить эти списки. Поэтому тут пришлось добавить метод provideState в дисплей, в который передавался колбэк и состояние для загрузки; а после завершения очередной итерации игрового цикла это состояние заменялось и вызывался колбэк, в котором происходила замена состояния тулкита.
Ещё был нюанс с остановкой воспроизведения аудио, да так, чтобы само аудио не подумало, что оно было полностью воспроизведено, и не самоуничтожилось (так как оно автоматически очищается, если было полностью проиграно), но это решилось введением пары методов и флагов (которые, вероятно, были избыточными и заменялись уже существующими, но это не точно).
4. Разработка игры
Тут я кратко опишу саму игру и процесс её разработки. Как в 2019-м и 2018-м, я решил начать с чего угодно, кроме графики, и начал с истории.
4.1. История и разработка логики игры
Так как разрабатывалась визуальная новелла, самое важно, что было в игре, — это история. Она была написана по лавкрафтовским мотивам и рассказывала про приключения пережившего кораблекрушение человека, попавшего на очень даже обитаемый остров. В истории было разветвление сюжета (с двумя концовками), один явный и несколько скрытых вотэтоповоротов, четыре полдесятка персонажей с разветвленными кое-где диалогами, 9 локаций (сцен), на каждой можно было найти что-то интересное (не влияющее на сюжет, но дающее немного подробностей (а иногда, наоборот, вопросов) о происходящем.
Стоит сказать, что игра линейная: игрок не сможет пойти туда, куда игра не хочет в данный момент, однако сможет позже, когда игре это будет надо. Отчасти это было сделано для уменьшения количества возможных вариантов развития событий, благодаря чему разработка становилась проще и быстрее, а количество возможных багов снижалось.
Как оказалось, для разработки задуманной игры мне не понадобилось 25% (5) существующих операторов, а именно: все операторы, связанные со счётчиками; оператор анимирования ( animate ), а также оператор внешнего вызова ( call_extern ).
Помимо основной игры, для ознакомления игроков с кор-механиками игры был разработан демоуровень (скриншот ниже), который состоял из одной сцены (двух, если быть точным, но вторая — просто заглушка, типа «You won»).
4.2. Графика
Во время разработки логики и тестирования вместо изображений использовались заглушки, как видно на скриншоте ниже:
Как видно по изображению до ката, рисование не моя стихия, однако нужно было как-то обеспечить хоть какую-то графику на уровне «не отвратительно». Достигнуть его помогли следующие условия:
- Рисовалось всё на графическом планшете (4×2.23»), во многом только благодаря ему с графикой многое удалось.
- Для рисования были выбраны только три цвета: черный, почти белый и оранжевый, — что сформировало достаточно интересный стиль.
- После рисования обычными круглыми кистями использовалась обработка художественными для придания эффекта царапин/тумана/шума/реалистичности/etc.
- Персонажи рисовались без лиц, что сильно ускорило работу, при этом ничего не испортив.
- Интересно, что некоторые части фонов рисовать не было надобности. Как видно на скриншоте до ката, они скрываются черной подложкой текста, следовательно, в этой месте фон можно оставить пустым. Это также дало значительное ускорение.
5. Статистика и итоги
На разработку первой версии движка и языка (с 11 операторами) у меня ушло 30 часов 40 минут. На реализацию дополнительных 9 операторов ушло ещё 4 часа 55 минут. Разработка логики непосредственно игры (вместе с демо) потребовала 7 часов 41 минуты. Рисование графики —
4-6 часов (графика не учтена в 45 часах из заголовка).
Примечание: Время кодинга мерялось через расширение «Darkyen’s Time Tracker» для продуктов JetBrains (абсолютно не реклама).
Примечание: Я начал писать это все за 2 дня до Нового года, но не успел — релиз пришлось перенести на Рождество. Так что 45 часов из заголовка были распределены примерно на 8 дней.
Количество чистых строк кода для движка: 4777, для игры (без демо) — 637.
Примечание: Количество строк мерялось через cloc .
Целью было создать 30 минут игрового времени. В итоге прохождение (с моими подсказками) заняло: демо —
8 минут, основная игра на первую концовку —
24 минуты, на вторую (с быстрым прохождением до момента разветвления) —
8 минут. То есть план был выполнен.
Размер квеста — 232 мегабайта (так много из-за музыки, записанной в WAV).
Так как аудиосистема была реализована через javax.sound.sampled.AudioSystem , которая поддерживает только WAV и AU файлы, то был выбран WAV.
Было нарисовано 28 изображений (и ещё 3 для демо). Это была моя первая игра с адекватным аудио сопровождением — всего с просторов интернета было скачано и запихнуто в игру 17 единиц звуков/музыки.
Все жалобы на игру заключались в абсолютно не юзер-френдли интерфейсе ввода: очень часто регулярные выражения не покрывали вводимых фраз, хотя смысл этих фраз был более чем верный. То есть, например, игрок вводил «вставить ключ в дверь», а предусмотрено было лишь «вставить ключ». Однако были и моменты (один, насколько я помню), когда пользовательский ввод с первого раза совпадал с задуманным (и это было даже не «да»/»нет» или что-то очевидное).
Очевидное решение — просто выдавать игроку список вариантов того, что он может сделать. Но тут есть и очевидная проблема: вся игра сведется к методичному прокликиванию всех вариантов. Скучно. Поэтому, в итоге после обсуждения был сделан вывод, что максимально приятным и не слишком «открытым» интерфейсом был бы следующий: варианты диалогов даются лишь при диалогах с NPC, а действия с миром производятся мышью, путем клика на определенные участки экрана (например, нажать на куст — обыскать его и т.д.).
Также я считаю, что не смог адекватно раскрыть историю: остались недосказанности, непонятные моменты и даже маленькие противоречия.
Ещё один косяк — в демо. Оно было сделано слишком подробным, а именно: найти ключ, вставить ключ, открыть дверь, выйти в неё. В то время как сама игра не была настолько требовательной к описанию каждого действия. Вероятно, это создало у игроков неправильное представление о том, чего ждет от них игра, что, конечно, нехорошо.
Теперь к коду. В целом качество кода меня устраивает (хотя это ещё ни о чём особо не говорит), однако есть следующие моменты:
- Нет тестов. Совсем нет. Это плохо.
- Имхо в коде многовато колбэков. И хоть они все вроде бы очевидны и еще не превратились в колбэк хэл, но тем не менее.
- Много мелких вещей, которые разбросаны в коде тут и там.
Кстати, во время игры багов выявлено не было и всё отработало без единого краша, что приятно.
Исходный код движка может быть найден по данной ссылке на GitHub.
Демо и основная игра находится в файлах (assets) в разделе «Releases» для версии «v1.0» там же.
Всем привет! Горю желанием поделиться личным опытом работы над сценарием для мобильной игры жанра Interactive Story.
Я, вместе с небольшой командой, занимаюсь разработкой такой игры с апреля прошлого года и за это время написал для нее 43 эпизода, суммарным объемом около 200 тысяч слов. Сразу хочу уточнить, что в других компаниях я сценаристом не работал, так что данная статья — опыт одного конкретного проекта и одной конкретной истории.
Пара слов о жанре для контекста
Говоря о мобильных визуальных новеллах, я имею в виду не классические ВИ, портированные на мобилки, а такие игры как «Клуб Романтики», «Chapters», «Choices», «Love Sick», «Tabou», «Novels», «Desires», «My Love», «Secrets», «Moments», «Episode», «Maybe». Это важно понимать.
С сюжетной точки зрения, мобильные новелки — это ни что иное как новая стадия эволюции дешевых любовных романов. В случае с играми, ориентированных на женскую аудиторию (таковых в жанре большинство), всё крутится вокруг любви-отношений, а игровой процесс завязан на том, что игрок романсит интересующих его персонажей (таковые зовутся Love Interest или просто LI).
Качество сюжета в мобильных Interactive Story… ну, оно разнится. Есть как хорошие тайтлы, так и способные нанести мозгу неподготовленного читателя тяжелый урон. Вторых, само собой, больше.
Сюжет и сеттинг чаще выполняют роль фона, поэтому стабильно держат планку на уровне с фильмами категории «B». Героиня может спасать мир от инопланетного вторжения, расследовать загадочное убийство, покорять Голливуд, сражаться с вампирами, отдыхать в летнем лагере или осваивать дикий запад — это не важно, основное внимание всё равно всегда сосредоточено на ее отношениях с персонажами-LI.
Если говорить о степени интерактивности в рамках истории — она встречается в разной степени, но практически всегда ограниченная. 90% развилок меняют только несколько строчек диалога. Также есть премиум-выборы, доступные за внутриигровую валюту. Вот они как раз разблокируют большие куски дополнительного контента и могут оказать влияние на историю.
Приложение-хост чаще всего представляет собой книжную полку с набором подобных историй. История может быть как короткой (10-15 эпизодов по 15 минут), так и растянуться на несколько сезонов.
Как мы искали концепцию для игры
Изначально мы тоже собирались делать приложение «книжную полку». Я даже написал несколько пилотных эпизодов под разные жанры и сеттинги.
Для этой цели был использован веб-сервис InkleWriter — бесплатный и очень удобный инструмент для прототипирования интерактивных историй. Максимально прост в освоении, но функциональность ограничена и нет нормальной техподдержки. Для коммерческого использования не подойдет, а вот сделать быстрый прототип — самое то.
После работы над пилотами я выяснил сразу две важные вещи:
1. Американские бета-ридеры не плакали от моего английского. Это было очень позитивным моментом, потому как с такими играми целиться нужно в страны из Tier 1. Мы со своей игрой целились конкретно на Америку.
2. Я осознал, что физически не смогу заставить себя писать приторный сюжет условных «50 Оттенков Сумерек» в течение следующих шести месяцев. Мое психическое здоровье этого просто не вывезло бы.
Второй пункт являлся серьезным препятствием в работе. Вариантов решения было два: а) нанимать авторов-фрилансеров, самому перейти на редактуру; б) думать над какой-то оригинальной концепцией, с которой было бы не так больно работать. Я решил пойти по второму пути.
Есть в Англии такое мегапопулярное дейтинг-шоу, называется «Love Island». Участники — группа молодых и красивых парней и девушек — живут вместе в одной вилле и строят отношения. Чем-то может быть похоже на Дом-2 или Каникулы в Мексике (если кто помнит), с той разницей, что в английском шоу все как-то более культурно и интеллигентно. Никто не дерется, матом не ругается. Внешний вид участников то и дело наталкивает на мысль, что пластмассовый мир победил и макет оказался сильней, однако в целом смотреть на девушек в купальниках и подкачанных парней эстетически приятно.
По IP этого телешоу была сделана и выпущена одноименная мобильная игра. И во время своих исследований жанра я на эту игру наткнулся, не зная при этом о существовании самого телешоу. Я решил, что передо мной история в концепции вымышленного дейтинг-шоу, и восхитился подобной идеей.
Это ведь то, ради чего люди и играют в мобильные визуальные новеллы, в самом чистом виде! Дейтинг, отношения с другими персонажами — причем без лишней обертки из плохо прописанного сюжета в набивших оскомину сеттингах.
Вскоре я узнал о том, что Love Island — никакое не вымышленное телешоу, а вполне себе настоящее, а игра «Love Island The Game» разработана по лицензии. Сначала я разочаровался. А потом меня осенило. Получалось, что идея «истории — симуляции вымышленного телешоу» существовала только у меня в голове. Если игра по Острову Любви была жестко ограничена в плане лицензии и исходного материала, то наше вымышленное дейтинг-шоу могло с стать каким угодно, с любыми правилами, локациями и персонажами. К примеру, мы могли отправлять участников в путешествия, в новое место в каждом сезоне! (в первом сезоне наши персонажи таки провели три дня в джунглях).
Я провел дополнительный ресёрч в поисках похожих игр, но ничего подобного на рынке не обнаружил. Тогда я решил рискнуть. Так появилась концепция нашей игры Couple Up! Love Show.
Особенности написания истории про дейтинг-шоу
Пять парней и пять девушек прибывают в поместье, где им предстоит жить вместе в течение 14 дней. Основная задача каждого — найти свою идеальную пару и построить отношения. Кто остался без партнера, тот выбывает из шоу.
Ежедневный досуг участников состоит из общения, плетения интриг, времяпрепровождения с партнерами, а также совместных игр и испытаний, которые устраивают для них организаторы.
С точки зрения сценарной работы такой сеттинг предоставляет много интересных возможностей. Разберем каждую по отдельности.
1. Персонажи — они как луковицы. У них есть слои!
Если вы с первого эпизода сразу вводите девять новых персонажей (не считая протагонистки), вам стоит позаботиться о том, чтобы все они были уникальными и запоминающимися. Шаблонными болванками обойтись не выйдет.
Участникам шоу предстоит общаться друг с другом на самые разные темы, делиться историями из своего прошлого и вести беседы, достаточно увлекательные, чтобы игрок не скучал. Следовательно, каждому персонажу нужны яркие личностные черты и понятный бэкграунд, который эти черты сформировал.
Более того, каждый должен иметь цель — зачем он пришёл на шоу? Формальная цель ясна — все пришли искать любовь. Но что за этим кроется на самом деле? Кто-то хочет засветиться на ТВ и раскрутиться. У кого-то задача устроить себе веселое летнее приключение. Кто-то просто отчаялся устроить личную жизнь во внешнем мире.
Мне очень хотелось, чтобы у каждого персонажа был свой секрет. В первых эпизодах разговоры с ним раскроют только то, как этот персонаж хочет себя презентовать. Чтобы докопаться до того, что же он за человек на самом деле, вам придется установить доверительные отношения.
К примеру, есть Адам. Весельчак, шутник, душа компании. Во всех общих сценах Адам — основной источник юмора и дурашливых историй. Однако в шутках Адама периодически мелькают мрачные нотки, особенно когда разговор заходит о его семье. Если вы выберете другого партнера, настоящий Адам вам за всё прохождение так и не откроется.
2. Продвижение по сюжету или игра в симулятор бога
Когда персонажи прописаны и понятны, написание сюжета начинает отдаленно напоминать игру в какой-нибудь Sims. У вас есть группа людей в закрытом пространстве. Ситуация для всех общая, однако справляется с ней каждый по-разному. А еще каждому хочется чего-то своего.
Такое положение дел позволяет двигать сюжетную линию двумя приемами:
- Прием первый. Сценарист берет на себя роль организатора шоу, и придумывает очередное испытание для участников. То, как они будут с ним разбираться и какие конфликты в итоге возникнут — логично выстроится из особенностей их характеров.
- Прием второй. Участники сами генерируют ситуации за счет своих различий. К примеру, представим себе, что в одной паре оказываются шутник и человек, вообще не понимающий юмора. Или что двум парням нравится одна девушка. Какие ситуации могут из этого возникнуть?
3. Эффект дивана Друзей
У себя в голове я определяю историю, с которой работал, как интерактивный ситком про дейтинг-шоу. А раз ситком — значит, нужно создавать «Диван Друзей».
Не знаю, есть ли научное название у этого эффекта, но уверен, что каждый с ним знаком. Когда хочется самому оказаться по ту сторону экрана, рядом с любимыми персонажами, в том самом легендарном месте, где они неизменно собираются в каждой серии. Посидеть за столом учебного кабинета вместе с гриндейлской семеркой из «Сообщества», выпить в пабе МакЛаренс с ребятами из «Как я встретил вашу маму» или зайти в бар, где после работы отдыхают служащие бруклинского участка 9-9.
Чтобы превратить локации Поместья в такое волшебное пространство, я уделял много времени сценам досуга участников вне испытаний и любовных интриг. Например, утро седьмого дня начинается с батальной сцены подушками, а на десятый день ребята развлекались, играя в «пол — это лава».
Такие сцены дополнительно помогают героям проявить себя, дают разрядку между важными сюжетными моментами… ну, и в конце концов, это весело.
Дополнительно я старался создать впечатление, что игрока здесь ждут. Персонаж ведущего (у нас это бесплотный Голос) всегда радуется, когда игрок заходит в новый эпизод.
4. Разветвления истории
Ключевой момент всей задумки заключается в том, что игрок может выбирать своего партнера. Делать выбор фиктивным в такой игре никак нельзя, это убило бы всю ценность сеттинга.
Здесь я попал в ловушку джокера по мере того, как выстраивалась следующая логическая цепочка:
- Игрок может выбрать себе партнера;
- Все персонажи должны объединяться в пары;
- В зависимости от выбора игрока другие участники формируют разные комбинации пар;
- Все участники обладают своими целями и хотелками, поэтому на разных партнеров реагируют по-разному;
- …
- записываемся на развилочки)))))
Я начинал сезон весьма осторожно, очень боялся не справиться с объемами и запутаться. Из-за этого первые эпизоды были достаточно линейной колбасой. Но чем глубже в сезон, тем шире становилось дерево разветвлений.
Кульминация наступила в эпизоде 33, где на каплинге (ритуале объединения в пары) игрок мог потенциально сойтись с 7 разными партнерами, и на выходе формировалось 11 комбинаций, кто из участников покидает шоу.
Работа в редакторе и менеджмент развилок
Я пишу и собираю историю прямо внутри Unity в кастомном редакторе графов, который был написан на базе бесплатного фреймворка xNode. Не буду выносить в статью, как мы пришли к такому техническому решению, поделюсь только опытом использования при работе с развилками.
Отдельно отмечу, что, насколько мне известно, обычно для игр жанра Interactive Story сценарии пишутся по-другому — в цельных скриптовых доках. На сайте у мастодонта жанра, компании Episode, есть полноценный сервис для создания истории под их приложение любым желающим. Кто интересуется, можно зайти и потыкать.
Итак, есть простые развилки, которые меняют только несколько строчек диалога — с ними все понятно. Написал три варианта ответа, протянул после каждого отдельную ветку, затем свёл к общему исходу.
Можно делать такие развилки короткими, а можно делать их массивными, существенно меняя содержание разговора. Однако полноценной интерактивности в такой развилке немного, потому что игра в итоге не запомнит выбор игрока, а значит, он не окажет влияния на историю.
Чтобы запоминать выборы игрока или определенные нелинейные игровые ситуации, мы используем особый граф Branch и два вида данных: булевые и цифровые (bool и float соответственно). Для удобства я дальше буду называть bool’ы — маркерами, a float’ы — параметрами.
Маркеры регистрируют, случилось какое-то событие или не случилось. Я, будучи, сторонником ручной проработки, полагаюсь в первую очередь именно на них, потому что это позволяет персонажам в своих речах отсылаться к тем или иным событиям и упоминать выборы, совершенные ранее игроком.
Цифровые параметры удобны, чтобы отслеживать общий уровень отношений с персонажами или, например, начислять игроку очки в конкурсах и испытаниях.
К примеру, вы сказали комплимент персонажу Шарлотте. Ей это понравилось, в графе Branch мы добавляем к параметру charlotteRelationship 1 балл. В будущем мы проверим, сколько у нас баллов отношений с Шарлоттой и, в зависимости от результата, она будет с нами вежлива или груба.
Хотя цифровые параметры очевидно более системны, большинство важных развилок в истории я вешал всё же на булевые маркеры. На мой взгляд, когда персонажи могут припомнить вам какой-то конкретный поступок, который вы совершили, или сказанную вами фразу — это классно.
Во многих случаях, я комбинировал параметры и маркеры. С одной участницей игрок может полсезона собачиться и быть в очень натянутых отношениях, но затем игроку представляется возможность спасти эту участницу от выбывания из шоу. И если вы ее спасете, она это запомнит и уже не станет злиться на вас так, как раньше.
Возможные комбинации пар я менеджерил тоже на булевых маркерах. Это чрезвычайно увлекательный процесс, который выглядит приблизительно следующим образом:
Маринка выбирает первой и всегда выбирает Сережу. Создаем маркер marinkaSerezha. Главгероиня выбирает второй и может выбрать кого угодно кроме Сережи, который уже занят. Создаем маркер под каждый выбор героини. Третьей выбирает Наташка. Наташка хочет выбрать Толика. Если Толик свободен, Наташка выбирает его. Если Толика уже выбрала главгероиня, Наташка выбирает Игоря…
Так закалялась сталь
Во второй половине сезона я решил усложнить себе жизнь, и игрок теперь не только выбирал себе партнера, но и косвенно влиял на выборы других персонажей. Теперь условная Наташка могла сойтись или не сойтись с условным Олегом в зависимости от того, что ей посоветовал игрок. Комбинаций стало еще больше.
К последним эпизодам я уже абсолютно преисполнился в своем познании и с трудом понимал, что происходит. Практически все участники находились в квантовой суперпозиции, в том плане, что в половине таймлайнов этот человек выбыл из шоу, а в другой половине он отлично себя чувствует и уверенно идет к финалу.
Если бы не графы, я бы, наверное, поломался. Основной минус сценарной работы с редактором графов — громоздкость — компенсировался его основным плюсом — наглядностью. Понятия не имею, как я бы работал с таким количеством переменных и развилок в доке со скриптами, где всё идёт одним сплошным полотном.
Игрок мог закончить игру с одним из семи возможных партнеров. У каждого из них при этом был свой характер, свой бэкграунд, своя манера разговаривать. С каждым нужно было выстроить свой уникальный конфликт. Продублировать один текст семь раз было нельзя (хотя из-за нехватки времени таким методом я тоже нередко грешил).
Пожалуй, тяжелее всего психологически было писать, думая, что «вот эту ветку увидит только небольшой процент игроков». И забить или схалтурить ты себе тоже не позволяешь, ведь нужно, чтобы каждый игрок получил полноценный экспириенс в той ветке, которую он выберет.
Усложнял дело тот факт, что вторая половина сезона писалась, когда игра была уже в открытом бета-тесте. Даты релиза новых эпизодов были анонсированы, и аудитория их ждала. Последние три месяца для меня стало привычным делом сидеть двое суток подряд без сна перед очередным релизом и с горящей задницей набрасывать десятки веток, чтобы успеть к сроку.
Счастливый конец и продолжение следует
Весь сезон был написан, в общей сложности, за полгода. По ощущениям — словно жизнь прожил. Получившийся сценарный продукт пока далек от идеала и полон огрехов, но я работаю над ошибками.
В истории есть явные проблемы с обязательными для жанра элементами. К примеру, страстные постельные сцены — я так и не научился их писать, чтоб получался не кринж. Однако похоже, что отчасти эти проблемы компенсировались другими вещами, которые я заботливо вкладывал в игру: шутейками, взаимными подколами и кул стори от персонажей, мемами и отсылками к поп-культуре, возможностью выбора.
Аудитория первый сезон Couple Up! восприняла тепло. Большинству игроков, оставшихся в игре после первых эпизодов, история понравилась. Я, конечно, допускаю, что аудитории жанра Interactive Story от сюжета много и не надо, но всё равно считаю это успехом, поскольку ничего подобного раньше не делал.
Сейчас мы работаем над повышением удобства нашего внутреннего редактора и параллельно готовимся к работе над вторым сезоном с новым набором персонажей.
А еще, под новые сезоны я намерен собирать авторскую группу, так что если ты творческая личность с хорошим английским, и давно хочешь попробовать себя в качестве сценариста — пиши в личку.
Всем большое спасибо за внимание! Надеюсь, вы нашли в этой статье что-то полезное для себя.
Короткий план-экскурс, что предстоит сделать.
0) Подготовить и адаптировать игру под Андроид
1) Скачать кучу всяких утилит, программ и программулин
2) Установить их всех себе на комп
3) Настроить основные параметры
4) Собственно, конвертировать игру
5) Порадоваться результату
Далее все эти пункты будут подробно рассмотрены.
Шаг 0: Подготовительный.
Первым делом у вас должна быть готовая игра на Ren’py и желание портировать ее на Андроид. И еще учтите, что при переносе часть функций может не поддерживаться, например, разные плавные красивые переходы. Возможно, в следующих версиях это будет уже не проблема.
Предварительно вам самим в Ren’py нужно будет:
— уменьшать размеры всех картинок, возможно, также вытянуть, то есть адаптировать так, чтоб это хорошо смотрелось на устройстве
— стандартное меню Ren’py нужно переправлять, чтобы оно не переезжало
— очень рекомендую сделать кнопки покрупнее — классический размер кнопок Ren’py не умещается в размер пальца среднестатистического человека
— не забудьте сделать покрупнее шрифт, если не хотите, чтобы люди, которые будут играть в вашу игру на андроиде, напрягали свое зрение
Когда игра будет выглядеть на Ren’py так, как бы вы хотели, чтобы она выглядела на телефоне, пора приступать к следующему шагу.
Шаг 1: Скачивательный
1.1. Ставим разные программки.
Для того, чтоб эта штука работала, нужно установить Java Development Kit. Причем не для юзеров, а для разработчиков. Если нет явы, то скачать отсюда www.oracle.com/technetwork/java/javase/downloads/index.html
Питон должен быть 2.7 , тройка Питона не пойдет. Если нет Питона, то скачать отсюда python.org/download/releases/2.7.2/
Здесь качаем драйвер для получения доступа к устройству Андроида на винде: developer.android.com/guide/developing/device.html#setting-up
Все эти программулины бесплатные, так что не беспокойтесь.
1.2. Ставим RAPT и учимся с ним общаться.
Здесь качать сам этот RAPT: www.renpy.org/dl/android/
Сначала его нужно распаковать, используя архиватор. В дальнейшем всё содержимое архива, извлеченное оттуда, будет именоваться как «директория РаПта». Лучше распаковывать эту штуку туда, где нет русских путей на всякий случай и куда нибудь в корень.
Все управление идет через командную строку, мы будем обращаться к android.py. Лучше сразу выяснить, как давать соответствующие команды в вашей системе. Нужно управлять внутри «директории РаПта». По-хорошему, нужно вводить туда полный путь в командную строку.
Совет:
Можно немного схитрить, как мы сделали, и перенести cmd.exe прямо в «директорию РаПта». Тогда у нас уже на автомате при его запуске оттуда будет этот путь проставляться, где мы находимся, что облегчает сей процесс. У меня он лежал в «C:WindowsSystem32», просто берем его и копипастим в нашу директорию. И в дальнейшем будем через него отдавать команды. Однако, стоит заметить, что вам этот способ может не подойти, это зависит от вашей системы.
В любом случае, файл android.py должен быть запущен из папки с RAPT’ом (та, собственно, в которой он и лежит).
Теперь открываем cmd.exe — появился черный экранчик с командной строкой, ожидающий ваших действий. На Windows, если расширение файла .py присвоено к Python 2.7, просто напишите:
android.py test
Иначе, вам нужно прописать полный путь к Python 2.7:
C:python27python.exe android.py test
Шаг 2: Установливательный
Следующим шагом нам надо:
— проверить, что всё стоит нормально
— установить Apache Ant.
— установить Android SDK
— используя Android SDK, установить нужные пакеты
— создать ключ, который необходим для доступа в Google Play
Много действий! Но чтобы это все сделать, нужно всего лишь написать в нашей командной строке одну строчку:
android.py installsdk
RAPT даст вам знать, что он делает. Он будет также предупреждать о лицензиях и спросит, хотите ли вы создать ключ.
Важно:
Ключ, созданный RAPT’ом, имеет обычное кодовое слово. Иногда есть смысл использовать специальный софт, чтобы сгенерировать свой ключ. Потом, сохраните файл android.keyring подальше на диске, дабы никому не достался. (ну или просто в безопасном месте). Создайте его копию где-нибудь, иначе без ключа вы не сможете загружать созданные приложения. Поверьте, это нереально важно, без него игра просто не будет работать!
Совет:
В ходе сборки выяснилось, что если вы создаете свой собственный ключ, игнорируя предложение системы создать его самостоятельно, то в дальнейшем возможны сбои. Так что я все же советую для сохранения ваших нервов согласиться, чтобы утилита сама создала ключ.
Шаг три: Настраивательный.
Перед постройкой исполняемого файла, вы должны сообщить RAPT’у некоторую информацию о игре. Сию команду должен использовать ты:
android.py configure mygame
Примечание для особо одаренных: не надо вбивать слово «mygame» в командную строку! Вместо этого надо ввести путь туда, где находится ваша папка с игрой:
Вас спросят информацию об игре, и поместят её в специальном файле в указанной игровой директории. Вопросы простые и ответить на них не составит труда. Единственный вопрос, который может быть сложным для вас — это вопрос о расположении игры.
Если нужно что-то изменить, к примеру, если вы выпустили новую версию игры, вы можете перезапустить эту команду для настройки. Ваши первоначальные ответы не будут удалены, программа их «вспомнит».
Шаг 4: Собирательный
Слава Богу, теперь вы можете создать и установить исполняемый файл! Сия команда это делает:
android.py build mygame release install
Нужно немного подождать. Оно создаст версию игры для релиза, чтобы вы могли ее установить на подключенное устройство. Не забудьте посмотреть, что программа выдаст в конце, чтобы быть уверенным в успешном выполнении.
После установки, нажмите на иконку запуска на устройстве, дабы (не поверите!) запустить.
Эта команда переходит к ant tool, который создает исполняемый файл Android. Для листинга прочих команд вводим следующее: android.py build mygame help
Примечание:
Если у вас не подключено устройство или его вообще нет, как у меня, то ни в коем случае не отчаивайтесь. Вы можете поставить себе эмулятор, например Blue stacks. Чтобы запустить игру на эмуле, достаточно всего лишь два раза щелкнуть на сгенерированном apk -файле. Он будет лежать в папке bin в «директории РаПта», конечно, если вы все правильно сделали.
Шаг 5 Заключительный
Вот, собственно, и все. Со счастливым видом на лице тщательно тестируем приложение, если надо, еще раз пересобираем, повторяя четвертый шаг.
Если вдруг что-то не получается, можете задавать вопросы. Также стоит отметить, что сам автор движка и этой утилиты PyTom- хороший человек. Поэтому он готов помочь вам, если возникнут трудности. Разве что делает он это, к сожалению, на английском языке.
Удачи вам. Надеюсь, мой материал оказался полезным!