Игровой движок Quake невероятно сильно повлиял на технологию разработки игр. Он активно лицензировался, в том числе, для использования в таких топовых играх как «Half-Life», «Call of Duty» и «Star Wars: Jedi Knights». Вероятно, наиболее серьёзное влияние оказали те многочисленные разработчики, которые научились писать игры, занимаясь его моддингом, а затем принесли с собой его принципы в другие студии, обогатив ими развитие других движков.
Большая часть того, что уже написано о движке Quake — это информация с акцентом на технологию 3D-рендеринга или многопользовательские сетевые игры. Но почти без внимания остаётся инновационная система объектов — парадигма, с опорой на которую дизайнеры уровней создают динамические взаимодействия, не прибегая к написанию кода.
В этой статье будет сделан краткий обзор системы объектов, сложившейся в Quake, и рассказано, на основе каких принципов она была спроектирована. С моей точки зрения особенно интересно, насколько сильно эта философия схожа с принципами, заложенными в основу UNIX. Оба решения можно резюмировать как системы, в каждой из которых есть одна базовая субстанция (в UNIX этот файл) и язык, на котором можно описывать сочетание простых поведений, комбинирующихся в эмерджентном порядке (оболочка).
Кисти
Прежде, чем как следует углубиться в изучение объектов, давайте обсудим другую важную техническую концепцию. Речь пойдёт о кистях. Кисть — это 3D-фигура, которая ограничена множеством из 3 или более плоскостей в трёх измерениях. Твёрдое тело определяется как объём, ограниченный этими плоскостями (также говорят о «пересечении полупространств»). Несложно продемонстрировать, что кисть обязательно должна иметь выпуклую форму.

Кисти полезны сразу по нескольким причинам:
С их помощью легко редактировать и текстурировать карту. Это особенно удобно при моделировании сооружений (архитектуры).
У них хороший набор математических свойств, благодаря чему они сильно помогают судить о столкновениях.
С их помощью удобно вычислять BSP-деревья (для двоичного разбиения пространства), что способствует эффективному рендерингу.
Кисти можно преобразовывать в трёхмерную сетку треугольников для рендеринга, добиваясь этого пересечением плоскостей. Берём одновременно 3 плоскости, решаем систему из 3 линейных уравнений на плоскости — и получаем вершину. Затем на основе полного списка вершин, лежащих в плоскости, можно собрать многоугольник.

Категории
Игровой мир в Quake подразделяется на несколько простых категорий. На самом верхнем уровне все сущности делятся на статические или динамические. Статическая составляющая — это в основном декорации и простейшие геометрические фигуры, образующие уровни. Декорации состоят преимущественно из кистей. Основная масса этого материала описывается единственным объектом worldspawn в файлах .map, хотя, некоторые другие записи в .map также являются статическими.

В идеальном игровом мире совершенно статических объектов быть не должно, так, чтобы игроки могли свободно взаимодействовать с чем угодно (кроме того, в такой ситуации становится проще редактировать уровни). Но уровни делаются статическими по соображениям, связанным с производительностью. Чтобы отображать сцену в режиме реального времени, требуется предвычислять данные, в частности, BSP-деревья и карты освещения. Обычно декорации занимают большую часть экрана, поэтому некоторое предвычисление данных делается в качестве компромисса, при котором интерактивностью частично жертвуют ради качества картинки. Например, при картировании освещения и теней/кэшировании поверхностей предвычисляются освещение сравнительно высокого качества и тени для данной сцены.
Всё остальное в игровом мире — динамическое. Все динамические сущности называются объектами и представляют собой отдельные подвижные предметы, принадлежащие к этому миру. К числу объектов относятся персонажи, пули, оружие, т.д.
Далее объекты подразделяются ещё на две категории. Первая категория — это твёрдые объекты, представляемые в виде объединения кистей. Их форму можно изменять прямо в редакторе уровней, поэтому при необходимости они могут превращаться в разные предметы, например, дверь, тумблер или платформу.

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

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

В исходном коде Quake 3 свойства хорошо объяснены. В Quake 1 всё устроено сложнее, так как объекты записываются в файлах .qc.
Каждая вещь - это отдельная единица
В компьютерных играх (как раньше, так и сейчас) распространена практика, при которой объекты и поведения образуют сложные иерархии в соответствии с объектно-ориентированным или компонентно-ориентированным проектированием. В Quake всё это отвергается, и вместо этого разработчики придерживаются принципа, подобного мантре UNIX «всё есть файл». Применительно к UNIX это означает, что программы — это файлы, данные — это файлы, сетевые интерфейсы — это файлы и даже аппаратные устройства — это файлы. Таким образом, когда все эти объекты являются сущностями одного порядка, для коммуникации между всеми ними нужен всего один интерфейс. Поскольку большинство программ просто читают и записывают данные, подход к исходным и конечным точкам как к файлам на практике очень хорош. Хотите получить ввод с аппаратного устройства? Читайте из него информацию как из файла. Так всё унифицируется.
В большинстве случаев это работает хорошо. Иногда эта концепция доводится до крайности, и появляются «специальные файлы», с которыми нужно обращаться осторожно.
В Quake все динамические компоненты являются объектами. Фактически, каждый объект является экземпляром одной и той же большой структуры, в которой содержится по полю для всего, что может понадобиться объекту в игре. Вот небольшая часть файла, по которой вы можете составить впечатление о том, что в него включается:
typedef struct
{
float modelindex;
vec3_t absmin;
vec3_t absmax;
float ltime;
float lastruntime;
float movetype;
float solid;
vec3_t origin;
...
string_t noise2;
string_t noise3;
} entvars_t;
Очевидно, что каждый объект определённого типа не использует сразу все поля. Но, поскольку сам геймплей — это минимальная часть той рабочей нагрузки, которая ложится на игры, небольшие избыточные затраты на него не являются проблемой. Объекты просто игнорируют то, что не используют.
Поскольку Quake спроектирована просто, игра пользуется примерно такими же преимуществами унификации, как и UNIX. Примитивные операции, создание, разрушение, обмен информацией — все эти операции одинаковы. Многим объектам требуется похожая физика, поэтому не составляет труда наладить между ними совместное использование этих возможностей. Все объекты необходимо объединить в сеть, поэтому алгоритм сетевой связи работает со всеми ними одинаково, а не синхронизирует каждый из типов по отдельности.
Приходится расплачиваться за то, что объекты не всегда чётко определены. Единственный способ узнать, какие именно свойства доступны и что они делают – следуйте соглашениям, заложенным в коде (или читайте исходный код).
Возможно, наибольшее достоинство данного подхода в том, что для всех снижается когнитивная нагрузка, в особенности – для проектировщиков. Когда приходится работать со сложным набором категорий, требуется по-настоящему тщательно продумывать, куда именно будет относиться каждая возможность. Приходится прилагать много усилий, чтобы рефакторить компоненты, приспосабливая их под новые возможности. В данном случае у вас нет выбора. Либо вы придумываете, какую пользу можно извлечь из объекта, либо не трогаете его. Вся функциональность переиспользуется автоматически.
Можно считать, что это общее правило. Если можно представить такой основной элемент, который унифицирует вашу систему, то, вероятно, стоит его использовать. В данном случае самое сложное — в достаточной степени хорошо понять стоящую перед вами задачу, чтобы обнаружить этот унифицирующий концепт.
Средства комбинирования
В UNIX признаётся следующий базовый факт о софте: программирование — это ресурсозатратная работа, на которую требуется много времени. Но, в то же время, компьютер должен одновременно выполнять множество различных задач. Как минимизировать объём кода, который потребуется для этого написать, одновременно выжимая из кода максимум пользы? Вот как эта проблема решается в UNIX: сначала список необходимых программ усекается до ядра, состоящего из самого важного кода. Затем ситуация упрощается ещё сильнее — регламентируется, что каждая программа должна решать всего одну простую задачу, например, извлекать строку из файла.
Каждая из этих крошечных программ сама по себе не кажется такой уж полезной. В Windows мы ожидаем от программы значительно большего, чем просто подсчитывание символов в файле! Но источник силы в UNIX — это оболочка, представляющая собой интерактивный терминал, а также язык оболочки, используемый для координации работ программ. Оболочка обеспечивает операции, то есть, помогает комбинировать функциональность различных программ между собой. Простые программы сочленяются друг с другом сложными способами. Оказывается, загрузчик файлов, HTML-конвертер и текстовый редактор можно скомбинировать — и получится веб-браузер. Зачастую программы могут быть полезны в таком качестве, о котором человек, исходно их проектировавший, никогда не предполагал.
В системе объектов Quake применяется такой же подход. Инженерия идёт медленно (в особенности при создании 3D-игр), но разработчики игр и проектировщики игровых уровней должны уметь в большом количестве выдавать контент, итерационно оттачивая идеи. Нужно обеспечить, чтобы когда игрок нажимает кнопку — открылась дверь, либо обхитрить игрока и в ответ на нажатие кнопки породить другие объекты. Всё это не сделать, не прибегая к программированию, верно?
В Quake предоставляется система событий, при помощи которой объекты могут обмениваться сообщениями друг с другом. У тех объектов, которые отправляют сообщения, есть свойство targetname, через которое можно ссылаться на другой объект. Объекты, получающие эти события, затем конфигурируются так, чтобы иметь возможность на них отвечать.

Среди объектов, производящих события, есть, например, следующие:
func_button: Твердотельный объект. Представляет собой кнопку, которую игрок может нажимать.trigger_once: Твердотельный объект. Инициирует событие, как только игрок пересекает этот объект.
Среди объектов, принимающих события, есть такие:
func_plat: Твердотельный объект. Представляет собой платформу, которая может подниматься или опускаться.func_door: Твердотельный объект. Движется, открывая или закрывая дверной проём.light_flouro: Точечный объект. Может включаться или выключаться.
Сама по себе система событий не кажется чем-то прорывным. Любой разработчик графических пользовательских интерфейсов напишет вам кнопку, к которой можно будет привязывать события. Наиболее интересно, какую роль играет в этой системе объект. Объекты могут представлять собой не только предметы, встречающиеся в игре, но и абстрактные концепции. Например, невидимый объект может опосредовать ввод и вывод. В данном случае можно задействовать логику, которая позволит фильтровать и перенаправлять события.
Можно считать, что эти объекты похожи на электронные логические вентили. Проектировщик уровней, оперируя ими, может как будто писать сценарий, выстраивая из простых частей интересные сценарии и действия. Например, на картинке есть кнопка func_button, вывод которой связан с trigger_counter. У этого trigger_counter также есть вывод, который связан с дверью. Каждый раз после того, как счётчик получает событие, значение счётчика увеличивается на единицу. Когда значение счётчика достигает некоторого порогового значения, происходит срабатывание. В данном случае это означает, что дверь откроется после того, как будут нажаты все три кнопки.

Кроме того, абстрактные объекты — это нечто большее, чем просто чистые функции. Оказывается, что у абстрактных сущностей часто есть состояние, поэтому может быть полезным моделировать их как живые части мира.
Также интересен тот факт, что абстрактные объекты занимают пространство — благодаря этому работать с ними удобнее, чем с текстовыми скриптовыми языками. Проектировщик может увязывать логику проектируемого уровня с теми элементами, которые на этом уровне находятся, и которыми приходится управлять. Если поставить trigger_counter рядом с кнопкой, то логично предположить, что два этих элемента работают вместе. Каждый, кто желает узнать, как устроен тот или иной участок уровня, может просто осмотреть его — и сделать вывод, с какие объекты нужно изучить подробнее. Форма следует за функцией.
Разделение ответственности
Рассмотрим, как можно запрограммировать 3D-кнопку для такой видеоигры как Quake. Для начала можно создать трёхмерную модель этой кнопки, добавив к ней анимацию, которая будет воспроизводиться при нажатии. Далее нужно придумать, как считывать клавиатурный ввод, который будет инициировать анимацию, а также как вызывать функцию при таком срабатывании. Всё это работает! Но что, если нужно предусмотреть для кнопки несколько вариантов внешнего вида? Можно запрограммировать несколько вариантов анимации, некоторые опции для задания модели и т.д. Что, если при нажатии на кнопку должен воспроизводиться звук? Сложность растёт с появлением каждой новой фичи.
В системе объектов Quake признаётся, что в большинстве случаев такая функциональность реализуется прямолинейно. Чисто с точки зрения геймплея, кнопка — это просто область, ассоциированная с действиями игрока и принимающая ввод с клавиатуры (большинство систем геймплея устроены гораздо проще, чем кажутся на первый взгляд). Кроме того, для воспроизведения звуков и выполнения анимации требуется написать множество разных компонентов. Давайте не будем сочетать всё это в одном объекте, а разделим на несколько. У нас будет объект, воспроизводящий звук, объект для анимации, а также объект, принимающий ввод с кнопки. Чтобы создать кнопку в игре, можно скомбинировать такую сеть игровых объектов, а затем накатить её на любую 3D-модель, в которой заложен внешний вид кнопки.
Это классическое разделение ответственности, характерное для качественно спроектированной программы. Здесь мы имеем дело с разновидностью паттерна модель-представление-контроллер, но для игр. Такой подход не только обеспечивает дополнительную гибкость при проектировании, но и требует от программиста чётко понимать, чего именно программист хочет добиться.
Как правило, в Quake декомпозиция не бывает настолько детальной (пожалуй, из-за ограниченности доступного инструментария), но описанный здесь принцип используется и другими способами, а также в других играх.
Развитие
О качестве спроектированной системы можно судить и по тому, насколько качественно она стареет. Возможно, она вполне справляется со своими исходными задачами, но так ли удачно она приспосабливается к новым потребностям и меняющимся методам? Что касается UNIX, он развивался, начиная с 1960-х, после чего породил современный Linux, составляющий костяк современного Интернета. Пожалуй, именно в Linux сосредоточены все самые интересные разработки, связанные с операционными системами. В Linux до сих пор сохранились оригинальные концепции UNIX и даже некоторые программы, происходящие непосредственно оттуда.
Как я уже говорил, движок Quake входит в состав многих современных игр. Особенно успешный пример — Source Engine от Valve, который можно считать естественным продолжением Quake в область современных технологий, где, однако, ключевые концепции Quake удалось сохранить. Да, возраст движка чувствуется, но он по-прежнему хорошо работает. В Source представлены почти все оригинальные объекты, исходно присутствовавшие в Quake (func_button, func_train, т.д.). Эти оригинальные объекты были улучшены, добавился целый набор новых объектов (вот полный список). Но в основном движок Source очень напоминает движок Quake, просто обеспечивает более подробную детализацию и предоставляет более разнообразные системы.
Вот пример, демонстрирующий, насколько изощрёнными могут становиться такие системы объектов. На следующей картинке показана интерактивная кодовая панель, которую разработал один из членов сообщества Source. Вот как она работает:
Панель даёт визуальный и звуковой отклик, если набрать на ней любой код, состоящий из четырёх цифр. Если код набран верно, то дверь открывается, звучит сигнал-подтверждение, а панель мигает зелёным. В противном случае звучит сигнал отказа, а панель мигает красным.

Как видите, в её состав входит множество объектов. Вот лишь некоторые из них:
func_button. По одному для каждой из кнопок клавиатуры. Всего 11.func_detailМодель клавиатуры.prop_dynamicМодель двери. Дверь может открываться.func_doorЛогический объект, управляющий движением двери.logic_caseЗдесь хранится последовательность из 4 ожидаемых чисел, а также информация о событиях, которые должны произойти, если код набран верно.logic_timerОбъект, сбрасывающий клавиатуру по истечении некоторого периода, в течение которого она была неактивна.Два
ambient_generic. Используются для воспроизведения звуков подтверждения или отказа.… всего 23 объекта!
Вот видите, насколько мощной может быть система объектов, впервые появившаяся в Quake!