1.13 Добавляем поддержку игры по сети
Содержание
- 1.13 Добавляем поддержку игры по сети
- Intro
- Поддержка сетевых технологий в DirectX
- Архитектура системы поддержки сети
- Обработка сетевых сообщений (Network message processing)
- Добавляем Network.h (Проект Engine)
- Исследуем код Network.h
- Добавляем Network.cpp (Проект Engine)
- Исследуем код Network.cpp (Проект Engine)
- Интегрируем систему поддержки сети в движок
- Тестовая перекомпиляция Engine.lib
- Модифицируем тестовое приложение (Проект Test)
- Итоги главы
- Источники
Intro
В этой Главе:- Поговорим о сетевых технологиях и о том, как они реализованы в DirectX (в частности, в компоненте DirectPlay).
- Рассмотрим различные сетевые архитектуры и набросаем проект нашей системы поддержки сетевой игры.
- Разработаем систему поддержки сетевой игры и интегрируем её в движок.1
По данной теме написана не одна сотня книг (многие из которых на русском языке). А так как данная статья не претендует на полноту изложения, настоятельно рекомендуем прочесть хотя бы пару из них. Не факт, что всё усвоишь, но основные моменты всё равно запомнишь.
Поддержка сетевых технологий в DirectX
В DirectX имеется всего один компонент, отвечающий за сетевые режимы - DirectPlay.Обрати внимание
DirectPlay признан устаревшим компонентом и уже много лет не поддерживается Майкрософт. Его даже перестали включать в последние версии DirectX SDK. (Здесь снова придёт на помощь DirectX SDK 8.) Вместо DirectPlay Майкрософт предлагает... ничего! Вернее, предлагает использовать различные реализации сетевых протоколов от сторонних разработчиков.
Но и это ещё не всё. В ОС Microsoft Windows 10 и более поздних компонент DirectPlay вообще отключен по умолчанию! Это является чуть-ли не единственной причиной, по которой многие игры, использующие его, просто не запускаются, выдавая сообщение об отсутствии библиотек DirectPlay. О том, как его всё-таки включить, есть отличный ролик на YouTube: https://www.youtube.com/watch?v=WrufOcVR_HU.
DirectPlay способен управлять практически всеми аспектами сетевых сессий, которые представляют собой сеанс (экземпляр) соединения (connection instance) с одним или более компьютерами, обменивающимися данными с одной определённой целью. Другими словами, когда ты хостишь (создаёшь) мультиплеерную игру на одном компьютере, ты создаёшь сессию. Другие игроки могут затем присоединяться (join) к этой сессии, если они играют в ту же игру. DirectPlay - это интерфейс, который позволяет создавать сессии (host sessions), вступать (join) в них, а также искать их внутри сети. Будучи подключённым к сессии, DirectPlay затем используется для отправки и получения данных между клиентами одной сессии. Клиентом в этом случае является любой компьютер, присоединившийся (join) к сессии. Компьютер, создавший сессию называется хостом (host) или сервер (server).
Это лишь основные функции DirectPlay. Кроме того, он годится и на многое другое. Но мы лишь рассмотрим те его функции, которые касаются создания сессий и вступления в них, обнаружения сессий в сети и отправки и получения данных.
Обрати внимание
Когда мы говорим о компьютерной сети, это может быть сеть любого типа, как например локальная сеть (Local Area Network - LAN) или обширная сеть (Wide Area Network - WAN), либо весь Интернет, который по сути тоже представляет собой большую компьютерную сеть. Для простоты изложения мы рассмотрим лишь локальную сеть, под которой подразумевается один или несколько компьютеров, соединённых друг с другом в одну сеть и расположенные на ограниченном пространстве (localized area) (чаще всего в одной комнате или здании и подключённые через хаб или неуправляемый коммутатор - свитч).
Рассмотрим общий принцип работы DirectPlay. DirectPlay может работать:
- с использованием т.н. пиринговой (peer-to-peer) архитектуры сети;
- с использованием клиент-серверной (client-server) архитектуры сети.
- Transmission Control Protocol/Internet Protocol (TCP/IP)
- Internetwork Packet Exchange (IPX)
- Modem
- Serial Link
Для коммуникации в сети DirectPlay использует адреса, представляющие собой уникальный способ идентифицировать тот или иной компьютер в данной сети. Каждый участник сетевой игры имеет свой уникальный адрес, который используется DirectPlay для отправки сообщений, а также идентификации отправителя полученных сообщений. DirectPlay-адрес представляет собой URL-строку, включающую в себя:
- схему (scheme)
- разделитель схемы (scheme separator)
- строку данных (data string).
А теперь немного теории о том, каким образом DirectPlay обрабатывает сообщения.
Сообщение отправляется с использованием специальной структуры, называемой пакет (packet). Каждый пакет имеет заголовок (header), который содержит информацию о типе сообщения, а также о том, от кого оно пришло. Оставшуюся часть пакета занимают текущие данные, которые "прикрепляются" к "хвосту" пакета. Для коммуникации со своим приложением DirectPlay использует функцию обратного вызова (callback function) точно также, как это делают функции обратного вызова ОС Windows и процедуры обработки сообщений. Суть данной концепции состоит в том, чтобы внедрить в приложение функцию обратного вызова, в которую DirectPlay сможет передавать полученные сообщения. Всякий раз, когда DirectPlay вызывает функцию обратного вызова, это означает, что приложение получило сообщение (=пакет). После этого остаётся лишь проверить идентификатор сообщения (содержащийся в его заголовке) для определения его типа. Как только станет известен тип полученного сообщения, приложение сможет обработать соответствующим образом данные, содержащиеся в нём.
Это лишь основные сведения, необходимые для начала проектирования системы поддержки игры по сети. Остальное рассмотрим по ходу дела.
Архитектура системы поддержки сети
Как мы уже говорили, DirectPlay использует два типа (модели) сетевой архитектуры: пиринговую и клиент-серверную. Тебе необходимо решить, какая из них наилучшим образом подойдёт для нашего игрового движка и уже затем разрабатывать его именно под выбранную архитектуру. Рассмотрим обе архитектуры более подробно.Пиринговая (peer-to-peer) архитектура
- За неё отвечает интерфейс IDirectPlay8Peer.
ДОСТОИНСТВА | Более простая модель и доступная для понимания. Нет необходимости разделять исходный код на "код сервера" и "код клиента". Все клиенты устроены одинаково и могут выступать в качестве хоста. |
НЕДОСТАТКИ | Легко допустить ошибки в реализации, которые часто приводят резкому росту объёма сетевого трафика. Плохая масштабируемость, т.к. с ростом числа клиентов количество отправляемых сообщений растёт в геометрической прогрессии. Не подходит для защищённых соединений, т.к. каждый клиент может прочитать все сообщения других клиентов, в том числе адресованные не ему. |
Несмотря на все недостатки, данная модель является хорошим выбором для простой игры, где число клиентов не превышает 20-30 штук (типичное максимальное число игроков в игре, согласно документации DirectX SDK). В свете её преимуществ и простоты реализации в нашем проекте мы будем использовать именно пиринговую архитектуру сети.
Клиент-серверная (client-server) архитектура
- Наиболее распространённая модель, используемая при разработке коммерческих игр, особенно тех, которые поддерживают игру через Интернет.
Сервер:
- Чаще всего это специальным образом спроектированное приложение, которое оптимизировано под управление игровой "логистикой". При этом в нём, как правило, отсутствуют все те атрибуты, которые делают игру играбельной, например графика и звуковые ресурсы.
- Руководит всеми процессами в игре. Клиенты, в свою очередь, являются своеобразными "порталами" (или "окнами") в игровой мир. Как только игроку необходимо обновить своё состояние (например он переместился из одной точки игровой карты в другую), его клиент отправляет сообщение на сервер, а тот уже (при необходимости) оповещает всех остальных клиентов.
ДОСТОИНСТВА | С точки зрения оптимизации сетевого трафика наиболее эффективная модель. С ростом числа игроков объём сетевого трафика возрастает линейно и чаще всего незначительно. Сервер способен управлять обработкой игровой логики (logic processing), позволяя клиентам сконцентрировать ресурсы на остальных аспектах игрового процесса (графика, звук, геймплей и т.д.). Более простое управление игровым миром, внесение изменений или исправлений (т.н. "багфиксов") напрямую из центрального сервера. Наилучшее решения для защищённых соединений, т.к. все сообщения, предназначенные данному клиенту, идут только ему через сервер. Клиентам не нужно заботится об изменении состояния игрового мира, т.к. обычно за это ответственен сервер. |
НЕДОСТАТКИ | Более сложная модель для понимания и реализации. Необходимость по отдельности разрабатывать код сервера (server side code) и код клиента (client side code). |
Клиент-серверная архитектура имеет намного больше преимуществ, чем пиринговая. Несмотря на это, ключевой особенностью пиринговой модели является простота разработки.
Обработка сетевых сообщений (Network message processing)
Чуть выше мы уже немного говорили о сетевых сообщениях. Там речь шла о пакетах. Сейчас мы более подробно рассмотрим методы, которые используются для обработки сетевых сообщений. Ты уже знаешь, что, как только приложение получает сообщение из сети (и не только), оно входит в функцию обратного вызова, которая проверяет тип поступившего сообщения и затем обрабатывает его соответствующим образом. Проблема заключается в том, что во время динамичной игры (игры жанра FPS считаются самыми динамичными) каждому клиенту поступает множество различных сообщений, обработка которых занимает какое-то время. Если клиенту внезапно поступает около тысячи сообщений от других игроков и в игре отсутствует какой-либо механизм контроля обработки поступивших сообщений, то он будет заметно "подвисать" и игра для игрока станет совершенно неиграбельной. С другой стороны нам необходимо быть уверенными, что система поддержки сети не будет занимать значительную часть процессингового времени. Если это произойдёт, игра просто рассинхронизируется с другими клиентами, т.к. данные не будут обрабатываться с достаточной периодичностью.Для решения этой проблемы мы применим связный список и таймер. Да, те самые, что мы обсуждали (и даже внедрили в движок) в предыдущих главах. Данное решение является частью контроля движка (engine control). Суть данной методы предельно проста. Всякий раз, когда наша функция обратного вызова получает созданное пользователем (user-defined) игровое сообщение, оно просто добавляется в связный список. Затем на каждом кадре (при его подготовке) мы вызываем специальную функцию объекта поддержки сети (network object), которая одно за другим обрабатывает сообщения в связном списке в течение определённого промежутка времени (как правило это доли секунды). Именно здесь вступает в дело наш таймер. Данная функция продолжает обрабатывать сетевые сообщения из связного списка по принципу "первый вошёл - первый вышел" до тех пор, пока таймер не достигнет предела. (Напомним, что наш таймер вычисляет время отображения каждого кадра.) После этого система поддержки сети немедленно возвращает управление движку. Все необработанные сообщения, оставшиеся в связном списке, будут обработаны в следующем кадре. Обработка сообщений таким способом позволяет клиентам обрабатывать достаточное количество сообщений за 1 кадр, чтобы сохранять синхронизированное состояние и обеспечить играбельность, предохраняя клиента от переполнения этими самыми сообщениями.
Поначалу вся эта метода может показаться дико сложной. Но как только мы напишем её реализацию всё сразу станет предельно ясно. Мы начнём с рассмотрения структуры сетевого сообщения, а затем создадим структуру для работы с ними.
Сетевые сообщения
К этому моменту ты уже знаешь, что из себя представляют сетевые сообщения, а также имеешь представление о том, для чего они нужны и как их обрабатывать. Если нет, то давай ещё раз "пробежимся" по ранее изученному материалу. Сетевое сообщение представляет собой пакет, который состоит из заголовка (header) и сегмента данных (data), в котором и содержится вся передаваемая информация. В заголовке указывается тип сообщения, а также от кого оно пришло. Представь, что данные из сегмента данных пакета являются своего рода письмом в почтовом конверте. Заголовок же можно сравнить с адресом, написанном на нём. В заголовке, как и на почтовом конверте, указана вся необходимая информация для правильной обработки отправления и успешной доставки адресату. Данные же, в свою очередь, являются тем самым письмом, которое будет прочтено только тем, кому оно адресовано.В то время как сетевые сообщения пересылаются между различными игроками в сетевой сессии, на компьютере каждого клиента работает специальный обработчик сетевых сообщений (network message handler), представляющий собой типичную функцию обратного вызова (call-back function), выполняющую обработку сетевых сообщений. Все сообщения чётко подразделяются на:
СИСТЕМНЫЕ (System-defined) | Определены и используются DirectPaly. Пример: сообщения о создании и вступлении в сессию, а также о выходе из неё и её уничтожении + множество других сообщений, которые определены DirectPlay и каждое их которых имеет свою собственную структуру данных, с которой работает программер. |
ПОЛЬЗОВАТЕЛЬСКИЕ (User-defined) | Их структуру определяет программер, который также решает, для чего они будут использоваться и каким образом обрабатываться. Такие сообщения обычно используются для передачи внутриигровых данных, например, об изменении положения игрока на игровой 3D-карте. |
Всякий раз, когда на компьютер клиента приходит пользовательское сообщение, оно сразу помещается в связный список сетевых сообщений (network messages linked list). В каждом кадре система поддержки сети обрабатывает столько сообщений из связного списка, сколько успеет в пределах временного интервала, отведённого таймером (= время показа одного кадра на экране). Затем каждое обработанное сообщение пересылается в нужное место, в соответствии с его типом, и только после этого начинается обработка данных, находящихся внутри него. На этом жизненный цикл сетевого сообщения завершается, а с каждым новым кадром вся процедура повторяется вновь уже с новой порцией сообщений.
Что касается системных сообщений, то тут дело обстоит иначе. Такие сообщения не добавляются в связный список, а обрабатываются немедленно! Причины:
- Системные сообщения довольно редки по сравнению с бесконечным потоком пользовательских сообщений. Как правило они возникают при создании/уничтожении сессии, а также при вступлении/выходе игрока из неё.
- Они часто влияют на все пользовательские сообщения, ожидающие обработки. Например, когда игрок уже вышел из игры, система обработки сетевых сообщений может попытаться обработать сообщения, содержащиеся в очереди, "думая" что игрок всё ещё в игре.
Вот и вся теория. А теперь за дело!
Добавляем Network.h (Проект Engine)
- Стартуй MSVC++ 2010 и открывай Решение GameProject01 (если не сделал это раньше).
- В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Заголовочные файлы" Проекта Engine.
- Во всплывающем меню Добавить->Создать элемент... (Add->New Item...)
- В появившемся окне выбери "Заголовочный файл (.h)" и в поле "Имя" введи "Network.h".
Добавленный файл сразу откроется в правой части MSVC++2010.
- В только что созданном и открытом файле Network.h набираем следующий код:
//----------------------------------------------------------------------------- // File: Network.h // Класс-обёртка (wrapper class) для DirectPlay. Поддержка игры по сети представлена // в виде пиринговой (peer-to-peer) архитектуры. // // Original SourceCode: // Programming a Multiplayer First Person Shooter in DirectX // Copyright (c) 2004 Vaughan Young //----------------------------------------------------------------------------- #ifndef NETWORK_H #define NETWORK_H //----------------------------------------------------------------------------- // Стандартные определения (defines) сообщений //----------------------------------------------------------------------------- #define MSGID_CREATE_PLAYER 0x12001 #define MSGID_DESTROY_PLAYER 0x12002 #define MSGID_TERMINATE_SESSION 0x12003 //----------------------------------------------------------------------------- // Структура сетевого сообщения //----------------------------------------------------------------------------- struct NetworkMessage { unsigned long msgid; // Message ID. DPNID dpnid; // DirectPlay player ID. }; //----------------------------------------------------------------------------- // Структура полученного сообщения //----------------------------------------------------------------------------- struct ReceivedMessage : public NetworkMessage { char data[32]; // Данные сообщения. }; //----------------------------------------------------------------------------- // Структура информации энумерированной сессии (Enumerated Session Information Structure) //----------------------------------------------------------------------------- struct SessionInfo { IDirectPlay8Address *address; // Session network address. DPN_APPLICATION_DESC description; // Application description. }; //----------------------------------------------------------------------------- // Структура информации игрока (Player Information Structure) //----------------------------------------------------------------------------- struct PlayerInfo { DPNID dpnid; // DirectPlay player ID. char *name; // Name of the player. void *data; // Данные игрока, специфичные для приложения (Application specific player data). unsigned long size; // Data size. //------------------------------------------------------------------------- // The player information structure constructor. //------------------------------------------------------------------------- PlayerInfo() { dpnid = 0; name = NULL; data = NULL; size = 0; } //------------------------------------------------------------------------- // The player information structure destructor. //------------------------------------------------------------------------- ~PlayerInfo() { SAFE_DELETE( name ); SAFE_DELETE( data ); } }; //----------------------------------------------------------------------------- // Network Class //----------------------------------------------------------------------------- class Network { public: Network( GUID guid, void (*HandleNetworkMessageFunction)( ReceivedMessage *msg ) ); virtual ~Network(); void Update(); void EnumerateSessions(); bool Host( char *name, char *session, int players = 0, void *playerData = NULL, unsigned long dataSize = 0 ); bool Join( char *name, int session = 0, void *playerData = NULL, unsigned long dataSize = 0 ); void Terminate(); void SetReceiveAllowed( bool allowed ); SessionInfo *GetNextSession( bool restart = false ); PlayerInfo *GetPlayer( DPNID dpnid ); DPNID GetLocalID(); DPNID GetHostID(); bool IsHost(); void Send( void *data, long size, DPNID dpnid = DPNID_ALL_PLAYERS_GROUP, long flags = 0 ); private: static HRESULT WINAPI NetworkMessageHandler( PVOID context, DWORD msgid, PVOID data ); private: GUID m_guid; // Application specific GUID. IDirectPlay8Peer *m_dpp; // DirectPlay peer interface. IDirectPlay8Address *m_device; // DirectPlay device address. unsigned long m_port; // Port for network communication. unsigned long m_sendTimeOut; // Таймаут для отпреаленных сетевых сообщений (Timeout for sent network messages.) unsigned long m_processingTime; /* Допустимый период времени для обработки полученных сетевых сообщений. (Allowed time period for processing received network messages.) */ DPNID m_dpnidLocal; // DirectPlay ID of the local player. DPNID m_dpnidHost; // DirectPlay ID of the host player. CRITICAL_SECTION m_sessionCS; // Enumerated session list critical section. LinkedList< SessionInfo > *m_sessions; // Связный список энумерированных сессий (Linked list of enumerated sessions.) CRITICAL_SECTION m_playerCS; // Player list critical section. LinkedList< PlayerInfo > *m_players; // Связный список игроков (Linked list of players.) bool m_receiveAllowed; /* Флаг, указывающий, разрешено ли сети получать сообщения, специфичные для приложения (Inidcates if the network is allowed to receive application specific messages.)*/ CRITICAL_SECTION m_messageCS; // Network message list critical section. LinkedList< ReceivedMessage > *m_messages; // Linked list of network messages. void (*HandleNetworkMessage)( ReceivedMessage *msg ); // Указатель на обработчик сообщений, специфичных для приложения //(Pointer to an application specific network message handler.) }; #endif
- Сохрани Решение (Файл->Сохранить все).
Исследуем код Network.h
Структуры сетевых сообщений
Исходный код начинается с создания двух структур (NetworkMessage и RecievedMessage), на основе которых будут создаваться сетевые сообщения:... //----------------------------------------------------------------------------- // Структура сетевого сообщения //----------------------------------------------------------------------------- struct NetworkMessage { unsigned long msgid; // Message ID. DPNID dpnid; // DirectPlay player ID. }; ...
Вот их описание:
NetworkMessage | Играет роль заголовка сетевого сообщения, который указывает на тип сообщения (идентификатор сообщения; Message ID; msgid) и какой игрок его отправил (dpnid). Идентификатор сообщения представляет собой уникальный номер, который присваивается каждому сообщению. Ты не раз увидишь его в деле, когда мы создадим собственную систему пользовательских (user-defined) сообщений в процессе создания игры, dpnid - это уникальный идентификатор игрока, применяемый для однозначной идентификации отдельных игроков в сетевой сессии. DirectPlay автоматически присваивает уникальный dpnid каждому игроку, который вступает в сессию (joins a session). Именно по идентификатору игрока происходит обращение к любому из участников игровой сессии. |
ReceivedMessage | Ветвится (наследуется) от структуры NetworkMessage. Представляет собой своеобразные "письмо и конверт" из рассмотренного ранее примера. Используется для хранения полученных сообщений в связном списке для последующей обработки. Всякий раз, когда на компьютер клиента приходит новое сообщение, его заголовок копируется (напомним, что он построен на основе структуры NetworkMessage), а его данные (message data) копируются в специальный массив data, имеющий тип char. При последующей обработке данные извлекаются из этого буфера в соответствии с типом сообщений и уже затем обрабатываются. |
Обрати внимание
Как можно видеть, в нашем случае в структуре RecievedMessage мы выделили всего 32 байта для хранения данных сетевого сообщения. Это означает, что если объём полученных данных превысит данное значение, то возникнет т.н. переполнение буфера (buffer overrun). На практике для предотвращения этого при тестировании программер выделяет достаточно большой буфер. И уже позднее, после оптимизации системы сетевых сообщений и определения конечного размера самых больших сообщений, он снижает размер буфера как раз приблизительно до величины 32 байта. Очевидно, что чем твои сетевые сообщения меньше, тем быстрее они пересылаются, обрабатываются и расходуют меньший объём памяти.
Структура игрока (player structure)
Только что мы рассмотрели структуры сетевых сообщений. Но, помимо них, система поддержки сети также работает со структурой игрока (player structure) для идентификации отправителя и получателя сетевых сообщений. Несмотря на то, что информация, содержащаяся в структуре игрока, сильно разнится в разных играх, системе поддержки сети необходим набор базовых сведений о каждом игроке в целях обеспечения коммуникации и обмена сообщениями. Исходя из этого, движок должен обслуживать конечный список игроков в данной сессии, кем бы они не являлись. В то же время, сама игра должна обслуживать свой собственный список игроков (list of players), в котором как раз и будут содержаться все данные, специфичные для конкретной игры (урон от выстрела, стоимость покупки юнита, мана, броня и т.д.).Обрати внимание
Важно помнить, что любой специфичный для игры список игроков должен постоянно обновляться, чтобы совпадать со списком игроков системы поддержки сети движка. Т.к. игроки могут периодически вступать в игру или покидать её.
В Network.h представлена структура Playerlnfo, хранящая базовые сведения о каждом игроке и используемая системой поддержки сети движка. Сам список игроков реализован в виде ещё одного связного списка (linked list) структур Playerlnfo. Как видим, связный список - действительно полезная штука, применяемая в самых разных частях движка.
... //----------------------------------------------------------------------------- // Структура информации игрока (Player Information Structure) //----------------------------------------------------------------------------- struct PlayerInfo { DPNID dpnid; // DirectPlay player ID. char *name; // Name of the player. void *data; // Данные игрока, специфичные для приложения (Application specific player data). unsigned long size; // Data size. //------------------------------------------------------------------------- // The player information structure constructor. //------------------------------------------------------------------------- PlayerInfo() { dpnid = 0; name = NULL; data = NULL; size = 0; } //------------------------------------------------------------------------- // The player information structure destructor. //------------------------------------------------------------------------- ~PlayerInfo() { SAFE_DELETE( name ); SAFE_DELETE( data ); } }; ...
Структура Playerlnfo предельно мала. Оно и понятно, ведь она содержит лишь самые необходимые данные об игроке. В основном она нужна лишь для хранения копии уникального идентификатора игрока (dpnid; player's identifier), присвоенного DirectPlay. Помимо этого в ней хранится копия имени игрока и его данных (с указанием размера этих данных), взятые из списка игроков приложения (игры; Application specific player data) во время создания игрока. DirectPlay позволяет отдельно передавать блок данных, специфичных для приложения, которые затем интерпретируются на компьютере получателя. Рассмотрим небольшой пример. Допустим, ты разрабатываешь FPS-игру, в которой у каждой команды есть свои отличительные особенности геймплея (gameplay team style). Прежде чем игрок вступит в игру, он должен выбрать, за какую команду он будет играть. Как только он сделает выбор, он сразу вступает в игру и система обмена сетевыми сообщениями отправляет всем клиентам сообщение об этом (хрестоматийный пример - игра CounterStrike). После этого каждому клиенту необходимо создать локальную копию (local instance) этого игрока, чтобы его данные учитывались в едином игровом пространстве (на игровой ЗО-карте). Помимо сообщения о вступлении в сессию нового игрока, DirectPlay пересылает о нём ещё три важных "куска" информации:
- Уникальный идентификатор dpnid, присваиваемый DirectPlay;
- Имя игрока (player name);
- Блок данных об игроке, специфичных для данного приложения.
Позднее, при разработке FPS-игры мы будем активно использовать блок данных, специфичных для приложения, для передачи информации о том, какую команду выбрал игрок, а также о том, какая карта назначена хостом.
Оставшаяся часть структуры Playerlnfo понятна без объяснений.
Критические секции (critical sections)
Статья по теме: Потоки (Threads) и многопоточность в Windows .
Для понимания данного термина рассмотрим принцип взаимодействия нашего приложения с DirectPlay. Также вновь затронем тему многопоточности (multithreading) в приложениях.
Каждый раз, когда ты запускаешь приложение на компьютере, при этом для него одновременно создаются один или несколько процессов. Все эти процессы можно увидеть, запустив Менеджер задач Windows (Windows Task Manager). Подавляющее большинство приложений, включая игру, которую мы будем создавать, используют всего один процесс. Каждый процесс имеет т.н. поток (thread), который представляет собой цепочку инструкций, последовательно выполняемых операционной системой. Часто (особенно когда работают приложение, активно использующие сетевые возможности) процесс имеет 2 потока. В этом случае ОС должна периодически переключаться между потоками, чтобы позволить каждому из них выполнить свои операции. Данное явление называется многопоточностью (multithreading).
DirectPlay активно использует многопоточность для выполнения сетевых функций в приложении. Другими словами, сетевые приложения (в том числе игры) как правило имеют один поток для обработки игровых данных и ещё один - для обработки поступающих сетевых сообщений. Откуда же приложение берёт этот второй поток? Ведь мы нигде открыто не указываем это в нашем исходном коде. А дело в том, что, когда мы реализовывали функцию обратного вызова обработчика сетевых сообщений (network message handler call-back function), фактически создали этот второй поток. К счастью, DirectPlay берёт управление созданными потоками на себя. Поэтому весь процесс прозрачен для нас, в определённой степени. Мы видим, что обработчик сетевых сообщений (который действует аналогично функциям обратного вызова для обычных и диалоговых окон) на деле относится к DirectPlay в довольно необычной манере. Ведь, даже будучи реализованным в движке, он не может быть вызван программером. DirectPlay вызывает его всякий раз, когда сетевое сообщение поступает на компьютер клиента. Из-за того, что сообщение может поступить в любое время (даже в тот момент, когда приложение уже выполняет множество других ресурсоёмких задач, например рендеринг), функция обратного вызова обработчика сетевых сообщений должна быть запущена в своём собственном отдельном потоке. Это позволит нашему приложению получать сетевые сообщения, без потери контроля над выполнением других задач.
Теперь, когда мы разобрались в том, что же собой представляет многопоточность (multithreading), обратим внимание на самую большую проблему, с которой ей приходится сталкиваться, которая, в свою очередь, привела к изобретению критических секций и других взаимоисключающих (mutual exclusion, связанных с взаимоисключающим доступом к одним и тем же ресурсам) техник. Из-за того, что несколько потоков работают одновременно в одном процессе (который создаёт приложение), они также находятся в одном участке выделяемой памяти. Это означает, что они вместе используют ресурсы из этой памяти (shares resources in this memory).
Допустим, у нас есть 2 потока и одна переменная, которую используют оба этих потока. Так как оба потока и переменная находятся в одном участке памяти, они должны каким-то образом разделять доступ к данной переменной. Что же произойдёт, когда оба потока одновременно обратятся к этой переменной? Если один поток попытается прочитать содержимое переменной в то время, как другой попытается в неё что-то записать, результат операции окажется непредсказуемым (и чаще всего неверным). Чуть выше мы уже решили, что всякий раз, когда DirectPlay вызывает обработчик сетевых сообщений нашего приложения, мы берём входящее сообщение и помещаем его в связный список, чтобы затем обработать его при первом удобном случае (в порядке общей очереди). Это также приводит к возникновению проблемы взаимного доступа (mutual exclusion). Если сетевое сообщение поступит в тот момент, когда мы уже обрабатываем сообщения из связного списка, то это может привести к многочисленным проблемам. Поток обработчика сетевых сообщений попытается добавить новое сетевое сообщение в связный список, в то время как движок будет пытаться прочесть другие сообщения из очереди.
Для решения этой проблемы мы применим критические секции. Своё название критические секции получили исходя из того факта, что они буквально защищают критические секции (участки) программного кода. Другими словами, если данный участок кода окажется незащищённым, то жди проблем связанных с взаимоисключающим доступом. Суть данной методы проста. Как только мы собираемся выполнять "одновременно всем нужный" участок кода, мы просто блокируем (lock) критическую секцию, которая ему назначена. Затем по окончании выполнения всех операций мы просто разблокируем (unlock) критическую секцию с данным участком кода. Если какой-либо другой процесс попытается повторно заблокировать ранее "залоченную" секцию, ему придётся дожидаться её разблокирования. Идея проста: мы "обносим" участок разделяемой (shared) памяти в отдельную секцию. И далее лочим и разлочим его при необходиости. См. Рис.3
В гипотетическом исходном коде это может выглядеть так:
EnterCriticalSection(SmyCS); // Любой код, размещённый здесь, защищен от внешни:': посягательств других потоков и функций. LeaveCriticalSection(SmyCS);
Обрати внимание
Объём кода, расположенного между тегами EnterCriticalSection и LeaveCriticalSection должен быть сведён к минимуму. Ведь если поток попытается захватить в критическую секцию фрагмент кода, который уже вошёл в критическую секцию от другого потока, он остановится и будет ждать до тех пор, пока тот не освободится. Это ожидание может катастрофически сказаться на производительности игры, особенно если ожидающий поток - это главный поток игры. В этом случае игра просто зависнет, ожидая освобождения критической секции. Кроме того, не следует вводить в критическую секцию фрагмент кода, уже введённный в ней другим потоком. Другими словами нельзя "вкладывать" одну критическую секцию в другую. Т.к. это может вызвать известную проблему под названием deadlock (от англ. "тупик"), когда два или более потока встают, т.к. каждый из них пытается войти в критическую секцию, залоченную другим потоком.
В остальных случаях критические секции можно смело использовать в исходном коде, не допуская вышеуказанных глупых ошибок.
Статистически уникальный идентификатор (Globally Unique Identifier - GUID)
Исследуя исходный код Network.h ты наверняка замечал странную аббревиатуру GUID. Если кратко, то GUID представляет собой строку из цифр и английских букв, которую (с некоторыми оговорками) можно назвать уникальной. Другими словами (теоретически) ты не сможешь повторно сгенерировать вторую такую. Генерируя новый GUID ты полностью застрахован от того, что больше никто не сгенерирует такую же комбинацию данного идентификатора.DirectPlay позволяет генерировать GUID "на лету", т.е. во время выполнения приложения, но в данном курсе такая задача не ставится. Ведь необходимость сгенерить новый GUID, как правило, возникнет всего один раз: при создании приложения с сетевыми возможностями. GUID необходим DirectPlay для однозначной идентификации нашего приложения в системе. Для генерации нового GUID воспользуемся специальной утилитой-генератором GUID-идентификаторов GUIDGEN. Раньше она шла в наборе с MS Visual С++ 6.0. В современных версиях MS Visual С++ Express её нет. Поэтому:
- либо берём её здесь: https://www.softpedia.com/get/Others/Miscellaneous/GuidGen.shtml ,
- либо пользуемся онлайн-сервисами вроде https://www.guidgen.com .
Система поддержки сети (Network system)
БОльшая часть подготовительной работы уже позади. Поэтому сразу перейдём к рассмотрению структуры нашей системы поддержки сети. По теории компьютерных сетей написана не одна сотня книг, поэтому данная Глава не претендует на полноту изложения. Мы рассмотрим лишь самые нужные нам моменты.Возвращаясь к исходному коду Network.h, рассмотрим определение класса Network:
... //----------------------------------------------------------------------------- // Network Class //----------------------------------------------------------------------------- class Network { public: Network( GUID guid, void (*HandleNetworkMessageFunction)( ReceivedMessage *msg ) ); virtual ~Network(); void Update(); void EnumerateSessions(); bool Host( char *name, char *session, int players = 0, void *playerData = NULL, unsigned long dataSize = 0 ); bool Join( char *name, int session = 0, void *playerData = NULL, unsigned long dataSize = 0 ); void Terminate(); void SetReceiveAllowed( bool allowed ); SessionInfo *GetNextSession( bool restart = false ); PlayerInfo *GetPlayer( DPNID dpnid ); DPNID GetLocalID(); DPNID GetHostID(); bool IsHost(); void Send( void *data, long size, DPNID dpnid = DPNID_ALL_PLAYERS_GROUP, long flags = 0 ); private: static HRESULT WINAPI NetworkMessageHandler( PVOID context, DWORD msgid, PVOID data ); private: GUID m_guid; // Application specific GUID. IDirectPlay8Peer *m_dpp; // DirectPlay peer interface. IDirectPlay8Address *m_device; // DirectPlay device address. unsigned long m_port; // Port for network communication. unsigned long m_sendTimeOut; // Таймаут для отпреаленных сетевых сообщений (Timeout for sent network messages.) unsigned long m_processingTime; /* Допустимый период времени для обработки полученных сетевых сообщений. (Allowed time period for processing received network messages.) */ DPNID m_dpnidLocal; // DirectPlay ID of the local player. DPNID m_dpnidHost; // DirectPlay ID of the host player. CRITICAL_SECTION m_sessionCS; // Enumerated session list critical section. LinkedList< SessionInfo > *m_sessions; // Связный список энумерированных сессий (Linked list of enumerated sessions.) CRITICAL_SECTION m_playerCS; // Player list critical section. LinkedList< PlayerInfo > *m_players; // Связный список игроков (Linked list of players.) bool m_receiveAllowed; /* Флаг, указывающий, разрешено ли сети получать сообщения, специфичные для приложения (Inidcates if the network is allowed to receive application specific messages.)*/ CRITICAL_SECTION m_messageCS; // Network message list critical section. LinkedList< ReceivedMessage > *m_messages; // Linked list of network messages. void (*HandleNetworkMessage)( ReceivedMessage *msg ); // Указатель на обработчик сообщений, специфичных для приложения //(Pointer to an application specific network message handler.) }; ...
Изучив данный фрагмент кода, ты найдёшь все элементы, рассмотренные ранее. В классе представлено огромное количество функций, которые позволяют:
- энумерировать (enumerate; опрашивать) сетевые сессии;
- создавать (create) сетевые сессии, а также вступать (join) в них;
- отправлять и получать сетевые сообщения;
- некоторые другие служебные функции.
Назначение большей части переменных членов класса Network вполне очевидно а их названия говорят сами за себя. В классе представлены 3 связных списка, для каждого из которых подготовлена отдельная критическая секция.
Добавляем Network.cpp (Проект Engine)
В файле исходного кода Network.cpp будут размещаться реализации функций, объявленных в Network.h.OK, приступаем.
- В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Файлы исходного кода" Проекта Engine.
- Во всплывающем меню Добавить->Создать элемент...
- В появившемся окне выбери "Файл С++ (.cpp)" и в поле "Имя" введи "Network.cpp".
- Жмём "Добавить".
- В только что созданном и открытом файле Network.cpp набираем следующий код:
//----------------------------------------------------------------------------- // File: Network.cpp // Реализация классов и функций, объявленных в Network.h. // Смотри объявление интерфейсов в Network.h для получения более подробной информации. // // Original SourceCode: // Programming a Multiplayer First Person Shooter in DirectX // Copyright (c) 2004 Vaughan Young //----------------------------------------------------------------------------- #include "Engine.h" //----------------------------------------------------------------------------- // The network class constructor. //----------------------------------------------------------------------------- Network::Network( GUID guid, void (*HandleNetworkMessageFunction)( ReceivedMessage *msg ) ) { // Иницииализируем критические секции. InitializeCriticalSection( &m_sessionCS ); InitializeCriticalSection( &m_playerCS ); InitializeCriticalSection( &m_messageCS ); // Обнуляем пиринговый (peer) интерфейс DirectPlay и экземпляр устройства. m_dpp = NULL; m_device = NULL; // Сохраняем GUID приложения. memcpy( &m_guid, &guid, sizeof( GUID ) ); // Создаём связный список энумеруемых сессий. m_sessions = new LinkedList< SessionInfo >; // Создаём связный список игроков. m_players = new LinkedList< PlayerInfo >; // Создаём связный список сетевых сообщений. m_messages = new LinkedList< ReceivedMessage >; // Загружаем сетевые настройки из файла NetworkSettings.txt. Script *settings = new Script( "NetworkSettings.txt" ); if( settings->GetNumberData( "processing_time" ) == NULL ) { m_port = 2509; m_sendTimeOut = 100; m_processingTime = 100; } else { m_port = *settings->GetNumberData( "port" ); m_sendTimeOut = *settings->GetNumberData( "send_time_out" ); m_processingTime = *settings->GetNumberData( "processing_time" ); } SAFE_DELETE( settings ); // Изначально системе сетевых сообщений запрещено получать сообщения, специфичные для приложения. m_receiveAllowed = false; // Устанавливаем обработчик сетевых сообщений (network message handler). HandleNetworkMessage = HandleNetworkMessageFunction; // Создаём и инициализируем пиринговый (peer) интерфейс DirectPlay. CoCreateInstance( CLSID_DirectPlay8Peer, NULL, CLSCTX_INPROC, IID_IDirectPlay8Peer, (void**)&m_dpp ); m_dpp->Initialize( (PVOID)this, NetworkMessageHandler, 0 ); // Создаём адрес устройства (device address). CoCreateInstance( CLSID_DirectPlay8Address, NULL, CLSCTX_INPROC, IID_IDirectPlay8Address, (LPVOID*) &m_device ); m_device->SetSP( &CLSID_DP8SP_TCPIP ); m_device->AddComponent( DPNA_KEY_PORT, &m_port, sizeof(DWORD), DPNA_DATATYPE_DWORD ); } //----------------------------------------------------------------------------- // The network class destructor. //----------------------------------------------------------------------------- Network::~Network() { // Сохраняем сетевые настройки. Script *settings = new Script( "NetworkSettings.txt" ); if( settings->GetNumberData( "processing_time" ) == NULL ) { settings->AddVariable( "port", VARIABLE_NUMBER, &m_port ); settings->AddVariable( "send_time_out", VARIABLE_NUMBER, &m_sendTimeOut ); settings->AddVariable( "processing_time", VARIABLE_NUMBER, &m_processingTime ); } else { settings->SetVariable( "port", &m_port ); settings->SetVariable( "send_time_out", &m_sendTimeOut ); settings->SetVariable( "processing_time", &m_processingTime ); } settings->SaveScript(); SAFE_DELETE( settings ); // Освобождаем адрес устройства. SAFE_RELEASE( m_device ); // Закрываем и освобождаем пиринговый (peer) интерфейс DirectPlay. if( m_dpp != NULL ) m_dpp->Close( 0 ); SAFE_RELEASE( m_dpp ); // Уничтожаем связный список энумерированных сессий. SAFE_DELETE( m_sessions ); // Уничтожаем связный список игроков. SAFE_DELETE( m_players ); // Уничтожаем связный список сетевых сообщений. SAFE_DELETE( m_messages ); // Уничтожаем критические секции. DeleteCriticalSection( &m_sessionCS ); DeleteCriticalSection( &m_playerCS ); DeleteCriticalSection( &m_messageCS ); } //----------------------------------------------------------------------------- // Обновляем (Update) объект сетевого устройства, позволяя ему обрабатывать сообщения. //----------------------------------------------------------------------------- void Network::Update() { EnterCriticalSection( &m_messageCS ); ReceivedMessage *message = m_messages->GetFirst(); unsigned long endTime = timeGetTime() + m_processingTime; while( endTime > timeGetTime() && message != NULL ) { HandleNetworkMessage( message ); m_messages->Remove( &message ); message = m_messages->GetFirst(); } LeaveCriticalSection( &m_messageCS ); } //----------------------------------------------------------------------------- // Энумерируем сессии в локальной сети (LAN). //----------------------------------------------------------------------------- void Network::EnumerateSessions() { // Очищаем связные списки. m_players->Empty(); m_messages->Empty(); m_sessions->Empty(); // Подготавливаем описание приложения (application description). DPN_APPLICATION_DESC description; ZeroMemory( &description, sizeof( DPN_APPLICATION_DESC ) ); description.dwSize = sizeof( DPN_APPLICATION_DESC ); description.guidApplication = m_guid; // Энумерируем сессии синхронно (synchronously). m_dpp->EnumHosts( &description, NULL, m_device, NULL, 0, 1, 0, 0, NULL, NULL, DPNENUMHOSTS_SYNC ); } //----------------------------------------------------------------------------- // Пытаемся захостить (host) сессию. //----------------------------------------------------------------------------- bool Network::Host( char *name, char *session, int players, void *playerData, unsigned long dataSize ) { WCHAR wide[MAX_PATH]; // Подготавливаем и заполняем структуру информации об игроке (player information structure). DPN_PLAYER_INFO player; ZeroMemory( &player, sizeof( DPN_PLAYER_INFO ) ); player.dwSize = sizeof( DPN_PLAYER_INFO ); player.pvData = playerData; player.dwDataSize = dataSize; player.dwInfoFlags = DPNINFO_NAME | DPNINFO_DATA; mbstowcs( wide, name, MAX_PATH ); player.pwszName = wide; if( FAILED( m_dpp->SetPeerInfo( &player, NULL, NULL, DPNSETPEERINFO_SYNC ) ) ) return false; // Подготавливаем описание приложения (application description). DPN_APPLICATION_DESC description; ZeroMemory( &description, sizeof( DPN_APPLICATION_DESC ) ); description.dwSize = sizeof( DPN_APPLICATION_DESC ); description.guidApplication = m_guid; description.dwMaxPlayers = players; mbstowcs( wide, session, MAX_PATH ); description.pwszSessionName = wide; // Host the session. if( FAILED( m_dpp->Host( &description, &m_device, 1, NULL, NULL, NULL, 0 ) ) ) return false; return true; } //----------------------------------------------------------------------------- // Пытаемся вступить (to join) в выбранную сессию из связного списка энумерированных сессий. //----------------------------------------------------------------------------- bool Network::Join( char *name, int session, void *playerData, unsigned long dataSize ) { WCHAR wide[MAX_PATH]; // Очищаем связные списки игроков (player list) и сетевых сообщений (newtork messages). m_players->Empty(); m_messages->Empty(); // Игнорим неправильные (invalid) сессии. if( session < 0 ) return false; // Подготавливаем и заполняем структуру информации об игроке (player information structure). DPN_PLAYER_INFO player; ZeroMemory( &player, sizeof( DPN_PLAYER_INFO ) ); player.dwSize = sizeof( DPN_PLAYER_INFO ); player.pvData = playerData; player.dwDataSize = dataSize; player.dwInfoFlags = DPNINFO_NAME | DPNINFO_DATA; mbstowcs( wide, name, MAX_PATH ); player.pwszName = wide; if( FAILED( m_dpp->SetPeerInfo( &player, NULL, NULL, DPNSETPEERINFO_SYNC ) ) ) return false; // Входим в критическую секцию связного списка энумерированных сессий (sessions linked list critical section). EnterCriticalSection( &m_sessionCS ); // Находим хост выбранной сессии. m_sessions->Iterate( true ); for( int s = 0; s < session + 1; s++ ) { if( m_sessions->Iterate() == NULL ) { LeaveCriticalSection( &m_sessionCS ); return false; } } // Вступаем (join) в сессию. if( FAILED( m_dpp->Connect( &m_sessions->GetCurrent()->description, m_sessions->GetCurrent()->address, m_device, NULL, NULL, NULL, 0, NULL, NULL, NULL, DPNCONNECT_SYNC ) ) ) { LeaveCriticalSection( &m_sessionCS ); return false; } LeaveCriticalSection( &m_sessionCS ); return true; } //----------------------------------------------------------------------------- // Уничтожаем текущую сессию. //----------------------------------------------------------------------------- void Network::Terminate() { // Разрешаем уничтожать (terminate) сессию только хосту. if( m_dpnidHost == m_dpnidLocal ) m_dpp->TerminateSession( NULL, 0, 0 ); // Закрываем соединение. Это также приводит к деинициализации пирингового (peer) интерфейса DirectPlay. if( m_dpp != NULL ) m_dpp->Close( 0 ); // Инициализируем пиринговый (peer) интерфейс DirectPlay. m_dpp->Initialize( (PVOID)this, NetworkMessageHandler, 0 ); } //----------------------------------------------------------------------------- // Устанавливаем флаг, разрешающий получение и обработку сетевых сообщений. //----------------------------------------------------------------------------- void Network::SetReceiveAllowed( bool allowed ) { m_receiveAllowed = allowed; } //----------------------------------------------------------------------------- // Возвращает следующую (next) итерированную сессию из связного списка энумерированных сессий. //----------------------------------------------------------------------------- SessionInfo *Network::GetNextSession( bool restart ) { EnterCriticalSection( &m_sessionCS ); m_sessions->Iterate( restart ); if( restart == true ) m_sessions->Iterate(); LeaveCriticalSection( &m_sessionCS ); return m_sessions->GetCurrent(); } //----------------------------------------------------------------------------- // Возвращает указатель (pointer) на структуру с информацией о данном игроке (player information structure of the given player). //----------------------------------------------------------------------------- PlayerInfo *Network::GetPlayer( DPNID dpnid ) { EnterCriticalSection( &m_playerCS ); m_players->Iterate( true ); while( m_players->Iterate() ) { if( m_players->GetCurrent()->dpnid == dpnid ) { LeaveCriticalSection( &m_playerCS ); return m_players->GetCurrent(); } } LeaveCriticalSection( &m_playerCS ); return NULL; } //----------------------------------------------------------------------------- // Возвращает DirectPlay ID локального игрока. //----------------------------------------------------------------------------- DPNID Network::GetLocalID() { return m_dpnidLocal; } //----------------------------------------------------------------------------- // Возвращает DirectPlay ID хоста. //----------------------------------------------------------------------------- DPNID Network::GetHostID() { return m_dpnidHost; } //----------------------------------------------------------------------------- // Определяет, является ли выбранный сетевой объект хостом или нет. //----------------------------------------------------------------------------- bool Network::IsHost() { if( m_dpnidHost == m_dpnidLocal ) return true; else return false; } //----------------------------------------------------------------------------- // Отправляет сетевое сообщение. //----------------------------------------------------------------------------- void Network::Send( void *data, long size, DPNID dpnid, long flags ) { DPNHANDLE hAsync; DPN_BUFFER_DESC dpbd; if( ( dpbd.dwBufferSize = size ) == 0 ) return; dpbd.pBufferData = (BYTE*)data; m_dpp->SendTo( dpnid, &dpbd, 1, m_sendTimeOut, NULL, &hAsync, DPNSEND_NOCOMPLETE); } //----------------------------------------------------------------------------- // Внутренний обработчик сетевых сообщений (internal network message handler). //----------------------------------------------------------------------------- HRESULT WINAPI Network::NetworkMessageHandler( PVOID context, DWORD msgid, PVOID data ) { // Получаем указатель на вызываемый сетевой объект. Network *network = (Network*)context; // Обрабатываем входящие сообщения, основываясь на их типе. switch( msgid ) { case DPN_MSGID_CREATE_PLAYER: { unsigned long size = 0; DPN_PLAYER_INFO *info = NULL; HRESULT hr = DPNERR_CONNECTING; PDPNMSG_CREATE_PLAYER msgCreatePlayer = (PDPNMSG_CREATE_PLAYER)data; // Создаём структуру информации об игроке (player information structure) для нового игрока. PlayerInfo *playerInfo = new PlayerInfo; ZeroMemory( playerInfo, sizeof( PlayerInfo ) ); playerInfo->dpnid = msgCreatePlayer->dpnidPlayer; // Продолжаем вызывать функцию GetPeerInfo(), т.к. он всё ещё может пытаться подключиться. while( hr == DPNERR_CONNECTING ) hr = network->m_dpp->GetPeerInfo( playerInfo->dpnid, info, &size, 0 ); // Проверяем, вернула ли функция GetPeerInfo() размер структуры DPN_PLAYER_INFO. if( hr == DPNERR_BUFFERTOOSMALL ) { info = (DPN_PLAYER_INFO*) new BYTE[size]; ZeroMemory( info, size ); info->dwSize = sizeof( DPN_PLAYER_INFO ); // Пытаемся снова, используя верный размер. if( SUCCEEDED( network->m_dpp->GetPeerInfo( playerInfo->dpnid, info, &size, 0 ) ) ) { // Сохраняем имя нового игрока. playerInfo->name = new char[wcslen( info->pwszName ) + 1]; ZeroMemory( playerInfo->name, wcslen( info->pwszName ) + 1 ); wcstombs( playerInfo->name, info->pwszName, wcslen( info->pwszName ) ); // Сохраняем данные нового игрока. playerInfo->data = new BYTE[info->dwDataSize]; memcpy( playerInfo->data, info->pvData, info->dwDataSize ); playerInfo->size = info->dwDataSize; // Сохраняем остальные установки локального игркоа и хоста. if( info->dwPlayerFlags & DPNPLAYER_LOCAL ) network->m_dpnidLocal = playerInfo->dpnid; if( info->dwPlayerFlags & DPNPLAYER_HOST ) network->m_dpnidHost = playerInfo->dpnid; } SAFE_DELETE_ARRAY( info ); } // Добавляем нового игрока в связный список игроков (player list). EnterCriticalSection( &network->m_playerCS ); network->m_players->Add( playerInfo ); LeaveCriticalSection( &network->m_playerCS ); // Если нет никакого обработчика сетевых сообщений, то немедленно прерываем (break). if( network->HandleNetworkMessage == NULL ) break; // Создаём сообщение игрока о создании игрока. ReceivedMessage *message = new ReceivedMessage; message->msgid = MSGID_CREATE_PLAYER; message->dpnid = playerInfo->dpnid; // Сохраняем сообщение для последующей обработки приложением. EnterCriticalSection( &network->m_messageCS ); network->m_messages->Add( message ); LeaveCriticalSection( &network->m_messageCS ); break; } case DPN_MSGID_DESTROY_PLAYER: { // Находим игрока, уничтожаем его и убираем из связного списка игроков (player list). EnterCriticalSection( &network->m_playerCS ); network->m_players->Iterate( true ); while( network->m_players->Iterate() ) { if( network->m_players->GetCurrent()->dpnid == ( (PDPNMSG_DESTROY_PLAYER)data )->dpnidPlayer ) { network->m_players->Remove( (PlayerInfo**)network->m_players->GetCurrent() ); break; } } LeaveCriticalSection( &network->m_playerCS ); // Если нет никакого обработчика сетевых сообщений, то немедленно прерываем (break). if( network->HandleNetworkMessage == NULL ) break; // Создаём сообщение игрока об уничтожении игрока. ReceivedMessage *message = new ReceivedMessage; message->msgid = MSGID_DESTROY_PLAYER; message->dpnid = ( (PDPNMSG_DESTROY_PLAYER)data )->dpnidPlayer; // Сохраняем сообщение для последующей обработки приложением. EnterCriticalSection( &network->m_messageCS ); network->m_messages->Add( message ); LeaveCriticalSection( &network->m_messageCS ); break; } case DPN_MSGID_ENUM_HOSTS_RESPONSE: { PDPNMSG_ENUM_HOSTS_RESPONSE response = (PDPNMSG_ENUM_HOSTS_RESPONSE)data; // Создаём структуру информации о сессии (session information structure) для новой сессии. SessionInfo *sessionInfo = new SessionInfo; response->pAddressSender->Duplicate( &sessionInfo->address ); memcpy( &sessionInfo->description, response->pApplicationDescription, sizeof( DPN_APPLICATION_DESC ) ); // Добавляем новую сессию в свзяный список сессий (session list). EnterCriticalSection( &network->m_sessionCS ); network->m_sessions->Add( sessionInfo ); LeaveCriticalSection( &network->m_sessionCS ); break; } case DPN_MSGID_RECEIVE: { // Если нет никакого обработчика сетевых сообщений, то немедленно прерываем (break). if( network->HandleNetworkMessage == NULL ) break; // Проверяем, разрешено ли системе поддержки сети получать сообщения, специфичные для приложения (application specific messages). if( network->m_receiveAllowed == false ) break; // Создаём сообщение о получении сообщения. ReceivedMessage *message = new ReceivedMessage; memcpy( message, ( (PDPNMSG_RECEIVE)data )->pReceiveData, ( (PDPNMSG_RECEIVE)data )->dwReceiveDataSize ); // Сохраняем сообщение для последующей обработки приложением. EnterCriticalSection( &network->m_messageCS ); network->m_messages->Add( message ); LeaveCriticalSection( &network->m_messageCS ); break; } case DPN_MSGID_TERMINATE_SESSION: { // Если нет никакого обработчика сетевых сообщений, то немедленно прерываем (break). if( network->HandleNetworkMessage == NULL ) break; // Создаём сообщение об уничтожении сессии. ReceivedMessage *message = new ReceivedMessage; message->msgid = MSGID_TERMINATE_SESSION; // Сохраняем сообщение для последующей обработки приложением. EnterCriticalSection( &network->m_messageCS ); network->m_messages->Add( message ); LeaveCriticalSection( &network->m_messageCS ); break; } } return S_OK; }
- Сохрани Решение (Файл->Сохранить все).
Исследуем код Network.cpp (Проект Engine)
Класс Network
Первым делом рассмотрим конструктор объекта класса Engine:... //----------------------------------------------------------------------------- // The network class constructor. //----------------------------------------------------------------------------- Network::Network( GUID guid, void (*HandleNetworkMessageFunction)( ReceivedMessage *msg ) ) { // Иницииализируем критические секции. InitializeCriticalSection( &m_sessionCS ); InitializeCriticalSection( &m_playerCS ); InitializeCriticalSection( &m_messageCS ); // Обнуляем пиринговый (peer) интерфейс DirectPlay и экземпляр устройства. m_dpp = NULL; m_device = NULL; // Сохраняем GUID приложения. memcpy( &m_guid, &guid, sizeof( GUID ) ); // Создаём связный список энумеруемых сессий. m_sessions = new LinkedList< SessionInfo >; // Создаём связный список игроков. m_players = new LinkedList< PlayerInfo >; // Создаём связный список сетевых сообщений. m_messages = new LinkedList< ReceivedMessage >; // Загружаем сетевые настройки из файла NetworkSettings.txt. Script *settings = new Script( "NetworkSettings.txt" ); if( settings->GetNumberData( "processing_time" ) == NULL ) { m_port = 2509; m_sendTimeOut = 100; m_processingTime = 100; } else { m_port = *settings->GetNumberData( "port" ); m_sendTimeOut = *settings->GetNumberData( "send_time_out" ); m_processingTime = *settings->GetNumberData( "processing_time" ); } SAFE_DELETE( settings ); // Изначально системе сетевых сообщений запрещено получать сообщения, специфичные для приложения. m_receiveAllowed = false; // Устанавливаем обработчик сетевых сообщений (network message handler). HandleNetworkMessage = HandleNetworkMessageFunction; // Создаём и инициализируем пиринговый (peer) интерфейс DirectPlay. CoCreateInstance( CLSID_DirectPlay8Peer, NULL, CLSCTX_INPROC, IID_IDirectPlay8Peer, (void**)&m_dpp ); m_dpp->Initialize( (PVOID)this, NetworkMessageHandler, 0 ); // Создаём адрес устройства (device address). CoCreateInstance( CLSID_DirectPlay8Address, NULL, CLSCTX_INPROC, IID_IDirectPlay8Address, (LPVOID*) &m_device ); m_device->SetSP( &CLSID_DP8SP_TCPIP ); m_device->AddComponent( DPNA_KEY_PORT, &m_port, sizeof(DWORD), DPNA_DATATYPE_DWORD ); } ...
Он принимает два вводных параметра:
GUID guid | GUID, который используется DirectPlay для однозначной идентификации приложения в локальной сети. |
void (*HandleNetworkMessageFunction)( ReceivedMessage *msg ) | Указатель на функцию обратного вызова обработчика сетевых сообщений, специфичную для приложения. Данная функция позволяет приложению делать обратный вызов (call-back), который даёт команду движку начать обработку пользовательских сетевых сообщений. |
Когда чуть позднее мы будем интегрировать систему поддержки сети в движок, мы добавим новый член в структуру EngineSetup, позволяющий назначить функцию обратного вызова для обработки пользовательских сетевых сообщений. Так вот, внутри конструктора класса Network первым делом инициализируем три критические секции, о которых говорилось ранее. Напомним, что для каждого из трёх заранее подготовленных связных списком мы создаём по одной критической секции. Это связано с тем, что каждый из этих связных списков одновременно используется как потоком приложения так и потоком обработчика сетевых сообщений.
Сразу после этого очищаем указатель на объект интерфейса IDirectPlay8Peer, объявленный в Network.h, и указатель адреса объекта устройства m_device. Данный указатель объекта устройства отличается оттого, что мы использовали при создании объекта устройства Direct3D. Здесь у нас тоже устройство, но оно ссылается на локальный адрес в сети (например, адрес инстанса данного приложения).
Далее мы копируем GUID приложения и сохраняем его в локальную переменную m_guid для дальнейшего использования. Следующий шаг - создание самих связных списков (linked lists):
... // Создаём связный список энумеруемых сессий. m_sessions = new LinkedList< SessionInfo >; // Создаём связный список игроков. m_players = new LinkedList< PlayerInfo >; // Создаём связный список сетевых сообщений. m_messages = new LinkedList< ReceivedMessage >; ...
Следующий шаг - чтение скрипта сетевых настроек (заранее подготовленного файла на жёстком диске NetworkSettings.txt) и применение содержащихся в нём настроек:
... // Загружаем сетевые настройки из файла NetworkSettings.txt. Script *settings = new Script( "NetworkSettings.txt" ); if( settings->GetNumberData( "processing_time" ) == NULL ) { m_port = 2509; m_sendTimeOut = 100; m_processingTime = 100; } else { m_port = *settings->GetNumberData( "port" ); m_sendTimeOut = *settings->GetNumberData( "send_time_out" ); m_processingTime = *settings->GetNumberData( "processing_time" ); } SAFE_DELETE( settings ); ...
- port - задаёт номер порта, который будет использоваться для обмена сообщениями по сети;
- send_time_out - задаёт таймаут для отправляемых сообщений;
- m_processingTime - расчётное время, отведённое на обработку сетевых сообщений в данном кадре.
А наш конструктор почти закончен. Осталось лишь установить флаг m_recieveAllowed в false, который отвечает за разрешение/запрет на получение пользовательских сетевых сообщений:
... // Изначально системе сетевых сообщений запрещено получать сообщения, специфичные для приложения. m_receiveAllowed = false; // Устанавливаем обработчик сетевых сообщений (network message handler). HandleNetworkMessage = HandleNetworkMessageFunction; ...
Он предохраняет приложение от получения сообщений о состоянии сессии до того, как оно будет готово обработать их. После этого сохраняем указатель функции обработчика сетевых сообщений, специфичную для приложения.
Финальные два шага включают в себя создание указателей на:
- пиринговый (peer) интерфейс DirectPlay;
- адрес объекта устройства (device address).
... // Создаём и инициализируем пиринговый (peer) интерфейс DirectPlay. CoCreateInstance( CLSID_DirectPlay8Peer, NULL, CLSCTX_INPROC, IID_IDirectPlay8Peer, (void**)&m_dpp ); m_dpp->Initialize( (PVOID)this, NetworkMessageHandler, 0 ); // Создаём адрес устройства (device address). CoCreateInstance( CLSID_DirectPlay8Address, NULL, CLSCTX_INPROC, IID_IDirectPlay8Address, (LPVOID*) &m_device ); m_device->SetSP( &CLSID_DP8SP_TCPIP ); m_device->AddComponent( DPNA_KEY_PORT, &m_port, sizeof(DWORD), DPNA_DATATYPE_DWORD ); } ...
В обоих случая используем функцию CoCreatelnstance, т.к. оба объекта являются СОМ-объектами. Ранее ты не раз видел эту функцию в деле, так что здесь всё должно быть понятно. Просто обрати внимание на идентификаторы класса и ссылки на интерфейсы, которые мы указываем здесь в этот раз.
Сразу после создания пирингового интерфейса DirectPlay, тут же инициализируем (Initialize) его путём вызова соответствующей функции. Функция Initialize принимает 3 параметра:
ПАРАМЕТР | ОПИСАНИЕ |
---|---|
(PVOID)this | Контекст нашего класса, который передаётся обработчику сетевых сообщений через DirectPlay. Данный параметр почти всегда устанавливается в this практически во всех случаях. |
NetworkMessageHandler | Указатель на функцию обратного вызова обработчика сетевых сообщений, специфичную для движка. Она будет вызвана движком для обработки пользовательских сетевых сообщений, которые не могут быть обработаны другими системами движка. |
0 | Последний параметр позволяет устанавливать различные флаги. Документация к DirectX SDK рекомендует выставлять здесь "0" (ноль) либо DPINITIALIZE_DISABLEPARAMVAL. |
Последним "штрихом" к нашему конструктору будет конечная установка и настройка объекта устройства. Вся процедура включает в себя:
- назначение сервис-провайдера путём вызова функции SetSP;
- передача в неё параметра CLSID_DP8SP_TCPIP для установки протокола TCP/IP.
... //----------------------------------------------------------------------------- // The network class destructor. //----------------------------------------------------------------------------- Network::~Network() { // Сохраняем сетевые настройки. Script *settings = new Script( "NetworkSettings.txt" ); if( settings->GetNumberData( "processing_time" ) == NULL ) { settings->AddVariable( "port", VARIABLE_NUMBER, &m_port ); settings->AddVariable( "send_time_out", VARIABLE_NUMBER, &m_sendTimeOut ); settings->AddVariable( "processing_time", VARIABLE_NUMBER, &m_processingTime ); } else { settings->SetVariable( "port", &m_port ); settings->SetVariable( "send_time_out", &m_sendTimeOut ); settings->SetVariable( "processing_time", &m_processingTime ); } settings->SaveScript(); SAFE_DELETE( settings ); // Освобождаем адрес устройства. SAFE_RELEASE( m_device ); // Закрываем и освобождаем пиринговый (peer) интерфейс DirectPlay. if( m_dpp != NULL ) m_dpp->Close( 0 ); SAFE_RELEASE( m_dpp ); // Уничтожаем связный список энумерированных сессий. SAFE_DELETE( m_sessions ); // Уничтожаем связный список игроков. SAFE_DELETE( m_players ); // Уничтожаем связный список сетевых сообщений. SAFE_DELETE( m_messages ); // Уничтожаем критические секции. DeleteCriticalSection( &m_sessionCS ); DeleteCriticalSection( &m_playerCS ); DeleteCriticalSection( &m_messageCS ); } ...
Здесь в первую очередь сохраняем текущие сетевые установки в скрипт. Если скрипт не существует (обычно при первом запуске игры так и есть), то файл скрипта будет создан автоматически (системой поддержки скриптов) и в случае изменения сетевых настроек во время работы игры, в него будут добавлены новые значения. После этого мы вызываем функцию SaveScript для сохранения скрипта и удаляем временную структуру settings.
Далее освобождаем (release) объекты адреса устройства (device address) и пирингового (peer) интерфейса DirectPlay. Перед освобождением пиринговый интерфейс должен быть предварительно закрыт (close). Это гарантирует безопасное отключение от текущей сетевой сессии, если таковая имеется.
После этого мы уничтожаем каждый из трёх связных списков и критические секции, созданные ранее.
Энумерирование сессий. Функция EnumerateSessions.
Как только ты создал объект класса Network, первым делом необходимо опросить (энумерировать, enumerate) локальную сеть на предмет существования уже запущенных сетевых сессий (sessions in progress). При энумерировании сети приложение рассылает сообщение об энумерировании (enumeration message) всем клиентам в сети, оснащённым DirectPlay. Когда удалённое клиентское приложение получает такое сообщение, DirectPlay проверяет, являются ли приложение-отправитель и приложение-получатель идентичными приложениями (играми) или нет. Он просто сравнивает GUID сетевых компонентов обоих приложений и выдаёт соответствующий результат. Если данные идентификаторы совпадают, то значит эти приложения одинаковы и можно приступать к энумерированию сессий.Следующий шаг - проверка, является ли удалённое клиентское приложение хостом в своей сессии. Если да, то удалённое приложение отправляет локальному соответствующее сообщение, содержащее адрес хоста и имя сессии, в которую мы можем затем при желании вступить.
Весь процесс скрыт от посторонних глаз и управляется напрямую средствами DirectPlay. Всё, что остаётся делать игрокодеру - это управлять ответами, которые приходят от каждого хоста. Эти ответы также накапливаются в обработчике сетевых сообщений локального приложения (как правило, они имеют тип DPN_MSGID_ENUM_HOSTS_RESPONSE). Затем мы просто сохраняем их в соответствующем связном списке m_sessions, специально созданном для хранения сведений об энумерованных сессиях. Сессии здесь сохраняются в структуре Sessionlnfo, определённой в Network.h следующим образом:
... //----------------------------------------------------------------------------- // Структура информации энумерированной сессии (Enumerated Session Information Structure) //----------------------------------------------------------------------------- struct SessionInfo { IDirectPlay8Address *address; // Session network address. DPN_APPLICATION_DESC description; // Application description. }; ...
Как видно, вся структура состоит всего из двух членов:
- адреса хоста (Он необходим для подключения к сессии)
- описания приложения (application description).
typedef struct _DPN_APPLICATION_DESC { DWORD dwSize; // Размер структуры DWORD dwFlags; // Флаги, описывающие поведение приложения GUID guidInstance; // GUID, сгенерированный во время инициализации DirectPlay GUID guidApplication; // GUID приложения DWORD dwMaxPlayers; // Максимальное число игроков, разрешённое в данной сессии или игре DWORD dwCurrentPlayers; // Количество игроков, подключенные к сессии в данный момент WCHAR *pwszSessionName; // Имя сессии WCHAR *pwszPassword; // Пароль для подключения к сессии PVOID pvReservedData; // Зарезервированные данные, которые не должны изменяться DWORD dwReservedDataSize; PVOID pvApplicationReservedData; // Данные, специфичные для приложения DWORD dwApplicationReservedDataSize; } DPN_APPLICATION_DESC, *PDPN_APPLICATION_DESC;
Структура не маленькая. Но большинству её членов зачастую значения присваиваются автоматически. Оставшиеся переменные мы рассмотрим подробнее по ходу изложения.
Но вернёмся к исходному коду Network.cpp. Для энумерирования сессий в сети вызываем функцию EnumerateSessions:
... //----------------------------------------------------------------------------- // Энумерируем сессии в локальной сети (LAN). //----------------------------------------------------------------------------- void Network::EnumerateSessions() { // Очищаем связные списки. m_players->Empty(); m_messages->Empty(); m_sessions->Empty(); // Подготавливаем описание приложения (application description). DPN_APPLICATION_DESC description; ZeroMemory( &description, sizeof( DPN_APPLICATION_DESC ) ); description.dwSize = sizeof( DPN_APPLICATION_DESC ); description.guidApplication = m_guid; // Энумерируем сессии синхронно (synchronously). m_dpp->EnumHosts( &description, NULL, m_device, NULL, 0, 1, 0, 0, NULL, NULL, DPNENUMHOSTS_SYNC ); } ...
Сразу после входа в функцию очищаем три ранее заготовленных связных списка. Сам факт того, что клиентское приложение энумерирует сессии в сети означает, что оно в данный момент не является ни хостом, ни клиентом, вступившим в какую-либо сессию. Так что начинаем, что называется, с чистого листа.
Далее заполняем базовые элементы структуры DPN_APPLICATION_DESC (сперва очистив её) и в частности назначаем GUID приложения. Это тот самый GUID, который будет использоваться DirectPlay для сравнения удалённого приложения с локальным. Данное сравнение выполняет сетевой компонент DirectPlay удалённого приложения, когда то получает соответствующее сообщение об энумерировании.
В финале вызываем функцию EnumHosts, экспонированную интерфейсом DirectPlay. Она содержит несколько параметров, поэтому взглянем на её прототип:
HRESULT EnumHosts( PDPN_APPLICATION_DESC const pApplicationDesc, IDirectPlay8Address *const pdpaddrHost, IDirectPlay8Address *const pdpaddrDeviceInfo, PVOID const pvUserEnumData, const DWORD dwUserEnumDataSize, const DWORD dwEnumCount, const DWORD dwRetryInterval, const DWORD dwTimeOut, PVOID const pvUserContext, HANDLE *const pAsyncHandle, const DWORD dwFlags );
Вот описание её параметров:
ПАРАМЕТР | ОПИСАНИЕ |
---|---|
PDPN_APPLICATION_DESC const pApplicationDesc | Указатель на структуру DPN_APPLICATION_DESC. Чуть раньше мы создали её в классе Network как раз для этого случая. Поэтому здесь передаём лишь указатель на неё. |
IDirectPlay8Address "const pdpaddrHost | Адрес хоста. В нашем случае устанавливаем в NULL, т.к. не знаем его. Собственно для этого и проводим энумерирование. |
IDirectPlay8Address "const pdpaddrDeviceInfo | Адрес объекта устройства. Указываем m_device. |
PVOID const pvUserEnumData | Данные энумерирования. Устанавливаем в NULL. |
const DWORD dwUserEnumDataSize | Размер данных энумерирования. Устанавливаем в 0. |
const DWORD dwEnumCount | Счётчик. Устанавливаем в 1, чтобы провести оповещение об энумерировании (enumeration broadcasting) всего 1 раз. |
const DWORD dwEntryInterval | Интервал ввода. Оставляем в 0. |
const DWORD dwTimeOut | Таймаут. Оставляем в 0. |
PVOID const pvUserContext | Контекст пользователя. Оставляем в NULL. |
HANDLE *const pAsyncHandle | Асинхронный обработчик. Оставляем в NULL. |
const DWORD dwFlags | Флаги. Указываем DPNENUMHOSTS_SYNC для проведения энумерирования в синхронном режиме. То есть во время вызова функции EnumHosts она не вернёт результат пока полностью не завершит свою работу. Поиск активных сетевых сессий - обычное дело во многих играх, особенно в мультиплеерных. Часто функция EnumHosts запускается по нажатию специальной кнопки Refresh, представленной на экране сетевых настроек игры. При её нажатии происходит синхронный опрос, результаты которого появляются на экране лишь спустя несколько секунд. |
Обрати внимание
Энумерацию (как и многие другие операции DirectPlay) можно также проводить в асинхронном режиме. В этом случае контроль возвращается к приложению сразу после вызова функции EnumHosts, без ожидания завершения её работы. Более того, ты можешь позволить выполнять приложению другие задачи в фоновом режиме во время выполнения DirectPlay запрошенных операций.
Мы не будем углубляться в детали техники асинхронного выполнения операций. Но чуть позднее ты увидишь её в деле, когда мы рассмотрим отправку сообщений. Синхронные операции более просты для понимания, поэтому мы будем использовать их везде, где только можно. Более подробная информация об асинхронных операциях представлена в документации DirectX SDK. особенно полезно изучить функцию CancelAsyncOperation в интерфейсе IDirectPlay8Peer. Когда энумерация вернётся в приложение с ответом, обработчик сетевых сообщений будет вызван для каждого из ответов. Сообщение о сессиях обычно имеет идентификатор DPN_MSGID_ENUM_HOSTS_RESPONSE и содержит детальную информацию о хосте и его сессии. В Network.cpp данное сообщение помимо всех прочих также обрабатывается внутренним обработчиком сетевых сообщений:
... case DPN_MSGID_ENUM_HOSTS_RESPONSE: { PDPNMSG_ENUM_HOSTS_RESPONSE response = (PDPNMSG_ENUM_HOSTS_RESPONSE)data; // Создаём структуру информации о сессии (session information structure) для новой сессии. SessionInfo *sessionInfo = new SessionInfo; response->pAddressSender->Duplicate( &sessionInfo->address ); memcpy( &sessionInfo->description, response->pApplicationDescription, sizeof( DPN_APPLICATION_DESC ) ); // Добавляем новую сессию в свзяный список сессий (session list). EnterCriticalSection( &network->m_sessionCS ); network->m_sessions->Add( sessionInfo ); LeaveCriticalSection( &network->m_sessionCS ); break; } ...
Здесь первым делом с помощью функции Duplicate мы копируем адрес хоста, который тут же сохраняем в структуре SessionInfo. Далее создаём копию описания приложения и также сохраняем его в виде члена структуры Sessionlnfo. После этого входим в критическую секцию связного списка сессий (m_sessions), добавляем в него новую сессию и выходим из критической секции.
Хостинг сессий и вступление в них
В данный момент ты уже умеешь энумерировать сессии и знаешь как обрабатывать ответные сообщения. В то же время это всё не имеет смысла, если наш движок не будет способен создавать сессии и вступать в них. Так что рассмотрим две функции, ответственные за эти операции. В классе Network представлены две функции:- Host для хостинга сетевых сессий;
- Join для входа в них.
Функция Host
Реализация созданной нами функции Host представлена в Network.cpp и выглядит следующим образом:... //----------------------------------------------------------------------------- // Пытаемся захостить (host) сессию. //----------------------------------------------------------------------------- bool Network::Host( char *name, char *session, int players, void *playerData, unsigned long dataSize ) { WCHAR wide[MAX_PATH]; // Подготавливаем и заполняем структуру информации об игроке (player information structure). DPN_PLAYER_INFO player; ZeroMemory( &player, sizeof( DPN_PLAYER_INFO ) ); player.dwSize = sizeof( DPN_PLAYER_INFO ); player.pvData = playerData; player.dwDataSize = dataSize; player.dwInfoFlags = DPNINFO_NAME | DPNINFO_DATA; mbstowcs( wide, name, MAX_PATH ); player.pwszName = wide; if( FAILED( m_dpp->SetPeerInfo( &player, NULL, NULL, DPNSETPEERINFO_SYNC ) ) ) return false; // Подготавливаем описание приложения (application description). DPN_APPLICATION_DESC description; ZeroMemory( &description, sizeof( DPN_APPLICATION_DESC ) ); description.dwSize = sizeof( DPN_APPLICATION_DESC ); description.guidApplication = m_guid; description.dwMaxPlayers = players; mbstowcs( wide, session, MAX_PATH ); description.pwszSessionName = wide; // Host the session. if( FAILED( m_dpp->Host( &description, &m_device, 1, NULL, NULL, NULL, 0 ) ) ) return false; return true; } ...
Она принимает 5 вводных параметров:
ПАРАМЕТР | ОПИСАНИЕ |
---|---|
char *name | Имя игрока, "хостящего" сессию. |
char *session | Имя сессии. |
int players | Максимально допустимое число игроков, одновременно подключенных к данной сессии, включая хост. |
void *playerData | Дополнительные сведения, запрашиваемые при создании игрока в сессии. |
unsigned long dataSize | Размер дополнительных сведений, запрашиваемых при создании игрока в сессии. |
Данные, ассоциированные с каждым игроком, также учитываются при его регистрации в сессии. Они содержатся в структуре DPN_PLAYER_INFO, которую мы заполняем здесь же. Информация об игроках, размещаемая в данной структуре, передаётся каждому клиенту в сессии. Это делается для того, чтобы каждый клиент мог создать у себя на компьютере и обслуживать локальные копии всех игроков в данной сессии.
Рассмотрим определение структуры DPN_PLAYER_INFO:
typedef struct _DPN_PLAYER_INFO { DWORD dwSize; // Размер структуры DWORD dwInfoFlags; // Идентифицирует содержимое структуры PWSTR pwszName; // Имя игрока PVOID pvData; // Данные, специфичные для игрока DWORD dwDataSize; // Размер данных, специфичных для пользователя DWORD dwPlayerFlags; // Идентифицирует гост/локальный игрок } DPN_PLAYER_INFO, *PDPN_PLAYER_INFO;
В нашем случае, оказавшись внутри функции Host, первым делом предварительно очищаем содержимое структуры DPN_PLAYER_INFO (ZeroMemory) и устанавливаем её размер (sizeof). Затем указываем данные игрока, а также размер этих данных. В элементе player.dwInfoFlags указываем два флага (объединённых логическим "или"): DPNINFO_NAME | DPNINFO_DATA, указывающих на то, что структура содержит имя игрока и его данные.
В финале просто назначаем имя игрока.
Обрати внимание
Внимательный читатель наверняка заметит, что мы так и не установили флаг DWORD dwPlayerFlags. А всё потому, что DirectPlay делает это автоматически. Как только приложение получает сетевое сообщение о том, что новый игрок вступил в сессию, мы можем проверить этот флаг. Если он имеет значение DPNPLAYER_LOCAL, то новый игрок является локальным, т.е. принадлежит данному локальному приложению (проще говоря, является рядовым клиентом). Если флаг имеет значение DPNPLAYERJHOST, то это означает, что новый игрок является хостом в данной сессии.
Последний штрих - назначение данных в только что заполненной структуре DPN_PLAYER_INFO локальному игроку (DirectPlay peer) путём вызова функции SetPeerInfo, экспонированной интерфейсом IDirectPlay8Peer:
... if( FAILED( m_dpp->SetPeerInfo( &player, NULL, NULL, DPNSETPEERINFO_SYNC ) ) ) return false; ...
Здесь в первом параметре передаём указатель на структуру (&player), а в последнем указываем флаг DPNSETPEERINFO_SYNC для проведения операции в синхронном режиме.
Во второй части реализации функции Host заполняем специальную структуру DPN_APPLICATION_DESC, содержащую детальную информацию для определения приложения и создаваемой сессии:
... // Подготавливаем описание приложения (application description). DPN_APPLICATION_DESC description; ZeroMemory( &description, sizeof( DPN_APPLICATION_DESC ) ); description.dwSize = sizeof( DPN_APPLICATION_DESC ); description.guidApplication = m_guid; description.dwMaxPlayers = players; mbstowcs( wide, session, MAX_PATH ); description.pwszSessionName = wide; ...
Как и в структуре DPN_PLAYER_INFO мы сперва очищаем её содержимое путём вызова функции ZeroMemory и устанавливаем её размер (sizeof).
Вот полное определение (шаблон) структуры DPN_APPLICATION_DESC:
typedef struct _DPN_APPLICATION_DESC { DWORD dwSize; DWORD dwFlags; GUID guidInstance; GUID guidApplication; DWORD dwMaxPlayers; DWORD dwCurrentPlayers; WCHAR *pwszSessionName; WCHAR *pwszPassword; PVOID pvReservedData; DWORD dwReservedDataSize; PVOID pvApplicationReservedData; DWORD dwApplicationReservedDataSize; } DPN_APPLICATION_DESC, *PDPN_APPLICATION_DESC;
В нашем случае мы используем всего 3 её элемента. Вот их описание:
ЭЛЕМЕНТ | ОПИСАНИЕ |
---|---|
description.guidApplication = m_guid | Устанавливаем GUID приложения. Он используется при энумерировании сессий для того, что бы отсеивать сессии других приложений с другими GUID-ами. |
description.dwMaxPiayers = players | Максимальное число одновременно подключенных пользователей, разрешённое в создаваемой сессии. |
mbstowcs( wide, session, MAX_PATH ); description.pwszSessionName = wide; | Имя создаваемой сессии. |
Далее вызываем функцию Host, экспонированную интерфейсом IDirectPlay8Peer:
... // Host the session. if( FAILED( m_dpp->Host( &description, &m_device, 1, NULL, NULL, NULL, 0 ) ) ) return false; ...
Вот её прототип:
HRESULT Host( const DPN_APPLICATION_DESC *const pdnAppDesc, // Специальная структура, описывающая приложение IDirectPlay8Address **const prgpDeviceInfo, // Указатель на интерфейс (Адрес устройства) const DWORD cDeviceInfo, // Указывает на количество объектов const DPN_SECURITY_DESC *const pdpSecurity, // Зарезервировано - NULL const DPN_SECURITY_CREDENTIALS *const pdpCredentials, // Зарезервировано - NULL VOID *const pvPlayerContext, // Необязательный параметр – увеличивается при присоединении клиента const DWORD dwFlags // Флаг );
В нашем случае мы указали следующие параметры:
ПАРАМЕТР | ОПИСАНИЕ |
---|---|
&description | Указатель на только что заполненную структуру DPN_APPLICATION_DESC. |
&m_device | Адрес объекта (сетевого) устройства, на котором будет создан хост. |
Остальные параметры можно проигнорировать, выставив значения по умолчанию.
Мы "обрамляем" вызов функции условным оператором if для проверки случая провала выполнения (в этом случае функция возвращает false). Приложение использует это выражение для определения, успешно ли создан хост или нет.
Функция Join
Если ты разобрался как работает функция Host, то с функцией Join не должно быть никаких проблем, т.к. об они во многом схожи. В классе Network мы создали собственную функцию Join:... //----------------------------------------------------------------------------- // Пытаемся вступить (to join) в выбранную сессию из связного списка энумерированных сессий. //----------------------------------------------------------------------------- bool Network::Join( char *name, int session, void *playerData, unsigned long dataSize ) { WCHAR wide[MAX_PATH]; // Очищаем связные списки игроков (player list) и сетевых сообщений (newtork messages). m_players->Empty(); m_messages->Empty(); // Игнорим неправильные (invalid) сессии. if( session < 0 ) return false; // Подготавливаем и заполняем структуру информации об игроке (player information structure). DPN_PLAYER_INFO player; ZeroMemory( &player, sizeof( DPN_PLAYER_INFO ) ); player.dwSize = sizeof( DPN_PLAYER_INFO ); player.pvData = playerData; player.dwDataSize = dataSize; player.dwInfoFlags = DPNINFO_NAME | DPNINFO_DATA; mbstowcs( wide, name, MAX_PATH ); player.pwszName = wide; if( FAILED( m_dpp->SetPeerInfo( &player, NULL, NULL, DPNSETPEERINFO_SYNC ) ) ) return false; // Входим в критическую секцию связного списка энумерированных сессий (sessions linked list critical section). EnterCriticalSection( &m_sessionCS ); // Находим хост выбранной сессии. m_sessions->Iterate( true ); for( int s = 0; s < session + 1; s++ ) { if( m_sessions->Iterate() == NULL ) { LeaveCriticalSection( &m_sessionCS ); return false; } } // Вступаем (join) в сессию. if( FAILED( m_dpp->Connect( &m_sessions->GetCurrent()->description, m_sessions->GetCurrent()->address, m_device, NULL, NULL, NULL, 0, NULL, NULL, NULL, DPNCONNECT_SYNC ) ) ) { LeaveCriticalSection( &m_sessionCS ); return false; } LeaveCriticalSection( &m_sessionCS ); return true; } ...
Функция Join принимает 4 вводных параметра:
ПАРАМЕТР | ОПИСАНИЕ |
---|---|
char *name | Имя локального игрока. |
int session | Порядковый номер (индекс) выбранной сессии в связном списке сессий m_sessions. |
void *playerData | Данные, специфичные для игрока. |
unsigned long dataSize | Размер данных, специфичных для игрока. |
Оказавшись в теле функции, первым делом очищаем связные списки игроков (m_players) и сообщений (m_messages):
... // Очищаем связные списки игроков (player list) и сетевых сообщений (newtork messages). m_players->Empty(); m_messages->Empty();nterCriticalSection( &m_sessionCS ); ...
Также проверяем на валидность индекс выбранной сессии. Если индекс сессии меньше 0, то возвращаем false. В конце заполняем структуру DPN_PLAYER_INFO, содержащую данные о локальном игроке.
Следующий шаг - поиск нужной сессии в связном списке сессий. Для этого мы входим в заранее подготовленную критическую секцию m_sessionCS для предотвращения внесения изменений в связный список сессий со стороны обработчика сетевых сообщений:
... // Входим в критическую секцию связного списка энумерированных сессий (sessions linked list critical section). EnterCriticalSection( &m_sessionCS ); // Находим хост выбранной сессии. m_sessions->Iterate( true ); for( int s = 0; s < session + 1; s++ ) { if( m_sessions->Iterate() == NULL ) { LeaveCriticalSection( &m_sessionCS ); return false; } } // Вступаем (join) в сессию. if( FAILED( m_dpp->Connect( &m_sessions->GetCurrent()->description, m_sessions->GetCurrent()->address, m_device, NULL, NULL, NULL, 0, NULL, NULL, NULL, DPNCONNECT_SYNC ) ) ) { LeaveCriticalSection( &m_sessionCS ); return false; } LeaveCriticalSection( &m_sessionCS ); ...
Внутри критической секции итерируем (опрашиваем) связный список сессий до тех пор, пока не будет найдена сессия с нужным индексом (т.е. та, в которую хотим вступить). Как только это будет сделано, немедленно прерываем итерацию. Теперь, когда всё готово к вступлению в сессию, делаем это путём вызова функции connect, экспонированную интерфейсом IDirectPlay8Peer:
HRESULT Connect( const DPN_APPLICATION_DESC *const pdnAppDesc, // Структура, описывающая сетевое приложение IDirectPlay8Address *const pHostAddr, // Адрес хоста IDirectPlay8Address *const pDeviceInfo, // Адрес устройства const DPN_SECURITY_DESC *const pdnSecurity, // Зарезервировано - NULL const DPN_SECURITY_CREDENTIALS *const pdnCredentials, // Зарезервировано - NULL const void *const pvUserConnectData,// Информация о соединяющемся пользователе (Опционально - NULL) const DWORD dwUserConnectDataSize, // Размер void *const pvPlayerContext, // Опционально - NULL void *const pvAsyncContext, // Опционально - NULL DPNHANDLE *const phAsyncHandle, // Переключатель (хэндл) асинхронного выполнения const DWORD dwFlags // Флаги );
Три первых и последний аргумент совпадают с первыми тремя аргументами метода Host , остальные в большинстве случаев устанавливаются в NULL. Следует обратить внимание на флаги.2
В нашем случае в первом параметре мы передаём указатель на структуру DPN_APPLICATION_DESC сессии, в которую хотим вступить. Во втором - адрес хоста этой сессии. В третьем параметре передаём адрес локального устройства (m_device). Остальные параметры (кроме последнего) нам не нужны, поэтому пропускаем их, выставив NULL. В последнем параметре выставляем флаг DPNCONNECT_SYNC для выполнения соединения в синхронном режиме. Это означает, что функция Join не вернёт значение до тех пор, пока попытка соединения не увенчается успехом (либо не завершится с ошибкой). Мы "обрамляем" вызов функции Join условным оператором if для корректной обработки неудачного соединения и возврата в этом случае значения false. В случае успешного соединения функция join возвращает true. Приложение затем может использовать возвращённое значение для определения успешного либо неуспешного соединения.
В конце мы обязательно покидаем (leave) критическую секцию m_sessionCS для того, чтобы вновь открыть доступ к этому связному списку другим потокам и предотвратить таким образом зависание приложения.
Отправка и получение сообщений
Мы уже умеем создавать сессии, вступать в них и даже производить их энумерирование. Следующий шаг - внедрить в нашу систему поддержки сети функции обмена сообщениями. Для этого в классе Network мы создали несколько функций. Первая из них - Send, предназначенная для отправки сетевых сообщений:... //----------------------------------------------------------------------------- // Отправляет сетевое сообщение. //----------------------------------------------------------------------------- void Network::Send( void *data, long size, DPNID dpnid, long flags ) { DPNHANDLE hAsync; DPN_BUFFER_DESC dpbd; if( ( dpbd.dwBufferSize = size ) == 0 ) return; dpbd.pBufferData = (BYTE*)data; m_dpp->SendTo( dpnid, &dpbd, 1, m_sendTimeOut, NULL, &hAsync, DPNSEND_NOCOMPLETE); } ...
Созданная нами функция Send принимает 4 вводных параметра:
ПАРАМЕТР | ОПИСАНИЕ |
---|---|
void *data | Указатель на отправляемые данные. |
long size | Размер отправляемых данных. |
DPNID dpnid | Идентификатор получателя сообщения. |
long flags | Флаги, используемые при отправке сообщения. |
Функция на удивление мала. В её теле проверяем размер отправляемых данных и затем копируем сами данные в заранее подготовленную структуру DPN_BUFFER_DESC, которая используется для их отправки.
Сама отправка осуществляется путём вызова функции SendTo, экспонированной интерфейсом IDirectPlay8Peer. Вот её прототип:
HRESULT SendTo( const DPNID dpnid, const DPN_BUFFER_DESC *const pBufferDesc, const DWORD cBufferDesc, const DWORD dwTimeOut, void *const pvAsyncContext, DPNHANDLE *const phAsyncHandle, const DWORD dwFlags );
В нашем случае в первом параметре мы передаём идентификатор DPNID игрока, которому хотим отправить сообщение. Для отправки сообщения всем игрокам в сессии здесь необходимо указать флаг DPNID_ALL_PLAYERS_GROUP. Следующие два параметра используются для передачи указателя на структуру DPN_BUFFER_DESC, куда мы чуть ранее скопировали необходимые данные.
В параметре cBufferDesc выставляем 1, что означает наличие всего одной заполненной структуры DPN_BUFFER_DESC. В параметре dwTimeOut выставляем значение m_sendTimeOut, которое конструктор класса Network считал в память из скрипта сетевых настроек. Таким образом мы указываем число миллисекунд, в течение которых DirectPlay должен ждать отправки сообщения. Если по прошествии данного промежутка времени сообщение остаётся не отправленным, оно удаляется. Это предотвращает скапливание сообщений, которые по каким-либо причинам оказались не отправлены. Следующие два параметра используются для отправки сообщения в асинхронном режиме, что нам и нужно. Это означает, что функция SendTo вернёт управления программе немедленно, не дожидаясь своего полного выполнения. Параметр pvAsyncContext мы и вовсе выставляем в NULL, т.к. нам не требуется предоставлять какой-либо контекст. Тем не менее нам необходимо задействовать переключатель phAsyncHandle для вызова функции. Поэтому в данном параметре выставляем &hAsync, хотя данный указатель нам в принципе не нужен. Переключатель действует как медицинский рецепт для вызова функции, который позволит отследить вызовы других функций (например CancelAsyncOperation, позволяющую по команде прервать выполнение функции). Проще говоря, если сообщение не отправлено, программер может прервать его отправку.
Последний параметр используется для указания различных флагов, контролирующих различные аспекты отправки сообщения. В нашем случае указываем DPNSEND_NOCOMPLETE. Полный список флагов представлен в таблице:
ПАРАМЕТР | ОПИСАНИЕ |
---|---|
DPNSEND_SYNC | Отправить сообщение в синхронном режиме. |
DPNSEND_NOCOPY | Не делать копию структуры DPN_BUFFER_DESC перед отправкой. Несмотря на то, что это может сделать процесс отправки более эффективным, здесь мы теряем в гибкости, т.к. изменение данных в структуре DPN_BUFFER_DESC до получения служебного сообщения DPN_MSGID_SEND_COMPLETE может вызвать проблемы. Данный флаг нельзя комбинировать с флагом DPNSEND_NOCOMPLETE. |
DPNSEND_NOCOMPLETE | Сообщение DPN_MSGID_SEND_COMPLETE не отправляется назад твоему приложению. Данный флаг нельзя комбинировать с флагами DPNSEND_NOCOPY и DPNSEND_GUARANTEED. Кроме того параметр pvAsyncContext должен быть равен NULL. |
DPNSEND_COMPLETEONPROCESS | Сообщение DPN_MSGID_SEND_COMPLETE отправляется назад твоему приложению всякий раз, когда отправленное сообщение достигнет адресата. Данный флаг может замедлить процесс обмена сообщениями. Вместе с ним обычно используют флаг DPN_SEND_GUARANTEED. |
DPNSEND_GUARANTEED | Гарантирует доставку сообщения до адресата. Данный флаг может замедлить процесс обмена сообщениями. |
DPNSEND_PRIORITY_HIGH | Даёт отправляемому сообщению высокий приоритет. Данный флаг нельзя комбинировать с флагом DPNSEND_PRIORITY_LOW. |
DPNSEND_PRIORITY_LOW | Даёт отправляемому сообщению низкий приоритет. Данный флаг нельзя комбинировать с флагом DPNSEND_PRIORITY_HIGH. |
DPNSEND_NONSEQUENTIAL | Данный флаг позволяет приложению-получателю получать сообщения сразу после их поступления. Если данный флаг не установлен, DirectPlay дополнительно проверяет, что сообщения будут поступать на компьютер получателя в том порядке, в котором они были отправлены. |
DPNSEND_NOLOOPBACK | Предотвращает отправку сообщений самому себе при массовой рассылке всем клиентам. |
Как только сообщение было отправлено, оно тут же поступает на обработчик сетевых сообщений приложения-получателя (target application's network message handler). Наш класс Network имеет свой внутренний обработчик сообщений, который обрабатывает все системные сообщения и сохраняет в связный список все пользовательские (user-defined) сообщения для последующей обработки. Он представлен в Network.cpp :
... //----------------------------------------------------------------------------- // Внутренний обработчик сетевых сообщений (internal network message handler). //----------------------------------------------------------------------------- HRESULT WINAPI Network::NetworkMessageHandler( PVOID context, DWORD msgid, PVOID data ) { // Получаем указатель на вызываемый сетевой объект. Network *network = (Network*)context; // Обрабатываем входящие сообщения, основываясь на их типе. switch( msgid ) { case DPN_MSGID_CREATE_PLAYER: { unsigned long size = 0; DPN_PLAYER_INFO *info = NULL; HRESULT hr = DPNERR_CONNECTING; PDPNMSG_CREATE_PLAYER msgCreatePlayer = (PDPNMSG_CREATE_PLAYER)data; // Создаём структуру информации об игроке (player information structure) для нового игрока. PlayerInfo *playerInfo = new PlayerInfo; ZeroMemory( playerInfo, sizeof( PlayerInfo ) ); playerInfo->dpnid = msgCreatePlayer->dpnidPlayer; // Продолжаем вызывать функцию GetPeerInfo(), т.к. он всё ещё может пытаться подключиться. while( hr == DPNERR_CONNECTING ) hr = network->m_dpp->GetPeerInfo( playerInfo->dpnid, info, &size, 0 ); // Проверяем, вернула ли функция GetPeerInfo() размер структуры DPN_PLAYER_INFO. if( hr == DPNERR_BUFFERTOOSMALL ) { info = (DPN_PLAYER_INFO*) new BYTE[size]; ZeroMemory( info, size ); info->dwSize = sizeof( DPN_PLAYER_INFO ); // Пытаемся снова, используя верный размер. if( SUCCEEDED( network->m_dpp->GetPeerInfo( playerInfo->dpnid, info, &size, 0 ) ) ) { // Сохраняем имя нового игрока. playerInfo->name = new char[wcslen( info->pwszName ) + 1]; ZeroMemory( playerInfo->name, wcslen( info->pwszName ) + 1 ); wcstombs( playerInfo->name, info->pwszName, wcslen( info->pwszName ) ); // Сохраняем данные нового игрока. playerInfo->data = new BYTE[info->dwDataSize]; memcpy( playerInfo->data, info->pvData, info->dwDataSize ); playerInfo->size = info->dwDataSize; // Сохраняем остальные установки локального игркоа и хоста. if( info->dwPlayerFlags & DPNPLAYER_LOCAL ) network->m_dpnidLocal = playerInfo->dpnid; if( info->dwPlayerFlags & DPNPLAYER_HOST ) network->m_dpnidHost = playerInfo->dpnid; } SAFE_DELETE_ARRAY( info ); } // Добавляем нового игрока в связный список игроков (player list). EnterCriticalSection( &network->m_playerCS ); network->m_players->Add( playerInfo ); LeaveCriticalSection( &network->m_playerCS ); // Если нет никакого обработчика сетевых сообщений, то немедленно прерываем (break). if( network->HandleNetworkMessage == NULL ) break; // Создаём сообщение игрока о создании игрока. ReceivedMessage *message = new ReceivedMessage; message->msgid = MSGID_CREATE_PLAYER; message->dpnid = playerInfo->dpnid; // Сохраняем сообщение для последующей обработки приложением. EnterCriticalSection( &network->m_messageCS ); network->m_messages->Add( message ); LeaveCriticalSection( &network->m_messageCS ); break; } case DPN_MSGID_DESTROY_PLAYER: { // Находим игрока, уничтожаем его и убираем из связного списка игроков (player list). EnterCriticalSection( &network->m_playerCS ); network->m_players->Iterate( true ); while( network->m_players->Iterate() ) { if( network->m_players->GetCurrent()->dpnid == ( (PDPNMSG_DESTROY_PLAYER)data )->dpnidPlayer ) { network->m_players->Remove( (PlayerInfo**)network->m_players->GetCurrent() ); break; } } LeaveCriticalSection( &network->m_playerCS ); // Если нет никакого обработчика сетевых сообщений, то немедленно прерываем (break). if( network->HandleNetworkMessage == NULL ) break; // Создаём сообщение игрока об уничтожении игрока. ReceivedMessage *message = new ReceivedMessage; message->msgid = MSGID_DESTROY_PLAYER; message->dpnid = ( (PDPNMSG_DESTROY_PLAYER)data )->dpnidPlayer; // Сохраняем сообщение для последующей обработки приложением. EnterCriticalSection( &network->m_messageCS ); network->m_messages->Add( message ); LeaveCriticalSection( &network->m_messageCS ); break; } case DPN_MSGID_ENUM_HOSTS_RESPONSE: { PDPNMSG_ENUM_HOSTS_RESPONSE response = (PDPNMSG_ENUM_HOSTS_RESPONSE)data; // Создаём структуру информации о сессии (session information structure) для новой сессии. SessionInfo *sessionInfo = new SessionInfo; response->pAddressSender->Duplicate( &sessionInfo->address ); memcpy( &sessionInfo->description, response->pApplicationDescription, sizeof( DPN_APPLICATION_DESC ) ); // Добавляем новую сессию в свзяный список сессий (session list). EnterCriticalSection( &network->m_sessionCS ); network->m_sessions->Add( sessionInfo ); LeaveCriticalSection( &network->m_sessionCS ); break; } case DPN_MSGID_RECEIVE: { // Если нет никакого обработчика сетевых сообщений, то немедленно прерываем (break). if( network->HandleNetworkMessage == NULL ) break; // Проверяем, разрешено ли системе поддержки сети получать сообщения, специфичные для приложения (application specific messages). if( network->m_receiveAllowed == false ) break; // Создаём сообщение о получении сообщения. ReceivedMessage *message = new ReceivedMessage; memcpy( message, ( (PDPNMSG_RECEIVE)data )->pReceiveData, ( (PDPNMSG_RECEIVE)data )->dwReceiveDataSize ); // Сохраняем сообщение для последующей обработки приложением. EnterCriticalSection( &network->m_messageCS ); network->m_messages->Add( message ); LeaveCriticalSection( &network->m_messageCS ); break; } case DPN_MSGID_TERMINATE_SESSION: { // Если нет никакого обработчика сетевых сообщений, то немедленно прерываем (break). if( network->HandleNetworkMessage == NULL ) break; // Создаём сообщение об уничтожении сессии. ReceivedMessage *message = new ReceivedMessage; message->msgid = MSGID_TERMINATE_SESSION; // Сохраняем сообщение для последующей обработки приложением. EnterCriticalSection( &network->m_messageCS ); network->m_messages->Add( message ); LeaveCriticalSection( &network->m_messageCS ); break; } } return S_OK; } ...
Когда DirectPlay вызывает функцию NetwokMessageHandler, он передаёт ей контекст, который представляет собой указатель, установленный ещё в конструкторе класса Network. По сути DirectPlay передаёт здесь указатель на инстанс класса Network. Всё, что нам нужно сделать, это передать context в указатель инстанса класса Network. Не забываем, что обработчик сетевых сообщений работает в отдельном потоке, что позволяет нам использовать наш объект класса Network для обработки сообщений.
Далее входим в переключатель switch, проверяющий разные значения параметра msgid (также передаваемые DirectPlay). Обрабатываем сообщения соответствующим образом, исходя из их значения msgid.
Указатель data, в свою очередь, указывает на адрес в памяти, где расположены данные сообщения.
Ещё раз внимательно изучи комментарии к функции NetwokMessageHandler в исходном коде Network.cpp для лучшего понимания принципа её работы.
Обрати внимание
К счастью тебе никогда не придётся беспокоится о том, каким образом обрабатываются системные сообщения. Всё внимание нужно уделить именно обработке твоим приложением пользовательских сообщений.
Рассмотрим, как себя ведёт внутренний обработчик сетевых сообщений класса Network, при поступлении пользовательского сообщения. DirectPlay присваивает всем пользовательским сообщениям идентификатор DPN_MSGID_RECIEVE, наличие которого проверяется оператором switch внутреннего обработчика сетевых сообщений класса Network. Если данный идентификатор у сообщения отсутствует, то оно считается не пользовательским и игнорируется. Здесь мы также проверяем состояние флага m_recieveAllowed, чтобы убедиться в том, что системе поддержки сети разрешена обработка пользовательских сетевых сообщений.
Как только мы успешно прошли эти две проверки, мы создаём инстанс структуры RecievedMessage и копируем в него данные сообщения.
В финале мы входим в критическую секцию m_messageCS, добавляем заполненную структуру RecievedMessage в связный список m_messages и покидаем критическую секцию. К этому моменту пользовательское сообщение сохранено и готово к обработке приложением.
Обработка сетевых сообщений
Когда приложение находится в состоянии готовности, оно вызывает функцию Update, экспонированную классом Network. Её реализация представлена в Network.cpp и выглядит так:... //----------------------------------------------------------------------------- // Обновляем (Update) объект сетевого устройства, позволяя ему обрабатывать сообщения. //----------------------------------------------------------------------------- void Network::Update() { EnterCriticalSection( &m_messageCS ); ReceivedMessage *message = m_messages->GetFirst(); unsigned long endTime = timeGetTime() + m_processingTime; while( endTime > timeGetTime() && message != NULL ) { HandleNetworkMessage( message ); m_messages->Remove( &message ); message = m_messages->GetFirst(); } LeaveCriticalSection( &m_messageCS ); } ...
Как только мы вошли в тело функции, мы сразу входим в критическую секцию m_messageCS. Это обезопасит обработчик сетевых сообщений от попытки добавить новые сообщения в связный список m_messages в то же самое время, когда мы уже обрабатываем сообщения из него.
Следующий шаг - получение указателя на первое сообщение в связном списке m_messages. В нашем случае это будет первое сообщение, отправляемое на обработку. Напомним, что новые сообщения добавляются в конец связного списка. Поэтому наиболее "старые" сообщения будут в самом его начале. Теперь всё готово ко входу в цикл while, контролируемый таймером.
Принцип работы таймера довольно прост. Берётся текущее время (путём вызова функции timeGetTime) и нему прибавляется время процессинга (processing time; его значение берётся их скрипта сетевых настроек). В результате получаем время окончания (останова) обработки сообщений (end time; в милисекундах). Затем при каждой итерации цикла while мы просто сравниваем текущее (системное) время с временем останова. Как только текущее время достигнет значения времени останова, это означает, что время вышло и обработка сетевых сообщений для данного кадра прекращается. В условии цикла while мы также проверяем, что указатель на текущее сетевое сообщение (current network message) не равен NULL. Если всё-таки равен, то в этом случае мы достигли окончания связного списка m_messages, и в нём просто не осталось сетевых сообщений, ожидающих обработки. В обоих случаях (будь то истечение времени таймера или достижение конца связного списка сообщений) мы немедленно прерываем выполнение цикла while.
Внутри цикла while мы передаём текущее сетевое сообщение в обработчик сетевых сообщений, специфичный для приложения (application specific message handler). Сразу по завершении этой операции мы безопасно удаляем переданное сообщение из связного списка m_messages, т.к. им впоследствии будет заниматься приложение. Далее цикл переходит к следующем сообщению и весь процесс повторяется вновь.
При выходе из цикла while (по любой причине) не забываем покинуть (leave) критическую секцию, открытую ранее, чтобы обработчик сетевых сообщений мог добавить в связный список новую порцию сообщений.
Обрати внимание
Будь осторожен при редактировании значения времени обработки сетевых сообщений в скрипте сетевых настроек. Если выставишь слишком большое значение, то в этом случае движок будет тратить чересчур много времени на обработку сетевых сообщений, что в результате снизит скорость работы приложения и даже может привести к подвисаниям обработчика сетевых сообщений, связанное с его ожиданием освобождения связного списка от очередной критической секции. При выставлении слишком малого значения у приложения может не оказаться возможностей для достаточно быстрой обработки всех поступивших сообщений и они в этом случае будут накапливаться в связном списке, что вызовет временные задержки в их обработки и последующие лаги в сетевой игре.
Принцип работы остальных функций изучи самостоятельно путём рассмотрения исходного кода Network.cpp.
Интегрируем систему поддержки сети в движок
Мы почти закончили изучение системы поддержки игры по сети (далее - системы поддержки сети). Пришло время интегрировать систему поддержки сети в движок. Принцип тот же, что и при интегрировании других систем.Изменения в Network.cpp (Проект Engine)
- Добавь инструкцию #include "Engine.h" в самом начале файла Network.cpp (проверь её наличие).
Изменения в Engine.h (Проект Engine)
- Добавь инструкцию #include "Network.h" в файл Engine.h, сразу после инструкции #include "Input.h":
... //----------------------------- // Engine Includes //----------------------------- #include "resource.h" #include "LinkedList.h" #include "ResourceManagement.h" #include "Geometry.h" #include "Font.h" #include "Scripting.h" #include "DeviceEnumeration.h" #include "Input.h" #include "Network.h" #include "SoundSystem.h" #include "State.h" ...
- Добавь инструкцию #include <dplay8.h> в файл Engine.h, сразу после инструкции #include <dinput.h>:
... //----------------- // DirectX Includes //----------------- #include <d3dx9.h> #include <dinput.h> #include <dplay8.h> #include <dmusici.h> ...
• Добавь два новых члена
GUID guid; void (*HandleNetworkMessage)( ReceivedMessage *msg );
в структуру EngineSetup (файл Engine.h) как показано здесь:
... struct EngineSetup { HINSTANCE instance; // Application instance handle. // Дескриптор инстанса приложения GUID guid; // Application GUID. char *name; // Name of the application. void (*StateSetup)(); // Функция подготовки стейта. float scale; // Масштаб (scale) в метрах/футах. unsigned char totalBackBuffers; // Число используемых бэкбуферов. void (*HandleNetworkMessage)( ReceivedMessage *msg ); // Обработчик сетевых сообщений ...
- Присвой добавленным членам в конструкторе структуры EngineSetup значения по умолчанию:
GUID defaultGUID = { 0x24215591, 0х24с0, 0x4316, { 0xb5, 0хЬ2, 0x67, 0x98, 0x2с, ОхЬЗ, 0x82, 0x54 } }; HandleNetworkMessage = NULL;
как показано здесь:
... //------------------------------------------------------------------------- // The engine setup structure constructor. //------------------------------------------------------------------------- EngineSetup() { GUID defaultGUID = { 0x24215591, 0х24с0, 0x4316, { 0xb5, 0хЬ2, 0x67, 0x98, 0x2с, ОхЬЗ, 0x82, 0x54 } }; instance = NULL; name = "Application"; HandleNetworkMessage = NULL; scale = 1.0f; totalBackBuffers = 1; StateSetup = NULL; } ...
Член guid позволяет указать GUID приложения, по которому его (приложение) будут однозначно идентифицировать в сети. Если при создании экземпляра движка ты не укажешь никакого GUID, то его значение будет взято из конструктора структуры EngineSetup. Делать так не рекомендуется, т.к. это приведёт к путанице при выполнении ряда операций, например при энумерировании сессий. И в результате возникнет ситуация, когда будут найдены в сети два приложения с одинаковым GUID. DirectPlay будет считать их одним и тем же приложением, хотя на самом деле это будет не так. Член *HandleNetworkMessage позволяет указать обработчик пользовательских (user-defined) сетевых сообщений, специфичный для приложения (он же внутренний обработчик сетевых сообщений). В конструкторе структуры EngineSetup ему присвоено значение NULL. Если при создании экземпляра движка оставить это значение, то приложение вообще не будет получать пользовательские сетевые сообщения.
- В объявлении класса Engine (файл Engine.h) в секцию private добавь новый член...
Network *m_network;
...сразу после объявления объекта класса Input:
... private: bool m_loaded; // Indicates if the engine is loading. // Флаг показывает, загружен ли движок HWND m_window; // Main window handle. // Дескриптор главного окна приложения bool m_deactive; // Indicates if the application is active or not. // Флаг активности приложения EngineSetup *m_setup; // Copy of the engine setup structure. // Копия структуры EngineSetup IDirect3DDevice9 *m_device; D3DDISPLAYMODE m_displayMode; ID3DXSprite *m_sprite; unsigned char m_currentBackBuffer; LinkedList< State > *m_states; // Связный список (Linked list) стейтов. State *m_currentState; // Указатель на текущий стейт. bool m_stateChanged; // Флаг показывает, изменён ли стейт в текущем кадре. ResourceManager< Script > *m_scriptManager; // Менеджер скриптов. Input *m_input; Network *m_network; // Объект класса Network. SoundSystem *m_soundSystem; // Объект класса SoundSystem. }; ...
В нём будет храниться экземпляр класса Network.
- В объявлении класса Engine (файл Engine.h) в секцию public добавь объявление функции
Network *GetNetwork();
сразу после объявления функции GetInput:
... class Engine { public: Engine( EngineSetup *setup = NULL ); virtual ~Engine(); void Run(); HWND GetWindow(); void SetDeactiveFlag( bool deactive ); float GetScale(); IDirect3DDevice9 *GetDevice(); D3DDISPLAYMODE *GetDisplayMode(); ID3DXSprite *GetSprite(); void AddState( State *state, bool change = true ); void RemoveState ( State *state ); void ChangeState( unsigned long id ); State *GetCurrentState(); ResourceManager< Script > *GetScriptManager(); Input *GetInput(); Network *GetNetwork(); SoundSystem *GetSoundSystem(); ...
Функция GetNetwork возвращает указатель на экземпляр класса Network. Она даёт доступ к сетевым возможностям практически из любого места движка для проведения типичных сетевых операций (создание сессий, вступление в них и обмен сообщениями).
Изменения в Engine.срр (Проект Engine)
- Добавь строку...
m_network = new Network( m_setup->guid, m_setup->HandleNetworkMessage );
...в конструктор класса Engine (файл Engine.срр), сразу после создания объекта Input:
... // Создаём экземпляр класса Input. m_input = new Input( m_window ); // Создаём экземпляр класса Network. m_network = new Network( m_setup->guid, m_setup->HandleNetworkMessage ); m_soundSystem = new SoundSystem( m_setup->scale ); ...
Так мы создаём объект класса Network при создании каждого инстанса движка. При создании объекта класса Network мы указываем в параметрах GUID и назначенный обработчик пользовательских сетевых сообщений. Новосозданному экземпляру класса Network присваивается указатель m_network.
Раз мы добавили новую строку в конструктор класса Engine, добавим также соответствующий ему оператор безопасного удаления объекта m_network в его деструктор.
- Добавь выражение
SAFE_DELETE( m_network );
в деструктор класса Engine, сразу после макроса безопасного удаления объекта m_sound:
... if( m_currentState != NULL ) { // Уничтожаем связные списки со стейтами. m_currentState->Close(); SAFE_DELETE( m_states ); SAFE_DELETE( m_soundSystem ); SAFE_DELETE( m_network ); ...
- Добавь строку
m_network->Update();
внутри цикла while функции Run класса Engine (файл Engine.срр), сразу после расчёта затраченного времени:
... else if( !m_deactive ) { // Calculate the elapsed time. // Подсчитываем затраченное время. unsigned long currentTime = timeGetTime(); static unsigned long lastTime = currentTime; float elapsed = ( currentTime - lastTime ) / 1000.0f; lastTime = currentTime; // Обновляем объект класса Network, обрабатываем поступившие // пользовательские сообщения. m_network->Update(); // Обновляем объект input, читаем ввод с клавиатуры и мыши. m_input->Update(); ...
- Добавь реализацию функции GetNetwork в конце Engine.cpp:
... //----------------------------------------------------------------------------- // Возвращает указатель на объект класса Network. //----------------------------------------------------------------------------- Network *Engine::GetNetwork() { return m_network; } ...
Система поддержки сети полностью интегрирована в движок и готова к работе.
Тестовая перекомпиляция Engine.lib
Перед компиляцией
В последних версиях DirectX SDK отсутствует файл библиотеки dplayx.lib и сопутствующие ему заголовки dplay8.h и dpaddr.h, необходимые для следующей компиляции библиотеки Engine.lib. Их можно без труда найти в Интернете или в DirectX SDK 8.0. После скачивания, данные файлы нужно скопировать в соответствующие подкаталоги lib и include установленного DirectX SDK, с которым работаешь в данный момент.
Для проверки работосопособности исходного кода, добавленного в этой Главе, перекомпилируем исходный код Проекта Engine:
- В Обозревателе решений щёлкаем правой кнопкой мыши по значку Проекта Engine. Во всплывающем меню выбираем "Перестроить"
Модифицируем тестовое приложение (Проект Test)
В Главе 1.6 для проверки работоспособности движка в нашем Решении GameProject01 мы создали второй Проект Test, после компиляции которого получили исполняемое приложение (файл .EXE), показывающее окно. В последующих главах его функционал изменялся. Применив полученные в текущей Главе знания на практике, снова дополним исходный код тестового приложения, оснастив его новым "функционалом". Если по каким-то причинам Проект Test отсутствует в Решении GameProject01, создай его, следуя инструкциям Главы 1.6.ОК, приступаем.
- Открой для редактирования файл исходного кода Main.cpp (Проект Test) и замени содержащийся в нём код на следующий:
//----------------------------------------------------------------------------- // System Includes //----------------------------------------------------------------------------- #include <windows.h> //----------------------------------------------------------------------------- // Engine Includes //----------------------------------------------------------------------------- #include "..\GameProject01\Engine.h" //----------------------------------------------------------------------------- // Test State Class //----------------------------------------------------------------------------- class TestState : public State { public: //------------------------------------------------------------------------- // Allows the test state to preform any pre-processing construction. //------------------------------------------------------------------------- virtual void Load() { m_font = new Font; m_text[0] = 0; m_connected = false; }; //------------------------------------------------------------------------- // Allows the test state to preform any post-processing destruction. //------------------------------------------------------------------------- virtual void Close() { SAFE_DELETE( m_font ); }; //------------------------------------------------------------------------- // Returns the view setup details for the given frame. // Возвращает настройки вьюера в данном кадре. //------------------------------------------------------------------------- virtual void RequestViewer( ViewerSetup *viewer ) { viewer->viewClearFlags = D3DCLEAR_TARGET; } //------------------------------------------------------------------------- // Updates the state. // Обновляем стейт //------------------------------------------------------------------------- virtual void Update( float elapsed ) { // Check if the user wants to host a session. // Проверяем случай, когда юзер собирается хостить сессию. if( g_engine->GetInput()->GetKeyPress( DIK_H ) == true ) m_connected = g_engine->GetNetwork()->Host( "host player", "session", 8 ); // Check if the user wants to enumerate the sessions on the network. // Проверяем случай, когда юзер запрашивает список сессий. if( g_engine->GetInput()->GetKeyPress( DIK_E ) == true ) g_engine->GetNetwork()->EnumerateSessions(); // Check if the user wants to join the first session. // Проверяем случай, когда юзер хочет вступить в первую сессию. if( g_engine->GetInput()->GetKeyPress( DIK_1 ) == true ) m_connected = g_engine->GetNetwork()->Join( "join player", 0 ); // Check if the user wants to join the second session. // Проверяем случай, когда юзер хочет вступить во вторую сессию. if( g_engine->GetInput()->GetKeyPress( DIK_2 ) == true ) m_connected = g_engine->GetNetwork()->Join( "join player", 1 ); // Check if the user wants to join the third session. // Проверяем случай, когда юзер хочет вступить в третью сессию. if( g_engine->GetInput()->GetKeyPress( DIK_2 ) == true ) m_connected = g_engine->GetNetwork()->Join( "join player", 2 ); // Check if the user wants to quit. // Выход из приложения по нажатию Q на клавиатуре. if( g_engine->GetInput()->GetKeyPress( DIK_Q ) == true ) PostQuitMessage( 0 ); // Create the menu text. // Создаём текст меню. strcpy( m_text, "\n\n\nH - Host Session\n\nE - Enumerate Sessions" ); // Go through the list of sessions found on the local network. // Перебираем (=итерируем) список сессий, найденных в локальной сети. char count = 0; char name[MAX_PATH]; SessionInfo *session = g_engine->GetNetwork()->GetNextSession( true ); while( session != NULL ) { // Add the session count. // Добавляем сессию в список. sprintf( name, "\n %d - ", ++count ); strcat( m_text, name ); // Add the session name. // Добавляем имя сессии. wcstombs( name, session->description.pwszSessionName, MAX_PATH ); strcat( m_text, name ); // Go to the next session. // Переходим к следующей сессии. session = g_engine->GetNetwork()->GetNextSession(); } // Add the quit option to the menu text. // Добавляем опцию выход в текст меню. strcat( m_text, "\n\nQ - Quit" ); // Add the connection status text. // Добавляем текст статуса соединения. if( m_connected == true ) { if( g_engine->GetNetwork()->IsHost() == true ) strcat( m_text, "\n\nCONNECTED - HOST" ); else strcat( m_text, "\n\nCONNECTED - CLIENT" ); } else strcat( m_text, "\n\nNOT CONNECTED" ); }; //------------------------------------------------------------------------- // Renders the state. // Рендерим стейт. //------------------------------------------------------------------------- virtual void Render() { m_font->Render( m_text, 0.0f, 0.0f ); }; private: Font *m_font; // Font to display the menu options. // Шрифт для отображения опций. char m_text[MAX_PATH]; // Text for the menu options. // Текст для отображения опций. bool m_connected; // Indicates if the network object is connected. // Показывает, есть ли подключение в данный момент. }; //----------------------------------------------------------------------------- // Application specific state setup. // Настройки стейта, специфичные для приложения. //----------------------------------------------------------------------------- void StateSetup() { g_engine->AddState( new TestState, true ); } //----------------------------------------------------------------------------- // Entry point for the application. // Точка входа в приложение. //----------------------------------------------------------------------------- int WINAPI WinMain( HINSTANCE instance, HINSTANCE prev, LPSTR cmdLine, int cmdShow ) { // A unique id for the test so it can be identified on a network. // Уникальный идентификатор для использования в сетевых подключениях. GUID guid = { 0x9ca7996f, 0xebb4, 0x4e9b, { 0xba, 0x6b, 0xe6, 0x4b, 0x45, 0x7f, 0x59, 0x98 } }; // Create the engine setup structure. // Создаём структуру настреок движка. EngineSetup setup; setup.instance = instance; setup.guid = guid; setup.name = "Network Test"; setup.StateSetup = StateSetup; // Create the engine (using the setup structure), then run it. // Создаём движок и запускаем его. new Engine( &setup ); g_engine->Run(); return true; }
И вновь в нашем Проекте Test всего 1 файл Main.срр, который содержит весь исходный код тестового приложения. Перед нами готовая система поддержки игры по сети (в виде стейта) в действии.
Исследуем код Main.cpp (Проект Test)
Наибольшим изменениям подвергся стейт TestState, "адаптированный" для проверки сетевых возможностей движка. В функции WinMain мы установили GUID приложения. В то же время мы нигде не указали обработчик пользовательских сетевых сообщений, специфичный для приложения (application-specific network message handler). А всё потому, что пока мы не будем обрабатывать пользовательские сетевые сообщения. Ты увидишь его в деле при разработке игры во второй части данного курса.При ближайшем рассмотрении стейта TestState, видно что вся суть кроется в его функции Update. В двух словах мы приделали вывод на экран нескольких опций меню а затем написали обработчики нажатия клавиш для них.
- При нажатии <Н> приложение хостит новую сессию.
- При нажатии <Е> приложение проведёт энумерирование (опрос) активных сессий в сети. Все найденные сессии отобразятся в окне в виде списка опций, к которым привязаны обработчики нажатия клавиш 1, 2 или 3. Игрок вступает в выбранную сессию путём нажатия соответствующей цифровой клавиши на клавиатуре. Наше приложение предлагает на выбор всего три первые найденные сессии. Но при желании количество сессий на выбор может быть легко увеличено.
Мы знаем, что обработчик сетевых сообщений работает, т.к. у нас есть возможность хостить сессии, вступать в них и энумерировать их. В то же время сейчас мы не сможем протестить обработку пользовательских сетевых сообщений, т.к. займёмся этим позже, при разработке игры.
Подготовка Проекта Test к перекомпиляции
- В Обозревателе решений щёлкни правой кнопкой мыши по названию Проекта Test.
- Во всплывающем контекстном меню выбираем: Перестроить.
В нашем случае, это: C:\Users\<Имя пользователя>\Documents\Visual Studio 2010\Projects\GameProject01\Debug. (В разных ОС путь к файлу может отличаться от представленного).
- Найди и запусти получившееся приложение.
Обрати внимание
В идеале для проверки работоспособности сетевых возможностей приложения его копии нужно запускать на двух отдельных компьютерах, соединённых в локальную сеть.
- Выйди из приложения, нажав <F12> или <Q> на клавиатуре.
Итоги главы
Глава 1.13 была настоящим погружением в мир сетевых технологий. И то мы лишь поверхностно пробежались по основным аспектам программирования сетевых функций. Мы обсудили DirectPlay и как он управляет сетевыми функциями. Узнали о существовании различных сетевых архитектур. Далее мы рассмотрели основные принципы работы нашей системы поддержи сети + охватили сопутствующий материал (например критические секции), прежде чем изучить реализацию сетевых функций. В конце мы изменили исходный код тестового приложения, в этот раз оснащённого поддержкой сетевых технологий.Данная тема очень сложна. Поэтому рекомендуется также почитать материалы из других источников (в Интернете или документацию к DirectX SDK 8.0). И, конечно же, всегда опирайся на исходный код, который всегда должен быть перед глазами. В нём нет ни одной лишней функции. Каждая из них для чего-то нужна.
Дальше будет ещё интереснее, т.к. мы переходим к изучению текстур и материалов. Уже к концу следующей главы мы выведем на экран полигональную меш-модель, покрытую текстурами и даже анимированную!
Источники
1. Young V. Programming a Multiplayer FPS in DirectX 9.0. - Charles River Media, 2005
ДАЛЕЕ ==> Кодим 3D FPS DX9. 1.14 Полигональные сетки (меши) и материалы
Последние изменения страницы Суббота 06 / Август, 2022 19:12:32 MSK
Последние комментарии wiki