В своей игре «Заповедник» — текстовых sci‑fi приключениях Маши Ежевикиной в духе Булычёва — я активно использую диалоги. Маша и Ромка постоянно болтают — по делу и просто так.
Для книги правила оформления диалогов понятны: каждая реплика — с новой строки, с чёрточки. Я попробовал делать так же в игре — и это быстро превратилось в… книгу. А мне нужна игра!
Эксперименты с диалогами
Путём нескольких экспериментов я пришёл к такому варианту: диалоги выводятся блоками с небольшими иконками. Выглядит это вот так:

Каждый такой блок — это пять (если не считать тегов SVK‑иконок) вложенных тегов. И вот тут начинается веселье.
За время экспериментов код менялся несколько раз: были у меня варианты с картинками, цветными столбиками и даже CSS‑анимацией. Всё это я тестировал на одном‑единственном диалоге. Диалогов в игре сотни. А завтра я могу захотеть внести изменения ещё раз!
И вот тут мысль о том, что придётся таскать за собой кучу тегов по всему проекту, начинает вызывать лёгкое головокружение — ну, вы понимаете, да?
Вот как выглядит код одного такого блока:
<div class="msg-box masha-box">
<div class="avatar-icon">
<svg>...</svg>
</div>
<div class="content">
<span class="name">Маша</span>
<br>
Ромка, слышишь? Это из сектора «Зелёной тишины». Там старая станция‑заповедник.
<br>
</div>
</div>
Дублирование — проклятие для проекта. Какой бы короткой история ни была, шансов её закончить примерно ноль, если код превращается в… спагетти.
Поиск решения
В своей истории я использую Twine. Для оформления — SugarCube 2, популярный формат разметки для текстовых игр на Twine. Его главное преимущество — расширяемость за счёт виджетов и макросов. Эти штуки реально спасают: позволяют закончить разработку быстрее и не посеять ошибки, копируя код по всему проекту.
Виджеты: удобно, но не для всего
Первый вариант — виджет. Это пассаж с тегом widget. В этом его сила и слабость одновременно.
Виджет отлично подходит для геймдизайна и игровых механик: он прячет часть кода и разметки внутри себя. И ему доступна вся мощь SugarCube. Но есть нюанс: это не JavaScript. Если нужно что‑то сложное — например, хитрое оформление блока или замысловатая игровая логика, — код виджета превращается в кашу. Вносить в него изменения — удовольствие ниже среднего.
Макросы: мощь и подводные камни
Макрос, с другой стороны, — это чистый JavaScript. SugarCube даёт API для работы с макросами, и возможности тут почти безграничны. Хочешь сложное оформление? Пожалуйста. Сложнейшая логика? Да запросто.
Но я, когда начал писать свой первый макрос, быстро разобрался с API, более‑менее понял, как работает jQuery, и… шарахнул .append() для контента между тегами. Вышло не очень.
На первый взгляд всё работало: диалоги отображались. Но потом начались странности:
- строчки съезжали;
- в диалогах появлялись лишние слэши;
- а когда я вставил в диалог тег от другого макроса, на экране появилось что‑то совсем некрасивое.
Причина проста: SugarCube оперирует не чистым HTML, а своей разметкой. Прямое вмешательство в DOM через jQuery вызывает конфликты — и вот уже ваш аккуратный диалог превращается в цифровой хаос.
Проблема дублирования: Маша, Ромка и кто‑то ещё
В диалоговых окнах нет особых различий. Они выглядят (и должны выглядеть) одинаково. Разница только в надписях, иконках и содержимом.
У меня сейчас в коде дубль: одинаковая структура для Маши и для Ромки. Скоро к ним подключится ещё один персонаж. Под него дублировать макрос? Нет, спасибо, я уже набегался.
Можно, конечно, написать обобщённый макрос и через параметры ему задавать, кто говорит и какие‑то параметры отображения. Это замечательно, но есть нюанс: я не хочу в тексте игры сильно завязываться на набор параметров макроса.
Я хочу вставить вот так:
<<masha>>
- Дублировать код нехорошо!
<</masha>>
или
<<romka>>
- Лишние зависимости — зло
<</romka>>
И всё. Без лишних телодвижений.
Решение: комбинация виджетов, объекта setup и обобщённого макроса
Идеального способа я не нашёл. Зато нашёл работающий — комбинацию виджетов, объекта setup и обобщённого макроса (подробнее про setup можно почитать тут).
Суть такая:
- Есть базовый макрос. Он умеет:
- через тело (то, что между тегами) получать диалог и отображать его с учётом заданного форматирования;
- через параметры получать имя говорящего и его иконку.
- Код иконок я положил в объект
setup. - Вокруг этого макроса я сделал виджеты под каждого героя, кто будет общаться в моей игре.
Получился такой код:
setup.masha_svg = '<svg>...</svg>';
setup.romka_svg = '<svg>...</svg>';
Macro.add('say', {
tags: null,
handler: function () {
var message = this.payload[0].contents;
var style = this.args[0];
var name = this.args[1];
var svg_name = this.args[2];
var container = $('<div/>', {class: 'msg-box ' + style});
container.append(
$('<div/>', {class: 'avatar-icon'}).append(setup[svg_name])
);
container.append(
$("<div/>", { class: 'content' }).append(
$("<span/>", { class: 'name', text: name })
).wiki(message)
);
container.appendTo(this.output);
}
});
<<widget masha container>>
<<say "masha-box" "Маша" "masha_svg">>
_contents
<</say>>
<</widget>>
<<widget romka container>>
<<say "romka-box" "Ромка" "romka_svg">>
_contents
<</say>>
<</widget>>
Почему это круто: принцип DRY в действии
В программировании DRY (Don’t Repeat Yourself — «не повторяйся») — один из важнейших принципов. Если ему следовать (а нарушать его почти всегда плохая идея), то:
- уменьшается количество багов: исправил в одном месте — починилось везде;
- вносить изменения быстрее (и, как ни парадоксально, сложнее — приходится думать о контексте вызовов, но это хорошая сложность);
- легче искать по проекту: теперь я могу за пару секунд найти все реплики Маши или Ромки;
- экономия времени: для инди‑разработчика это просто подарок. Часы разработки не закапываются в рутине, а тратятся на что‑то более продуктивное. Или хотя бы весёлое.
Побочный эффект: свобода экспериментов
А ещё я уже три раза переделывал отображение диалогов. Были у меня:
- портреты персонажей;
- цветные столбики;
- чудовищная анимация появления текста.
Сейчас остановился на иконках. Останется этот вариант до конца — я не знаю. Но всегда можно попробовать что‑то новое! Ведь нужно поменять только в одном месте — в базовом макросе. И всё сразу обновится по всей игре. Красота!
Так что да, идеального способа не бывает. Но бывает рабочий — тот, что позволяет не сойти с ума от дублирования кода и даёт свободу экспериментировать. И это, пожалуй, самое ценное.