- PVSM.RU - https://www.pvsm.ru -
[Примечание переводчика: перевод первой части этой статьи уже есть [1] на Хабре, но её автор почему-то не завершил работу.]
Рендерер Quake III стал эволюционным развитием рендерера Quake II с аппаратным ускорением: классическая часть построена на архитектуре «двоичного разбиения»/«потенциально видимых наборов», но добавлены два новых заметных ключевых аспекта:
Рендерер полностью содержится в renderer.lib
и статично связан с quake3.exe
:
Общая архитектура повторяет Quake Classic: в ней используется знаменитое сочетание BSP/PVS/карт освещения:
q3vis.exe
использует систему порталов и генерирует PVS (потенциально видимый набор) для каждого листа. Каждый PVS сжимается и хранится в файле bsp, как описано в предыдущей статье.q3light.exe
вычисляет освещение для каждого полигона на карте и сохраняет результат как текстуры карт освещённости в файле bsp.Этап мультитекстурирования и карт освещения чётко заметен, если изменить движок и отображать только одно или другое:
Текстура, нарисованная дизайнером уровня/художниками:
Карта освещения, сгенерированная q3light.exe
:
Окончательный результат при соединении с помощью мультитекстурирования во время выполнения:
Архитектура рендеринга была рассмотрена Брайаном Хуком (Brian Hook) на Game Developer Conference в 1999 году. К сожалению, видео с GDC Vault [3] уже недоступно! [Зато оно есть на youtube [4].]
Система шейдеров построена поверх фиксированного конвейера OpenGL 1.X, а потому очень затратна. Разработчики могут программировать модификации вершин, но также и добавлять проходы текстур. Это подробно рассмотрено в библии шейдеров Quake 3 Shader bible:
[5]
Многим неизвестно, что Quake III Arena была выпущена с поддержкой SMP [6] с помощью cvariable r_smp
. Фронтэнд и бекэнд обмениваются информацией через стандартную схему Producer-Consumer. Когда r_smp
имеет значение 1, рисуемые поверхности попеременно сохраняются в двойной буфер, расположенный в ОЗУ. Фронтэнд (который называется в этом примере Main thread (основным потоком)), попеременно выполняет запись в один из буферов, в то время как из другого выполняет чтение бекэнд (в этом примере называемый Renderer thread (потоком рендерера)).
Пример демонстрирует, как всё работает:
t0-t1:
t1-t2: повсюду начинаются процессы:
Заметьте, что в t2:
Этот случай (когда Renderer thread блокирует Main thread) очень часто возникает при игре в Quake III:
Продемонстрируем ограничение блокировки одного из методов OpenGL API.
После t2:
Примечание: синхронизация выполняется через Windows Event Objects [7] в winglimp.c [8] (часть с ускорением SMP внизу).
Сетевая модель Quake3 — это, без всяких сомнений, наиболее элегантная часть движка. На низком уровне Quake III по-прежнему абстрагирует обмен данными с модулем NetChannel, впервые появившимся в Quake World [9]. Самое важное, что нужно понять:
В окружениях с быстрым ритмом изменений любая информация, не полученная при первой передаче, не стоит повторной отправки, потому что она всё равно устареет.
Поэтому в результате движок использует UDP/IP: в коде нет следов TCP/IP, потому что «надёжная передача» создаёт недопустимую задержку. Сетевой стек был улучшен двумя взаимоисключающими слоями:
Но самый удивительный дизайн находится на стороне сервера, где элегантная система минимизирует размер каждой датаграммы UDP и компенсирует ненадёжность UDP: история снапшотов генерирует дельта-паркеты с помощью самоанализа памяти.
Сторона клиента в сетевой модели довольно проста: клиент каждый кадр отправляет серверу команды и получает обновления состояния игры. Сторона сервера немного более сложна, потому что она должна передавать общее состояние игры на каждый клиент с учётом потерянных пакетов UDP. Этот механизм содержит три основных элемента:
Когда сервер решает отправить клиенту обновление, он использует по порядку все три элемента, чтобы сгенерировать сообщение, которое потом передаётся через NetChannel.
Интересный факт: хранение такого количества состояний игры для каждого игрока занимает большой объём памяти: по моим измерениям, 8 МБ для четверых игроков.
Чтобы понять систему снапшотов, приведу пример со следующими условиями:
Кадр 1 сервера:
Сервер получил несколько обновлений от каждого клиента. Они повлияли на общее состояние игры (зелёного цвета). Настало время передать состояние клиенту Client1:
Чтобы сгенерировать сообщение, сетевой модуль ВСЕГДА делает следующее:
Именно это мы видим на следующем изображении.
Важнее всего здесь понять то, что если в истории клиента нет правильных снапшотов, то движок берёт для генерирования дельта-сообщения пустой снапшот. Это приводит к полному обновлению, отправляемому клиенту в 132 битах (каждому полю предшествует битовый маркер [10]): [1 A_on32bits 1 B_on32bits 1 B_on32bits 1 C_on32bits]
.
Кадр 2 сервера:
Давайте теперь переместимся немного в будущее: вот второй кадр сервера. Как мы видим, каждый клиент отправил команды, и все они повлияли на общее состояние игры Master gamestate: Client2 переместился по оси Y, поэтому теперь pos[1] равно E (синего цвета). Client1 тоже отправил команды, но, что более важно, он подтвердил получение предыдущего обновления, поэтому Snapshot1 был помечен как подтверждённый («ACK»):
Выполняется тот же самый процесс:
В результате по сети пересылается только частичное обновление (pos[1] = E ). В этом заключается красота такого дизайна: процесс всегда одинаков.
Примечание: поскольку каждому полю предшествует битовый маркер [10] (1=изменилось, 0=не изменилось), для частичного обновления из примера выше используется 36 бит: [0 1 32bitsNewValue 0 0]
.
Кадр 3 сервера:
Сделаем ещё один шаг вперёд, чтобы посмотреть, как система справляется с утерянными пакетами. Теперь мы находимся в кадре 3. Клиенты продолжают отправлять команды серверу.
Client2 потерпел урон и здоровье теперь равно H. Но Client1 не подтвердил последнее обновление. Возможно, потерялся UDP сервера, возможно, потерялся ACK клиента, но в результате его невозможно использовать.
Несмотря на это, процесс остаётся тем же:
В результате, сообщение отправляет его частично и содержит сочетание старых и новых изменений: (pos[1]=E и health=H). Заметьте, что snapshot1 может быть слишком устаревшим для использования. В этом случае движок снова использует «пустой снапшот», что приводит к полному обновлению.
Красота и элегантность системы — в её простоте. Один алгоритм автоматически:
Вы можете задаться вопросом — как Quake3 сравнивает снапшоты самоанализом… ведь в C нет самоанализа.
Ответ заключается в следующем: каждое местоположение поля для netField_t
создаётся предварительно с помощью массива и умных директив предварительной обработки:
typedef struct {
char *name;
int offset;
int bits;
} netField_t;
// используем оператор преобразования в строку для сохранения типизации...
#define NETF(x) #x,(int)&((entityState_t*)0)->x
netField_t entityStateFields[] =
{
{ NETF(pos.trTime), 32 },
{ NETF(pos.trBase[0]), 0 },
{ NETF(pos.trBase[1]), 0 },
...
}
Полный код этой части находится в MSG_WriteDeltaEntity
из snapshot.c [11]. Quake3 даже не знает, что сравнивает: он слепо использует индекс, смещение и размер entityStateFields
и отправляет по сети различия.
Углубившись в код, можно увидеть, что модуль NetChannel разрезает сообщения на блоки по 1400 байт (Netchan_Transmit
[12]), даже несмотря на то, что максимальный размер датаграммы UDP составляет 65507 байт. Так движок избегает разбивания пакетов роутерами при передаче через Интернет, потому что у большинства сетей максимальный размер пакета (MTU) равен 1500 байтам. Избавление от фрагментации в роутерах очень важно, потому что:
Хоть система снапшотов и компенсирует утерянные в сети датаграммы UDP, некоторые сообщения и команды должны быть доставлены обязательно (например, когда игрок выходит из игры или когда серверу нужно, чтобы клиент загрузил новый уровень).
Такая обязательность абстрагирована модулем NetChannel: я писал об этом в одном из предыдущих постов [9].
Один из разработчиков, Брайан Хук, написал небольшую статью о сетевой модели [13].
Автор Unlagged Нил «haste» Торонто (Neil «haste» Toronto) тоже её описывал [14].
Если предыдущие движки отдавали виртуальной машине на откуп только геймплей, то idtech3 поручает ей существенно более важные задачи. Среди прочего:
Более того, её дизайн гораздо более сложен: в нём сочетается защита/портируемость виртуальной машины Quake1 с высокой производительностью нативных DLL Quake2. Это достигается компиляцией на лету байт-кода в команды x86.
Интересный факт: виртуальная машина изначально должна была стать простым интерпретатором байт-кода, но производительность оказалась очень низкой. Поэтому команда разработчиков написала компилятор x86 времени выполнения. Согласно файлу .plan от 16 августа 1999 года [15], с этой задачей справились за один день.
Виртуальная машина Quake III называется QVM. Постоянно загружены три её части:
cgame
: получает сообщения в фазе боя. Выполняет только отсечение невидимой графики, предсказания и управляет renderer.lib
.q3_ui
: получает сообщения в режиме меню. Использует системные вызовы для отрисовки меню.
game
: всегда получает сообщения, выполняет игровую логику и использует bot.lib
для работы ИИ.Прежде чем приступить к описанию использования QVM, давайте проверим, как генерируется байт-код. Как обычно, я предпочитаю объяснять с помощью иллюстраций и краткого сопроводительного текста:
quake3.exe
и его интерпретатор байт-кода сгенерированы с помощью Visual Studio, но в байт-коде ВМ применяется совершенно другой подход:
text
, data
и bss
с экспортом и импортом символов.q3asm.exe
получает все текстовые файлы сборок и собирают их вместе в файл .qvm. Кроме того, он преобразует всю информацию из текстового в двоичный вид (ради скорости, на случай, если невозможно применить нативные преобразованные файлы). Также q3asm.exe
распознаёт вызываемые системой методы.quake3.exe
преобразует его в команды x86 (не обязательно требуется).Вот конкретный пример, начинающийся с функции, которую нам нужно запустить в виртуальной машине:
extern int variableA;
int variableB;
int variableC=0;
int fooFunction(char* string){
return variableA + strlen(string);
}
Сохранённый в модуле трансляции module.c
lcc.exe
вызывается со специальным флагом, чтобы избежать генерации объекта Windows PE и выполнить вывод в промежуточное представление. Это файл вывода .obj LCC, соответствующий представленной выше функции C:
data
export variableC
align 4
LABELV variableC
byte 4 0
export fooFunction
code
proc fooFunction 4 4
ADDRFP4 0
INDIRP4
ARGP4
ADDRLP4 0
ADDRGP4 strlen
CALLI4
ASGNI4
ARGP4 variableA
INDIRI4
ADDRLP4 0
INDIRI4
ADDI4
RETI4
LABELV $1
endproc fooFunction 4 4
import strlen
bss
export variableB
align 4
LABELV variableB
skip 4
import variableA
Несколько замечаний:
text
, data
и bss
): мы чётко видим bss
(неинициализированные переменные), data
(инициализированные переменные) и code
(обычно называемую text
)proc
, endproc
.ARGP4
, ADDRGP4
, CALLI4
...). Каждый параметр и результат передаётся в стек.import strlen
, потому что ни q3asm.exe, ни интерпретатор ВМ не обращаются к стандартной библиотеке C, strlen
считается системным вызовом и выполняется виртуальной машиной.Такой текстовый файл генерируется для каждого файла .c в модуле ВМ.
q3asm.exe
получает текстовые файлы промежуточного представления LCC и собирает их вместе в файл .qvm:
Здесь можно заметить следующее:
vmMain
, потому что это диспетчер вводимых сообщений. Кроме того, он должен находиться в 0x2D
текстового сегмента байт-кода.Снова рисунок, демонстрирующий уникальную точку входа и уникальную точку выхода, выполняющие диспетчеризацию:
Некоторые подробности:
Сообщения (Quake3 -> ВМ) отправляются виртуальной машине следующим образом:
VM_Call( vm_t *vm, int callnum, ... )
.VMCall
может получать до 11 параметров и записывает каждое 4-битное значение в байт-код ВМ (vm_t *vm
) с 0x00 по 0x26.VMCall
записывает идентификатор сообщения в 0x2A.q3asm.exe
записал vmMain
).vmMain
используется для диспетчеризации и маршрутизации сообщения к соответствующему методу байт-кода.Список сообщений, отправляемых клиентской ВМ [18] и серверной ВМ [19], представлены в конце каждого файла.
Системные вызовы (ВМ -> Quake3) выполняются так:
VM_CallInterpreted
).CALLI4
, то проверяет индекс метода в int.int (*systemCall)( int *parms )
).systemCall
, используется для диспетчеризации и маршрутизации системного вызова к нужной части quake3.exeСписок системных вызовов, предоставляемых клиентской ВМ [18] и серверной ВМ [19], находится в начале каждого файла.
Интересный факт: параметры всегда имеют очень простые типы, они или примитивные (char,int,float), или являются указателями на примитивные типы (char*,int[]). Подозреваю, что так сделано для минимизации проблем связи struct между Visual Studio и LCC.
Интересный факт: ВМ Quake3 не выполняет динамическое подключение, поэтому разработчик мода QVM не имел доступа ни к каким библиотекам, даже к стандартной библиотеке C (strlen, memset здесь есть, но на самом деле являются системными вызовами). Некоторым удавалось эмулировать их с помощью предварительно заданного буфера: Malloc in QVM [20].
Благодаря переносу функций в виртуальную машину сообщество моддеров получило гораздо более широкие возможности. В Unlagged Нила Торонто [21] система предсказаний была переписана с использованием «обратного согласнования».
Из-за такого длинного тулчейна разработка кода ВМ была сложной:
Поэтому idTech3 также имелась возможность загрузки нативных DLL для частей ВМ, и это решило все проблемы:
В целом система ВМ была очень гибкой, потому что виртуальная машина имеет возможность выполнения:
Сообщество моддеров написало системы ботов для всех предыдущих движков idTech. В своё время довольно известными были две системы:
Но для idTech3 система ботов была фундаментальной частью геймплея, поэтому её необходимо было разработать внутри компании и она должна была присутствовать в игре изначально. Но при разработке возникли серьёзные проблемы:
Источник: страница 275 книги «Masters of Doom»:
К тому же не был готов фундаментальный ингредиент игры — боты. Боты — это персонажи, управляемые компьютером. Правильный бот должен хорошо вписываться в игру, дополнять уровни и взаимодействовать с игроком. Для Quake III, которая была исключительно многопользовательской игрой, боты оказались неотъемлемой частью игры в одиночку. Они должны были стать безусловно сложными и действовать подобно человеку.
Кармак впервые решил передать задачу создания ботов другому программисту компании, но потерпел неудачу. Он снова посчитал, что все, как и он, целеустремлённы и преданы работе. Но Кармак ошибался.
Когда Грэм попытался остановить работу, обнаружилось, что боты совершенно неэффективны. Они не вели себя, как люди, и были просто ботами. Команда начала паниковать. Это был март 1999 года, и причины для страха конечно же были.
В результате над ботами работал Жан-Поль ван Ваверен (Mr.Elusive), и это забавно, ведь он написал «Omicron» и «Gladiator». Это объясняет, почему часть серверного кода ботов выделена в отдельный проект bot.lib
:
Я мог бы написать об этом, но Жан-Поль ван Ваверен (Jean-Paul van Waveren) сам написал
103-страничный труд [26] с подробным объяснением. Более того, Алекс Шампана (Alex J. Champandard) создал обзор кода системы ботов [27], в котором описывается местоположение каждого модуля, упомянутого в труде ван Ваверена. Этих двух документов достаточно для понимания ИИ Quake3.
Автор: PatientZero
Источник [28]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/razrabotka-igr/258174
Ссылки в тексте:
[1] есть: https://habrahabr.ru/post/170139/
[2] инструменте Doom3 Dmap: http://fabiensanglard.net/doom3/dmap.php
[3] видео с GDC Vault: http://www.gdcvault.com/play/1016403/The-Quake-3-Arena-Rendering
[4] youtube: https://www.youtube.com/watch?v=TFfkX_ahl94
[5] Image: http://fd.fabiensanglard.net/quake3/Q3 Shaders.pdf
[6] SMP: https://ru.wikipedia.org/wiki/%D0%A1%D0%B8%D0%BC%D0%BC%D0%B5%D1%82%D1%80%D0%B8%D1%87%D0%BD%D0%B0%D1%8F_%D0%BC%D1%83%D0%BB%D1%8C%D1%82%D0%B8%D0%BF%D1%80%D0%BE%D1%86%D0%B5%D1%81%D1%81%D0%BE%D1%80%D0%BD%D0%BE%D1%81%D1%82%D1%8C
[7] Windows Event Objects: http://msdn.microsoft.com/en-us/library/windows/desktop/ms686915(v=vs.85).aspx
[8] winglimp.c: https://github.com/id-Software/Quake-III-Arena/blob/master/code/win32/win_glimp.c
[9] модулем NetChannel, впервые появившимся в Quake World: https://habrahabr.ru/post/324804/
[10] предшествует битовый маркер: https://github.com/id-Software/Quake-III-Arena/blob/master/code/qcommon/msg.c#L1200
[11] snapshot.c: https://github.com/id-Software/Quake-III-Arena/blob/master/code/qcommon/msg.c
[12] Netchan_Transmit
: https://github.com/id-Software/Quake-III-Arena/blob/master/code/qcommon/net_chan.c
[13] написал небольшую статью о сетевой модели: http://The%20Quake3%20Networking%20Mode.html
[14] тоже её описывал: http://www.ra.is/unlagged/network.html#Q3NP
[15] .plan от 16 августа 1999 года: http://fd.fabiensanglard.net/doom3/pdfs/johnc-plan_1999.pdf
[16] syscall для клиентской ВМ: https://github.com/id-Software/Quake-III-Arena/blob/master/code/cgame/cg_syscalls.asm
[17] для серверных ВМ: https://github.com/id-Software/Quake-III-Arena/blob/master/code/game/g_syscalls.asm
[18] клиентской ВМ: https://github.com/id-Software/Quake-III-Arena/blob/master/code/cgame/cg_public.h
[19] серверной ВМ: https://github.com/id-Software/Quake-III-Arena/blob/master/code/game/g_public.h
[20] Malloc in QVM: http://icculus.org/homepages/phaethon/q3/malloc/malloc.html
[21] Unlagged Нила Торонто: http://www.ra.is/unlagged/
[22] Image: http://en.wikipedia.org/wiki/Compilers:_Principles,_Techniques,_and_Tools
[23] Image: https://sites.google.com/site/lccretargetablecompiler/
[24] Image: http://fd.fabiensanglard.net/quake3/building_a_c_based_processor.pdf
[25] Omicron: http://botepidemic.no-origin.net/gladiator/obots/obots.html
[26] 103-страничный труд: http://fd.fabiensanglard.net/quake3/The-Quake-III-Arena-Bot.pdf
[27] обзор кода системы ботов: http://aigamedev.com/open/article/quake3-engine/
[28] Источник: https://habrahabr.ru/post/330818/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.