Примерно год назад вышла моя статья, которую можно назвать "первой частью" данной статьи. В первой части я насколько смог подробно разобрал тернистый путь разработчика-энтузиаста, который мне удалось когда-то самостоятельно пройти от начала и до конца. Результатом этих усилий стала игра жанра RTS "Земля онимодов" созданная мною в домашних условиях без движков, конструкторов и прочих современных средств разработки. Для проекта использовались C++ и Ассемблер, ну, и в качестве основного инструмента моя собственная голова.
В этой статье я постараюсь рассказать о том, как я решил взять на себя роль «реаниматора» и попытаться «воскресить» этот проект. Много внимания будет уделено написанию собственного игрового сервера.
Продолжение статьи: GUI
Конец статьи: Сеть
Короткое видео я прилагаю к статье, чтобы сразу было понятно о чем идет речь:
Вступление
Вся эта история уходит корнями в 1998 год, когда мир IT был совсем иным. Игра, естественно, изначально проектировалась на существующие в тот момент условия. В частности, я бы сейчас навряд ли стал использовать ассемблер для вывода графики, но в тот момент это мне казалось чуть ли ни единственным решением. Если кому-то интересно, как всё это работает (игровая механика, AI и прочее) и каким было российское «издательство» игр в конце прошлого века, отсылаю вас к первой части статьи.
Также существует отдельное описание алгоритма поиска пути, который я когда-то разработал для своей RTS. Этой статье уже более 10 лет и писалась она, скорее, «для себя», но писалась достаточно подробно, чтобы я сам мог вспомнить как всё это работает. Использованное решение, на мой взгляд, обладает высокой эффективностью с точки зрения скорости работы и гарантированно строит путь до цели на клеточном поле с любой степенью сложности расположения препятствий. Ознакомиться с этим способом можно тут.
Почему я решил продолжить всю эту «эпопею»? Мне всегда было больно осознавать, что работа такого масштаба была мною в своё время просто похоронена по экономическим причинам. И когда мне показалось, что есть хоть какой-то шанс дать этой игре вторую попытку, то я, естественно, попытался это сделать. Последний год своей жизни я почти полностью посвятил этому вопросу. Благодаря в основном поддержке читателей первой части статьи, игра прошла Greenlight, и я решил со свойственной мне целеустремленностью привести всё в порядок. И именно этой моей деятельности будет посвящена данная статья. В начале я думал описать подробно весь процесс, начиная с создания собственного GUI (графический интерфейс пользователя) и заканчивая написанием игрового сервера. Но, к сожалению, обнаружил, что этой информации получается слишком много. В результате я больше внимания уделил описанию сети, так как, мне показалось, что эта тема для многих более интересна. Я постарался дать объяснения в таком виде, чтобы это можно было как-то применить на практике. Однако у меня нет уверенности, что в результате статья не получилось слегка «тяжеловатой» для понимания.
«Оболочка» для игры или то, что можно скачать и посмотреть в исходном коде C++
Как ни странно, этот раздел, который находится в самом начале статьи, я написал в последнюю очередь. По ходу написания статьи я понял, что мне нужно хоть что-то показать «изнутри», иначе все мои объяснения не имеют большого практического смысла и на деле превращаются в словоблудие. А мне бы этого очень не хотелось.
Поэтому дополнительно я снабдил статью примером, по которому желающие могут ознакомиться с некоторыми аспектами моего подхода к написанию больших проектов:
Визуально этот пример мало похож на мою игру, но на деле игра использует именно этот код, только в ней применяется другая «оформляющая тема ». Настройка игрового сеанса выглядит в игре так:
Также на всякий случай уточняю, что это не «кнопки Windows-а», как кто-то может подумать, а мои собственные компоненты, которые я сделал в том объеме, который требовала моя игра.
Пример является не просто «формальным» примером, а содержит в себе оболочку, которую я создал, чтобы использовать её в составе игры. Названия сей продукт не имеет, но он создавался мною с учетом того, что возможно игре потребуется портирование на другую платформу. В настоящий момент в полностью работоспособном состоянии имеется только Windows-версия, однако все обращения к операционной системе выполнены через виртуальные функции, которые можно заменить. На практике мне это понадобилось, когда пришло время выкладывать игровой сервер на просторы Интернета. Для бесплатного тестирования мне был доступен только вариант сервера, которым управляла Unix-подобная ОС. В результате пришлось дописать в оболочку ветку для Unix-а. Сам сервер при этом я не менял вообще, только заменил функции, которые требовались серверу для взаимодействия с ОС. Я не особенно разбираюсь в API Unix-а, но у меня есть хороший знакомый программист (на данном ресурсе носящий кодовое имя Komei), который прекрасно понимает в этой теме. С его помощью портирование сервера было выполнено в течение нескольких дней. Кроме того мой приятель любезно предоставил мне свой Unix-сервер для запуска и тестирования моего игрового Интернет-сервера, а это, как минимум, была приличная материальная помощь моему проекту, так как держать собственный выделенный сервер не такое уж и дешевое удовольствие.
Пример в составе статьи мне понадобился по причине того, что вся вторая часть статьи посвящена устройству сетевой игры. И там я вынужден периодически переходить в объяснениях на код. А сами по себе отдельные куски кода представляются мне достаточно бессмысленными. Обнародовать исходный код игры я посчитал «перебором», всё же это для меня коммерческий продукт, кроме того, относительно небольшой пример представляется мне гораздо более понятным. Поэтому я решил обнародовать исходный код оболочки снабдив её для понятности собственным примером. Пример показывает работу с моим GUI, проигрывает звук и демонстрирует сетевое взаимодействие. Т.е. пользователь может нажать кнопку «Включить сеть», потом кнопку «Создать» и будет создан игровой сеанс, к которому можно присоединиться из примера, запущенного на другом компьютере. Конфигурация игрока в сеансе сводится к возможности менять его цвет, но для демонстрации принципа этого вполне достаточно.
Описывать подробно организацию GUI я уже не стал, так как я не уверен, что это сильно кому-то интересно, а статья и так получилась достаточно большой. В любом случае пример демонстрирует работу со всеми имеющимися в моем GUI компонентами.
Я не возражаю против того, чтобы кто-то воспользовался моей работой в своих корыстных или бескорыстных целях. Но я не обещаю, что буду развивать этот проект или что я буду поддерживать совместимость с текущей версией. Также я уточняю, что GUI проектировался не для скорости, а для универсальности, что и требовалось в моем случае. Кроме того, у меня реализована ветка, которая работает только с 16-битным цветом, который нужен моей игре, однако никто не мешает дописать ветку кода, которая будет работать с 32-битными цветами, а также использовать аппаратное ускорение GPU там, где это возможно. Скачать этот проект можно здесь.
Проект примера выполнен на Visual Studio 2008 и использует язык C++ и DirectX9.
Проблемы, которые мне предстояло решить
Для начала вот список проблем, которые существовали в игре год назад:
- Игра полноценно работала только под Windows XP. Под Windows 7 игра нормально запускалась, но локальная сеть не функционировала вообще. А Windows 8/10 просто отказывались запускать игру.
- Игра была намертво «прибита гвоздями» к платформе Windows. Активно использовался DirectX, а также MFC.
- Оформление меню было очень «стрёмным» даже на мой взгляд. Связано это было с тем, что я занимаюсь программированием и к рисованию имею весьма далекое отношение, а доделывал игру я уже в одиночку.
- Визуально с «иконками исследований» дело обстояло еще хуже, чем с меню.
- Голосовая озвучка оставляла, мягко говоря, желать лучшего.
- Не было интернет-сервера, который мне всегда хотелось иметь, так как я считаю, что самое интересное в RTS — это игра по сети.
Постепенно шаг за шагом я решал эти проблемы и сейчас чувствую, что наконец-то результат меня вполне устраивает. Однако так было не всегда…
С чего начать
Проект игры когда-то делался в Visual Studio C++ 6.0. Теперь же этот Visual Studio C++ 6.0 даже не пожелал работать в Windows 8. Поэтому первым делом, мне нужно было перенести проект в новую для него среду. С одной стороны всё должно быть просто, так как Visual Studio умеет конвертировать проекты из прошлых версий в собственный формат, однако не тут-то было. Дело в том, что я использовал ассемблер tasm32, который создавал мне obj-файл. Этот файл без проблем линковался с Visual Studio C++ 6.0, но совсем не хотел линковаться с Visual Studio 2008. Новой версии tasm32 на свете не оказалось, а другие ассемблеры типа masm почему-то пылали к синтаксису tasm32 лютой ненавистью и выдавали множество ошибок при попытке подсунуть им мой asm-файл.
Поразмыслив, я решил, что не готов на данной стадии переписывать код ассемблера под другой синтаксис, так как по началу я вообще не был уверен, что я двигаюсь в правильном направлении. В результате я принял следующее решение: установил Windows XP, поставил в неё Visual Studio C++ 6.0, создал проект типа DLL, написал экспортируемые функции, которые просто вызывали через себя функции ассемблера и прилинковал к нему свой ассемблерный obj-файл, который в Windows XP прекрасно создавался через tasm32.exe. В результате я получил библиотеку asm.dll, которую я уже смог без проблем подключить к новому проекту в Visual Studio 2008. Такое решение заработало, практически, сразу и пока я решил остановится на нем. Я, естественно, понимаю, что это решение абсолютно не переносимо на другие платформы, но если уж действительно встанет вопрос о портировании, то можно «собрать волю в кулак » и перевести ассемблер в другой синтаксис. Пока же судьба этой игры для меня туманна, хотя определенно я её за последний год в прямом смысле реанимировал. В любом случае, я потратил на этот проект уже достаточно сил, чтобы наконец-то получить от вселенной четкий ответ по данному поводу.
После того, как я смог запустить игру в Windows 8 с помощью Visual Studio 2008 у меня в руках на-конец -то появился отладчик. Для начала мне нужно было разобраться в причинах, по которым новые версии Windows не желали запускать игру. Я давно не занимался играми и не особенно следил за той эволюцией ограничений, которую Windows стала накладывать на работу старых программ. Лично для меня эта причина оказалась очень неожиданной. Я обнаружил, что из Windows 8/10 удален 16-битный цветовой режим, который использовался моей игрой. После нескольких «магических ритуалов» мне всё же удалось запустить игру и даже немного поиграть, но работало всё угрожающе медленно, а вместо ландшафта на основной части экрана был чистый и прекрасный черный цвет. Для себя я отметил, что 16-битный режим теперь эмулируется и имеет ограничения по использованию.
Битва за 16-битный видеорежим
Как бороться с этой проблемой? Как минимум имеется 2 выхода:
- Переделать всю графику в игре так, чтобы она могла работать в 24-битном разрешении. В моем случае этот вариант плох тем, что вся графика хранилась в сжатом виде и рисовалась ассемблером. Все функции ассемблера умели работать только с 16-битной графикой, самих функций было много и работали они достаточно быстро, предполагали много нюансов при рисовании и, главное, за эти годы я порядком забыл как всё это функционирует в деталях.
- Создать в ОЗУ собственную поверхность, которая для игры будет являться экранной памятью. Рисовать всё на этой поверхности, а потом копировать её на реальный экран. Этот вариант я и выбрал в качестве единственно верного. При таком решении также появлялся жирнейший плюс — мне теперь почти не нужен DirectX для работы с графикой, так как единственное что он теперь должен был делать — это копировать мою поверхность на экран. Но есть в таком решении и не сразу очевидный минус — DirectX позволял мне запросто выводить текст, и я, естественно, так и делал. А теперь получалось: «нет DirectX — нет текста».
В результате я решил сделать следующее — написать что-то типа «оболочки», через которую игра общалась бы с операционной системой. Любые обращения к ресурсам ОС выполнять через виртуальные функции, чтобы в случае чего их можно было заменить. Практически, это даёт возможность относительно быстро портировать игру на другую ОС, но как всегда не всё так просто… почти любой программе, которая хоть как-то общается с пользователем нужен интерфейс. Интерфейс же состоит из разных маленьких (но милых) элементиков типа "Текстовое поле", "Списочек", "Дерево", "Диалоговое окошечко" и прочее. Иными словами, если идти таким путём, то нужно делать собственный GUI, который будет работать одинаково в условиях любой ОС. Кроме этого нужно как-то решить проблему с проигрыванием звуков и сетевым взаимодействием.
Для звуков я решил использовать библиотеку Squall. Плюсов её использования было несколько… во-первых, эта библиотека уже использовалась в старой версии игры, во-вторых, по сравнению с другими бесплатными аналогичными решениями она играла почти все wav-файлы, которые я ей подсовывал, в-третьих, я был лично знаком с автором cesium, что тоже немаловажно. Главный минус Squall — это то, что она уже давно не развивается и существует только под Windows. Однако в последнее время я стал стараться делать такую структуру у своих программ, чтобы можно было относительно без труда заменить внешнюю библиотеку каким-нибудь аналогом. В результате я сделал всё взаимодействие со Squall через небольшую прослойку из собственного класса. Чуть позже я в качестве эксперимента заменил Squall на BASS, однако не особенно ощутив пользу от этой замены, вернул Squall обратно. Зато теперь я знал, что если понадобиться другая платформа, то я смогу использовать BASS, который есть везде.
Так как мне предстояло, практически, избавиться от DirectX в составе игры, то для начала я просто закомментировал все h-файлы DirectX-а. Нажал на Build и приготовился оценить объем предполагаемой работы по замене функций DirectX на свои. Когда длинный «список проблем» наконец остановился, то внизу очень красноречиво отобразилось количество найденных ошибок в общей сложности чуть более 9 тысяч. Меньше всего ошибки касались DirectInput-а, так как ввод данных с мыши и клавиатуры — это относительно маленькая часть кода, а вот зато DirectDraw и DirectPlay отметились, как говорится, «по полной».
Существовала и еще одна мощная проблема. Дело в том, что к игре прилагается редактор карт и кампаний. А он был полностью написан с использованием MFC, а так как это, практически, оконное приложение Windows, то я сразу принял решение даже не думать о том, чтобы редактор в перспективе мог работать на другой платформе. Но дело в том, что и игра, и редактор — это, в моем случае, был один и тот же проект (в прямом смысле это был один и тот же exe-файл, который запускался в разными параметрами). Соответственно, мне нужно было разделить этот проект на 2 части: игра должна была работать почти полностью независимо от Windows, а редактор, напротив, оставался MFC-проектом. В результате когда я выдирал игру из фреймворка MFC я частенько натыкался на ситуацию, когда общие для этих проектов функции должны теперь выполнять свою задачу по-разному. Приходилось вставлять в код условную компиляцию и порождать вторую ветку кода.
Первым делом я стал создавать собственную оболочку, которая помогла бы мне освободиться от DirectX-зависимости. Мне было необходимо, чтобы оболочка умела работать с 16-битным разрешением, имела бы в своем составе стандартные компоненты для формирования интерфейса, а также позволяла бы выводить текст.
Структура «оболочки»
Базовым классом всей оболочки я сделал класс GGame, который уже будет содержать в себе все остальные важные объекты. Класс GPlatform отвечает у меня за общение с ОС, исключая графические операции. Зато всё что касается работы с файловой системой, обработкой цикла сообщений, завершением работы приложения, запуском потоков и процессов относится к GPlatform. Класс GSound отвечает за звук, а класс GNet является либо сервером GNetServer, либо клиентом GNetClient, в зависимости от задачи. Самым сложным был класс GDevice, который заведует графикой.
Класс GDevice работает с поверхностями GSurface. Эти поверхности определены мною весьма абстрактно, т.е. там нет никаких намеков на количество битов на пиксель, зато определено множество абстрактных функций, которые должны были заменить мне стандартный GDI. Однако в реальности в игре никогда не создаются объекты типа GSurface, вместо этого используются дочерние объекты типа GSurface16, которые как раз умеют выполнять работу GDI применительно к 16-битным поверхностям. Я рассудил так, что если мне потребуется дать своей оболочке возможность работать с 24-битной графикой, то я всегда смогу создать аналогичный класс GSurface24 и это не потребует каких-то глобальных изменений общей структуры оболочки.
Класс GDevice также отвечает за работу с разрешениями экрана, он имеет абстрактные функции, которые определяют доступные разрешения экрана, позволяет выбрать новое разрешение. GDevice также умеет масштабировать размер сцены под размер монитора. Например, игра использует любое разрешение просто за счёт увеличения видимой области игрового поля, а вот меню всегда работает только в разрешении 1920 x 1080. Однако если реальное разрешение монитора меньше, то GDevice выполняет масштабирование меню под текущее разрешение монитора. Возможно, что более хорошим решением было бы сделать несколько видов меню под разные разрешения монитора, но я всё же не корпорация и подобные решения в моем случае, будут просто нерациональны. В любом случае, на ноутбуке с разрешением 1366 x 768 меню выглядит вполне приемлемо.
Все эти базовые классы GPlatform, GDevice, GNet и т.д. содержат множество абстрактных функций. В реальной программе вместо класса GPlatform используется дочерний класс GPlatformWindows, вместо класса GDevice — класс GDeviceOnimodLandDX9, вместо класса GNet — либо класс GNetServerLibUV, либо класс GNetClientLibUV, т.е. я использую объекты классов, которые уже приспособлены под нужную платформу и мою игру. Сама «оболочка», не имеет ни об игре, ни об ОС, ни малейшего понятия.
Да и самый основной класс на самом деле превращается из GGame в класс GGameOnimodLandDX9. Как же всё это связывается вместе? Ответ заключается в том, чтобы никогда не создавать в программе важные объекты напрямую через оператор new. Вместо этого существуют виртуальные функции, которые осуществляют создание объекта нужного типа. Например, абстрактные функции класса GGame:
// Создание платформы
virtual GPlatform* GGame::NewPlatform(GGame* owner, const char* game_name)=0;
// Создание звука
virtual GSound* GGame::NewSound()=0;
выглядят для класса GGameOnimodLand примерно так:
// Создание платформы
GPlatform* GGameOnimodLand::NewPlatform(GGame* owner, const char* game_name)
{
return new GPlatformWindows(owner, game_name);
}
// Создание звука
GSound* GGameOnimodLand::NewSound()
{
#ifdef SQUALL_SOUND
return new GSoundSquall();
#endif
#ifdef BASS_SOUND
return new GSoundBass();
#endif
}
А теперь как всё это создается через базовый класс:
// Создание всего, что есть в игре
bool GGame::CreateGame(void* init, int view_width, int view_height)
{
platform=NewPlatform(this, game_name.c_str());
platform->Create();
device=NewDevice(init);
device->Create(view_width, view_height);
sound=NewSound();
....
....
....
}
Таким образом, получается так, что базовый класс создает нужные ему объекты, не вникая в тонкости специфики ОС или графического API. Такой подход позволяет отделить основной код программы от особенностей среды выполнения. Также это позволяет относительно легко произвести портирование на другую платформу, а DirectX заменить на OpenGL, так как вся специфика среды выполнения вынесена в небольшие и полностью отделенные классы и их функции.
Продолжение статьи: GUI
Конец статьи: Сеть