Давайте напишем ... MMO! Часть 15: Огромные пространства

Опубликовано NowhereMan -

24 марта 2011

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

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

Я также должен признать, что есть причина, по которой я вышел на пенсию и живу на инвалидности - мое здоровье не очень. Если бы я мог работать по 40 часов в неделю, у меня все еще была бы работа в Кремниевой Долине. В среднем я работаю по 20 часов в неделю. Иногда 30-часовые недели прерываются 5-ти часовыми, как последняя.

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

Чанки

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

Полностью процедурный мир просто генерировал бы ландшафт на лету, с любой необходимой детализацией. В конце концов, мой астероидный фон будет работать таким образом. Небо, солнце и кольцо будут зафиксированы, далекий астероидный пейзаж будет процедурным, и из базы данных будет поступать только близлежащий ландшафт.

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

В этой части я просто заинтересован в рендеринге большого мира, поэтому я импортировал больше данных Minecraft и использовал подобную организацию БД. Где в тестовом примере я рендерил блок 128 на 128, теперь я рендерю ландшафт 1024 на 1024 на 128. Данные разбиты на куски. Куски загружаются фоновыми потоками в программу и рендерятся в потоке пользовательского интерфейса.

Minecraft использует чанки размером 16 на 16 на 128. В моей игре мир не будет состоять из кубиков. Будет многоугольный пейзаж, с кубическими зданиями на вершине. На данный момент я использую чанки размером 32 на 32 на 32 кубика. Это то же самое количество кубов, что и чанк в Minecraft (32K). Это достаточно небольшое количество данных для загрузки и отображения.

Ресурсы

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

Дисковое пространство

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

В демо для этой части, или в любой игре от одного человека, это на самом деле не рассматривается. Весь мир БД на диске. Единственное, что мы можем сделать, это попытаться сжать наши данные лучше.

Память

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

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

Процессор

Использование процессора будет серьезным вопросом. Как мы видели, отрисовка прозрачных данных в правильном порядке требует значительного процессорного времени. В тесте на скорость вывод прозрачного списка вершин занимал около 10 мс. Так как для рендеринга со скоростью 60 кадров в секунду требуется всего 16 мс, то это действительно большое количество. В конечном счете, в игре будет много других объектов, таких как аватары и твари, а еще скрипты.

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

Графическая память

В настоящее время вершина - это позиция (x,y,z), нормаль (x,y,z) и координаты текстуры (u,v, и индекс текстуры). Это девять чисел с плавающей точкой, или 36 байт на вершину. Плюс в индексном буфере будет как минимум еще 4 байта, чтобы вызвать эту вершину. В тесте скорости было 697 576 вершин, поэтому мы использовали около 28 мегабайт памяти дисплея для хранения этих данных.

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

Это несколько проблематично в реализации. Я понятия не имею, как драйвер устройства управляет графической памятью. Хранит ли он список вершин в памяти одним блоком? В этом случае память будет фрагментирована и не позволит переместить новый список вершин в видеокарту? Если это произойдет, то OpenGL просто будет хранить его в системной памяти, но производительность резко упадет.

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

Я также надеюсь, что шейдеры значительно сократят использование памяти. В конце концов, мои координаты текстуры не являются произвольными значениями u,v. Я просто рендерю целую текстуру на лицевой стороне куба, поэтому координаты всегда будут одна из (0,0), (0, 1), (1, 0), (1,1). Я должен уметь просто послать один байт, чтобы указать, в каком углу вершина, и пусть шейдер преобразует ее в реальные u,v. Окончательное z-значение текстурной координаты - это целочисленный индекс в текстурном массиве, который имеет менее 256 записей. Таким образом, вместо 12 байт на вершину текстуры я смогу сделать это за 2 байта.

То же самое касается и моих нормалей. Они всегда обращены наружу от куба, поэтому возможных значений всего шесть - (1,0,0), (0,1,0), (0,0,1) и т. д. Более умный шейдер мог бы сократить их с 12 байт до одного байта.

Кроме того, вы можете помнить, что каждый куб имеет 24 возможные вершины. Это объясняется тем, что нормаль сочетается с вершиной. Точка, обращенная вверх, не совпадает с точкой, обращенной наружу, то есть каждый угол образует три вершины. Если я скажу шейдеру, какую из шести сторон он отображает, сделав нормаль вычисляемой в шейдере, то количество вершин на куб уменьшится с 24 до 8.

Наконец, если я смогу заставить шейдеры понимать кубы, а не треугольники, то каждый куб действительно может быть указан тремя байтами (целочисленные координаты относительно начала), один байт для видимости поверхности и один байт для типа (для выбора текстур из таблицы.) Вместо 24 вершин по 40 байт каждая (960 байт) на куб, мы будем иметь 5 байт на куб.

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

GPU

Удивительно, но GPU даже не напрягается при рендеринге всех моих 128 на 128 кубов. Если бы не использование CPU для работы с этими прозрачными данными, мы бы летали на всех платформах, даже с интегрированной графикой.

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

Что хорошего в быстрой видеокарте?

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

Это то, чего я хочу? Или было бы лучше, если бы люди с плохими дисплеями просто получали более низкую частоту кадров, или менее детальное изображение, но при этом имели такое же расстояние обзора? Я знаю игроков в WoW, которые жалуются, что города непригодны для использования из-за низкой частоты кадров. Будет ли лучше, если все остальные игроки в городах будут заменены на фигурки из палок или убраны полностью? Что вы думаете?

Так как я все равно не знаю, как запрашивать возможности видеокарты или адаптироваться к скорости каждой системы, я просто использовал фиксированное расстояние просмотра. В демо-версии нажмите клавиши плюс и минус (- и =, на самом деле, так что вам не придется нажимать shift), чтобы изменить расстояние отображения. Значение по-умолчанию прописано в параметре "viewDistance" файла "options.xml" в единицах мира. Так как отрисовываются целые куски, то на самом деле вы увидите немного дальше.

Видео

Демо отлично работает несколько минут, затем падает где-то в недрах OpenGL. Я все еще его отлаживаю. Тем временем, вот видео:.

{"preview_thumbnail":"/sites/default/files/styles/video_embed_wysiwyg_preview/public/video_thumbnails/ccch3W3PXxI.jpg?itok=AjABwwLE","video_url":"https://youtu.be/ccch3W3PXxI","settings":{"responsive":1,"width":"854","height":"480","autoplay":0},"settings_summary":["Embedded Video (Адаптивный)."]}

Update

Я работал над одним и тем же куском кода всю неделю, так что я просто продолжу эту часть, а не буду начинать другую. Это была неудачная неделя!

Многопоточность

Поскольку у нас в процессоре несколько ядер, имеет смысл использовать в программе несколько потоков. А в фоновом режиме у меня есть две отличные работы: 1) загрузка чанков с диска и 2) создание вершинных списков, используемых для отрисовки чанков.

С другой стороны, отладка многопоточных программ - огромная головная боль. Во-первых, (во всяком случае, под Visual C++) отладчик становится намного менее полезным. По мере того, как вы проходите один шаг через кусок многопоточного кода, отладчик постоянно переключается на другие потоки, что очень сложно отслеживать.

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

Чтобы обойти это, я обычно пишу свои потоки, чтобы они делали работу маленькими кусочками. Затем для отладки логики я вызываю все эти "рабочие" процедуры одну за другой, из одного потока, имитируя многопотоковое использование. Например, в демо, я вызываю рабочие процедуры после каждого обновления экрана.

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

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

Оказывается, это была просто утечка памяти. Я создавал вершинные объекты, но никогда не освобождал их. После того, как суммарное использование памяти приблизилось к двум гигабайтам, выделение начинает давать сбой, как в моем коде, так и (я думаю) где-то внутри OpenGL. Это довольно быстро сломало библиотеку (хотя и не сразу, что удивительно!) В любом случае, это было легко исправить, как только я понял, что происходит.

Многопоточный OpenGL

Когда не-потоковая версия работала хорошо, я создавал потоки и вызывал свои рабочие процедуры. Конечно, это не сработало.

На самом деле, он отлично работал под DirectX, который является потокобезопасной библиотекой. Под OpenGL она не работала, несмотря на то, что я создал блокировку (мьютекс) над вызовами графики. Поиск в Google показал некоторые страницы, которые говорили, что я должен просто задавать текущий контекст рендеринга (используя wglMakeCurrent) перед каждой партией графических вызовов. Я не мог заставить это работать.

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

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

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

Я взломал решение только для того, чтобы убедиться, что эта техника действительно работает и стоит того. Я создал все контексты и поделился списками так, как рекомендовала страница, получил хорошие коды возврата со всего, заставил OpenGL создавать объекты в рабочих потоках, и попытался нарисовать их в основном потоке рендеринга. Но это все равно не сработало!

В этот момент мне надоело с этим возиться. Я потратил пару дней на борьбу за то, чтобы заставить OpenGL сделать то, что DirectX делал вообще без проблем, и я был раздражен.

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

Немного подсуетясь, я реструктурировал код, чтобы реализовать это. На самом деле, мне не очень нравится эта версия, как предыдущая. Она хрупкая, и читая код на верхнем уровне, не так понятно, когда что-то происходит. Но он делает все вызовы OpenGL в главном потоке, и (наконец-то!) это сработало.

Затем я запустил все, используя 5 рабочих потоков одновременно, чтобы максимизировать производительность моего 6-ядерного процессора. Это не сработало... Ох. Оказалось, что я забыл кое-что о своем собственном коде: чтобы ускорить Octrees, я сам управляю распределением узлов дерева. Этот механизм распределения был разделен между всеми экземплярами Octree, и он был небезопасен для потоков. Исправление, которое дало мне рабочую 6-ядерную версию, в которой я мог бродить.

Раньше я говорил, что не буду управлять системной памятью, так как мир не использует более 120 мегабайт. Оказалось, что это неправда. Сейчас я поддерживаю немного состояния на каждом узле Octree, и они добавляют гораздо больше памяти, чем я ожидал. Я решил добавить лимит системной памяти и удалять объекты при превышении лимита памяти.

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

На данный момент работает только версия Windows. Я добавлю другие версии позже, и исходный код, когда все три платформы снова будут работать.

Обновление 2: Исходник и демо доступны для всех трех платформ.

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

Демо

Файл мира был заменен каталогом, содержащим образец из 3300 блоков. В сжатом виде это около 25 мегабайт. Так как некоторые из вас захотят попробовать это на нескольких платформах, я выделил мир в отдельный zip-файл.

Качайте Часть 15 Демонстрационный мир. Распакуйте его в тот же каталог, что и Демо. Каталог "world" должен быть рядом с "docs" и "options.xml". Или вы можете отредактировать атрибут "worldDir" в "options.xml", чтобы указать его.

Для Windows, качайте The Part 15 Demo - Windows. Я вернул 2D-графику в демо, так что вы можете просто нажать F1.

Для Linux, качайте The Part 15 Demo - Linux.Поддержки двумерной графики в Linux пока нет.

Для Mac, качайте The Part 15 Demo - Mac. На Mac пока нет поддержки двухмерной графики.

Если программа падает, то в каталоге демо вы найдете файл трассировки под названием "errors.txt". Пожалуйста, пришлите мне этот файл по адресу: mailto:mgoodfel@sea-of-memes.com">mgoodfel@sea-of-memes.com.

Исходники

Качайте архив The Part 15 Source с исходниками всех трех версий. В отличие от предыдущих частей, эта не содержит собранные версии демо. Тут есть каталог docs и файл options.xml. Используйте совместно с каталогом world, который скачивается отдельно.