Давайте напишем ... MMO! Часть 3: Старая добрая 2D графика

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

8 декабря 2010

Рис. 1: Надо больше мусора

Если вы загружали демо-версию, вы знаете, что текущий вид - это просто мир, выглядящий как на рисунке 1. Очевидно, что он как-то отличается от коммерческой игры... Я знаю! Нет ни здоровья, ни маны, ни состояния оружия, ни выбранной цели, ни мини-карты, ни текста квеста, ни текста в чате, ни сообщений о "входе в бой", ни открытых мешков, ни палитры заклинаний, ни окон помощи ... ни хлама по всему экрану! (см. Рисунок 2) Чтобы добавить это, нам нужна 2D-графика.

Рис. 2: Где-то подо всем этим мир

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

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

Когда я впервые занимался программированием 3D, размер текстур должен был быть кратным двум. Для такого использования текстур это будет проблемой. Если ваш экран 1920 на 1080, то следующая по величине степень двойки - 2048 на 2048. При этом почти половина данных текстуры по вертикали будет тратиться впустую. В документации к OpenGL рекомендуется придерживаться степени двойки, но это не обязательно. DirectX допускает произвольный размер текстур до заданного устройством лимита (8192 на моей машине), и ничего не говорит о производительности, кроме того, что текстуры должны быть небольшими (говорят, что 256 на 256 - это оптимально).

Так что, возможно, мы могли бы создать текстуру 1920 на 1080, и она бы отлично работала. Но я из каменного века, когда мегабайта памяти было достаточно для мейнфрейма, к которому было подключено 100 терминалов. Не знаю, смогу ли я заставить себя выделить даже 1920 на 1080 по 4 байта на пиксель = 8 294 400 байт в одном фрагменте! Даже если бы и так, это пустая трата времени. Большую часть времени оверлей будет прозрачным, и нам не нужна текстура, полная нулей. И хотя почти на каждой видеокарте есть как минимум 256 мегабайт памяти, я не уверен, что хочу потратить 8 мегабайт одним куском на оверлейную графику. Поэтому я делю ее на 256 на 256 фрагментов. В качестве оптимизации я могу выделить фрагмент только тогда, когда он мне нужен для графики.

Рис. 3: Нам нужна прозрачность

Есть еще одна проблема, которая очень неприятна. Нужно откуда-то взять двухмерную графику в виде текстуры. До сих пор мы использовали изображения, хранящиеся в JPG или GIF файлах. Это работает для большинства статических элементов, таких как индикаторы здоровья, но чтобы сделать полный набор оверлеев, который вы видите на рисунке 2, нужно построить его в коде из примитивов 2D графики. Текст чата, например, нуждается в произвольном тексте. Это невозможно сделать с любой комбинацией консервированных изображений.

Очевидно, что у Windows есть такая возможность. Мы можем создать растровое изображение, создать над ним контекст устройства, а также нарисовать строки, текст, изображения и т.д. в этом контексте. Когда мы закончили, мы вытаскиваем все байты из растрового изображения, и вот наша текстура.

За исключением того, что это будет 24-битное изображение с красными, зелеными и синими планами. В нем не будет четвертого "альфа-плана", содержащего прозрачность. Глядя на Рис. 3, видно, что мы хотим, чтобы прозрачность была повсюду - между и вокруг значков заклинания, а также в тексте чата. Мы не можем просто заполнить альфа-плоскость фиксированным значением. 50%-й чёрный должен отображаться в виде полупрозрачного чёрного прямоугольника. Нам нужны альфа-значения от 0 (полностью прозрачный) до 255 (полностью непрозрачный) для графики, которую мы рисуем. Стандартная графика Windows GDI не сможет этого сделать.

Есть более новый интерфейс GDI+, который понимает альфа-смешивание, но не так, как мы хотим. Он будет смешивать линии, текст и изображения вместе, чтобы сделать более красивое RGB изображение, а не задавать альфа-плоскость, которая нам нужна для 3D текстуры.

В Windows 7 есть еще более новый интерфейс Direct2D, который будет делать все, что мы хотим, но я не собираюсь его использовать. Во-первых, он ограничит демо-версию только для пользователей Windows 7. С другой стороны, это... отвратительно. Это из их документа "Начало работы с Direct2D":

Рис 4: У меня пальцы начинают болеть, даже когда я читаю это!
// Create a gray brush.
HRESULT hr = m_pRenderTarget->CreateSolidColorBrush(
               D2D1::ColorF(D2D1::ColorF::LightSlateGray),
               &m_pLightSlateGrayBrush
             );

D2D1_SIZE_F rtSize = m_pRenderTarget->GetSize();

// Draw a grid background.
int width = static_cast<int>(rtSize.width);
int height = static_cast<int>(rtSize.height);

for (int x = 0; x < width; x += 10)
{
  m_pRenderTarget->DrawLine(
    D2D1::Point2F(static_cast<FLOAT>(x), 0.0f),
    D2D1::Point2F(static_cast<FLOAT>(x), rtSize.height),
    m_pLightSlateGrayBrush,
    0.5f
   );
}

Microsoft, Вы действительно заставляете всех своих программистов теперь писать "static_cast(A)" вместо "(int) A"? Обязательно было называть класс точки "D2D1::Point2F"? Читая документацию, я вижу имена типа "ID2D1RoundedRectangleGeometry" Вы пытаетесь свести нас всех с ума? Нам придется набирать эти имена сотни раз!

Рис. 5: Кернинг

Другая проблема с Direct2D заключается в том, что они, кажется, еще раз ударились о текстовый движок. Теперь у него есть собственное имя - "DirectWrite". Я думаю, что здесь произошло то, что Microsoft наняла несколько типографов и спросила их, как нарисовать самый красивый текст. Это начинается с простых вещей вроде "кернинга" (см. рис. 5), где нужно нарисовать "е" в "Testing" под полоской "Т", чего не сделаешь с "h" в "Things". Оттуда все сложнее и сложнее. В новых текстовых движках буквы отрисовываются с "субпиксельным разрешением", так что все виды сглаживания применяются для того, чтобы сделать текст красивее.

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

Поэтому компания Microsoft создала этот текстовый движок и позволила вам настраивать его различными способами. Однако этот движок находится под контролем, и если нет опции сделать то, что вы хотите, то вы застряли. Я не прочитал достаточно далеко, чтобы понять, можно ли написать что-то простое, например, текстовую метку, которая задает свой размер, или область ввода текста, которая управляет собственным курсором. Вся система DirectWrite будет бесполезна для меня, если это невозможно. Может показаться маловероятным, что Microsoft сделает это с программистами, но у меня были похожие проблемы с интерфейсом GDI+, которые я никогда не мог разобрать.

Итог

В итоге я внедрил свой собственный RGB+alpha движок рисования, выделяя два битовых изображения и делая всю графику дважды. В первый раз непрозрачная графика нарисована в RGB формате. Затем цвет устанавливается серым, соответствующим нужному нам альфа-значению (от чёрного для прозрачного до белого для непрозрачного), и графика снова рисуется в альфа-формате. Я вытаскиваю байты из обоих растровых изображений, объединяю их в единую 4-х плановую текстуру, и мы готовы использовать ее на экране.

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

Новые возможности

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

Рис. 6: Полезная информация!

 

Рис. 7: Больше помощи, чем в Minecraft

 

Рис. 8: Выбери блок. Любой блок!

 

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

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

Вы также можете добавить новые типы блоков (см. "Форматы файлов" ниже), так что вы сможете повеселиться с этой демо-версией. Сделайте скриншот (F2) и разместите ссылку на него в комментариях, если вы хотите поделиться чем-нибудь.

Вы можете сохранить мир с помощью F4. Нет никаких " слотов мира", как в Minecraft. Просто сохраните ваш docs/world.txt файл, если вы получите вариант, который захотите оставить. Там есть файл defaultWorld.txt, если вам нужно восстановить оригинальную версию.

Проблема пользовательского интерфейса

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

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

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

В режиме "шутер" (без аватара) он работает как Minecraft, кнопками мыши.

Форматы файлов

По мере того, как вы будете писать свою огромную игру или другую программу, в конце концов вам придется решать, как хранить вещи. Вам нужно ответить на два вопроса: 1) насколько важна производительность? И 2) Кто будет использовать этот файл?

Ответ на вопрос №1 зависит от использования файла, а не от того, что в нем содержится. Кусок данных, который мы загружаем только один раз в начале, не является критичным для производительности (если только он не огромен). То, что мы постоянно читаем и записываем, является критичным. В общем, я ничего не могу сказать по этому поводу, кроме того, что так как системы сейчас намного быстрее, этот вопрос чуть менее важен. Что-то вроде Microsoft Word, которая пишет все свои документы в бинарных форматах, вероятно, не было бы написано так сегодня.

Ответ на вопрос №2 может показаться банальным - моя программа использует этот файл. Моя программа - единственная вещь, которая когда-либо прочтет или напишет его, конец истории. Но это редко бывает правдой.

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

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

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

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

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

Лично у меня есть ряд мелких проблем с XML, и для моих предыдущих попыток в этом проекте я внедрил свой собственный формат. Поскольку все вы смотрите на это, у меня не хватает смелости ввести новый формат, просто чтобы обращаться к нескольким домашним питомцам. Поэтому я написал простой парсер XML и добавил его в свою директорию Util. Я использую его для нескольких вариантов:

  • Параметры. Теперь в корневом каталоге есть файл options.xml, который устанавливает всевозможные опции выполнения для демо. Это позволяет мне вытащить все жестко закодированные имена файлов, которые были в коде ранее. Если вы загрузите последнюю демо-версию, вы сможете создавать новые изображения для границы выбора блоков, последовательности анимации при уничтожении блоков и т.д.
  • Курсоры. Шаблон курсора - это растр, но также нужна "точка" (x,y), указывающая на то, где находится курсор. Без формата файла, мне пришлось жестко закодировать это. Теперь в docs/cursors есть два файла, arrowCursor.xml и crossCursor.xml, которые определяют шаблоны курсора.
  • Блоки. Вместо того, чтобы жестко кодировать различные изображения текстуры блоков в демо, теперь есть файл, который их задает. В docs/blocks найдите blocks.xml, в котором перечислены изображения, используемые для составления набора блоков. Вы можете создавать собственные изображения блоков, описывать их в аналогичном XML файле и указывать на файл в options.xml.

Заметки по изображениям: Библиотека JPEG, которую я использую, древняя и не читает файлы формата с плоскостью "альфа" (прозрачность). Поэтому я указываю все прозрачные изображения как пару файлов. В опциях и других xml файлах вы увидите значение типа "texture.jpg;textureAlpha.gif" Первое изображение задает значения RGB, а второе - Alpha. Изображения могут быть в формате JPEG, GIF или BMP. Если альфа-изображение является форматом RGB, то используется красная плоскость. Размеры двух изображений должны совпадать. Так как большинство из них являются текстурами, их размер должен быть кратен двум. Практические размеры для текстур блоков или курсоров - 32 на 32, 64 на 64, 128 на 128 или 256 на 256.

Об этом цикле

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

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

Я мог бы делать шаги поменьше. Например, я мог бы выпустить раздел "Форматы файлов" отдельно, с демкой, которая не делала бы ничего нового визуально, но имела опции, с которыми можно было бы поиграть. Если бы вы скачали эту демо-версию, вы могли бы изменить шаблоны поверхностей блоков или другие текстуры. Я не думаю, что это достаточно интересно, чтобы сделать отдельную часть серии, так что я перешел к 2D графике, что приводит к более полезным функциям.

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

Я открыт для предложений. Дайте мне знать, что вы думаете в комментариях.

Демо

Новое демо The Part 3 Demo. Оно было протестировано на Windows 7, 32-bit и 64-bit, и Windows XP, 32-bit.

Так как у нас теперь есть экран помощи, просто нажмите F1 за помощью. Нажмите ESC, чтобы выйти из демо.

Мы не делаем ничего амбициозного с графикой, так что если вы можете запустить любую 3D игру, вы должны быть в состоянии запустить демо. Если вы получаете сообщение об ошибке об отсутствии "d3dx9_42.dll", вам нужно обновить версию DirectX.

Исходники

Качайте архив The Part 3 Source с C++ кодом, дорожную карту к исходникам и каталог сборки. Сюда входит исполняемая демо-версия и необходимые файлы.