Примерно год назад вышла моя статья, которую можно назвать "первой частью" данной статьи. В первой части я насколько смог подробно разобрал тернистый путь разработчика-энтузиаста, который мне удалось когда-то самостоятельно пройти от начала и до конца. Результатом этих усилий стала игра жанра RTS "Земля онимодов" созданная мною в домашних условиях без движков, конструкторов и прочих современных средств разработки. Для проекта использовались C++ и Ассемблер, ну, и в качестве основного инструмента моя собственная голова.
В этой статье я постараюсь рассказать о том, как я решил взять на себя роль «реаниматора» и попытаться «воскресить» этот проект. Много внимания будет уделено написанию собственного игрового сервера.
Это продолжение статьи, а начало тут.
> Начало статьи: Воскрешение игры
> Конец статьи: Сеть
Немного о моем GUI
Для GUI мне пришлось создать все необходимые компоненты, которые использовала моя игра:
1) Текстовое поле / GStatic — позволяет отобразить любой текст. Дополнительно у текста есть некоторые свойства — можно в любом месте изменять цвет и шрифт текста. По началу, я не был уверен, что эта возможность мне понадобиться, но, оказалось, что она более чем полезна.
2) Поле ввода / GEdit — однострочный редактор текста. Я старался, чтобы он по своим возможно-стям был похож на аналогичный компонент Windows. Под конец даже добавил работу с буфером обмена. Вынудило меня к этому исключительно необходимость вводить лицензионный ключ.
3) Кнопка / GButton — обычная кнопка, которая реагирует на наведение на неё мышью и нажатие.
4) Графическая кнопка / GButtonImage — обычная кнопка, только её внешний вид определяется четырьмя графическими изображениями (обычное состояние, под мышью, нажата и неактивна).
5) Поле-флаг / GCheckBox — поле, которое позволяет отмечать себя галочкой по принципу ДА/НЕТ. Справа от поля указывается описание.
6) Список / GListBox — это просто список из текстовых строк. В списке можно выбрать любую строку.
7) Выпадающий список / GComboBox — компонент выглядит как поле ввода, у которого имеет дополнительная кнопка, открывающая список. Редактирование можно запретить, тогда значение поля можно будет выбирать только с помощью выпадающего списка. Вообще говоря, это достаточно «стрёмный» компонент для реализации, так как содержит в себе сразу три компонента: Поле ввода, Список и Кнопка.
8) Конфигуратор / GDiscretBox — компонент используется для настройки уровня, например, гром-кости звука или скорости движения мыши.
9) Картинка / GPicture — компонент позволяет загрузить любое количество графических изображений одинакового размера и выбирать из них любое для отображения.
10) Дерево / GTree — сложный компонент, который состоит из узлов, внутри которых могут быть другие узлы. В общем, сложность в том, что можно делать целую иерархию, которую еще и нужно отображать графически. Также есть возможность использовать иконки.
11) Всплывающее меню / GMenu — аналог меню, которое обычно появляется в Windows при нажатии правой кнопки мыши. Основная сложность заключается в возможности строить многоуровневые меню.
12) Диалоговое окно / GDialog — окно с заголовком или без заголовка для расстановки внутри него других компонентов.
13) Сообщение / GMessageBox — частный случай диалогового окна, которое конструируется автоматически по заголовку, тексту и наличию кнопок типа OK и Cancel.
14) Сцена / GScene — частный случай диалогового окна, который обычно закрывает собой всю область монитора. У сцены можно установить фоновую картинку, а поверх расставлять другие компоненты. Компонент сцены присутствует всегда. Практически, смена экранов меню является сменой разных сцен.
Некоторые компоненты обладают одной важной особенностью — они могут содержать произвольное количество информации, которая может не помещаться в окно компонента. Это касается компонентов "Текстовое поле", "Список" и "Дерево". В этом случае необходимо предусмотреть скроллирование содержимого компонента при помощи линеек прокрутки. По сложности реализация линейки прокрутки обычно превышает сложность реализации самого компонента.
Еще некоторые важные моменты в реализации GUI
- Чтобы не сильно привязываться к особенностям ОС, я создаю только одно главное окно. Потом я ловлю движение мыши по этому окну и определяю свой компонент, в который попадает мышь.
- В отличии от стандартной логики рисования в играх, когда вся сцена перерисовывается полностью, я стараюсь перерисовывать только те компоненты, которые должны быть перерисованы. Например, если мышь наехала на кнопку, то нужно перерисовать только кнопку.
- Я не стал реализовывать «честную» модальность, посчитав, что это усложнит портирование, если оно всё же понадобится. Что такое модальность? Это когда какой-то компонент захватывает мышь на себя и никакие другие компоненты на эту мышь не реагируют. У меня всё это тоже есть, но по уму внутри модального компонента должен быть собственный цикл обработки, который и держит управление, пока компонент не будет закрыт.
Пожалуй, поясню примером. В MFC я могу вывести MessageBox примерно так:
if (MessageBox("Файл C:\Temp\test.avi существует. Хотите перезаписать его ?", "Внимание", MB_OKCANCEL | MB_ICONQUESTION)==IDOK)
В результате появится такой MessageBox:
Но важно то, что пока я не нажму на кнопку "OK" или "Отмена" функция MessageBox(), которую я вызвал, не будет завершена. И это очень удобно. В моем же GUI сообщение тоже появится, мышь и клавиатура также будут реагировать только на этот MessageBox, но программа не будет ожидать, пока я его закрою — она будет использовать свой основной цикл обработки. Поэтому придется отдельно писать реакцию на нажатие кнопок "OK" или "Отмена" для каждого MessageBox-а. Это, на мой взгляд, неприятный минус моего GUI, но, по счастью, в играх они не так уж часто встречаются.
- Внутри себя GUI не пользуется понятием «сообщение» как это сделано в Windows.
- Текущая версия GUI не имеет возможностей для размещения обработчиков в родительском компоненте. Например, в Windows можно расположить на диалоговом окне кнопку и реакцию на нажатие кнопки осуществлять в классе диалогового окна. В моем случае придется писать реакцию OnLButtonClick() именно для кнопки.
- Для оформление компонентов GUI реализовано понятие "Тема". Работает "Тема" по аналогии с темами в Windows, т.е. позволяет полностью поменять внешний вид компонентов. Моя первая тема была «слизана» с Windows, и она так и называлась GThemeWindows.
Меню было вполне функционально, но выглядело оно примерно так:
Для игры я создал тему GThemeOnimod, которая выглядит куда более подходяще для моего случая. Здесь я уже стал использовать прозрачность на бордюре и прочие симпатичные штучки. Важно то, что замена темы ведет к полной замене внешнего вида всех компонентов.
О перерисовывании игрового меню
Фоновые картинки для меню я заказывал удаленно у незнакомого мне художника. На начальном этапе нас сближало только то, что ему понравилась моя игра, но потихоньку наши отношения стали вполне дружескими.
Долго не получалось принять решение по поводу того, что же всё-таки должно быть изображено на фоне меню. Так как игра предполагала противостояние двух рас, одна из которых летает на космических кораблях, а вторая до сих пор постреливает из лука, то не было полной ясности, как связать эти две концепции на фоновых рисунках. После того, как пара картинок была забракована, я вспомнил, что когда-то обсуждалась идея с «фресками». В результате «фрески» превратились в «наскальные рисунки», которые было относительно несложно нарисовать. От высоких технологий меню получило сканер, который периодически сканирует «наскальные рисунки» и обнаруживает новые. Чтобы добавить динамики, я вставил 3 декоративных компонента на правую сторону меню, которые постоянно ведут какую-то бурную деятельность, смысл которой и мне не очень ясен. Вообще идея была такой, что космонавт смотрит на вход в храм через скафандр, но, по-моему, этот визуальный эффект получить не удалось.
В момент перехода между сценами меню я постарался запрограммировать эффект «приближения». И здесь, как мне кажется, мне удалось достичь желаемой цели, по крайней мере, лично у меня создается впечатление, что «наблюдатель» движется вперед и как бы входит внутрь храма.
После того, как проблема с меню была решена, я озадачился внешним видом иконок для апгрейдов игре. Я понимал, что в таком виде это оставлять нельзя так как это — «безобразие» уж очень сильно бросается в глаза.
Чтобы было понятно, почему внешний вид существующих иконок действовал на меня очень удручающе, я привожу их внешний вид.
Как следует из первой части статьи, художники покинули проект задолго до того, как он был мною завершен, и иконками уже особо никто не занимался.
После перерисовки новый вариант этих же иконок выглядел так:
Этот вариант мне кажется вполне пригодным для применения. К слову говоря, таких иконок в игре достаточно много — примерно 80 штук. К счастью, многие из них были похожи по внешнему виду, например, 1-ый уровень брони и 2-ой уровень брони отличаются лишь некоторыми деталями и цветом фона.
Шрифты
Использовать шрифты Windows-а для меня стало теперь невозможным делом. Значит нужно было опять что-то нахимичить. Самый очевидный вариант — это заранее сгенерировать шрифт в виде картинки и отдельно в текстовый файл сохранить координаты каждого символа с этой картинки. Тогда, зная эти координаты, можно будет рисовать символы по одному. По счастью, несколько лет назад я написал небольшую утилиту, которая как раз этим и занима-лась. Утилиту можно скачать здесь.
(Эмоция: Ух, только что заметил на скриншоте слева-снизу… оказывается я это делал, аж, в 2006 году. А мне казалось что не так много времени прошло.)
Думаю, что как пользоваться утилитой примерно понятно. Выбираем шрифт и его характеристики, а также длину текстуры. Лучше в поле Кернинг вписать 1, так как иначе символы могут начать соприкасаться, что совсем нехорошо. Утилита сохраняет текстуру в виде TGA-файла, причем сам рисунок находится на альфа-канале, поэтому программы, которые не показывают альфа-канал выдадут вам чистый белый лист. И тем не менее, на альфа-канале изображение будет присутствовать.
Полученную текстуру можно наблюдать в отдельном окне. В общем просто играемся настройками, пока результат не будет более-менее устраивать и потом сохраняем текстуру.
Я использовал формат TGA, так как его можно запросто без всяких сторонних API сохранить самостоятельно.
Реанимация игры
Когда мой GUI с темой в стиле Windows более менее «начал дышать», у меня наконец-то появилась возможность начать осуществлять реанимацию игры под Windows 8/10. Но на начальном этапе у меня не было собственной сети, что не давало мне начать переделывать саму игру. Поэтому я начал осуществлять свой план с редактора карт. Ему не требовались ни сеть, ни мой пока что вялоработающий GUI, он также не использовал DirectInput, а все данные от мыши и клавиатуры принимал через стандартные сообщения Windows. Но зато он активно использовал ассемблер для рисования игровых объектов.
И было логично начать с малого, потому что я понимал, что стоит мне начать всё крушить, как потом там «концов не найдешь». В общем я опять удалил h-файлы DirectX из проекта, но теперь уже мне предстояло не просто оценить масштабы работы, но и проделать эту работу.
Первым делом я заменил все указатели на поверхности IDirectDrawSurface указателями на собственные поверхности GSurfaceOL. В класс GSurfaceOL добавил функции, которые должны были эмулировать работу поверхностей DirectDraw, только моим способом:
long BltFast(unsigned int dwX, unsigned int dwY, GSurfaceOL* lpDDSrcSurface, RECT* lpSrcRect, unsigned int dwFlags);
Такой подход сразу резко уменьшил количество ошибок выдаваемых компилятором, так как и мои поверхности, и мои функции к ним теперь существовали.
Далее я прошелся по всем оставшимся ошибкам, коих было несколько сотен, и проанализировал каждый конкретный случай. К счастью большинство ошибок было вызвано одинаковыми причинами и мне требовалось лишь аккуратно применить одно и то же решение.
В общем, как мне сейчас кажется, я потратил примерно неделю, прежде чем мне удалось выскрести из редактора все упоминания о DirectX. Далее еще какое-то время ушло на то, чтобы заработали мои функции и, наконец, я получил первый важный результат.
Ясное дело, что всё было не так просто, как я тут описал в двух словах, приходилось частенько упираться в какие-то мелкие сложности, которые очень замедляют движение вперед. Например, это скриншот с уже работающего редактора, но на нем есть дефект, который я отметил красным подчеркиванием.
На индикаторе жизни, который состоит их сегментов рисуемых ассемблером, видно, что изображение сильно смазано. Я долго тупил, пока не понял, что дело тут в финальной стадии, которую контролировал DirectX.
Как я уже говорил, я всё изображение рисовал на 16-битной собственной поверхности в ОЗУ. Так-же я создавал уже в видеопамяти через DirectX текстуру формата A1R5G5B5 размером во весь экран (сначала я использовал формат X1R5G5B5, но, к счастью, вовремя заметил, что некоторые компьютеры такой формат не понимают) Далее я выполняю операцию Lock() для текстуры DirectX-а, и копирую в неё свою текстуру из ОЗУ через функцию memcpy(). Освобождаю текстуру DirectX-а через Unlock() и, собственно, я получаю своё изображение на стандартной текстуре DirectX-а. И теперь её можно нарисовать в виде двух текстурированных треугольников. Правда, размер этой текстуры 16 бит, а разрешение экрана 24 бита. Но тут DirectX9 оказался на высоте — он без проблем выполняет конвертацию цветов в момент рендеринга треугольников. Должен признать, я, по началу, пробовал провести эту конвертацию самостоятельно, чтобы загнать данные из ОЗУ напрямую во вторичный видеобуфер, но результат по сравнению с DirectX меня не порадовал.
Итак, я начал это всё объяснять по причине смазанности изображения. Так вот… я сначала упорно искал проблему в этой цепочке, которую я описал чуть выше. А оказалось, что у меня неправильно настроена функция DirectX-а Present(), которая выполняет финальный перенос изображения из вторичного буфера в первичный.
У меня эта функция выглядела так:
device_3d->Present(NULL, NULL, NULL, NULL);
Но для оконного режима нельзя указывать NULL в первых двух параметрах, здесь должны быть указаны прямоугольники «откуда» и «куда» выполняется копирование. Я же поторопился и скопировал функцию Present() из полноэкранного решения, где было вполне нормально использовать NULL.
И такие мелочи попадаются постоянно, поэтому оценка труда программиста вообще не поддается четким временным рамкам — любая мелочь может задержать разработку на неопределнный срок. Лично я всегда следовал правилу, что «кажущийся срок нужно умножать на 3». Главная задача разработчика — это не «поскорее исправить ошибку», а «постараться при исправлении одной ошибки не создать новую». Провозившись с игрой примерно год, я даже почти не трогал руками саму игру (за исключением сетевого взаимодействия и меню), а лишь решал косвенные задачи по созданию универсальной «оболочки».
После запуска редактора, мне удалось наконец-то перейти к игре. Но здесь нужно было не просто подменять одно другим, а именно переделывать. Общий объем работы был куда более значительным. Меню, несмотря на схожесть расположения компонентов, было переделано, практически, полностью. Надеюсь, что мне удалось при этом многое улучшить.
Но… думаю, что я достаточно уже рассказал про GUI и сейчас мне надо либо превращать статью в документацию по работе с моей оболочкой, либо пока оставить эту тему до того момента, когда это может кому-нибудь понадобиться.
Зато я решил подробнее остановиться на сети. В моей прошлой статье я описал Сеть очень коротко и заострил внимание не столько на создании сетевого взаимодействия, сколько на особенностях сетевой игры применительно к RTS. Я также обратил внимание, что некоторые читатели моей прошлой статьи высказали сожаление по поводу малого объема информации на эту тему с моей стороны. Поэтому в этот раз я решил пояснить те принципы, которые я использовал для написания Интернет-сервера и сетевого клиента. На мой взгляд, материал здесь куда более «суровый» по отношению человеку, который решил «вникнуть в суть».
> Начало статьи: Воскрешение игры
> Конец статьи: Сеть
Поделиться с друзьями
Комментарии (6)
Darthman
11.05.2017 17:44Очень любопытно.
Сам проходил путь разработки своей системы GUI "с нуля". Рисовал, анимировал, программировал. Нелёгкое это дело, надо сказать.Odin_KG
11.05.2017 18:22Сам проходил путь разработки своей системы GUI «с нуля». Рисовал, анимировал, программировал. Нелёгкое это дело, надо сказать.
Это точно. Я когда пример для статьи делал еще пару ошибок исправил, хотя в игре всё работало нормально.
Shtucer
Мне в свое время более очевидным показалось использовать моноширный шрифт, а координаты символа определять несложными вычислениями.
Odin_KG
Ну, моноширный еще проще, конечно. Но я смысла в этом особого не вижу, так как в любом случае такого рода утилиты достаточно простые, а не хочется ограничивать себя в выборе шрифта.
Shtucer
Ну, просто если можно не парсить файл, я стараюсь его не парсить.
А кто-то морочится и рендерит налету. Средствами OpenGL, DirectX или еще чем-то. Так что, лучший способ, это тот, который удобен и дает приемлемый результат. Я с этим не спорю. Мне показалось, что "самый очевидный вариант" далеко не всегда очевиден. :)
Odin_KG
Если делать для себя, то проще всего бинарник с коориднатами записать и читать его потом без всякого парсинга. Просто в моем случае я сделал утилиту так, чтобы могли и другие воспользоваться, а в этом случае, нужно сделать понятнее и текстовый файл — это хороший вариант для понятности.
Именно так. Но мне удобнее, когда я могу выбрать любой шрифт, который установлен в Windows, и не думать при этом о размере символов, иначе, начинаются ограничения. Просто в эпоху ZX-Specturm все шрифты состояли из символов одинакового размера 8 x 8 пикселей, но сейчас-то такого нет.