Программирование стратегических игр с DirectX 9.0

       

Анимация частиц


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


Рис. 13.2. Изменение частиц с течением времени

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

netlib.narod.ru< Назад | Оглавление | Далее >



Члены данных


В классе частицы я объявляю несколько переменных, предназначенных для описания характеристик частицы. Вот они:

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

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

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

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

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

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

Элемент m_iTextureType сообщает как именно должна анимироваться текстура. Можно использовать единственную текстуру (т.е. анимация отсутствует), либо можно в цикле перебрать текстуры от начальной до конечной и остановиться. Также можно перебрать текстуры от начальной до конечной (или в обратном порядке), а затем повторять цикл анимации снова. Основное назначение этого параметра— предоставить возможность контроллировать стиль анимации текстур.

Элемент m_iTextureCur сообщает частице, какая именно текстура используется в данный момент времени.

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

Элемент m_iTextureCurStep сообщает сколько еще тактов игры должно проити до смены текстуры. Отсчет начинается с 0 и идет до значения, заданного переменной m_iTextureSteps. Как только счетчик достигнет максимального значения, будет изменено значение переменной m_iTextureCur а отсчет в переменной m_iTextureCurStep начнется снова с 0.





Движение частиц


Чтобы частицы произвели какой-нибудь эффект, они должны двигаться. Возьмем для примера фейерверк; когда заряд разрывается на сотни частиц, именно их движение определяет, насколько красивым будет салют. Одни заряды образуют сферы, другие— огенные полосы. Так же работают и системы частиц в играх. Вы, как разработчик, должны написать алгоритм движения, которому будут следовать частицы. Два примера показаны на рис. 13.1.


Рис. 13.1. Примеры движения частиц

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



Функция CParticle::vUpdate()


Код реализации методов класса находится в файле CParticle.cpp. В нем достаточно много функций, но сравнительно сложной является только функция обновления данных. Цель этой функции — вносить требуемые изменения в состояние частицы на каждом такте игры. Это включает изменение скорости, местоположения и состояния анимации текстуры частицы. Вот как выглядит выполняющий эти действия код:

// Изменяем скорость с учетом ускорения m_vecCurSpeed.fX += m_vecAcceleration.fX; m_vecCurSpeed.fY += m_vecAcceleration.fY; m_vecCurSpeed.fZ += m_vecAcceleration.fZ; // Изменяем скорость с учетом гравитации m_vecCurSpeed.fX += m_vecGravity.fX; m_vecCurSpeed.fY += m_vecGravity.fY; m_vecCurSpeed.fZ += m_vecGravity.fZ; // Обновляем местоположение m_vecPos.fX += m_vecCurSpeed.fX; m_vecPos.fY += m_vecCurSpeed.fY; m_vecPos.fZ += m_vecCurSpeed.fZ; // // Обновление текстуры // // Статическая структура if(m_iTextureType == 0) { m_iTextureCur = m_iTextureStart; } // Покадровая анимация else { m_iTextureCurStep++; if(m_iTextureCurStep >= m_iTextureSteps) { // Линейная if(m_iTextureType == 1) { if(m_iTextureCur != m_iTextureEnd) { m_iTextureCur++; } } // Циклическая прямая else if(m_iTextureType == 2) { m_iTextureCur++; if(m_iTextureCur > m_iTextureEnd) { m_iTextureCur = m_iTextureStart; } } // Циклическая обратная else if(m_iTextureType == 3) { m_iTextureCur--; if(m_iTextureCur < m_iTextureStart) { m_iTextureCur = m_iTextureEnd; } } // Сброс счетчика текстур m_iTextureCurStep = 0; } } // Уменьшение счетчика времени жизни m_iLife--;

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

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


Следующий блок кода проверяет используемый метод анимации текстуры и соответствующим образом меняет текущую текстуру.

В коде реализованы четыре типа анимации. Первый тип — использование статической текстуры. Это означает, что в цикле анимации используется только одна, стартовая текстура и никаких изменений с ней в ходе игры не происходит.

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

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

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

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

netlib.narod.ru< Назад | Оглавление | Далее >

Характеристики частиц


Теперь, когда вы познакомились с примерами частиц, пришло время узнать их основные характеристики. Для разработки игр вам необходима система частиц, которая будет обрабатывать используемые в игре частицы. Я предпочитаю использовать простой класс, но вы можете решить, что вашему проекту требуется диспетчер или иное, более сложное, решение. Основными характеристиками частиц являются:

Изображение

Движение

Анимация



Инициализация частиц


Код инициализации частиц выглядит следующим образом:

void vInitParticles(void) { // Инициализация каждой частицы for(int i = 0; i < TOTAL_PARTICLES; i++) { // Установка последовательности анимации текстур g_partExplosion[i].vSetTextures( rand() % 3, // Тип анимации 0, // Начальная текстура 5, // Конечная текстура 10); // Пауза между сменой текстур // Установка начального местоположения g_partExplosion[i].vSetPos( 0.0f + (rand() % g_iWindowWidth), // X 0.0f + (rand() % g_iWindowHeight), // Y 0.0f); // Z // Установка начальной скорости g_partExplosion[i].vSetSpeed( -1.0f + rand() % 2, // X -8.0f + rand() % 4, // Y 0.0f); // Z // Установка гравитационного воздействия g_partExplosion[i].vSetGravity( 0.0f, // X 0.1f, // Y 0.0f); // Z // Установка длительности жизни частицы g_partExplosion[i].vSetLife(200); } }

Функция в цикле перебирает все частицы, количество которых задается определенной в заголовочном файле константой TOTAL_PARTICLES. Для каждой частицы задается случайное местоположение и скорость. Затем задается гравитационное воздействие и, в самом конце, продолжительность жизни частицы устанавливается равной 200. Это сообщает системе, что частица будет существовать в течение 200 игровых тактов.

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

Тип анимации выбирается случайным образом, а диапазон используемых при анимации текстур для всех частиц будет от 0 до 5. Кроме того, я задаю паузу между сменой текстур, равной 10 для того, чтобы кадры анимации не сменяли друг друга слишком быстро.



Изображение частиц


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

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



Класс CParticle


Класс CParticle предназначен для хранения всей информации, необходимой системе частиц для управления отдельной частицей. Он не предназначен для управления набором частиц. Для создания системы частиц вам потребуется написать диспетчер частиц.



Класс CVector


В начале заголовочного файла расположена реализация очень простого класса вектора. Я мог бы воспользоваться для представления векторов вспомогательным классом DirectX, но предпочел создать собственный класс, чтобы обеспечить переносимость кода. Мой класс вектора используется для хранения значений X, Y и Z таких параметров частиц, как местоположение и скорость. Как видно из кода, класс является только хранилищем данных и ничем более.



Методы класса


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

Функция CParticle() является конструктором класса и ее основная задача — очистить все члены данных, присвоив им значения по умолчанию.

Функция ~CParticle() — это деструктор класса, и она освобожает занятую память, когда объект класса уничтожается.

Функция vUpdate() вызывается на каждом такте игры и обновляет местоположение, скорость и состояние текстур частицы.

Функция bIsAlive() сообщает вам жива еще частица или нет. Если она возвращает 0, значит частица уже уничтожена. Если она возвращает 1 — частица еще жива. Чтобы определить, какое значение возвращать, функция проверяет значение члена данных m_iLife.

Функция vSetTextures() устанавливает информацию об анимации текстур, которая будет использоваться частицей.

Функция vSetPos() устанавливает начальное местоположение частицы.

Функция vSetAcceleration() устанавливает начальное ускорение частицы.

Функция vSetGravity() задает гравитационное воздействие на частицу.

Функция vSetSpeed() задает начальную скорость частицы.

Функция vSetLife() устанавливает период жизни частицы.



Основные сведения о частицах


Сейчас вы в лагере работающих с частицами новобранцев. Первый вопрос повестки дня — что такое частицы? Если вы откроете корпус своего компьютера и дунете внутрь, то, скорее всего, увидите летающие по комнате частицы пыли. Если вы изучали электромагнитные явления, то наверняка использовали железные опилки. Запустив фейерверк вы увидите летящие в разные стороны искры. Фактически, частица это очень маленькая часть чего-нибудь. Слово «чего-нибудь» допускает многочисленные толклвания. У вас могут быть частицы древесины, песка, грязи, воды и т.д. Вот несколько примеров частиц, которые часто встречаются в стратегических играх:

Частицы огня для взрывов.

Частицы дыма для следов ракет.

Частицы пыли при передвижении подразделений.

Частицы воды для дождя.

Частицы льда для снега.

Частицы энергии для силовых лучей.

netlib.narod.ru< Назад | Оглавление | Далее >



Реализация системы частиц


Теперь загрузите проект D3D_Particles, если вы еще не сделали этого, и скомпилируйте его. Запустите программу и вы увидите сцену, похожую на ту, что показана на рис.13.3.


Рис. 13.3. Окно программы D3D_Particles

Обратите внимание на разбросанные по экрану разноцветные частицы. Программа показывает как создать случайный набор частиц и подбрасывать их в воздух. Вы можете назвать это демонстрацией попкорна, я оставляю выбор за вами. Кажется я знаю, что вы думаете сейчас: «Какое это имеет отношение к программированию стратегических игр?». Ответ прост — предполагается, что программа покажет вам как можно быстро начать работать с частицами. Более сложные формирования, такие как взрывы и ударные волны, придут позднее, когда вы освоитесь с основными приемами использования частиц.

Проект состоит из четырех файлов: main.cpp, main.h, CParticle.cpp и CParticle.h. Кроме того, для компиляции потребуются следующие библиотеки: d3d9.lib, dxguid.lib, d3dx9dt.lib и d3dxof.lib.



Структура класса частиц


В качестве примера работы с классом частиц я включил в сопроводительные файлы книги проект с именем D3D_Particles. Загрузите его и следуйте вперед к созданному мной примеру класса. Код класса находится в двух файлах проекта: CParticle.cpp и CParticle.h. Вот как выглядит заголовочный файл:

class CVector { public: float fX; float fY; float fZ;

CVector() { fX=0.0f, fY=0.0f, fZ=0.0f; }; };

class CParticle { public: CVector m_vecPos; CVector m_vecCurSpeed; CVector m_vecAcceleration; CVector m_vecGravity; int m_iLife; int m_iTextureStart; int m_iTextureEnd; int m_iTextureType; int m_iTextureCur; int m_iTextureSteps; int m_iTextureCurStep;

CParticle(); ~CParticle(); void vUpdate(void); bool bIsAlive(void); void vSetTextures(int iType, int iStart, int iStop, int iSteps); void vSetPos(float x, float y, float z); void vSetAcceleration(float x, float y, float z); void vSetGravity(float x, float y, float z); void vSetSpeed(float x, float y, float z); void vSetLife(int iLife); };



Структура проекта D3D_Particles


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


Рис. 13.4. Ход выполнения программы D3D_Particles

На рис. 13.4 видно, как функция WinMain() выполняет инициализацию системы, последовательно обращаясь к функциям InitD3D(), vInitInterfaceObjects() и vInitParticles(). С первыми двумя функциями мы уже встречались в предыдущих примерах, а вот функция инициазизации частиц новая и появляется в этом примере впервые. Ее цель — создание частиц для сцены и установка их атрибутов для анимации.



Визуализация частиц


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

// Цикл перебора частиц for(int i = 0; i < TOTAL_PARTICLES; i++) { // Проверяем, жива ли частица if(g_partExplosion[i].bIsAlive()) { // Визуализация частицы vDrawInterfaceObject(g_partExplosion[i].m_vecPos.fX, g_partExplosion[i].m_vecPos.fY, (float)g_iParticleSize, (float)g_iParticleSize, g_pTexture[ g_partExplosion[i].m_iTextureCur ]); // Обновление частицы g_partExplosion[i].vUpdate(); } else { // Сброс частицы, если она уничтожена vInitParticles(); } }

Функция визуализации в цикле перебирает все частицы, количество которых задается определенной в заголовочном файле константой TOTAL_PARTICLES. Для каждой частицы функция сперва проверяет жива ли она еще. Если частица жива, функция отображает ее в текущем местоположении. Чтобы сообщить функции визуализации, какую текстуру следует использовать, применяется хранящийся в объекте частицы номер текущей текстуры. После визуализации частицы ее данные обновляются путем вызова функции vUpdate().

СОВЕТ

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

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

netlib.narod.ru< Назад | Оглавление | Далее >



BNC — шаг вперед!


Давным давно в далекой галактике... Ну, дальше вы знаете сами. Так или иначе, в не столь отдаленном прошлом существовало несколько методов объединения компьютеров в локальную сеть. Один старый метод использовал BNC-коннекторы и коаксиальный кабель. Я видел и такую сеть, где использовалось самодельное оборудование и параллельные кабели. К счастью для вас, большинство локальных сетей сегодня используют витую пару и соответствующие коннекторы. Витая пара и коаксиальный кабель с коннекторами показаны на рис. 14.1.


Рис. 14.1. Витая пара и коаксиальный кабель

Самое досадное в BNC и коаксиальных кабелях — необходимость создать цепочку из компьютеров соединенных вместе, проходя от одного к другому. Вы не можете просто поставить концентратор (hub), чтобы все подключались к нему; вам необходимо объединить все компьютеры одной линией и поставить на ее концах терминаторы, чтобы предотвратить блуждание сигнала туда-сюда по линии. На рис. 14.2 изображена сеть, использующая BNC и коаксиальный кабель.


Рис. 14.2. Компьютеры, соединенные коаксиальным кабелем

Как видно на иллюстрации, компьютер A соединен с компьютером B. Компьютер B соединен с компьютером C, а компьютер C соединен с компьютером D. На компьютерах A и D установлены терминаторы, блокирующие сигнал. Они очень важны — без них сеть не будет правильно функционировать. Если вы когда-нибудь пользовались устройствами SCSI, то должны знать назначение терминаторов. В основе работы терминаторов лежит следующее физическое явление: электрический сигнал в сети распространяется от компьютера к компьютеру, пока не будет поглощен терминатором. Если в конце линии не будет терминатора, сигнал будет отражен и станет распространяться в обратном направлении. В результате компьютеры в сети будут многократно получать одну и ту же информацию из-за отражений сигнала. Можете представить себе, сколько проблем это вызовет.



Функции программы Sockets_TurnGame


Далее в заголовочном файле main.h приведены прототипы используемых в программе функций. Вот они:

void vHost(); void vInitializeSockets(void); void vShutdownSockets(void); void vConnect(void); void vSendTurnMessage(void); void vReceiveTurnMessage(void); void vTurnDone(void);

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

Функция vInitializeSockets() используется для начальной инициализации WinSock.

Функция vShutdownSockets() отключает все активные соединения и систему WinSock.

Функция vConnect() вызывается когда пользователь щелкает по кнопке Connect. Она пытается подключиться к ведущему компьютеру, чей IP-адрес указан в окне. После того, как соединение установлено, клиент получает контроль над игрой и может закончить ход в выбранный им момент времени.

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

Функция vReceiveTurnMessage() ждет, пока другой игрок не пришлет сообщающий о завершении хода пакет. Функция будет сидеть и ждать, пока пока рак на горе не свистнет.

Функция vTurnDone() вызывается функциями отправки и приема хода для завершения хода. Это происходит когда пользователь щелкает по кнопке Turn Done.

Остальные перечисленные в заголовочном файле main.h функции являются стандартным каркасом приложения для Windows и не слишком интересны, поэтому я не буду их рассматривать. Вы же не хотите, чтобы я по сто раз описывал одно и то же? Лучше я лишний раз сыграю в Age of Mythology!



Функция vConnect()


Функция vConnect() вызывается, когда игрок щелкает по кнопке Connect. Вот как выглядит ее код:

sockaddr_in saServerAddress; int iPort = 6001,iStatus; LPHOSTENT lpHost; char szHost[128];

// Установка глобальных переменных g_bIsServer = 0; // Инициализация параметров сервера, смените указанный здесь IP-адрес // на корректный IP-адрес вашей сети sprintf(szHost, "192.168.0.2"); // Инициализация дескриптора сокета g_skClientSocket = INVALID_SOCKET; // Создание сокета g_skClientSocket = socket(AF_INET, SOCK_STREAM, 0); // Проверка наличия ошибок if(g_skClientSocket == INVALID_SOCKET) { vShowText("** ERROR ** Could Not Create Socket"); return; } vShowText("<- Socket Created ->"); // Инициализация структуры данных адреса сервера memset(&saServerAddress, 0, sizeof(sockaddr_in)); // Установка значений по умолчанию saServerAddress.sin_family = AF_INET; // Загрузка IP-адреса saServerAddress.sin_addr.s_addr = inet_addr(szHost);

// Если задано имя сервера, а IP-адрес отсутствует, // попытаемся получить требуемое значение if(saServerAddress.sin_addr.s_addr == INADDR_NONE) { vShowText("<- Looking Up Host ID ->"); // Получаем имя сервера lpHost = gethostbyname(szHost); // Проверяем, получили ли мы что-нибудь if (lpHost != NULL) { // Загружаем адрес сервера из его данных saServerAddress.sin_addr.s_addr = ((LPIN_ADDR)lpHost->h_addr)->s_addr; } else { vShowText("** ERROR ** Could Not locate host"); return; } } // Устанавливаем порт сервера saServerAddress.sin_port = htons(iPort); // Пытаемся подключиться к серверу iStatus = connect(g_skClientSocket, (struct sockaddr*)&saServerAddress, sizeof(sockaddr)); // Проверяем наличие ошибок if(iStatus == SOCKET_ERROR) { vShowText("** ERROR ** Could Not Connect To Server"); return; } // Убираем кнопки DestroyWindow(hBU_Connect); DestroyWindow(hBU_Host); vShowText("<- Connected To Server ->"); // Устанавливаем флаг подключения g_bConnected = 1; // Устанавливаем флаг, указывающий что право хода принадлежит нам g_bMyTurn = 1; // Отображаем кнопку Turn Done hBU_TurnDone = CreateWindow( "BUTTON", "Turn Done", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 5, 280, 100, 28, g_hWnd, (HMENU)IDC_hBU_TurnDone, g_hInst, NULL); vShowText(":Server waiting, make your turn");

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


Рис. 14.17. Ход выполнения функции vConnect()



Функция vHost()


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

sockaddr_in saServerAddress; sockaddr_in saClientAddress; int iClientSize = sizeof(sockaddr_in); int iPort = 6001; int iStatus;

// Установка глобальных переменных g_bIsServer = 1; // Инициализация дескриптора сокета g_skListenSocket = INVALID_SOCKET; // Создание сокета g_skListenSocket = socket(AF_INET, SOCK_STREAM, 0); // Проверим, не произошла ли ошибка if(g_skListenSocket == INVALID_SOCKET) { vShowText("** ERROR ** Could Not Create Socket"); return; } vShowText("<- Socket Created ->"); // Очищаем структуру адреса сокета memset(&saServerAddress, 0, sizeof(sockaddr_in)); // Инициализируем структуру адреса сокета saServerAddress.sin_family = AF_INET; saServerAddress.sin_addr.s_addr = htonl(INADDR_ANY); saServerAddress.sin_port = htons(iPort); // Пытаемся выполнить привязку if(bind(g_skListenSocket, (sockaddr*) &saServerAddress, sizeof(sockaddr)) == SOCKET_ERROR) { vShowText("** ERROR ** Could Not Bind Socket"); return; } vShowText("<- Socket Bound ->"); // Прослушиваем подключения iStatus = listen(g_skListenSocket, 32); if(iStatus == SOCKET_ERROR) { vShowText("** ERROR ** Could Not Listen"); // Закрываем сокет closesocket(g_skListenSocket); return; } vShowText("<- Socket Listening ->"); g_skClientSocket = accept(g_skListenSocket, (struct sockaddr*)&saClientAddress, &iClientSize); if(g_skClientSocket == INVALID_SOCKET) { vShowText("** ERROR ** Could Not Accept Client"); // Закрываем сокет closesocket(g_skListenSocket); return; } // Убираем кнопки DestroyWindow(hBU_Connect); DestroyWindow(hBU_Host); vShowText("<- Client Connected ->"); // Устанавливаем флаг подключения g_bConnected = 1; // Устанавливаем флаг, сообщающий, что сейчас // ход делает другой игрок g_bMyTurn = 0; // Ждем первый ход клиента vTurnDone();

Первая часть кода реализует логику для подключения клиента. Фактически программа прослушивает порт, ожидая соединения и принимает соединение, когда оно происходит. После этого код убирает кнопки Host и Connect, чтобы пользователь не мог щелкнуть по ним еще раз. Затем программа устанавливает переменную хода, чтобы она указывала, что контроль над игрой находится у клиента. И, наконец, ход заканчивается вызовом функции завершения хода. Это переводит сервер в режим приема, чтобы он мог получить сообщение о завершении хода от клиента. Все эти действия показаны на рис.14.16.


Рис. 14.16. Ход выполнения функции vHost()



Функция vReceiveTurnMessage()


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

void vReceiveTurnMessage(void) { char szTurnPacket[32]; intiBytes = 0;

iBytes = recv(g_skClientSocket, szTurnPacket, 32, 0); // Проверка возвращенного кода if(iBytes != SOCKET_ERROR) { } else { vShowText("** ERROR ** Receiving"); return; } // Переключение в режим отправки g_bMyTurn = 1;

// Отображение кнопки Turn Done hBU_TurnDone = CreateWindow( "BUTTON", "Turn Done", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 5, 280, 100, 28, g_hWnd, (HMENU)IDC_hBU_TurnDone, g_hInst, NULL); }

В функции приема я вызываю функцию recv для приема пакета от другого игрока. Как только пакет пришел, код устанавливает флаг хода и создает кнопку Turn Done. Вот и все об отправке и получении пакетов!

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

netlib.narod.ru< Назад | Оглавление | Далее >



Функция vSendTurnMessage()


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

void vSendTurnMessage(void) { char szTurnPacket[32]; intiBytes = 0;

// Создаем пакет-заглушку sprintf(szTurnPacket, "turnpacket"); // Отправляем пакет iBytes = send(g_skClientSocket, szTurnPacket, 32, 0); if(iBytes != SOCKET_ERROR) { } else { vShowText("** ERROR ** Sending"); return; } // Устанавливаем режим приема g_bMyTurn = 0; }

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



Функция vTurnDone()


Функция завершения хода выполняет две различных задачи. Если сейчас ваш ход, она отправляет сообщение о завершении хода другому игроку и ждет получения сообщения. Если сейчас ход другого игрока, функция ждет, пока он не завершит свой ход. Ход выполнения функции показан на рис.14.18.


Рис. 14.18. Ход выполнения функции vTurnDone()

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

// Если соединение установлено, проверяем // надо получать или отправлять сообщение о ходе if(g_bConnected) { // Мой ход, отправляю сообщение if(g_bMyTurn) { // Убираем кнопку завершения хода DestroyWindow(hBU_TurnDone); // Отправляем сообщение о завершении хода vSendTurnMessage(); // Ждем получения сообщения vReceiveTurnMessage(); } else { // Ждем получения сообщения } }

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



Глобальные переменные программы Sockets_TurnGame


Загрузите заголовочный файл main.h, и вы увидите в нем такой код:

// Переменные сокетов SOCKET g_skListenSocket; SOCKET g_skClientSocket; bool g_bIsServer = 0; bool g_bMyTurn = 0; bool g_bConnected = 0;

В приведенном выше коде объявлены два дескриптора сокетов. Ведущий компьютер для прослушивания сети в ожидании новых соединений использует дескриптор g_skListenSocket. Другой дескриптор, g_skClientSocket, используется клиентом для подключения к серверу или сервер назначает его подключившемуся клиенту. Так или иначе, клиентский сокет используется для управления соединением между клиентом и сервером.

Логическое значение g_bIsServer сообщает вам работает ли программа в режиме ведущего компьютера. Если значение равно1, значит программа является сервером и должна ожидать подключения клиента. Если значение равно 0, программа является клиентом и должна установить соединение с ведущим игровым компьютером.

Логическое значение g_bMyTurn сообщает принадлежит ли вам в данный момент право хода в игре. Если сейчас ваш ход, будет отображаться кнопка Turn Done, щелкнув по которой вы передадите ход другому игроку. Если сейчас ваш ход, значение переменной равно 1, если нет — значение переменной равно 0.

Логическое значение g_bConnected сообщает вам установила ли программа соединение с другим игроком. 1 означает что соединение существует, 0 — что нет.

Есть и еще несколько глобальных переменных, но они относятся к элементам управления Windows и другим подобным вещам.



Ход выполнения походовой игры


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


Рис. 14.12. Ход выполнения походовой сетевой игры

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



Игра по локальной сети


Игра по локальной сети (LAN) позволяет вам играть против других компьютеров, находящихся в закрытой сети. Некоторые игры позволяют одновременно играть лишь паре игроков, в то время как другие поддерживают одновременную игру десятков пользователей. Самое лучшее в игре по локальной сети— отсутствие задержек, связанных с сетевым траффиком. Играя в закрытой сети вы не страдаете от непредсказуемости Интернета.



Маршрутизаторы — ворота в Интернет


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

Количество портов

Скорость

Поддержка брандмауэров

Возможность беспроводных соединений

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


Рис. 14.6. Компьютеры, соединенные через маршрутизатор

Как видно на рис. 14.6, все компьютеры соединены между собой через маршрутизатор. Главное отличие между этой иллюстрацией и теми, где была показана сеть с концентраторами, в том, что маршрутизатор обеспечивает также и выход в Интернет. Это означает, что каждый компьютер в сети имеет доступ не только к другим компьютерам, но и к Интернету. Здорово, да?

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

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

Количество портов

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

Скорость

Маршрутизаторы, в зависимости от модели, поддерживают скорость обмена 10 мегабит, 100 мегабит или 1 гигабит. Лично меня вполне устраивают маршрутизаторы со скоростью 100 мегабит, поскольку они быстрее, чем DSL.


Поддержка брандмауэра

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

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

Беспроводные соединения

В наши дни можно приобрести практически любое сетевое оборудование с встроенной поддержкой беспроводных соединений. Беспроводные соединения позволяют объединять компьютеры через высокочастотный радиоканал. Если вы приобретаете беспроводной маршрутизатор, как сделал я, убедитесь, что приобретаемые для компьютеров беспроводные сетевые карты соответствуют тому же стандарту. Некоторые стандарты совместимы с предыдущими версиями, а многие — нет. Также следует обратить внимание на поддержку вашего беспроводного оборудования. Когда я проверял последний раз, некоторые игровые компании отказались предоставлять техническую поддержку людям, использующим беспроводные соединения. Не буду показывать пальцем на кого-нибудь конкретного, но скажу, что изготовитель поселившейся под моим телевизором черной коробки с большой буквой X на крышке не любит поддерживать мои беспроводные соединения.

netlib.narod.ru< Назад | Оглавление | Далее >

Одноранговые сети


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


Рис. 14.8. Пример одноранговой сети

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

netlib.narod.ru< Назад | Оглавление | Далее >



Отключение сокетов


Когда вы полностью завершили работу с сокетами, необходимо отключить коммуникационную систему сокетов, вызвав функцию WSACleanup(). Ее требуется вызывать один раз в конце вашей программы.

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



Отправка данных серверу


Теперь, когда соединение с сервером установлено, можно отправить пакет с HTTP-запросом. Этот пакет сообщает серверу, что вы хотите увидеть содержимое предоставляемой по умолчанию веб-страницы. Для отправки пакета необходимо воспользоваться функцией send(). Она получает сокет, через который будут отправлены данные, сами отправляемые данные и их размер. В рассматриваемом примере я отправляю содержимое буфера szSendBuffer через сокет, идентификатор которого хранится в переменной skSocket.

Если какие-либо данные были переданы, функция возвращает количество отправленных байт.



Пакеты


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


Рис. 14.7. Пример пакета

На рис. 14.7 показана структура простого пакета. Первая часть пакета, называемая заголовком пакета, на рисунке выделена более темным цветом фона. Заголовок содержит информацию о пакете, а область с более светлым цветом фона содержит передаваемые в пакете данные. Как видно на иллюстрации, в заголовке есть ячейки для данных отправителя, данных получателя и типа пакета. В данных отправителя и получателя чаще всего передаются соответствующие IP-адреса, а поле типа пакета, сообщает, какие именно в нем содержатся данные. В рассматриваемом примере тип пакета — пакет данных о передвижении подразделения. Для того, чтобы сообщить о передвижении подразделения в паккете передается идентификатор подразделения, новые координаты по осям X и Y и новая ориентация подразделения. Эта информация говорит получателю, какое подразделение переместилось, куда, и как оно расположено на новом месте.

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

netlib.narod.ru< Назад | Оглавление | Далее >



Подключение к серверу


После того, как заданы IP-адрес и порт, вы можете подключаться к серверу. Это делается путем вызова предоставляемой сокетом функции connect(). Ей передаются дескриптор сокета, который вы хотите использовать, и параметры сервера к которому вы хотите подключиться. Если функция возвращает значение SOCKET_ERROR, значит произошла ошибка; в противном случае соединение установлено.



Поиск сервера по URL


Если вы хотите установить соединение с сервером, используя URL, а не IP-адрес, вам сперва надо будет найти IP-адрес по URL. Это делается с помощью функции gethostbyname(). Она получает имя сервера и преобразует его в соответствующий IP-адрес.



Получение данных от сервера

После того, как вы отправили HTTP-запрос серверу, следует получить ответ от него. Чтобы увидеть ответ, вы должны вызвать функцию recv(), которая вернет данные из буфера связи сокета. Самое лучшее в сокетах то, что они автоматически принимают данные и помещают их в системный буфер, так что вам не следует беспокоиться, что данные могут быть потеряны из-за того, что ваша программа занята. Тем не менее, следует проявлять осторожность и не ждать слишком долго, поскольку данные, которые находятся в буфере слишком долго будут потеряны.

Функции приема в параметрах передаются идентификатор сокета, от которого вы хотите получить информацию, буфер для размещения данных и его размер. Как только появятся какие-нибудь данные для получения, они будут переданы в буфер приема и программа продолжит работу. Если данные никогда не будут отправлены, функция будет ждать вечно, пока вы не завершите программу. Такова природа синхронных сокетов (blocking socket).

ПРИМЕЧАНИЕ

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

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



Пример программирования сокетов


Я люблю учить на примерах, так что как насчет программы, использующей TCP/IP, которая подключается к Интернету, посылает HTTP-запрос на Web-сервер и отображает главную страницу сайта? Перед тем, как я перейду к коду, посмотрите на рис.14.10, где показано что именно мы будем делать.


Рис. 14.10. Ход выполнения простой программы, использующей сокеты

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



Программа Sockets_Receive


Я реализовал код, необходимый для воссоздания этапов, изображенных на рис. 14.10. Загрузите программу Sockets_Receive и следуйте за мной. Проект состоит из файла main.cpp и единственной библиотеки ws2_32.lib, которая содержит все, что необходимо для программирования сокетов в Windows. Скомпилируйте программу и запустите ее. Вы увидите окно консольного приложения, в котором отображается содержимое главной страницы сайта, имя которого задано в коде. Как это должно выглядеть, показано на рис. 14.11.


Рис. 14.11. Окно программы Sockets_Receive

Открыв файл main.cpp вы увидите следующий код:

#include <iostream.h> #include <winsock.h> #include <stdio.h>

void main(void) { SOCKET skSocket; sockaddr_in saServerAddress; int iPort = 80; int iStatus; WSADATA wsaData; WORD wVersionRequested; LPHOSTENT lpHost; char szHost[128]; char szSendBuffer[256]; char szRecvBuffer[32768]; int iBytesSent; int iBytesReceived;

sprintf(szHost,"www.lostlogic.com"); // Сообщаем WinSock, что нам нужна версия 2 wVersionRequested = MAKEWORD(2, 0); // Инициализируем дескриптор сокета skSocket = INVALID_SOCKET; // Запускаем WinSock iStatus = WSAStartup(wVersionRequested, &wsaData); // Создаем сокет skSocket = socket(AF_INET, SOCK_STREAM, 0); // Проверяем наличие ошибок if(skSocket == INVALID_SOCKET) { cout << "**ERROR** Could Not Create Socket" << endl; exit(1); } memset(&saServerAddress, 0, sizeof(sockaddr_in)); saServerAddress.sin_family = AF_INET; saServerAddress.sin_addr.s_addr = inet_addr(szHost);

if(saServerAddress.sin_addr.s_addr == INADDR_NONE) { lpHost = gethostbyname(szHost); if (lpHost != NULL) { // Получаем адрес сервера из информации хоста saServerAddress.sin_addr.s_addr = ((LPIN_ADDR)lpHost->h_addr)->s_addr; } else { cout << "**ERROR** Could Not Locate Host" << endl; exit(1); } } // Задаем порт сервера saServerAddress.sin_port = htons(iPort); // Пытаемся подключиться к серверу iStatus = connect(skSocket, (struct sockaddr*)&saServerAddress, sizeof(sockaddr));

// Проверяем наличие ошибок if(iStatus == SOCKET_ERROR) { cout << "**ERROR** Could Not Connect To Server" << endl; exit(1); } sprintf(szSendBuffer,"GET / HTTP/1.0\n\n"); // Отправляем HTTP-запрос iBytesSent = send(skSocket, szSendBuffer, 256, 0); memset(szRecvBuffer, 0x00, 32768); // Получаем данные iBytesReceived = recv(skSocket, szRecvBuffer, 32768, 0); cout << szRecvBuffer << endl; // Завершаем работу closesocket(skSocket); WSACleanup(); }



Программа Sockets_TurnGame


Программа Sockets_TurnGame предлагает пример реализации схемы, изображенной на рис. 14.12. Запустите программу и вы увидите окно, изображенное на рис. 14.13.


Рис. 14.13. Окно программы Sockets_TurnGame

На рис. 14.13 изображено небольшое окно с элементами управления, позволяющими работать как главный компьютер или установить соединение. Щелчок по кнопке Host переключает программу в режим игрового сервера, а щелчок по кнопке Connect переключает программу в режим клиента. Так как нельзя получить цыпленка раньше яйца, вы должны сначала запустить программу главного компьютера и уже потом подключаться к ней посредством клиента.

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


Рис. 14.14. Клиент установил соединение с сервером

На рис. 14.14 и на вашем экране вы видите два экземпляра программы. Программа сервера должна ожидать, пока клиент сделает свой ход, и у программы клиента должна быть готовая к использованию кнопка Turn Done. Игрок, у которого видна кнопка Turn Done в данный момент получил контроль над игрой и может передать его другому игроку, щелкнув по кнопке. Так вы можете передавать ход туда и обратно, щелкая по появляющейся кнопке. Я знаю, что понадобится напрячь воображение, но попытайтесь представить себе, что между щелчками по кнопке вы выполняете сложные игровые ходы.

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


Рис. 14.15. Ход выполнения программы Sockets_TurnGame

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

Код проекта содержится в файлах main.cpp и main.h. Для работы программе требуются две библиотеки: winmm.lib и ws2_32.lib. Библиотека winmm.lib не требуется для работы с сетью, я использую ее для воспроизведения звукового сигнала, когда пользователь щелкает по кнопке завершения хода.



Программирование сетевых походовых игр


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



Протокол TCP/IP


Протокол TCP/IP обеспечивает обмен сообщениями между компьютерами с гарантированной доставкой данных. Он также делает такие приятные вещи, как упорядочивание пакетов. Главное преимущество TCP/IP в том, что он делает за вас большую часть работы, гарантируя доставку сообщений. Что означает гарантия доставки информации? Это значит, что если система сообщает вам о том, что отправила сообщение другому компьютеру, вы можете быть уверены, что данные действительно отправлены. Я знаю, что данная особенность кажется очевидной, но в мире сетевого программирования это не так.

Основной недостаток протокола TCP/IP— медленная передача данных. Это вызвано гарантируемой доставкой данных. Чтобы гарантировать доставку, система должна отправить информацию, получить ответ и проконтроллировать правильность передачи. Это отнимает драгоценное время и на сегодняшнем оборудовании задержки часто оказываются недопустимыми для игр.



Протокол UDP


Протокол UDP — это более простая версия TCP/IP. С одной стороны он не гарантирует доставку информации. Может быть данные добрались до цели, а может и нет — кто знает? Кроме отсутствия гарантии доставки, UDP передает данные в произвольном порядке. Возьмем, к примеру, следующую фразу:

«Шустрая рыжая лиса перепрыгнула через ленивую собаку»

Если вы передаете эту фразу по протоколу UDP, данные могут следовать в таком порядке:

«собаку рыжая Шустрая лиса ленивую перепрыгнула через»

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

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



Сеть с коммутатором


Следующий логический шаг после использования концентраторов— коммутаторы (switch). Основной принцип действия коммутаторов ничем не отличается от принципа действия концентраторов, но они предоставляют каждому подключенному к ним компьютеру собственный канал связи. Это устраняет имеющиеся у концентраторов проблемы с выдачей ненужных данных и потерей пакетов. Главное различие между концентраторами и коммутаторами — их стоимость. Пятипортовый коммутатор будет стоить от $50 до $70 USD, в то время как стоимость пятипортового концентратора составляет от $20 до $30 USD. Лично я всегда приобретаю коммутаторы. Может они и стоят больше, но и работают гораздо лучше, чем концентраторы.

Вы можете найти коммутаторы и концентраторы, которые стоят дешевле, чем упомянутые выше цены. Некоторые фирмы, например Linksys, производят очень дешевое оборудование. Глваная проблема в этом, что скупой, как уже давно известно, платит дважды. Я предпочитаю заплатьть на 50 процентов больше за высококачественное сетевое оборудование, чем сэкономить деньги, приобретая дешевые устройства. Лично мне больше всего нравятся устройства фирмы Netgear. Они не обременены сотнями дополнительных возможностей, но надежны и хорошо выполняют свою работу.



Сеть с концентратором


Концентраторы (hub) используются для объединения нескольких компьютеров в сеть витой парой. Некоторые концентраторы очень маленькие, а другие достаточно большие. Есть два параметра по которым разделяются концентраторы: количество портов и скорость.

Поскольку концентратор позволяет соединять между собой несколько компьютеров, у него должен быть отдельный порт для каждого подключаемого компьютера. Именно поэтому так важно количество портов. У большинства концентраторов есть как минимум пять портов, так что они позволяют объединить четыре или пять компьютеров. Вы удивляетесь, почему я сказал четыре или пять, а не пять? Дело в том, что некоторые концентраторы позволяют объединять их в последовательную цепочку. В этом случае последний порт используется для подключения следующего в цепочке концентратора (или коммутатора). Перед тем, как я подробнее расскажу об этом, взгляните на рис. 14.4, где изображена базовая архитектура сети с концентратором.


Рис. 14.4. Компьютеры, соединенные через единственный концентратор

Простейший концентратор, позволяющий объединить четыре или пять компьютеров, стоит около $30 USD. Некоторые из них продаются в комплекте с несколькими сетевыми картами. Когда вы переходите к концентраторам, позволяющим подключать до восьми компьютеров, цена обычно подскакивает вдвое. Для концентраторов, позволяющих подключать 16 и более компьютеров цена еще выше. Обычно дешевле приобрести несколько простых концентраторов и объединить их в последовательную цепочку, чем приобретать единственный концентратор, рассчитанный на большое количество соединений. На рис. 14.5 изображены несколько соединенных вместе концентраторов.


Рис. 14.5. Компьютеры, соединенные с помощью нескольких концентраторов

Главная проблема концентраторов заключается в том, что все подключенные к нему компьютеры совместно используют одну внутреннюю полосу пропускания. Это значит, что каждый подключенный к концентратору компьютер соревнуется с другими за установление соединения. Результатом является увеличение объема передаваемых ненужных данных и потеря пакетов в том случае, если к концентратору подключено много компьютеров. Лучшей аналогией концентратора, которую я могу придумать, является спаренная телефонная линия. Если к такой линии подключено пять человек, то между тем, кто говорит, и теми кто хочет говорить будут постоянно возникать конфликты. Те же проблемы есть и у концентраторов.

Если говорить о скорости концентратора, то на текущий момент доступны три варианта: 10 мегабит, 100 мегабит и 1 гигабит. Скорость задает ширину полосы пропускания, доступной каждому каналу концентратора. Если на ваших компьютерах установлены 10-мегабитные сетевые карты, вам будет достаточно 10-мегабитного концентратора. Если вы используете 100-мегабитные сетевые карты, потребуется 100-мегабитный концентратор. То же самое справедливо и для гигабитных концентраторов и сетевых карт. Естественно, увеличение скорости концентратора увеличивает и его стоимость.



Сетевые протоколы


Когда речь идет о разработке игр, есть только два протокола, которые следует рассмотреть: TCP/IP и UDP. Это два основных используемых сегодня протокола, и, если вы не читаете эту книгу через много лет после ее издания, только они представляют разумный выбор для разработчика игр.



Сетевые соединения


Чтобы играть с другим человеком, сидящим за другим компьютером вам сперва необходимо установить соединение с этим компьютером. Существует множество типов соединений, но в игровом сообществе все они делятся на две основные категории: локальная сеть и Интернет.



Сети клиент-сервер


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


Рис. 14.9. Пример сети клиент-сервер

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



Соединение через кросс-кабель


Когда в сети всего два компьютера, можно использовать специальный сетевой кабель, обычно называемый кросс-кабелем. Фактически это обычная витая пара со специальным порядком подключения проводов в разъемах. Она позволяет соединять компьютеры не используя никакой дополнительной аппаратуры. Вы можете приобрести такой кабель в CompUSA или другой фирме примерно за $3USD. На рис. 14.3 изображены два компьютера, соединенные кросс-кабелем.


Рис. 14.3. Компьютеры, соединеные кросс-кабелем

Как видно на рис. 14.3, компьютер A соединен с компьютером B единственным кабелем. Это достаточно просто — и в этом главная красота. Если у вас только два компьютера, я рекомендую пользоваться именно этим способом. Я также рекомендую приобрести кросс-кабель, если у вас есть ноутбук. Никогда не знаешь, в какой момент понадобится подключиться к другому компьютеру для обмена файлами.



Соединения витой парой


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

Кросс-кабель

Концентратор

Коммутатор

Маршрутизатор



Сокеты и DirectPlay


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

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

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

Главное преимущество DirectPlay заключается в том, что он выполняет за вас значительную часть работы. Вам уже не придется беспокоиться об упорядочивании пакетов, гарантии доставки и даже об управлении сессиями. Главный вред от DirectPlay — утрата полного контроля за происходящим. Еще одна проблема заключается в том, что DirectPlay нельзя использовать на операционных системах, отличных от Windows. Чтобы сделать правильный выбор учтите следующие правила:

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

Если вы хотите, чтобы DirectX делал за вас всю сложную работу, используйте DirectPlay.

netlib.narod.ru< Назад | Оглавление | Далее >



Создание сокета

Чтобы подключиться к внешнему миру вам нужен канал связи в виде сокета. Чтобы создать такой канал вызовите функцию socket(), предоставляемую библиотекой сокетов. Успешно завершившаяся функция возвращает идентификационный номер (дескриптор) сокета.

СОВЕТ

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

Сравнение одноранговых сетей и сетей клиент-сервер


И сновы вы в недоумении. Какую архитектуру использовать: одноранговую или клиент-сервер? Приведенные ниже сценарии должны прояснить для вас этот вопрос.

Походовые стратегические игры

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

Стратегические игры реального времени

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

Многопользовательские сетевые ролевые игры (MMORPG)

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

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

netlib.narod.ru< Назад | Оглавление | Далее >



TCP/IP против UDP


Может быть вы думаете о том, какой протокол и когда следует использовать. Позвольте мне помочь вам, рассмотремв несколько сценариев.

Пошаговые стратегические игры

Для пошаговых стратегических игр вы, скорее всего, должны остановить свой выбор на TCP/IP. Поскольку данные о ходах передаются регулярно, вам не нужна высокая скорость и проблемы с UDP.

Стратегические игры реального времени

Для стратегий реального времени вам нужен UDP. Вам постоянно придется передавать различную информацию: от сообщений игроков до данных о передвижении подразделений. Даже и не думайте использовать для этой цели TCP/IP.

Многопользовательские сетевые ролевые игры (MMORPG)

Эй, это же книга о стратегических играх! (Шутка.) Здесь ответ не столь однозначен. Многопользовательские ролевые игры передают огромные объемы данных, так что они кажутся созданными для использования протокола UDP. Проблема в том, что гарантированная доставка с протоколом UDP не столь эффективна, как с TCP/IP, а в многопользовательских ролевых играх вам очень часто будет требоваться именно гарантированная доставка данных. Я не хочу оставить вас в растерянности, так что советую попробовать сначала UDP, а затем, если не сможете заставить его работать, переключиться на TCP/IP.

netlib.narod.ru< Назад | Оглавление | Далее >



Установка номера порта


Сервер может принимать подключения по нескольким линиям связи. Каждая из таких линий называется портом. Поскольку предоставляется несколько портов на выбор, необходимо указывать конкретный порт, с которым вы хотите соединиться. Это делается путем указания номера порта во время инициализации структуры адреса сервера sockaddr_in. Требуемая информация содержится в поле sin_port вышеупомянутой структуры. Задайте значение переменной sin_port и вы готовы идти дальше.



Установка версии WinSock


Перед тем, как вы вообще сможете использовать какие-либо сокеты, необходимо инициализировать систему сокетов Windows, вызвав функцию WSAStartup(). Эта функция получает номер версии сокетов, которую вы намереваетесь использовать и инициализирует коммуникационную систему. Раз вы хотите использовать сокеты версии2, установите номер запрашиваемой версии равным 2.



Включение заголовочного файла WinSock


Заголовочный файл winsock.h содержит всю необходимую информацию для работы с библиотекой сокетов ws2_32.lib. Убедитесь, что включаете его в любой ваш код, который работает с сокетами. Остальные заголовочные файлы применяются ежедневно в обычном программировании.



Задержка


Ох, ужасное слово для разработчиков многопользовательских игр. Задержка (latency) — это время, которое тратится на передачу пакета от одного компьютера к другому. В локальных сетях задержка создает не слишком много проблем, поскольку высокоскоростные соединения сверхдешевы и отвечают большинству потребностей. Но когда на сцене появляется Интернет, возникают новые проблемы. Пакеты могут задерживаться из-за аппаратуры отправителя, аппаратуры получателя или аппаратуры, расположенной между отправителем и получателем. Главное то, что Интернет вностит фактор неопределенности в любые прогнозы относительно работы сетевых игр. Этого не следует бояться — существуют варианты обхода подобных явлений.

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

netlib.narod.ru< Назад | Оглавление | Далее >



Закрытие сокета


Теперь, когда работа с сокетом завершена, необходимо закрыть его. Это делает функция closesocket(). Она получает идентификатор сокета, который будет закрыт и отключен.