Загрузка...
 
Печать
ИГРОКОДИНГ  »  ИГРОКОДИНГ: Учебный курс  »  Программируем 3D-шутер от первого лица (FPS) (Win32, Cpp, DirectX9)  »  Часть 1. Создание движка  »  1.13 Добавляем поддержку игры по сети
Программируем 3D-шутер от первого лица (FPS) (Win32, C++, DirectX9)

1.13 Добавляем поддержку игры по сети


В этой Главе:

  • Поговорим о сетевых технологиях и о том, как они реализованы в DirectX (в частности, в компоненте DirectPlay).
  • Рассмотрим различные сетевые архитектуры и набросаем проект нашей системы поддержки сетевой игры.
  • Разработаем систему поддержки сетевой игры и интегрируем её в движок.


Содержание

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

По теме сетевых режимов в DirectX написана не одна сотня книг. И, так как данная Глава не претендует на полноту изложения, рекомендуется прочитать хотя бы несколько из них.

Поддержка сетевых технологий в DirectX

В DirectX имеется всего один компонент, отвечающий за сетевые режимы - DirectPlay.

Закрыть
noteОбрати внимание

DirectPlay признан устаревшим компонентом и уже много лет не поддерживается Майкрософт. Его даже перестали включать в последние версии DirectX SDK. (Здесь снова придёт на помощь DirectX SDK 8.) Вместо DirectPlay Майкрософт предлагает... ничего! Вернее, предлагает использовать различные реализации сетевых протоколов от сторонних разработчиков. Как-то так.
Но и это ещё не всё. В ОС Microsoft Windows 10 компонент DirectPlay вообще отключен по умолчанию! Это является чуть-ли не единственной причиной, по которой многие игры, использующие его, просто не запускаются, выдавая сообщение об отсутствии библиотек DirectPlay. О том, как его всё-таки включить, есть отличный ролик на YouTube: https://www.youtube.com/watch?v=WrufOcVR_HU(external link)


DirectPlay способен управлять практически всеми аспектами сетевых сессий, которые представляют собой сеанс (экземпляр) соединения (connection instance) с одним или более компьютерами, обменивающимися данными с одной определённой целью. Другими словами, когда ты хостишь (создаёшь) мультиплеерную игру на одном компьютере, ты создаёшь сессию. Другие игроки могут затем присоединяться (join) к этой сессии, если они играют в ту же игру. DirectPlay - это интерфейс, который позволяет создавать сессии (host sessions), вступать (join) в них, а также искать их внутри сети. Будучи подключённым к сессии, DirectPlay затем используется для отправки и получения данных между клиентами одной сессии. Клиентом в этом случае является любой компьютер, присоединившийся (join) к сессии. Компьютер, создавший сессию называется хостом (host) или сервер (server).
Это лишь основные функции DirectPlay. Кроме того, он годится и на многое другое. Но мы лишь рассмотрим те его функции, которые касаются создания сессий и вступления в них, обнаружения сессий в сети и отправки и получения данных.

Закрыть
noteОбрати внимание

Когда мы говорим о компьютерной сети, это может быть сеть любого типа, как например локальная сеть (Local Area Network - LAN) или обширная сеть (Wide Area Network - WAN), либо весь Интернет, который по сути тоже представляет собой большую компьютерную сеть. Для простоты изложения мы рассмотрим лишь локальную сеть, под которой подразумевается один или несколько компьютеров, соединённых друг с другом в одну сеть и расположенные на ограниченном пространстве (localized area) (чаще всего в одной комнате или здании и подключённые через хаб или неуправляемый коммутатор - свитч).


Рассмотрим общий принцип работы DirectPlay.
DirectPlay может работать:

  • с использованием т.н. пиринговой (peer-to-peer) архитектуры сети;
  • с использованием клиент-серверной (client-server) архитектуры сети.

Как только программер решит, какую архитектуру применять в системе поддержки сети, он вызывает соответствующие функции DirectPlay и использует соответствующие интерфейсы. Кроме того, он должен решить, с каким протоколом передачи данных (transport protocol) будет работать DirectPlay. Протокол передачи данных используется DirectPlay для передачи/получения всех сообщений и позволяет программисту абстрагироваться от премудростей реализаций конкретного сервис-провайдера (service provider). Сервис-провайдер - это технология, которая позволяет DirectPlay обмениваться данными по сети посредством протокола передачи данных. В данный момент DirectPlay поддерживает следующие транспортные протоколы:

  • Transmission Control Protocol/Internet Protocol (TCP/IP)
  • Internetwork Packet Exchange (IPX)
  • Modem
  • Serial Link

TCP/IP является наиболее часто используемым сервис-провайдером при разработке приложений с поддержкой DirectPlay. Главным преимуществом данного протокола является то, что он изначально разрабатывался для обмена данными в сети Интернет. TCP/IP устроен так, что любая локальная сеть внутри него будет считаться миниатюрной версией Интернета. И весь парадокс заключается в том, что, если ты разрабатываешь свою систему поддержки локальной сети, TCP/IP позволит намного проще и быстрее управлять ею посредством Интернет-технологий. По этой причине в данном курсе мы будем использовать протокол TCP/IP, сфокусировав внимание на соответствующем сервис-провайдере. Напомним, что полную информацию о каждом сервис-провайдере можно найти в документации DIrectX SDK.
Для коммуникации в сети DirectPlay использует адреса, представляющие собой уникальный способ идентифицировать тот или иной компьютер в данной сети. Каждый участник сетевой игры имеет свой уникальный адрес, который используется DirectPlay для отправки сообщений, а также идентификации отправителя полученных сообщений. DirectPlay-адрес представляет собой URL-строку, включающую в себя:

  • схему (scheme)
  • разделитель схемы (scheme separator)
  • строку данных (data string).

Строка данных включает в себя всю необходимую информацию для выполнения DirectPlay коммуникации между отправителем и получателем. Всё это выглядит несколько сложно, но к счастью DirectPlay имеет специальный объект адреса (adress object), который "оборачивает" (wraps up) адрес в простой интерфейс. Объект адреса является важной частью сетевой коммуникации. Чуть позднее мы обязательно увидим его в деле при разработке системы поддержки игры по сети.

А теперь немного теории о том, каким образом DirectPlay обрабатывает сообщения.
Сообщение отправляется с использованием специальной структуры, называемой пакет (packet). Каждый пакет имеет заголовок (header), который содержит информацию о типе сообщения, а также о том, от кого оно пришло. Оставшуюся часть пакета занимают текущие данные, которые "прикрепляются" к "хвосту" пакета. Для коммуникации со своим приложением DirectPlay использует функцию обратного вызова (call-back function) точно также, как это делают функции обратного вызова ОС Windows и процедуры обработки сообщений. Суть данной концепции состоит в том, чтобы внедрить в приложение функцию обратного вызова, в которую DirectPlay сможет передавать полученные сообщения. Всякий раз, когда DirectPlay вызывает функцию обратного вызова, это означает, что приложение получило сообщение (=пакет). После этого остаётся лишь проверить идентификатор сообщения (содержащийся в его заголовке) для определения его типа. Как только станет известен тип полученного сообщения, приложение сможет обработать соответствующим образом данные, содержащиеся в нём.

Это лишь основные сведения, необходимые для начала проектирования системы поддержки игры по сети. Остальное рассмотрим по ходу дела.

Архитектура системы поддержки сети

Как мы уже говорили, DirectPlay использует два типа (модели) сетевой архитектуры: пиринговую и клиент-серверную. Тебе необходимо решить, какая из них наилучшим образом подойдёт для нашего игрового движка и уже затем разрабатывать его именно под выбранную архитектуру. Рассмотрим обе архитектуры более подробно.

Рис.1 Пиринговая (peer-to-peer) архитектура построения сети
Рис.1 Пиринговая (peer-to-peer) архитектура построения сети

Пиринговая (peer-to-peer) архитектура

  • За неё отвечает интерфейс IDirectPlay8Peer

Пиринговая модель работает путём прямого соединения с каждым из компьютеров, находящихся в одной сетевой сессии. В то же время на одном из компьютеров запущен т.н. хост, который контролирует сессию. Несмотря на это, все клиентские компьютеры (или пиры, от англ "peer" - приятель) могут свободно общаться друг с другом без каких-либо ограничений со стороны хоста. Суть данной архитектуры состоит в том, что каждый клиент отслеживает изменения состояния игры и состояние других клиентов (в игре сейчас данный клиент или нет, его координаты на игровой карте, число набранных очков и т.д.). Как только один из клиентов обновляет текущую информацию о своём состоянии в игре, он должен отправить сообщение всем остальным клиентам для информирования о произошедших изменениях. Один из клиентов (обычно, это создатель сетевой сессии) назначается хостом. Хост ответственен за управление внутриигровой "логистикой", например, контроль вступивших в игру игроков и вышедших из неё. Также хост контролирует игровую логику и поведение искусственного интеллекта (игровых ботов), если таковые имеются.

ДОСТОИСНТВА Более простая модель и доступная для понимания. Нет необходимости разделять исходный код на "код сервера" и "код клиента". Все клиенты устроены одинаково и могут выступать в качестве хоста.
НЕДОСТАТКИ Легко допустить ошибки в реализации, которые часто приводят резкому росту объёма сетевого трафика. Плохая масштабируемость, т.к. с ростом числа клиентов количество отправляемых сообщений растёт в геометрической прогрессии. Не подходит для защищённых соединений, т.к. каждый клиент может прочитать все сообщения других клиентов, в том числе адресованные не ему.

Несмотря на все недостатки, данная модель является хорошим выбором для простой игры, где число клиентов не превышает 20-30 штук (типичное максимальное число игроков в игре, согласно документации DirectX SDK).
В свете её преимуществ и простоты реализации в нашем проекте мы будем использовать именно пиринговую архитектуру сети.

Рис.2 Клиент-серверная архитектура построения сети
Рис.2 Клиент-серверная архитектура построения сети

Клиент-серверная (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.
  • Во всплывающем меню Добавить->Создать элемент...
  • В появившемся окне выбери "Заголовочный файл (.h)" и в поле "Имя" введи "Network.h".
  • Жмём "Добавить".

Добавленный файл сразу откроется в правой части MSVC++2010.

  • В только что созданном и открытом файле Network.h набираем следующий код:

Network.h (Проект Engine)
//-----------------------------------------------------------------------------
// Класс-обёртка (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), на основе которых будут создаваться сетевые сообщения:

Фрагмент Network.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// Структура сетевого сообщения
//-----------------------------------------------------------------------------
struct NetworkMessage
{
	unsigned long msgid; // Message ID.
	DPNID dpnid; // DirectPlay player ID.
};

//-----------------------------------------------------------------------------
// Структура полученного сообщения
//-----------------------------------------------------------------------------
struct ReceivedMessage : public NetworkMessage
{
	char data[32]; // Данные сообщения.
};
...

Вот их описание:

NetworkMessage Играет роль заголовка сетевого сообщения, который указывает на тип сообщения (идентификатор сообщения; Message ID; msgid) и какой игрок его отправил (dpnid). Идентификатор сообщения представляет собой уникальный номер, который присваивается каждому сообщению. Ты не раз увидишь его в деле, когда мы создадим собственную систему пользовательских (user-defined) сообщений в процессе создания игры. dpnid - это уникальный идентификатор игрока, применяемый для однозначной идентификации отдельных игроков в сетевой сессии. DirectPlay автоматически присваивает уникальный dpnid каждому игроку, который вступает в сессию (joins a session). Именно по идентификатору игрока происходит обращение к любому из участников игровой сессии.
ReceivedMessage Ветвится (наследуется) от структуры NetworkMessage. Представляет собой своеобразные "письмо и конверт" из рассмотренного ранее примера. Используется для хранения полученных сообщений в связном списке для последующей обработки. Всякий раз, когда на компьютер клиента приходит новое сообщение, его заголовок копируется (напомним, что он построен на основе структуры NetworkMessage), а его данные (message data) копируется в специальный массив data, имеющий тип char. При последующей обработке данные извлекаются из этого буфера в соответствии с типом сообщений и уже затем обрабатываются.

Закрыть
noteОбрати внимание

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

Структура игрока (player structure)

Только что мы рассмотрели структуры сетевых сообщений. Но, помимо них, система поддержки сети также работает со структурой игрока (player structure) для идентификации отправителя и получателя сетевых сообщений. Несмотря на то, что информация, содержащаяся в структуре игрока, сильно разнится в разных играх, системе поддержки сети необходим набор базовых сведений о каждом игроке в целях обеспечения коммуникации и обмена сообщениями. Исходя из этого, движок должен обслуживать конечный список игроков в данной сессии, кем бы они не являлись. В то же время, сама игра должна обслуживать свой собственный список игроков (list of players), в котором как раз и будут содержаться все данные, специфичные для конкретной игры (урон от выстрела, стоимость покупки юнита, мана, броня и т.д.).

Закрыть
noteОбрати внимание

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


В Network.h представлена структура PlayerInfo, хранящая базовые сведения о каждом игроке и используемая системой поддержки сети движка. Сам список игроков реализован в виде ещё одного связного списка (linked list) структур PlayerInfo. Как видим, связный список - действительно полезная штука, применяемая в самых разных частях движка.

Фрагмент Network.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// Структура информации игрока (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 );
	}
};
...

Структура PlayerInfo предельно мала. Оно и понятно, ведь она содержит лишь самые необходимые данные об игроке. В основном она нужна лишь для хранения копии уникального идентификатора игрока (dpnid; player's identifier), присвоенного DirectPlay. Помимо этого в ней хранится копия имени игрока и его данных (с указанием размера этих данных), взятые из списка игроков приложения (игры; Application specific player data) во время создания игрока. DirectPlay позволяет отдельно передавать блок данных, специфичных для приложения, которые затем интерпретируются на компьютере получателя.
Рассмотрим небольшой пример. Допустим, ты разрабатываешь FPS-игру, в которой у каждой команды есть свои отличительные особенности геймплея (gameplay team style). Прежде чем игрок вступит в игру, он должен выбрать, за какую команду он будет играть. Как только он сделает выбор, он сразу вступает в игру и система обмена сетевыми сообщениями отправляет всем клиентам сообщение об этом (хрестоматийный пример - игра CounterStrike). После этого каждому клиенту необходимо создать локальную копию (local instance) этого игрока, чтобы его данные учитывались в едином игровом пространстве (на игровой 3D-карте). Помимо сообщения о вступлении в сессию нового игрока, DirectPlay пересылает о нём ещё три важных "куска" информации:

  • Уникальный идентификатор dpnid, присваиваемый DirectPlay;
  • Имя игрока (player name);
  • Блок данных об игроке, специфичных для данного приложения.

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

Оставшаяся часть структуры PlayerInfo понятна без объяснений.

Критические секции (critical sections)

Для понимания данного термина рассмотрим принцип взаимодействия нашего приложения с DirectPlay. Также вновь затронем тему многопоточности (multithreading) в приложениях.
Каждый раз, когда ты запускаешь приложение на компьютере, при этом для него одновременно создаются один или несколько процессов. Все эти процессы можно увидеть, запустив Менеджер задач Windows (Windows Task Manager). Подавляющее большинство приложений, включая игру, которую мы будем создавать, используют всего один процесс. Каждый процесс имеет т.н. поток (thread), который представляет собой цепочку инструкций, последовательно выполняемых операционной системой. Часто (особенно когда работают приложение, активно использующие сетевые возможности) процесс имеет 2 потока. В этом случае ОС должна периодически переключаться между потоками, чтобы позволить каждому из них выполнить свои операции. Данное явление называется многопоточностью (multithreading).
DirectPlay активно использует многопоточность для выполнения сетевых функций в приложении. Другими словами, сетевые приложения (в том числе игры) как правило имеют один поток для обработки игровых данных и ещё один - для обработки поступающих сетевых сообщений. Откуда же приложение берёт этот второй поток? Ведь мы нигде открыто не указываем это в нашем исходном коде. А дело в том, что, когда мы реализовывали функцию обратного вызова обработчика сетевых сообщений (network message handler call-back function), фактически создали этот второй поток. К счастью, DirectPlay берёт управление созданными потоками на себя. Поэтому весь процесс прозрачен для нас, в определённой степени. Мы видим, что обработчик сетевых сообщений (который действует аналогично функциям обратного вызова для обычных и диалоговых окон) на деле относится к DirectPlay в довольно необычной манере. Ведь, даже будучи реализованным в движке, он не может быть вызван программером. DirectPlay вызывает его всякий раз, когда сетевое сообщение поступает на компьютер клиента. Из-за того, что сообщение может поступить в любое время (даже в тот момент, когда приложение уже выполняет множество других ресурсоёмких задач, например рендеринг), функция обратного вызова обработчика сетевых сообщений должна быть запущена в своём собственном отдельном потоке. Это позволит нашему приложению получать сетевые сообщения, без потери контроля над выполнением других задач.

Рис.3 Принцип работы критических секций
Рис.3 Принцип работы критических секций

Теперь, когда мы разобрались в том, что же собой представляет многопоточность (multithreading), обратим внимание на самую большую проблему, с которой ей приходится сталкиваться, которая, в свою очередь, привела к изобретению критических секций и других взаимоисключающих (mutual exclusion, связанных с взаимоисключающим доступом к одним и тем же ресурсам) техник. Из-за того, что несколько потоков работают одновременно в одном процессе (который создаёт приложение), они также находятся в одном участке выделяемой памяти. Это означает, что они вместе используют ресурсы из этой памяти (shares resources in this memory).
Допустим, у нас есть 2 потока и одна переменная, которую используют оба этих потока. Так как оба потока и переменная находятся в одном участке памяти, они должны каким-то образом разделять доступ к данной переменной. Что же произойдёт, когда оба потока одновременно обратятся к этой переменной?
Если один поток попытается прочитать содержимое переменной в то время, как другой попытается в неё что-то записать, результат операции окажется непредсказуемым (и чаще всего неверным). Чуть выше мы уже решили, что всякий раз, когда DirectPlay вызывает обработчик сетевых сообщений нашего приложения, мы берём входящее сообщение и помещаем его в связный список, чтобы затем обработать его при первом удобном случае (в порядке общей очереди). Это также приводит к возникновению проблемы взаимного доступа (mutual exclusion). Если сетевое сообщение поступит в тот момент, когда мы уже обрабатываем сообщения из связного списка, то это может привести к многочисленным проблемам. Поток обработчика сетевых сообщений попытается добавить новое сетевое сообщение в связный список, в то время как движок будет пытаться прочесть другие сообщения из очереди.
Для решения этой проблемы мы применим критические секции. Своё название критические секции получили исходя из того факта, что они буквально защищают критические секции (участки) программного кода. Другими словами, если данный участок кода окажется незащищённым, то жди проблем связанных с взаимоисключающим доступом. Суть данной методы проста. Как только мы собираемся выполнять "одновременно всем нужный" участок кода, мы просто блокируем (lock) критическую секцию, которая ему назначена. Затем по окончании выполнения всех операций мы просто разблокируем (unlock) критическую секцию с данным участком кода. Если какой-либо другой процесс попытается повторно заблокировать ранее "залоченную" секцию, ему придётся дожидаться её разблокирования. Идея проста: мы "обносим" участок разделяемой (shared) памяти в отдельную секцию. И далее лочим и разлочим его при необходиости. См. Рис.3

В гипотетическом исходном коде это может выглядеть так:

Прототип какого-то кода
...
EnterCriticalSection(&myCS);

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

LeaveCriticalSection(&myCS);
...

Закрыть
noteОбрати внимание

Объём кода, расположенного между тегами EnterCriticalSection и LeaveCriticalSection должен быть сведён к минимуму. Ведь если поток попытается захватить в критическую секцию фрагмент кода, который уже вошёл в критическую секцию от другого потока, он остановится и будет ждать до тех пор, пока тот не освободится. Это ожидание может катастрофически сказаться на производительности игры, особенно если ожидающий поток - это главный поток игры. В этом случае игра просто зависнет, ожидая освобождения критической секции. Кроме того, не следует вводить в критическую секцию фрагмент кода, уже введённный в ней другим потоком. Другими словами нельзя "вкладывать" одну критическую секцию в другую. Т.к. это может вызвать известную проблему под названием deadlock (от англ. "тупик"), когда два или более потока встают, т.к. каждый из них пытается войти в критическую секцию, залоченную другим потоком.


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

Статистически уникальный идентификатор (Globally Unique Identifier - GUID)

Исследуя исходный код Network.h ты наверняка замечал странную аббревиатуру GUID(external link). Если кратко, то GUID представляет собой строку из цифр и английских букв, которую (с некоторыми оговорками) можно назвать уникальной. Другими словами (теоретически) ты не сможешь повторно сгенерировать вторую такую. Генерируя новый GUID ты полностью застрахован от того, что больше никто не сгенерирует такую же комбинацию данного идентификатора.
DirectPlay позволяет генерировать GUID "на лету", т.е. во время выполнения приложения, но в данном курсе такая задача не ставится. Ведь необходимость сгенерить новый GUID, как правило, возникнет всего один раз: при создании приложения с сетевыми возможностями. GUID необходим DirectPlay для однозначной идентификации нашего приложения в системе. Для генерации нового GUID воспользуемся специальной утилитой-генератором GUID-идентификаторов GUIDGEN. Раньше она шла в наборе с MS Visual C++ 6.0. В современных версиях MS Visual C++ Express её нет. Поэтому:

Система поддержки сети (Network system)

Большая часть подготовительной работы уже позади. Поэтому сразу перейдём к рассмотрению структуры нашей системы поддержки сети. По теории компьютерных сетей написана не одна сотня книг, поэтому данная Глава не претендует на полноту изложения. Мы рассмотрим лишь самые нужные нам моменты.
Возвращаясь к исходному коду Network.h, рассмотрим определение класса Network:

Фрагмент Network.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// 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.cpp, который мы создадим уже через несколько мгновений.
Большая часть переменных членов класса Network вполне очевидны и их названия говорят сами за себя. В классе представлены 3 связных списка, для каждого из которых подготовлена отдельная критическая секция.

Создаём Network.cpp (Проект Engine)

В файле исходного кода Network.cpp будут размещаться реализации функций, объявленных в Network.h.
ОК, приступаем.

  • Стартуй MSVC++ 2010 и открывай Решение GameProject01 (если не сделал это раньше).
  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Файлы исходного кода" Проекта Engine.
  • Во всплывающем меню Добавить->Создать элемент...
  • В появившемся окне выбери "Файл С++ (.cpp)" и в поле "Имя" введи "Network.cpp".
  • Жмём "Добавить".

Добавленный файл сразу откроется в правой части MSVC++2010.

  • В только что созданном и открытом файле Network.cpp набираем следующий код:

Network.cpp (Проект Engine)
//-----------------------------------------------------------------------------
// Реализация классов и функций, объявленных в 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:

Фрагмент Network.cpp (Проект 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 ) );
...

Он принимает два входных параметра:

GUID guid GUID, который используется DirectPlay для однозначной идентификации приложения в локальной сети.
void (*HandleNetworkMessageFunction)( ReceivedMessage *msg ) Указатель на функцию обратного вызова обработчика сетевых сообщений, специфичную для приложения. Данная функция позволяет приложению делать обратный вызов (call-back), который даёт команду движку начать обработку пользовательских сетевых сообщений.

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

Так вот, внутри конструктора класса Network первым делом инициализируем три критические секции, о которых говорилось ранее. Напомним, что для каждого из трёх заранее подготовленных связных списком мы создаём по одной критической секции. Это связано с тем, что каждый из этих связных списков одновременно используется как потоком приложения так и потоком обработчика сетевых сообщений.
Сразу после этого очищаем указатель на объект интерфейса IDirectPlay8Peer, объявленный в Network.h, и указатель адреса объекта устройства m_device. Данный указатель объекта устройства отличается от того, что мы использовали при создании объекта устройства Direct3D. Здесь у нас тоже устройство, но оно ссылается на локальный адрес в сети (например, адрес инстанса данного приложения).
Далее мы копируем GUID приложения и сохраняем его в локальную переменную m_guid для дальнейшего использования.

Следующий шаг - создание самих связных списков (linked lists):

Фрагмент Network.cpp (Проект Engine)
...
	// Создаём связный список энумеруемых сессий.
	m_sessions = new LinkedList< SessionInfo >;

	// Создаём связный список игроков в текущей сессии.
	m_players = new LinkedList< PlayerInfo >;

	// Создаём связный список полученых сетевых сообщений, ожидающих ообработки.
	m_messages = new LinkedList< ReceivedMessage >;
...


Следующий шаг - чтение скрипта сетевых настроек (заранее подготовленного файла на жёстком диске NetworkSettings.txt) и применение содержащихся в нём настроек:

Фрагмент Network.cpp (Проект Engine)
...
	// Загружаем сетевые настройки из файла 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 - расчётное время, отведённое на обработку сетевых сообщений в данном кадре.

Данный скрипт работает по тому же принципу, что и скрипт графических настроек, в который мы учились сохранять и считывать из него данные. Единственная разница состоит в том, что мы не создавали диалогового окна для него. Вместо этого мы просто создали вручную текстовый файл (например с помощью Блокнота Windows), который, согласно замысла, конечный пользователь будет редактировать вручную (при необходимости). Причина проста: данные настройки нет необходимости изменять без крайней необходимости. Только те пользователи, которые будут испытывать технические неполадки при игре по сети, смогут изменить их на своё усмотрение. Чуть ниже мы обязательно подробно рассмотрим назначение каждого из этих трёх параметров.

А наш конструктор почти закончен. Осталось лишь установить флаг m_recieveAllowed в false, который отвечает за разрешение/запрет на получение пользовательских сетевых сообщений.

Фрагмент Network.cpp (Проект Engine)
...
	// Изначально системе сетевых сообщений запрещено получать сообщения, специфичные для приложения.
	m_receiveAllowed = false;

	// Устанавливаем обработчик сетевых сообщений (network message handler).
	HandleNetworkMessage = HandleNetworkMessageFunction;
...

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

Финальные два шага включают в себя создание указателей на:

  • пиринговый (peer) интерфейс DirectPlay;
  • адрес объекта устройства (device address).
Фрагмент Network.cpp (Проект Engine)
...
	// Создаём и инициализируем пиринговый (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 );
}
...

В обоих случая используем функцию CoCreateInstance, т.к. оба объекта являются COM-объектами. Ранее ты не раз видел эту функцию в деле, так что здесь всё должно быть понятно. Просто обрати внимание на идентификаторы класса и ссылки на интерфейсы, которые мы указываем здесь в этот раз.
Сразу после создания пирингового интерфейса DirectPlay, тут же инициализируем (Initialize) его путём вызова соответствующей функции. Функция Initialize принимает 3 параметра:

Параметр Описание
(PVOID)this Контекст нашего класса, который передаётся обработчику сетевых сообщений через DirectPlay. Данный параметр почти всегда устанавливается в this практически во всех случаях.
NetworkMessageHandler Указатель на функцию обратного вызова обработчика сетевых сообщений, специфичную для движка. Она будет вызвана движком для обработки пользовательских сетевых сообщений, которые не могут быть обработаны другими системами движка.
0 Последний параметр позволяет устанавливать различные флаги. Документация к DirectX SDK рекомендует выставлять здесь "0" (ноль) либо "DPINITIALIZE_DISABLEPARAMVAL".

Последним штрихом к нашему конструктору будет конечное установка и настройка объекта устройства. Вся процедура включает в себя:

  • выбор сервис-провайдера путём вызова функции SetSP;
  • передача в неё параметра CLSID_DP8SP_TCPIP для установки протокола TCP/IP.

Выбранный порт устанавливается путём вызова функции AddComponent, которая позволяет добавлять к адресу различные компоненты, как например номер порта. Для этого мы указываем флаг DPNA_KEY_PORT для выбора компонента порта, и затем указываем номер порта, сохранённый в переменной m_port. Последние два параметра указывают функции на тип передаваемых данных и на их размер.

===

Вслед за конструктором объекта класса Network кратко рассмотрим его деструктор:

Фрагмент Network.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// 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, специально созданном для хранения сведений об энумерованных сессиях. Сессии здесь сохраняются в структуре SessionInfo, определённой в Network.h следующим образом:

Фрагмент Network.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// Структура информации энумерированной сессии (Enumerated Session Information Structure)
//-----------------------------------------------------------------------------
struct SessionInfo
{
	IDirectPlay8Address *address; // Session network address.
	DPN_APPLICATION_DESC description; // Application description.
};
...

Как видно вся структура состоит всего из двух членов:

  • адреса хоста (Он необходим для подключения к сессии)
  • описания приложения (application description).

Описание представляет собой структуру DPN_APPLICATION_DESC, в которой хранится вся информация о сетевом компоненте приложения, являющегося хостом. Структура экспонирована DirectPlay. Вот её прототип:

Прототип (шаблон) структуры DPN_APPLICATION_DESC
typedef struct _DPN_APPLICATION_DESC {
   // Размер структуры
   DWORD dwSize;

   // Флаги, описывающие поведение приложения
   DWORD dwFlags;

   // GUID, сгенерированный во время инициализации DirectPlay
   GUID guidInstance;

   // GUID приложения
   GUID guidApplication;

   // Максимальное число игроков, разрешённое в данной сессии или игре
   DWORD dwMaxPlayers;

   // Количество игроков, подключенных к сессии в данный момент
   DWORD dwCurrentPlayers;

   // Имя сессии
   WCHAR *pwszSessionName;

   // Пароль для подключения к сессии
   WCHAR *pwszPassword;

   // Зарезервированные данные, которые не должны изменяться
   PVOID pvReservedData;
   DWORD dwReservedDataSize;

   // Данные, специфичные для приложения (Application specific data)
   PVOID pvApplicationReservedData;
   DWORD dwApplicationReservedDataSize;
} DPN_APPLICATION_DESC, *PDPN_APPLICATION_DESC;

Структура не маленькая. Но большинству её членов зачастую значения присваиваются автоматически. Оставшиеся переменные мы рассмотрим подробнее по ходу изложения.

===
Но вернёмся к исходному коду Network.cpp. Для энумерирования сессий в сети вызываем функцию EnumerateSessions:

Фрагмент Network.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Энумерируем сессии в локальной сети (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. Она содержит несколько параметров, поэтому взглянем на её прототип:

Прототип функции EnumHosts
EnumHosts
(
  PDPN_APPLICATION_DESC const pApplicationDesc,
  IDirectPlay8Address *const pdpaddrHost,
  IDirectPlay8Address *const pdpaddrDeviceInfo,
  PVOID const pvUserEnumData,
  const DWORD dwUserEnumDataSize,
  const DWORD dwEnumCount,
  const DWORD dwEntryInterval,
  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, представленной на экране сетевых настроек игры. При её нажатии происходит синхронный опрос, результаты которого появляются на экране лишь спустя несколько секунд.

Закрыть
noteОбрати внимание

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

Мы не будем углубляться в детали техники асинхронного выполнения операций. Но чуть позднее ты увидишь её в деле, когда мы рассмотрим отправку сообщений. Синхронные операции более просты для понимания, поэтому мы будем использовать их везде, где только можно. Более подробная информация об асинхронных операциях представлена в документации DirectX SDK. особенно полезно изучить функцию CancelAsyncOperation в интерфейсе IDirectPlay8Peer.
Когда энумерация вернётся в приложение с ответом, обработчик сетевых сообщений будет вызван для каждого из ответов. Сообщение о сессиях обычно имеет идентификатор DPN_MSGID_ENUM_HOSTS_RESPONSE и содержит детальную информацию о хосте и его сессии. В Network.cpp данное сообщение помимо всех прочих также обрабатывается внутренним обработчиком сетевых сообщений:

Фрагмент Network.cpp (Проект Engine)
...
		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.
Далее создаём копию описания приложения и также сохраняем его в виде члена структуры SessionInfo.
После этого входим в критическую секцию связного списка сессий (m_sessions), добавляем в него новую сессию и выходим из критической секции.

Хостинг сессий и вступление в них. Функции Host и Join.

В данный момент ты уже умеешь энумерировать сессии и знаешь как обрабатывать ответные сообщения. В то же время это всё не имеет смысла, если наш движок не будет способен создавать сессии и вступать в них. Так что рассмотрим две функции, ответственные за эти операции.
В классе Network представлены две функции:

  • Host для хостинга сетевых сессий;
  • Join для входа в них.

Обе функции имеют много общего. Начнём с рассмотрения функции Host. Затем перейдём к функции Join, фокусируясь на её отличиях от функции Host.

Реализация созданной нами функции Host представлена в Network.cpp и выглядит следующим образом:

Фрагмент Network.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Пытаемся захостить (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:

Определение структуры 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, указывающих на то, что структура содержит имя игрока и его данные. В финале просто назначаем имя игрока.

Фрагмент Network.cpp (Проект Engine)
...
	// Подготавливаем и заполняем структуру информации об игроке (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;
...
Закрыть
noteОбрати внимание

Внимательный читатель наверняка заметит, что мы так и не установили флаг DWORD dwPlayerFlags. А всё потому, что DirectPlay делает это автоматически. Как только приложение получает сетевое сообщение о том, что новый игрок вступил в сессию, мы можем проверить этот флаг. Если он имеет значение DPNPLAYER_LOCAL, то новый игрок является локальным, т.е. принадлежит данному локальному приложению (проще говоря, является рядовым клиентом). Если флаг имеет значение DPNPLAYER_HOST, то это означает, что новый игрок является хостом в данной сессии.

Последний штрих - назначение данных в только что заполненной структуре DPN_PLAYER_INFO локальному игроку (DirectPlay peer) путём вызова функции SetPeerInfo, экспонированной интерфейсом IDirectPlay8Peer:

Фрагмент Network.cpp (Проект Engine)
...
	if( FAILED( m_dpp->SetPeerInfo( &player, NULL, NULL, DPNSETPEERINFO_SYNC ) ) )
		return false;
...

Здесь в первом параметре передаём указатель на структуру (&player), а в последнем указываем флаг DPNSETPEERINFO_SYNC для проведения операции в синхронном режиме.

Во второй части реализации функции Host заполняем специальную структуру DPN_APPLICATION_DESC, содержащую детальную информацию для определения приложения и создаваемой сессии:

Фрагмент Network.cpp (Проект Engine)
...
	// Подготавливаем описание приложения (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:

Определение структуры 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.dwMaxPlayers = players Максимальное число одновременно подключенных пользователей, разрешённое в создаваемой сессии.
mbstowcs( wide, session, MAX_PATH ); description.pwszSessionName = wide; Имя создаваемой сессии.

Далее вызываем функцию Host, экспонированную интерфейсом IDirectPlay8Peer:

Фрагмент Network.cpp (Проект Engine)
...
	// Host the session.
	if( FAILED( m_dpp->Host( &description, &m_device, 1, NULL, NULL, NULL, 0 ) ) )
		return false;

	return true;
}
...

Вот её прототип:

Прототип функции Host
HRESULT Host(
 const DPN_APPLICATION_DESC *const pdnAppDesc, // Специальная структура, описывающая приложение
 IDirectPlay8Address **const prgpDeviceInfo, // Указатель на интерфейс (Адрес устройства)
 const DWORD cDeviceInfo, // Указывает на количество объектов в rgpDeviceInfo
 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:

Фрагмент Network.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Пытаемся вступить (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):

Фрагмент Network.cpp (Проект Engine)
...
	// Очищаем связные списки игроков (player list) и сетевых сообщений (newtork messages).
	m_players->Empty();
	m_messages->Empty();
...

Также проверяем на валидность индекс выбранной сессии. Если индекс сессии меньше 0, то возвращаем false.
В конце заполняем структуру DPN_PLAYER_INFO, содержащую данные о локальном игроке.
Следующий шаг - поиск нужной сессии в связном списке сессий. Для этого мы входим в заранее подготовленную критическую секцию m_sessionCS для предотвращения внесения изменений в связный список сессий со стороны обработчика сетевых сообщений:

Фрагмент Network.cpp (Проект Engine)
...
	// Входим в критическую секцию связного списка энумерированных сессий (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;
		}
	}
...

Внутри критической секции итерируем (опрашиваем) связный список сессий до тех пор, пока не будет найдена сессия с нужным индексом (т.е. та, в которую хотим вступить). Как только это будет сделано, немедленно прерываем итерацию.
Теперь, когда всё готово к вступлению в сессию, делаем это путём вызова функции connect, экспонированную интерфейсом IDirectPlay8Peer:

Фрагмент Network.cpp (Проект Engine)
...
	// В ступаем (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;
}
...

Прототип функции connect выглядит так:

Прототип функции connect
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,   // Размер данных pvUser
  void *const pvPlayerContext,  // Опционально - NULL
  void *const pvAsyncContext,   // Опционально - NULL
  DPNHANDLE *const phAsyncHandle,  // Переключатель (хэндл) асинхронного выполнения
  const DWORD dwFlags  // Флаги
);

В нашем случае в первом параметре мы передаём указатель на структуру DPN_APPLICATION_DESC сессии, в которую хотим вступить. Во втором - адрес хоста этой сессии. В третьем параметре передаём адрес локального устройства (m_device). Остальные параметры (кроме последнего) нам не нужны, поэтому пропускаем их, выставив NULL. В последнем параметре выставляем флаг DPNCONNECT_SYNC для выполнения соединения в синхронном режиме. Это означает, что функция Join не вернёт значение до тех пор, пока попытка соединения не увенчается успехом (либо не завершится с ошибкой).
Мы "обрамляем" вызов функции Join условным оператором if для корректной обработки неудачного соединения и возврата в этом случае значения false. В случае успешного соединения функция join возвращает true. Приложение затем может использовать возвращённое значение для определения успешного либо неуспешного соединения.
В конце мы обязательно покидаем (leave) критическую секцию m_sessionCS для того, чтобы вновь открыть доступ к этому связному списку другим потокам и предотвратить таким образом зависание приложения.

Отправка и получение сообщений

Мы уже умеем создавать сессии, вступать в них и даже производить их энумерирование. Следующий шаг - внедрить в нашу систему поддержки сети функции обмена сообщениями. Для этого в классе Network мы создали несколько функций.
Первая из них - Send, предназначенная для отправки сетевых сообщений:

Фрагмент Network.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Отправляет сетевое сообщение.
//-----------------------------------------------------------------------------
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. Вот её прототип:

Прототип функции SendTo
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 и в общих чертах имеет следующую структуру:

Снипет структуры внутреннего обработчика сетевых сообщений класса Network
HRESULT WINAPI Network::NetwokMessageHandler(PVOID context, DWORD msgid, PVOID data)
{
  Network *network = (Network*)context;

  switch(msgid)
  {
    // Здесь обрабатываем сетевые сообщения. Системные сообщения обрабатываются немедленно.
    // Пользовательские сообщения сохраняются в связный список для последующей обработки
    // в момент готовности приложения.
  }
}

Когда DirectPlay вызывает функцию NetwokMessageHandler, он передаёт ей контекст, который представляет собой указатель, установленный ещё в конструкторе класса Network. По сути DirectPlay передаёт здесь указатель на инстанс класса Network. Всё, что нам нужно сделать, это передать context в указатель инстанса класса Network. Не забываем, что обработчик сетевых сообщений работает в отдельном потоке, что позволяет нам использовать наш объект класса Network для обработки сообщений.
Далее входим в переключатель switch, проверяющий разные значения параметра msgid (также передаваемые DirectPlay). Обрабатываем сообщения соответствующим образом, исходя из их значения msgid.
Указатель data, в свою очередь, указывает на адрес в памяти, где расположены данные сообщения.
Ещё раз внимательно изучи комментарии к функции NetwokMessageHandler в исходном коде Network.cpp для лучшего понимания принципа её работы.

Закрыть
noteОбрати внимание

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

Рассмотрим, как себя ведёт внутренний обработчик сетевых сообщений класса Network, при поступлении пользовательского сообщения:

Фрагмент Network.cpp (Проект Engine)
...
		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;
		}
...

DirectPlay присваивает всем пользовательским сообщениям идентификатор DPN_MSGID_RECIEVE, наличие которого проверяется оператором switch внутреннего обработчика сетевых сообщений класса Network. Если данный идентификатор у сообщения отсутствует, то оно считается не пользовательским и игнорируется.
Здесь мы также проверяем состояние флага m_recieveAllowed, чтобы убедиться в том, что системе поддержки сети разрешена обработка пользовательских сетевых сообщений.
Как только мы успешно прошли эти две проверки, мы создаём инстанс структуры RecievedMessage и копируем в него данные сообщения.
В финале мы входим в критическую секцию m_messageCS, добавляем заполненную структуру RecievedMessage в связный список m_messages и покидаем критическую секцию. К этому моменту пользовательское сообщение сохранено и готово к обработке приложением.

Обработка сетевых сообщений

Когда приложение находится в состоянии готовности, оно вызывает функцию Update, экспонированную классом Network. Её реализация представлена в Network.cpp и выглядит так:

Фрагмент Network.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Обновляем (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) критическую секцию, открытую ранее, чтобы обработчик сетевых сообщений мог добавить в связный список новую порцию сообщений.

Закрыть
noteОбрати внимание

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

Принцип работы остальных функций изучи самостоятельно путём рассмотрения исходного кода Network.cpp.

Интегрируем систему поддержки сети в движок

Мы почти закончили изучение системы поддержки игры по сети (далее - системы поддержки сети). Пришло время интегрировать систему поддержки сети в движок.
Принцип тот же, что и при интегрировании других систем.

Изменения в Network.cpp (Проект Engine)

  • Добавь инструкцию #include "Engine.h" в самом начале файла Network.cpp (проверь её наличие).

Изменения в Engine.h (Проект Engine)

  • Добавь инструкцию #include "Network.h" в файл Engine.h, сразу после инструкции #include "Input.h":
Фрагмент Engine.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// 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>:
Фрагмент Engine.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// DirectX Includes
//-----------------------------------------------------------------------------
#include <d3dx9.h>
#include <dinput.h>
#include <dplay8.h>
#include <dmusici.h>
...

  • Добавь два новых члена

GUID guid;
void (*HandleNetworkMessage)( ReceivedMessage *msg );
в структуру EngineSetup (файл Engine.h) как показано здесь:

Фрагмент Engine.h (Проект Engine)
...
struct EngineSetup
{
	HINSTANCE instance; // Дескриптор экземпляра приложения.
	GUID guid; // Application GUID.
	char *name; // Название приложения.
	float scale; // Масштаб (scale) в метрах/футах.
	unsigned char totalBackBuffers; // Число используемых бэкбуферов.
	void (*HandleNetworkMessage)( ReceivedMessage *msg ); // Обработчик сетевых сообщений.
	void (*StateSetup)(); // Функция подготовки стейта.
...

  • Присвой добавленным членам в конструкторе структуры EngineSetup значения по умолчанию:

GUID defaultGUID = { 0x24215591, 0x24c0, 0x4316, { 0xb5, 0xb2, 0x67, 0x98, 0x2c, 0xb3, 0x82, 0x54 } };
HandleNetworkMessage = NULL;
как показано здесь:

Фрагмент Engine.h (Проект Engine)
...
	//-------------------------------------------------------------------------
	// The engine setup structure constructor.
	//-------------------------------------------------------------------------
	EngineSetup()
	{
		GUID defaultGUID = { 0x24215591, 0x24c0, 0x4316, { 0xb5, 0xb2, 0x67, 0x98, 0x2c, 0xb3, 0x82, 0x54 } };

		instance = NULL;
		name = "Application"; // Название игрового приложения
		scale = 1.0f;
		totalBackBuffers = 1;
		HandleNetworkMessage = NULL;
		StateSetup = NULL;
	}
...

Член guid позволяет указать GUID приложения, по которому его (приложение) будут однозначно идентифицировать в сети. Если при создании экземпляра движка ты не укажешь никакого GUID, то его значение будет взято из конструктора структуры EngineSetup. Делать так не рекомендуется, т.к. это приведёт к путанице при выполнении ряда операций, например при энумерировании сессий. И в результате возникнет ситуация, когда будут найдены в сети два приложения с одинаковым GUID. DirectPlay будет считать их одним и тем же приложением, хотя на самом деле это будет не так.
Член *HandleNetworkMessage позволяет указать обработчик пользовательских (user-defined) сетевых сообщений, специфичный для приложения (он же внутренний обработчик сетевых сообщений). В конструкторе структуры EngineSetup ему присвоено значение NULL. Если при создании экземпляра движка оставить это значение, то приложение вообще не будет получать пользовательские сетевые сообщения.

  • В объявлении класса Engine (файл Engine.h) в секцию private добавь новый член

Network *m_network;
сразу после объявления объекта класса Input:

Фрагмент Engine.h (Проект Engine)
...
private:
	bool m_loaded; // Флаг показывает, загружен ли движок или нет.
	HWND m_window; // Дескриптор главного окна.
	bool m_deactive; // Флаг показывает, активно приложение или нет.

	EngineSetup *m_setup; // Копия (инстанс) структуры EngineSetup.
...
	Input *m_input; // Объект класса Input.
	Network *m_network; // Объект класса Network.
	SoundSystem *m_soundSystem; // Объект класса SoundSystem.
};
...

В нём будет храниться экземпляр класса Network.

  • В объявлении класса Engine (файл Engine.h) в секцию public добавь объявление функции

Network *GetNetwork();
сразу после объявления функции GetInput:

Фрагмент Engine.h (Проект Engine)
...
class Engine
{
public:
	Engine( EngineSetup *setup = NULL );
	virtual ~Engine();

	void Run();
...
	ResourceManager< Script > *GetScriptManager();

	Input *GetInput();
	Network *GetNetwork();
	SoundSystem *GetSoundSystem();
...

Функция GetNetwork возвращает указатель на экземпляр класса Network. Она даёт доступ к сетевым возможностям практически из любого места движка для проведения типичных сетевых операций (создание сессий, вступление в них и обмен сообщениями).

Изменения в Engine.cpp (Проект Engine)

  • Добавь строку

m_network = new Network( m_setup->guid, m_setup->HandleNetworkMessage );
в конструктор класса Engine (файл Engine.cpp), сразу после создания объекта Input:

Фрагмент Engine.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Конструктор класса Engine.
//-----------------------------------------------------------------------------
Engine::Engine( EngineSetup *setup )
{
	// Указываем, что движок ещё не загружен.
	m_loaded = false;
...
	// Создаём менеджеры ресурсов.
	m_scriptManager = new ResourceManager< Script >;

	// Создаём экземпляр класса Input
	m_input = new Input( m_window );

	// Создаём экземпляр класса Network.
	m_network = new Network( m_setup->guid, m_setup->HandleNetworkMessage );

	// Создаём экземпляр (объект) класса SoundSytem
	m_soundSystem = new SoundSystem( m_setup->scale );

	// Создаём генератор случайных чисел на основе текущего времени.
	srand( timeGetTime() );
...

Так мы создаём объект класса Network при создании каждого инстанса движка. При создании объекта класса Network мы указываем в параметрах GUID и назначенный обработчик пользовательских сетевых сообщений. Новосозданному экземпляру класса Network присваивается указатель m_network.

Раз мы добавили новую строку в конструктор класса Engine, добавим также соответствующий ему оператор безопасного удаления объекта m_network в его деструктор.

  • Добавь выражение

SAFE_DELETE( m_network );
в деструктор класса Engine, сразу после макроса безопасного удаления объекта m_sound:

Фрагмент Engine.cpp (Проект Engine)
...
Engine::~Engine()
{
	// Проверяем, что движок загружен.
	if( m_loaded == true )
	{
...
		// Уничтожаем ранее созданные объекты.
		SAFE_DELETE( m_soundSystem );
		SAFE_DELETE( m_network );
		SAFE_DELETE( m_input );
		SAFE_DELETE( m_scriptManager );

		// Уничтожаем интерфейс спрайта.
		SAFE_RELEASE( m_sprite );

		// Уничтожаем объект устройства.
		SAFE_RELEASE( m_device );
	}
...

  • Добавь строку

m_network->Update();
внутри цикла while функции Run класса Engine (файл Engine.cpp), сразу после расчёта затраченного времени:

Фрагмент Engine.cpp (Проект Engine)
...
void Engine::Run()
{
	// Убеждаемся, что движок загружен.
	if( m_loaded == true )
	{
		// Показываем окно.
		ShowWindow( m_window, SW_NORMAL );

...
			else if( !m_deactive )
			{
				// Подсчитываем затраченное время.
				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 перед реализацией функции GetSoundSystem:
Фрагмент Engine.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Возвращает указатель на объект класса input.
//-----------------------------------------------------------------------------
Input *Engine::GetInput()
{
	return m_input;
}

//-----------------------------------------------------------------------------
// Возвращает указатель на объект класса network.
//-----------------------------------------------------------------------------
Network *Engine::GetNetwork()
{
	return m_network;
}

//-----------------------------------------------------------------------------
// Возвращает указатель на объект класса SoundSystem.
//-----------------------------------------------------------------------------
SoundSystem *Engine::GetSoundSystem()
{
	return m_soundSystem;
}


Система поддержки сети полностью интегрирована в движок и готова к работе.

Тестовая перекомпиляция Engine.lib

Закрыть
warningПеред компиляцией

В последних версиях DirectX SDK отсутствует файл библиотеки dplayx.lib и сопутствующие ему заголовки dplay8.h и dpaddr.h, необходимые для следующей компиляции библиотеки Engine.lib. Их можно без труда найти в Интернете или в DirectX SDK 8.0. После скачивания, данные файлы нужно скопировать в соответствующие подкаталоги lib и include установленного DirectX SDK.

Для проверки работосопособности исходного кода, добавленного в этой Главе, перекомпилируем исходный код Проекта Engine:

  • В Обозревателе решений щёлкаем правой кнопкой мыши по значку Проекта Engine. Во всплывающем меню выбираем "Перестроить" (применяется в случае, когда код уже был успешно скомпилирован ранее).

Image
По окончании компиляции (обычно, при создании небольших проектов, она занимает менее 1 секунды) в панели "Вывод" (в нижней части главного окна IDE) будет представлен отчёт (лог) об успешной (либо неуспешной) компиляции.
В нашем случае, несмотря на многочисленные предупреждения, компиляция прошла успешно.

Закрыть
noteОбрати внимание...

Напомним о том, что следует отличать ошибки (errors) от предупреждений (warnings). Компиляция может завершится успешно даже при выводе сотен всевозможных предупреждений. Чаще всего они связаны с использованием устаревших (deprecated) либо небезопасных (unsafe), по мнению Microsoft, функций. За основу учебного курса была взята книга Young V. "Programming Multiplayer FPS in DirectX", вышедшая в свет в 2005 году, где все примеры написаны под классический C++ в IDE MS Visual C++ 6.0 аж 1998 года выпуска. С тех пор очень многое изменилось. Microsoft развивала C++, Windows API и свои IDE семимильными шагами. В результате многие функции, успешно применявшиеся тогда, сегодня признаны Microsoft устаревшими и не рекомендованными к использованию. К предупреждениям можно прислушиваться, а можно и нет. Для простоты изложения мы будем их игнорить. А чтобы они не выводились совсем, пропиши строку _CRT_SECURE_NO_WARNINGS в свойствах Проекта Engine (Свойства -> Свойства конфигурации -> С/С++ -> Препроцессор -> Определения препроцессора).

Полученная в результате компиляции двоичная библиотека Engine.lib перезаписывается по тому же пути (там же её будет искать тестовое приложение из Проекта Test).

Модифицируем тестовое приложение (Проект Test)

В Главе 1.6(external link) для проверки работоспособности движка в нашем Решении GameProject01 мы создали второй Проект Test, после компиляции которого получили исполняемое приложение (файл .EXE), показывающее окно. В последующих главах его функционал изменялся. Применив полученные в текущей Главе знания на практике, снова дополним исходный код тестового приложения, оснастив его новым "функционалом". Если по каким-то причинам Проект Test отсутствует в Решении GameProject01, создай его, следуя инструкциям Главы 1.6(external link) .
ОК, приступаем.

  • Открой для редактирования файл исходного кода Main.cpp (Проект Test) и замени содержащийся в нём код на следующий:
Main.cpp (Проект Test)
//-----------------------------------------------------------------------------
// System Includes
//-----------------------------------------------------------------------------
#include <windows.h>

//-----------------------------------------------------------------------------
// Engine Includes
//-----------------------------------------------------------------------------
#include "..\GameProject01\Engine.h"

//-----------------------------------------------------------------------------
// Test State Class
//-----------------------------------------------------------------------------
class TestState : public State
{
public:
	//-------------------------------------------------------------------------
	// Выполняем здесь пре-процессинговые операции класса TestState.
	//-------------------------------------------------------------------------
	virtual void Load()
	{
		m_font = new Font;
		m_text[0] = 0;
		m_connected = false;
	};

	//-------------------------------------------------------------------------
	// Выполняем здесь пост-процессинговые операции класса TestState,
	// связанные в основном с освобождением ресурсов.
	//-------------------------------------------------------------------------
	virtual void Close()
	{
		SAFE_DELETE( m_font );
	};

	//-------------------------------------------------------------------------
	// Возвращает текущие установки вьюера для данного кадра.
	//-------------------------------------------------------------------------
	virtual void RequestViewer( ViewerSetup *viewer )
	{
		viewer->viewClearFlags = D3DCLEAR_TARGET;
	}

	//-------------------------------------------------------------------------
	// Обновляет (Updates) стейт.
	//-------------------------------------------------------------------------
	virtual void Update( float elapsed )
	{
		// Игрок даёт команду на хостинг сессии.
		if( g_engine->GetInput()->GetKeyPress( DIK_H ) == true )
			m_connected = g_engine->GetNetwork()->Host( "host player", "session", 8 );

		// Игрок даёт команду на энумерирование сессий в сети.
		if( g_engine->GetInput()->GetKeyPress( DIK_E ) == true )
			g_engine->GetNetwork()->EnumerateSessions();

		// Игрок даёт команду на вступление (join) в сессию №1 в списке.
		if( g_engine->GetInput()->GetKeyPress( DIK_1 ) == true )
			m_connected = g_engine->GetNetwork()->Join( "join player", 0 );

		// Игрок даёт команду на вступление (join) в сессию №2 в списке.
		if( g_engine->GetInput()->GetKeyPress( DIK_2 ) == true )
			m_connected = g_engine->GetNetwork()->Join( "join player", 1 );

		// Игрок даёт команду на вступление (join) в сессию №3 в списке.
		if( g_engine->GetInput()->GetKeyPress( DIK_3 ) == true )
			m_connected = g_engine->GetNetwork()->Join( "join player", 2 );

		// Игрок выходит из приложения.
		if( g_engine->GetInput()->GetKeyPress( DIK_Q ) == true )
			PostQuitMessage( 0 );

		// Создаём текст меню.
		strcpy( m_text, "\n\n\nH - Host Session\n\nE - Enumerate Sessions" );

		// Проходим через список сессий, найденых в локальной сети.
		char count = 0;
		char name[MAX_PATH];
		SessionInfo *session = g_engine->GetNetwork()->GetNextSession( true );
		while( session != NULL )
		{
			// Прибавляем 1 к счётчику сессий.
			sprintf( name, "\n   %d - ", ++count );
			strcat( m_text, name );

			// Добавляем имя сессии.
			wcstombs( name, session->description.pwszSessionName, MAX_PATH );
			strcat( m_text, name );

			// Переходим к следующей сессии.
			session = g_engine->GetNetwork()->GetNextSession();
		}

		// Добавляем опцию выхода из приложения в меню пользователя.
		strcat( m_text, "\n\nQ - Quit" );

		// Добавляем сведения о статусе соединения.
		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" );
	};

	//-------------------------------------------------------------------------
	// Рендерим стейт.
	//-------------------------------------------------------------------------
	virtual void Render()
	{
		m_font->Render( m_text, 0.0f, 0.0f );
	};

private:
	Font *m_font; // Шрифт для отображения меню.
	char m_text[MAX_PATH]; // Текст для отображения опций меню.
	bool m_connected; // Индикатор статуса соединения (подключен/отключен).
};

//-----------------------------------------------------------------------------
// Application specific state setup.
//-----------------------------------------------------------------------------
void StateSetup()
{
	g_engine->AddState( new TestState, true );
}

//-----------------------------------------------------------------------------
// Входная точка приложения.
//-----------------------------------------------------------------------------
int WINAPI WinMain( HINSTANCE instance, HINSTANCE prev, LPSTR cmdLine, int cmdShow )
{
	// Уникальный id для тестового приложения, чтобы его можно было идентифицировать в сети.
	GUID guid = { 0x9ca7996f, 0xebb4, 0x4e9b, { 0xba, 0x6b, 0xe6, 0x4b, 0x45, 0x7f, 0x59, 0x98 } };

	// Создаём структуру EngineSetup.
	EngineSetup setup;
	setup.instance = instance;
	setup.guid = guid;
	setup.name = "Network Test";
	setup.StateSetup = StateSetup;

	// Создаём движок (используя структуру EngineSetup), затем запускаем его.
	new Engine( &setup );
	g_engine->Run();

	return true;
}


И вновь в нашем Проекте Test всего 1 файл Main.cpp, который содержит весь исходный код тестового приложения. Перед нами готовая система поддержки игры по сети (в виде стейта) в действии.

Исследуем код

Наибольшим изменениям подвергся стейт TestState, "адаптированный" для проверки сетевых возможностей движка.
В функции WinMain мы установили GUID приложения. В то же время мы нигде не указали обработчик пользовательских сетевых сообщений, специфичный для приложения (application-specific network message handler). А всё потому, что пока мы не будем обрабатывать пользовательские сетевые сообщения. Ты увидишь его в деле при разработке игры во второй части данного курса.
При ближайшем рассмотрении стейта TestState, видно что вся суть кроется в его функции Update. В двух словах мы приделали вывод на экран нескольких опций меню а затем написали обработчики нажатия клавиш для них.

  • При нажатии <H> приложение хостит новую сессию.
  • При нажатии <E> приложение проведёт энумерирование (опрос) активных сессий в сети. Все найденные сессии отобразятся в окне в виде списка опций, к которым привязаны обработчики нажатия клавиш 1, 2 или 3. Игрок вступает в выбранную сессию путём нажатия соответствующей цифровой клавиши на клавиатуре. Наше приложение предлагает на выбор всего три первые найденные сессии. Но при желании количество сессий на выбор может быть легко увеличено.

Под меню приложения расположена строка вывода статуса (status read-out), показывающая текущий статус подключения. Если приложение не подключено к сессии, то выводится статус "NOT CONNECTED". При хосте сессии выводится статус "CONNECTED-HOST". При нахождении в состоянии подключения к сессии выводится статус CONNECTED-CLIENT.
В состоянии соединения пользователь может выбрать только одну опцию - "disconnect".
Мы знаем, что обработчик сетевых сообщений работает, т.к. у нас есть возможность хостить сессии, вступать в них и энумерировать их. В то же время сейчас мы не сможем протестить обработку пользовательских сетевых сообщений, т.к. займёмся этим позже, при разработке игры.

Подготовка Проекта Test к перекомпиляции

Напомним, что для успешной компиляции библиотека Engine должна быть добавлена в Проект Test (см. Главу 1.6).
Помимо этого, MS Visual C++ 2010 при компиляции нашего обновлённого Проекта Test активно юзает внешние библиотеки и требует их принудительного указания в настройках компоновщика (= линковщика, линкера) Проекта. Некоторые из них мы уже добавляли ранее.
Проверим наличие всех необходимых библиотек:

  • В Обозревателе решений щёлкни правой кнопкой мыши по названию Проекта Test.
  • Во всплывающем контекстном меню выбираем пункт "Свойства".
  • В появившемся окне: Свойства конфигурации -> Компоновщик -> Ввод. Во всплывающем списке напротив пункта "Дополнительные зависимости" выбираем "Изменить...".
  • В появившемся окне "Дополнительные зависимости" в поле ввода должны быть указаны (обязательно в столбик и без запятых!) 7 библиотек:

d3dx9d.lib, d3d9.lib, dinput8.lib, dxguid.lib, winmm.lib, odbc32.lib, odbccp32.lib

  • Жмём ОК, ОК.
  • Сохрани изменения в Решении (Файл->Сохранить все).


Сразу после этого компилируем Проект Test:

  • В Обозревателе решений щёлкни правой кнопкой мыши по названию Проекта Test.
  • Во всплывающем контекстном меню выбираем: Перестроить.

Закрыть
warningerror LNK2019

При компиляции Проекта Test с использованием последних версий DirectX SDK часто возникает несколько ошибок вида "error LNK2019: ссылка на неразрешенный внешний символ...".
Причина: DirectSound-у не нравится библиотека dxguid.lib из последних версий DirectX SDK. Интерфейсы DirectSound и DirectMusic давно не поддерживаются и признаны устаревшими. Для корректной компиляции необходимо найти dxguid.lib из Microsoft DirectX SDK 8.0 и скопировать в каталог lib установленного DirectX SDK, согласившись на замену.


Компиляция завершилась успешно. Итоговый исполняемый двоичный файл (Test.exe) расположен на жёстком диске ПК в той же директории, что и библиотека движка.
В нашем случае, это: С:\Users\<Имя пользователя>\documents\visual studio 2010\Projects\GameProject01\Debug. (В разных ОС путь к файлу может отличаться от представленного).
*Найди и запусти получившееся приложение.

Закрыть
noteОбрати внимание...

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

Приложение стартует прибл. 30-40 секунд. Да и вообще выглядит не очень информативным. Но для образовательных целей оно очень ценно.

Итоги главы

Глава 1.13 была настоящим погружением в мир сетевых технологий. И то мы лишь поверхностно пробежались по основным аспектам программирования сетевых функций. Мы обсудили DirectPlay и как он управляет сетевыми функциями. Узнали о существовании различных сетевых архитектур. Далее мы рассмотрели основные принципы работы нашей системы поддержи сети + охватили сопутствующий материал (например критические секции), прежде чем изучить реализацию сетевых функций. В конце мы изменили исходный код тестового приложения, в этот раз оснащённого поддержкой сетевых технологий.
Данная тема очень сложна. Поэтому рекомендуется также почитать материалы из других источников ( в Интернете или документацию к DirectX SDK 8.0). И, конечно же, всегда опирайся на исходный код, который всегда должен быть перед глазами. В нём нет ни одной лишней функции. Каждая из них для чего-то нужна.
Дальше будет ещё интереснее, т.к. мы переходим к изучению текстур и материалов. Уже к концу следующей главы мы выведем на экран полигональную меш-модель, покрытую текстурами и даже анимированную!


ИГРОКОДИНГ  »  ИГРОКОДИНГ: Учебный курс  »  Программируем 3D-шутер от первого лица (FPS) (Win32, Cpp, DirectX9)  »  Часть 1. Создание движка  »  1.13 Добавляем поддержку игры по сети

Contributors to this page: slymentat .
Последнее изменение страницы Воскресенье 21 / Май, 2017 19:18:30 MSK автор slymentat.

Последние комментарии

No records to display