Хотите узнать больше о Reset Hard? Хотите продолжить о чем-то в этой статье? Заходите на сайт или приходите пообщаться со мной прямо в Discord!

Тактическое путешествие во времени

Reset Hard — это соревновательный шутер с путешествиями во времени, в котором вам предстоит расставлять ловушки, перехитрить своих друзей и научиться делать невозможные вещи.

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

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

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

Игра должна быть полностью последовательной.

Эта последовательность диктует, что в Reset Hard никогда не должно быть момента, когда вы видите что-то неожиданное или неинтуитивное, и я говорю вам не думать об этом. Буквально все, что вы видите в игре, является механикой.

Как это сделать?

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

Это оказывает глубокое влияние на то, как я говорю о механике. Когда я объясняю, как работает путешествие во времени, у меня появляется дополнительное чувство авторитета. Я никогда не переживаю, что забыл о каком-то пограничном случае или парадоксе в игровой механике. Когда я говорю о механике, я не использую такие фразы, как «во вселенной», потому что, насколько я понимаю, в Reset Hard нет воображаемой механики или отдельных объяснений во вселенной для чего-либо.

В отличие от таких игр, как Pokemon, Animal Crossing или Amnesia, где моей целью было бы использование скрытой механики для создания глубины и предоставления игроку возможности поделиться со мной иллюзией, Reset Hard — это внутренне непротиворечивая симуляция. Игровая механика - это факты.

Это означает, что головоломки и учебные пособия внутри Reset Hard на самом деле сводятся к тому, что я учу вас тому, как работает мой код, и мы вместе изучаем последствия этого кода. И пока я остаюсь в этой области, по большей части мне не нужно сильно беспокоиться о механических «сюжетных дырах».

Теория против практики

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

Объяснение времени в Reset Hard состоит в том, что отдельные «кадры» на временной шкале содержат идеальные копии мира. Когда игрок перемещается во времени, на самом деле происходит то, что он последовательно осознает, каким будет следующий кадр. Мир Reset Hard полностью детерминирован. Если игрок перемещается назад во времени, все, что он на самом деле делает, — это переключает свое внимание на предыдущий кадр.

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

var timeline = {
   length : 30 * 60 * 10, //30 fps 10 minutes
   frames : [{ /*json serialized initial frame */ }],
   waves : [{
       frame: 0,
   }],
};

function update() {
    _.each(waves, function (wave) {
        var world = frames[wave.frame];
        var next_world = world_update(world);
        frames[wave.frame++] = next_world;
    });
}

Уровни в Reset Hard длятся максимум 10 минут, а требования к памяти для 10 минут игровой логики со скоростью 30 кадров в секунду тривиальны. Если память действительно станет проблемой в будущем, или если я просто хочу сделать игру более эффективной на младших компьютерах, я переключусь на более эффективный формат сжатия данных для состояний мира. Я не буду менять реализацию, чтобы делать что-то умное, например, добавлять частичные состояния мира или записывать ввод с клавиатуры.

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

«Но подождите», — слышу я ваш вопрос. «Если мир детерминирован, как игрок перезаписывает прошлое? Наверняка там есть какая-то хитрость реализации».

Не так!

var timeline = {
   length : 30 * 60 * 10,
   frames : [{}],
   waves : [{ frame: 0 }]
};

var meta = {
    player : {
        focus : 0
    }
};

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

Это распространяется даже на физического человека, сидящего в кресле перед компьютером.

Как действия игроков передаются в игру? Игрок существует в состоянии, которое я называю «мета-мета-время». Его действия проецируются через клавиатуру и операционную систему в мета-время (глобальное состояние игры). Вот почему никто в игре и глазом не моргнет, когда, с их точки зрения, однажды появится кто-то новый. Игрок существует в мета-мета-времени, аватар игрока проецируется в мета-время, а затем его физическое присутствие проецируется из мета-времени на обычную временную шкалу.

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

чрезмерное усложнение

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

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

Когда я разговариваю с людьми о Reset Hard, они часто отмечают, что внутренне непротиворечивая и надежная система многопользовательских путешествий во времени должна быть чрезвычайно сложной. Это не. В текущем состоянии игры около 300 строк кода посвящены работе с путешествиями во времени. Я ожидаю, что к концу разработки это может вырасти до 600–800 строк кода.

Reset Hard не имеет сложной кодовой базы.

И я могу быть уверен в том, как будет развиваться эта кодовая база, потому что я уже знаю, как она будет структурирована. Он будет структурирован так, чтобы отражать игровую механику; и я примерно знаю, как будет работать большая часть основных механик. Конечно, все будет развиваться; Я буду принимать решения, основываясь на том, что мне нравится, или на том, что добавляет дополнительные уровни стратегии. Но я знаю, что если не изменить что-то фундаментальное в игре, мне не нужно будет делать много крупномасштабных переписываний.

Предостережение

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

//Loop through entities, attach events
entityA.on('x', function (properties) {
    entityB.storeProperties(properties); //maybe I need these later
});
entityA.on('x-end', function (properties) {
    entityB.removeProperties(properties); //But I shouldn't have them now
});
//I need to be very careful about update order
//A needs to be updated before B so the events will trigger.
//Also if A ever misses an event, the world enters a broken state...
entityA.update();

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

Есть несколько причин, почему это было плохой идеей:

  • На самом деле я не знал, что вся основная игровая механика будет связана со скрытой информацией. Я создавал реализацию до того, как создал дизайн.
  • Я мог бы начать с более простой реализации (предоставив каждому объекту доступ ко всему миру) и позже сделать что-то вроде фильтрации этого состояния мира, если мне действительно нужно.
  • Реализация, которая соответствовала бы игровому процессу на данном этапе, была бы значительно сложнее, чем та, которая не соответствовала бы.
  • И, наконец, когда я проектировал уровни и разговаривал о них с игроками, я не думал в этих терминах. Таким образом, я не получил никаких преимуществ, о которых я говорил выше.

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

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

Подведение итогов

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

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

Эта статья могла бы быть намного длиннее, но я оставлю вам цитату Манасобы Танаки, ведущего аниматора The Last Guardian. Обсуждая процесс анимации ветра Guardian, интервьюер выразил удивление, что Танака сам придумал многие алгоритмы. Разве это не работа программиста?

Танака ответил:

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

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