В своей игре «Заповедник» — текстовых 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 можно почитать тут).

Суть такая:

  1. Есть базовый макрос. Он умеет:
    • через тело (то, что между тегами) получать диалог и отображать его с учётом заданного форматирования;
    • через параметры получать имя говорящего и его иконку.
  2. Код иконок я положил в объект setup.
  3. Вокруг этого макроса я сделал виджеты под каждого героя, кто будет общаться в моей игре.

Получился такой код:

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 — «не повторяйся») — один из важнейших принципов. Если ему следовать (а нарушать его почти всегда плохая идея), то:

  • уменьшается количество багов: исправил в одном месте — починилось везде;
  • вносить изменения быстрее (и, как ни парадоксально, сложнее — приходится думать о контексте вызовов, но это хорошая сложность);
  • легче искать по проекту: теперь я могу за пару секунд найти все реплики Маши или Ромки;
  • экономия времени: для инди‑разработчика это просто подарок. Часы разработки не закапываются в рутине, а тратятся на что‑то более продуктивное. Или хотя бы весёлое.

Побочный эффект: свобода экспериментов

А ещё я уже три раза переделывал отображение диалогов. Были у меня:

  • портреты персонажей;
  • цветные столбики;
  • чудовищная анимация появления текста.

Сейчас остановился на иконках. Останется этот вариант до конца — я не знаю. Но всегда можно попробовать что‑то новое! Ведь нужно поменять только в одном месте — в базовом макросе. И всё сразу обновится по всей игре. Красота!

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